Codeforces
CF Step
Youtube Linkedin Discord Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Editorial: Graph Cutting (Two Vertices, Fixed Size)

After E0 (count all connected supersets of $\{a,b\}$), we restrict to $|S|=d$. This is the main technical step before E2 (all pairs, minimal subgraph) and E (three vertices).


Setup

Let $P(a,b)$ have $L$ vertices. Every valid $S$ contains $P(a,b)$, so

$$|S| = L + t$$

where $t \ge 0$ is the number of vertices taken from side branches (subtrees hanging off the path, not on it). We need $t = d - L$; if $d < L$, answer $0$.


Size polynomial for one side branch

For a side vertex $x$, define

$$F_x(z)=\sum_{s \ge 1} \mathrm{ways}_s(x)\, z^s,$$

where $\mathrm{ways}_s(x)$ counts connected subsets of $x$’s subtree that contain $x$ and have size $s$.

Leaf: $F_x(z)=z$.

Recursion: process side children $y$ of $x$. Each child is skipped ($1$) or contributes $F_y(z)$:

$$F_x(z)=z\cdot\prod_{y}\bigl(1+F_y(z)\bigr).$$

Implementation: start cur[1]=1, for each child cur = conv_skip(cur, child) where conv_skip multiplies by $(1+F_y)$ — copy coefficients (skip) plus shift-convolve (take).


All side branches at one path vertex

At $v \in P(a,b)$, branches are independent:

$$G_v(z)=\prod_{x \text{ side neighbor of } v}\bigl(1+F_x(z)\bigr).$$

Again use conv_skip for each factor $(1+F_x)$.


Whole path

Side regions at different path vertices are disjoint:

$$G(z)=\prod_{v \in P(a,b)} G_v(z).$$

Answer: coefficient of $z^{d-L}$ in $G(z)$.


Example

Star center $1$, leaves $2,3,4$; $a=2$, $b=3$, $d=4$.

  • Path length $L=3$.
  • Side branch at $1$: $F_4(z)=z$, so $G_1(z)=1+z$.
  • Need $d-L=1$ extra vertex: coefficient of $z^1$ is $1$ → unique set $\{2,1,3,4\}$.

For $d=3$: need $z^0$ coefficient → only $1$ way without $4$ → $\{2,1,3\}$.


Complexity

Per test case

Step Cost
Find $P(a,b)$ $O(n)$
Side DP with size polynomials Each conv_skip on polynomials of length $O(d)$ costs $O(d^2)$; at most $O(n)$ side roots and path attachment points
Final coefficient $[z^{d-L}]$ $O(d)$

Time: $O(n d^2)$ per test case in the worst case (each conv_skip is $O(d^2)$, up to $O(n)$ side attachments). With $n, d \le 2000$ that is $O(8 \cdot 10^9)$ in theory, but on typical trees (long Steiner path, few side branches) it is much faster — often $O(n)$ after marking the core.

Space: $O(n + d)$.

All test cases

$\sum n \le 2000$ caps the input size. E1 is one fixed pair per test, so you are not paying $O(n^2)$ unless you loop over all pairs yourself (E2 does, and needs $O(n^2)$).

Comparison

Version Time per test Notes
E0 $O(n)$ one fixed pair
E1 $O(n d^2)$ worst case one fixed pair
E1 + NTT $O(n d \log d)$ worst case atcoder::convolution on capped merges
Brute all pairs $O(n^2 \cdot n d^2) = O(n^3 d^2)$ run E1 for every pair — too slow
E2 $O(n^2)$ all pairs, distance only

Version What you count
E1 (here) Connected supersets of $\{a,b\}$ with $\|S\|=d$
E2 Pairs $(a,b)$ whose minimal connected subgraph has size $d$ → $
E Triples whose minimal (Steiner) subgraph has size $d$ — path case + three-branch case

The polynomial / convolution machinery from E1 reappears in E when you track depths from a root instead of sizes from side branches.

Trick 7 connection (blog entry 100910)

Side-branch conv_skip merges children one at a time:

$$\text{cur} \leftarrow \text{cur} \cdot (1 + F_{\text{child}})$$

