题解:P16418 【MX-X28-T7】「FAOI-R12」副旋律

· · 题解

这是一篇简单易懂且详细的题解,只需要初中数学基础就可以理解。部分地方可能不严谨,比如用 f 指代函数值。

设 f(w_{1,2},w_{1,3},\dots,w_{1,n},w_{2,1},w_{2,3},\dots,w_{n,n-1}) 表示在 i\to j 边权为 w_{i,j} 的图中,所有强连通生成子图的权值之和。如果有重边,设边权分别为 a_1,a_2,\dots,a_k,那么在 f 中这些重边就等价于一个边权为 (\prod(a_i+1))-1 的边。

最终的答案要求对于每一条边分别求出包含这条边的强连通生成子图的权值之和,那么容斥一下就是所有强连通生成子图的权值之和减去不包含这条边的强连通生成子图的权值之和。不包含这条边就是把这条边删掉,剩下的图中所有强连通生成子图的权值之和。因此假设没有重边,那么边 u\to v 的答案即 f(w_{1,2},\dots,w_{u,v},\dots,w_{n,n-1})-f(w_{1,2},\dots,0,\dots,w_{n,n-1})。

我们分析一下 f 的性质,可以发现:如果对于 f 的除了 w_{i,j} 以外的自变量全都不变,将 f 函数写成 f_{\dots,w_{i,j-1},w_{i,j+1},\dots}(w_{i,j}) 的形式,那么此时 f 函数为一次函数。这一点比较显然,因为我们考虑将图中的 i\to j 边权增加 1,那么答案的增量就是原图中所有包含 i\to j 的强连通分量,计算边权之积时不算入 w_{i,j} 贡献的权制之和。由于我们固定了除了 w_{i,j} 以外的其他所有边权,所以这个增量就是一个定值。那么此时 f 就可以写成一次函数的形式了。

如果我们对于每一个 i\to j 都求出了这个增量,并且求出了 f(w_{1,2},\dots,w_{n,n-1}),那么自然就可以求出答案。因此我们考虑如何求出增量。

考虑将 f 函数展开,那么由于 f 对每个变量都是一次函数,所以展开式中不可能存在关于 w_{i,j} 的二次项。如果我们称这样的不存在二次项的形式为一种线性组合,那么 f 函数值就是自变量 w_{1,2},\dots,w_{n,n-1} 的线性组合。而我们计算 f(w_{1,2},\dots,w_{n,n-1}) 时,不是直接由这些边权值计算的,而是通过许多中间变量,这些中间变量都是更早计算的变量的线性组合。

考虑如果最终结果 f(w_{1,2},\dots,w_{n,n-1}) 是由 x_1,x_2,\dots,x_p 这些变量直接计算得到的,且是这些变量的一个线性组合,由于我们已知计算过程中任何中间变量的值,我们很容易就可以直接对于每个 x_i 计算出 f 对 x_i 的一次函数的斜率,即只将 x_i 增加 1 时答案的增量。比如 f=x_1x_2+x_3,那么我们在算 f 前 x_1,x_2,x_3 的值已经确定,并且我们知道具体的值。此时假设我们将 x_1 增加 1(不影响其他任何变量),那么 f'=(x_1+1)x_2+x_3,\Delta f=f'-f=x_2 可直接计算。同样的,假设我们将 x_2,x_3 分别增加 1,我们也可以求出对应的 f 的增量。

将更广泛地,如果我们的计算过程中计算了一个变量 x,x 是由若干个更早计算的变量 y_1,y_2,\dots,y_p 直接计算得到,且是这些变量的一个线性组合,那么如果我们已经求出 x 增加 1 对最终函数值的增量,且我们知道 y_1,y_2,\dots,y_p 的具体值,我们也可以直接得出 y_1,y_2,\dots,y_p 分别增加 1 对最终函数值的增量。还用上面那个例子,x=y_1y_2+y_3,那么 y_1 增加 1 时 \Delta x=y_2。而我们求出了 x 增加 1 时的 \Delta f_0,那么 x_1 增加 1 时 \Delta f=\Delta x\Delta f_0,可以直接求出。