Each merge loops over indices of two vectors whose lengths are bounded by subtree size (or by $d$ after restricting to size $\le d$). That is exactly the sequential sibling merge from Trick 7 / Miss Punyverse: total work $O(n^2)$ on the side tree, or $O(n d)$ with the $O(nk)$ cap $k = d$.

See model solution.


Knapsack on path + small-to-large (model_sol_opt)

Two further optimizations on top of the baseline DP:

  1. Cap at $\mathrm{need} = d - L$. Side-branch conv_skip only needs degrees up to $\mathrm{need}$; truncate after every merge.

  2. Knapsack on the mandatory path. Instead of multiplying full polynomials along $P(a,b)$, keep $\mathrm{dp}[s]$ = number of ways to allocate exactly $s$ extra vertices from the processed prefix. Update with the capped branch polynomial at each path vertex — $O(L \cdot \mathrm{need}^2)$ instead of growing a degree-$(\mathrm{need})$ polynomial explicitly.

  3. Small-to-large sibling merge. When a side vertex has several off-path children, merge their capped polynomials smallest first so intermediate vectors stay short (same Trick 7 idea as Miss Punyverse).

Part Bound
Side subtrees (capped merges, STL order) $O(n \cdot \mathrm{need})$ amortized (Trick 7); $O(n \cdot \mathrm{need}^2)$ worst case per naive merge
Path knapsack $O(L \cdot \mathrm{need}^2)$
Total $O(n \cdot \mathrm{need} + L \cdot \mathrm{need}^2)$; $O(n d^2)$ when $L = \Theta(n)$ and $\mathrm{need} = \Theta(d)$

With $\mathrm{need} \le d$, this is $O(n d + L d^2)$ per query — much better on typical trees when the path is short or $\mathrm{need} \ll d$, but still $O(n d^2)$ in the adversarial worst case (see NTT section below).

See the Knapsack on path + small-to-large block in code.

See the O(n d log d) via Atcoder NTT block in code for model_sol_ntt.cpp.


Worst case via Atcoder NTT

The $O(n d^2)$ worst case comes entirely from naive polynomial multiplication at degree $\mathrm{need} \le d$:

  • Each capped conv_skip_cap costs $O(\mathrm{need}^2)$.
  • There are $O(n)$ such merges over side edges plus $O(L)$ path knapsack updates, each $O(\mathrm{need}^2)$ when done naively.

Replace every multiply of two capped polynomials with Atcoder Library NTT (#include <atcoder/convolution>, atcoder::convolution on modint998244353 coefficients). A single multiply of degree $\le d$ then costs $O(d \log d)$.

conv_skip is a standard convolution

If side polynomial $F$ has $F(0)=0$, then conv_skip(a, F) is multiplication by $(1+F(z))$. Build

$$c = [1,\; F_1,\; F_2,\; \ldots,\; F_{\mathrm{need}}]$$

and set nxt = convolution(a, c), then truncate to length $\mathrm{need}+1$. Same mod $998244353$ as the answer.

Path knapsack step

At path vertex $v$, instead of the double loop over $s,t$:

$$\mathrm{ndp} = \mathrm{convolution}(\mathrm{dp},\; G_v)\bmod z^{\mathrm{need}+1}$$

(keep only the first $\mathrm{need}+1$ coefficients).

Complexity

Part Naive (opt) With atcoder::convolution
Side merges ($O(n)$) $O(n \cdot d^2)$ worst case $O(n \cdot d \log d)$
Path ($L$ steps) $O(L \cdot d^2)$ $O(L \cdot d \log d)$
Total $O(n d^2)$ $O(n d \log d)$

Since $L \le n$ and $\mathrm{need} \le d$, the whole test case is $O(n d \log d)$ in the worst case.

Implementation notes

  • Use the same mint = modint998244353 as the rest of the tutorial chain; ACL convolution expects that modulus.
  • NTT has larger constants than naive $O(d^2)$ for small degrees. A hybrid (naive below a threshold, NTT above) is fine for speed; all NTT is what gives the $O(n d \log d)$ worst-case proof.
  • This does not beat $O(n d \log d)$ within the generating-function DP: you still perform $\Theta(n)$ convolutions of degree $\Theta(d)$ on adversarial trees (long path, every side branch a full degree-$d$ factor).
  • The same idea applies to E4 (Steiner core instead of a path); E2 has no polynomial merges.