这样的话,我们正着做一遍求出来 f(w_{1,2},\dots,w_{n,n-1}) 的值,并且记下所有中间变量在每一时刻的值,那么我们就可以按照时间顺序倒推出每个中间变量增加 1 对答案的增量。那么最终就可以推出对于每个 w_{i,j} 增加 1 对答案的增量,即我们的所求。如果有重边,答案即 f(w_{1,2},\dots,w_{u,v},\dots,w_{n,n-1})-f(w_{1,2},\dots,w_{u,v}',\dots,w_{n,n-1}),也是可以直接通过增量求的。

正着做就是 P11714 [清华集训 2014] 主旋律,这里不过多赘述了,只给出核心 dp 式子:

dp_S=E(S,S)-\sum_{T\subseteq S,T\ne \emptyset}f_TE(S,S-T)\\ f_S=dp_S-\sum_{T\subseteq S,T\ne\emptyset,T\ne S,\operatorname{lowbit}(T)=\operatorname{lowbit}(S)}dp_Tf_{S\backslash T}\\ E(S,T)=\prod_{i\in S}\prod_{j\in T}(w_{i,j}+1)

下面主要来说怎么倒着推增量。我们对于每一个变量,新开一个变量表示该位置增加 1 对答案的增量。考虑倒着做的时候,由于我们需要知道所有原变量在当前时刻的值,所以我们倒推的时候顺便把原变量做一遍逆操作作为时间倒流。下面逐一解释正着做时所有可能涉及到的操作以及如何倒着推增量。(下面的式子中如果 a 表示原变量,那么 A 就表示新开的记录增量的变量,且都在模意义下)

1. a += b

- $a'$ 增加 $1$ 时,$a$ 增加 $1$,答案增加 $A$; - $b'$ 增加 $1$ 时,$a,b$ 增加 $1$,答案增加 $A+B$。 因此增量的变化为 $(A,A+B)\to(A,B)$。 最终反推时的操作就是 `a -= b, B += A`。 ### 2. `a -= b * c` $(a',b',c')\to(a'-b'c',b',c')$,$a'-b'c'=a$,$b'=b$,$c'=c$。 - $a'$ 增加 $1$ 时,$a$ 增加 $1$,答案增加 $A$; - $b'$ 增加 $1$ 时,$a$ 增加 $-c'$,$b$ 增加 $1$,答案增加 $B-c'A$; - $c'$ 增加 $1$ 时,$a$ 增加 $-b'$,$c$ 增加 $1$,答案增加 $C-b'A$。 因此增量的变化为 $(A,B-c'A,C-b'A)\to(A,B,C)$。 最终反推时的操作就是 `a += b * c, B -= A * c, C -= A * b`。 ### 3. `a = b` $(a',b')\to (b',b')$,$b'=a=b$。 - $a'$ 增加 $1$ 时,$a,b$ 均无变化,答案增加 $0$; - $b'$ 增加 $1$ 时,$a,b$ 增加 $1$,答案增加 $A+B$。 这个操作没有逆操作,因为覆盖后我们不可能复原出 $a'$ 的值。但是,由于 $a'$ 一定会被覆盖,其变化对答案没有影响,因此在后面的反推中其具体值对答案也没有影响。所以这里 $a'$ 赋任意值即可。增量的变化为 $(0,A+B)\to(A,B)$。 最终反推时的操作就是 `a = 0, B += A, A = 0`。 ### 4. `a = b * c` $(a',b',c')\to(b'c',b',c')$,$b'c'=a$,$b'=b$,$c'=c$。 - $a'$ 增加 $1$ 时,$a,b,c$ 均无变化,答案增加 $0$; - $b'$ 增加 $1$ 时,$a$ 增加 $c'$,$b$ 增加 $1$,答案增加 $B+c'A$; - $c'$ 增加 $1$ 时,$a$ 增加 $b'$,$c$ 增加 $1$,答案增加 $C+b'A$。 同 3,这里 $a'$ 赋任意值即可。增量的变化为 $(0,B+c'A,C+b'A)\to(A,B,C)$。 最终反推时的操作就是 `a = 0, B += A * c, C += A * b, A = 0`。 ### 5. `a = 1` $(a')\to(1)$,$1=a$。 - $a'$ 增加 $1$ 时,$a$ 无变化,答案增加 $0$。 同3,这里 $a'$ 赋任意值即可。增量的变化为 $(0)\to(A)$。 最终反推时的操作就是 `a = A = 0`。 --- 最后,反推的时间复杂度与正推相同,因此最终时间复杂度与主旋律的时间复杂度相同,可以做到 $O(3^n+n2^n)$。 :::info[代码] ```cpp #include <bits/stdc++.h> using namespace std; //#define int long long const int N = 20, M = (1 << 16), K = 1e5 + 5, mod = 998244353; int n, m, u[K], v[K], w[K], lg[M]; int e[N][N], sum[N][M], val[M], num[M], f[M], dp[M]; int E[N][N], Sum[N][M], Val[M], Num[M], F[M], Dp[M]; int qpow(int x, int y) { if (!y) return 1; int now = qpow(x, y >> 1); if (!(y & 1)) return 1ll * now * now % mod; return 1ll * now * now % mod * x % mod; } int main() { //freopen(".in", "r", stdin); //freopen(".out", "w", stdout); ios::sync_with_stdio(false); cin.tie(0), cout.tie(0); cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) e[i][j] = 1; for (int i = 1; i <= m; i++) { cin >> u[i] >> v[i] >> w[i]; e[u[i]][v[i]] = 1ll * e[u[i]][v[i]] * (w[i] + 1) % mod; } for (int i = 1; i <= n; i++) sum[i][0] = 1; for (int i = 1; i < (1 << n); i++) lg[i] = lg[i >> 1] + 1; for (int i = 1; i <= n; i++) for (int j = 1; j < (1 << n); j++) { int now = j & (-j); sum[i][j] = 1ll * sum[i][j ^ now] * e[lg[now]][i] % mod; } for (int i = 1; i < (1 << n); i++) { val[0] = 1; for (int j = (i - 1) & i; j; j = (j - 1) & i) { int now = (i ^ j), cur = now & (-now); val[now] = 1ll * val[now ^ cur] * sum[lg[cur]][i] % mod; } int cur = i & (-i); val[i] = 1ll * val[i ^ cur] * sum[lg[cur]][i] % mod; dp[i] = val[i]; for (int j = (i - 1) & i; j; j = (j - 1) & i) { if ((i & (-i)) != (j & (-j))) continue; f[i] += mod - 1ll * dp[j] * f[i ^ j] % mod; if (f[i] >= mod) f[i] -= mod; } for (int j = i; j; j = (j - 1) & i) { dp[i] += mod - 1ll * f[j] * val[i ^ j] % mod; if (dp[i] >= mod) dp[i] -= mod; } f[i] = (f[i] + dp[i]) % mod; } Dp[(1 << n) - 1] = 1; for (int i = (1 << n) - 1; i >= 1; i--) { val[0] = 1; for (int j = (i - 1) & i; j; j = (j - 1) & i) { int now = (i ^ j), cur = now & (-now); val[now] = 1ll * val[now ^ cur] * sum[lg[cur]][i] % mod; } int cur = i & (-i); val[i] = 1ll * val[i ^ cur] * sum[lg[cur]][i] % mod; f[i] = (f[i] - dp[i] + mod) % mod; Dp[i] = (Dp[i] + F[i]) % mod; for (int j = i; j; j = (j - 1) & i) { dp[i] += 1ll * f[j] * val[i ^ j] % mod; if (dp[i] >= mod) dp[i] -= mod; F[j] += mod - 1ll * Dp[i] * val[i ^ j] % mod; if (F[j] >= mod) F[j] -= mod; Val[i ^ j] += mod - 1ll * Dp[i] * f[j] % mod; if (Val[i ^ j] >= mod) Val[i ^ j] -= mod; } for (int j = (i - 1) & i; j; j = (j - 1) & i) { if ((i & (-i)) != (j & (-j))) continue; f[i] += 1ll * dp[j] * f[i ^ j] % mod; if (f[i] >= mod) f[i] -= mod; Dp[j] += mod - 1ll * F[i] * f[i ^ j] % mod; if (Dp[j] >= mod) Dp[j] -= mod; F[i ^ j] += mod - 1ll * F[i] * dp[j] % mod; if (F[i ^ j] >= mod) F[i ^ j] -= mod; } Val[i] = (Val[i] + Dp[i]) % mod, Dp[i] = dp[i] = 0; for (int j = i; j; j = (j - 1) & i) { int cur = j & (-j); Val[j ^ cur] += 1ll * Val[ j] * sum[lg[cur]][i] % mod; if (Val[j ^ cur] >= mod) Val[j ^ cur] -= mod; Sum[lg[cur]][i] += 1ll * Val[j] * val[j ^ cur] % mod; if (Sum[lg[cur]][i] >= mod) Sum[lg[cur]][i] -= mod; Val[j] = val[j] = 0; } Val[0] = val[0] = 0; } for (int i = 1; i <= n; i++) for (int j = (1 << n) - 1; j >= 1; j--) { int now = j & (-j); Sum[i][j ^ now] = (Sum[i][j ^ now] + 1ll * Sum[i][j] * e[lg[now]][i] % mod) % mod; E[lg[now]][i] = (E[lg[now]][i] + 1ll * Sum[i][j] * sum[i][j ^ now] % mod) % mod; sum[i][j] = Sum[i][j] = 0; } for (int i = 1; i <= m; i++) { int now = (e[u[i]][v[i]] - 1ll * e[u[i]][v[i]] * qpow(w[i] + 1, mod - 2) % mod + mod) % mod; cout << 1ll * now * E[u[i]][v[i]] % mod << '\n'; } return 0; } ``` :::