P11834 [省选联考 2025] 岁月 题解
状压图计数究极版本,好困难啊
感觉大家认为直接用概率做会简单一点,可我还是延续了场上计数的思路
定义与记号
\(U\) 表示全集,\(S/T\) 表示 \(T\subseteq S\) 时在 \(S\) 中且不在 \(T\) 中元素组成的集合
\(rt_s\) 表示点集 \(s\) 中能作为外向树的根的点的集合,即缩点后入度为 \(0\) 的唯一的强连通分量,连通块的根即指它
\(to_s\) 表示 \(s\) 中的点分别所在的连通块的所有点的并集
\(e(s,t)\) 表示一端点在 \(s\) 中,另一端点在 \(t\) 中的边的数量,\(e(s)\) 表示 \(s\) 中包含的边数,\(e(s,t)=e(s\cup t)-e(s)-e(t)\)
SCC 计数
类似的题是 P11714 [清华集训 2014] 主旋律
设 \(f_S\) 表示 \(S\) 中的点成为强连通分量的方案数,\(sum_s\) 为 \(s\) 中点组成的图的方案数,\(sum_s=4^{e(s)}\)
考虑容斥,用所有图的数量减去强连通分量个数 \(>1\) 的方案数,而后者剥掉所有入度为 \(0\) 的强连通分量后仍非空,即能被划分为两部分
枚举划分出的拓扑序更小的一部分,即缩点后入度为 \(0\) 的强连通分量,假设钦定有 \(x\) 个,则容斥系数为 \((-1)^x\)
设 \(h_S\) 表示 \(S\) 中点划分为若干个强连通分量且带上容斥系数的方案数,每次新加入一个强连通分量 \(t\),注意每个集合固定一种加入顺序
则 \(f_s=sum_s-\sum_{t\subseteq s,t\neq\varnothing}h_t\times sum_{s/t}\times 2^{e(t, s/t)}\)
注意转移时 \(f_s\) 可以贡献给 \(h_s\),应该计算 \(f_s\) 后再 \(h_s\gets h_s-f_s\),初始化 \(h_{\varnothing}=1\)
时间复杂度为 \(O(3^n)\),瓶颈在于枚举子集
for(int i = 1; i < (1 << n); ++i) {int hb = 1 << __lg(i);for(int j = (i ^ hb); ; j = (j - 1) & (i ^ hb)) {int s = j ^ hb;Add(h[i], mod - 1ll * f[s] * h[i ^ s] % mod);if(j == 0) break;}f[i] = sum[i];for(int j = i; j; j = (j - 1) & i) {int tmp = pw[num[i] - num[j] - num[i ^ j]]; // num[i] 即为 e(i)Add(f[i], 1ll * h[j] * sum[i ^ j] % mod * tmp % mod);}Add(h[i], mod - f[i]);
}
特殊性质 C
当所有边边权相同时,整张图只需要存在一棵外向树,这等价于将强连通分量缩点后只有一个入度为 \(0\) 的强连通分量
类似的,考虑容斥,钦定图中入度为 \(0\) 的强连通分量,设 \(g_{i,s}\) 表示 \(s\) 中的点被划分为 \(i\) 个入度为 \(0\) 强连通分量的方案数,可用类似于 \(h_s\) 的方法 \(O(3^nn)\) 转移
容斥系数是 \((-1)^{i-1}\times i\),原因是要使 \(\sum_{i=0}^x coef_i{x\choose i}=[x=1]\),可惜赛时离这个性质就差这个系数没求对
实际上我们可以不记录个数,枚举当前入度为 \(0\) 的强连通分量 \(s\) 后枚举与它不相交的集合 \(t\),表示又钦定 \(t\) 中若干强连通分量也入度为 \(0\),那么若 \(t\) 中实际有 \(x\) 个强连通分量,要求 \(\sum_{i=0}^x coef_i{x\choose i}=[x=0]\),发现容斥系数又是 \((-1)^x\),则直接复用上面求出的 \(h_t\),
\(e(s)\) 可以做一遍高维前缀和求出,时间复杂度为 \(O(3^n+2^nn^2)\)
solution
考虑求无向图最小生成树的过程,将边从小到大排序后依次加入
受性质 B 的启发,我们一次性加入所有边权 \(=w\) 的边,如果某条边连接的两点在仅加入边权 \(<w\) 的边时就弱连通了,则这条边已经没用了,否则有可能成为最终方案中的一条边
于是可以分每种边权的边考虑,一次加入边权 \(=w\) 的边中符合要求的边
每次加入边后要求每个弱连通块中都可以形成外向树,而原来的弱连通块可以看成一个点,新加入的边要把这些点连成外向树且新加的边边权相同,这就转化为了类似于 C 性质的问题
\(e(s)\) 这里只考虑当前加入的边,\(e(s,t)\) 同理,注意后面 DP 数组的定义与前面不同
设 \(f_s\) 表示 \(s\) 中点恰好为 \(to_s\) 的根的方案数,这是我们每次要求的
设 \(sum_s\) 表示 \(s\) 仅满足 \(s\) 中的点恰好作为原来连通块的根时 \(to_s\) 的总数
设 \(g_s\) 表示 \(s\) 中点在恰好作为原来连通块根的同时加入边后形成强连通分量的方案数
类比 C 性质,求出 \(h_s\) 表示 \(s\) 划分为若干个原来连通块根组成的强连通分量且它们入度为 \(0\),带容斥系数 \((-1)^x\) 的方案数
但与 C 性质很不同的一点是这里连通块看成点后的入度为 \(0\) 不是指没有边连向整个连通块,而是指没有边指向连通块的根
因此 \(h_s\) 中 \(s\) 代表的若干个连通块之间的边也要算上方案数,恰好一端点在根的边不能有指向根的边,只有 \(2\) 种方案,端点均不在根的边可以任选
而且转移时要随时注意原来的连通块是一个整体,不能拆开为两部分分开加入,因此要每次都要判断两个集合 \(s,t\) 的 \(to_s,to_t\) 无交,所有转移式中为了方便省略了
类比 SCC 计数中的方式,枚举 \(h_t\) 后剩下的连通块能随意连边,保持 \(t\) 入度为 \(0\) 即可
令新求出的 \(f_s\) 为 \(f'_s\) 以区分上一次得到的
设当前处理的加边后连通块全集为 \(U\),同理性质 C,求 \(f'_s\) 时枚举与它不相交的集合 \(t\),\(s\) 为目标中连通块根组成的入度为 \(0\) 的强连通分量,但钦定 \(t\) 中也有若干个这样的强连通分量,剩下的原来连通块的根任取,但每个连通块都得取出根
这样一看要枚举 \(3\) 个集合复杂度就爆炸了,但剩下的根显然只与 \(s\cup t\) 有关
因此预处理 \(dp_s\) 表示 \(U/s\) 中所有原来连通块取出根的总方案数,要算上 \(s\) 向 \(U/s\) 的连边,枚举 \(U/s\) 中所有根的并集 \(t\)
则可以求出 \(f'_s\),要算上 \(s,t\) 之间连边的方案数
最后只把发生更新的点集 \(s\) 的 \(f_s\) 更新为 \(f's\),继续下一轮加边
答案为 \(ans=\sum_s f_s\),注意最后求概率要除以 \(4^k\) 而不是 \(4^m\),\(k\) 为加入的总边数
最多只会做 \(n\) 次,每次只计算了有新加入边的连通块,总的时间复杂度依然是 \(O(3^n+2^nn^2)\)
inline int in(int x, int s) {return (s >> (x - 1)) & 1;}
struct DSU {int pa[N], grp[N];void init() {for(int i = 1; i <= n; ++i) pa[i] = i, grp[i] = 1 << (i - 1);}int find(int x) {return x != pa[x] ? pa[x] = find(pa[x]) : x;}void merge(int x, int y) {x = find(x), y = find(y);if(x != y) pa[x] = y, grp[y] |= grp[x];}
}dsu, dsu2;
vector<int> subset(int s) {vector<int> res;for(int i = s; i; i = (i - 1) & s) res.pb(i);reverse(res.begin(), res.end()); return res;
}
void solve(int l, int r) {memset(num, 0, sizeof(num)), memset(h, 0, sizeof(h)), memset(g, 0, sizeof(g)), memset(vl, 0, sizeof(vl));memset(book, 0, sizeof(book)), memset(to, 0, sizeof(to)), memset(sum, 0, sizeof(sum)), memset(tmp, 0, sizeof(tmp));int vis = 0;for(int i = l, x = ord[i]; i <= r; ++i, x = ord[i]) {vis |= dsu.grp[dsu.find(u[x])], vis |= dsu.grp[dsu.find(v[x])];num[(1 << (u[x] - 1)) | (1 << (v[x] - 1))] = 1;}vector<int> lin = subset(vis);for(int j = 0; j < n; ++j)for(int i = 1; i < (1 << n); ++i) if((i >> j) & 1) Add(num[i], num[i ^ (1 << j)]);for(int i = 1; i <= n; ++i) if(dsu.find(i) == i && ((vis >> (i - 1)) & 1)) {for(int j = dsu.grp[i]; j; j = (j - 1) & dsu.grp[i]) book[j] |= dsu.grp[i];}for(int i = 1; i < (1 << n); ++i)for(int j = 1; j <= n; ++j) if((i >> (j - 1)) & 1) to[i] |= book[1 << (j - 1)];dsu2 = dsu;for(int i = l, x = ord[i]; i <= r; ++i, x = ord[i]) dsu2.merge(u[x], v[x]);sum[0] = g[0] = h[0] = 1;for(int i : lin) {int t = i & (-i);for(int j = (i ^ t); ; j = (j - 1) & (i ^ t)) {if(book[j ^ t] && (to[j ^ t] & to[i ^ j ^ t]) == 0) Add(sum[i], 1ll * sum[i ^ j ^ t] * f[j ^ t] % mod); if(j == 0) break;}}for(int i : lin) sum[i] = 1ll * sum[i] * pw4[num[to[i]]] % mod;auto get2 = [&](int i, int j) -> int { // i & j = 0 i -> jreturn num[i ^ j] - num[i] - num[j];};for(int i : lin) {int hb = 1 << __lg(i);for(int j = (i ^ hb); ; j = (j - 1) & (i ^ hb)) {int s = j ^ hb, t = i ^ s;if((to[s] & to[i ^ s]) == 0) Add(h[i], mod - 1ll * g[s] * h[t] % mod * pw4[get2(to[s] ^ s, to[t] ^ t)] % mod * pw[get2(s, to[t] ^ t) + get2(t, to[s] ^ s)] % mod);if(j == 0) break;}for(int j = i; ; j = (j - 1) & i) if((to[j] & to[i ^ j]) == 0) {Add(g[i], 1ll * h[j] * sum[i ^ j] % mod * pw[get2(to[i ^ j], j)] % mod * pw4[get2(to[j] ^ j, to[i ^ j])] % mod);if(j == 0) break;}Add(h[i], mod - g[i]);}vl[0] = 1;for(int k = 1; k <= n; ++k) if(((vis >> (k - 1)) & 1) && dsu2.find(k) == k) {int S = dsu2.grp[k];vector<int> nw = subset(S);for(int i : nw) {int s = S ^ i;for(int j = s; j >= 0; j = (j - 1) & s) {if((to[j] & to[i]) == 0 && (to[j] | to[i]) == to[S])Add(vl[i], 1ll * sum[j] * pw[get2(i, to[j])] % mod * pw4[get2(to[i] ^ i, to[j])] % mod);if(j == 0) break;}}for(int i : nw) {int s = S ^ i;for(int j = s; j >= 0; j = (j - 1) & s) { if((to[j] & to[i]) == 0)Add(tmp[i], 1ll * h[j] * g[i] % mod * vl[i ^ j] % mod * pw[get2(i, to[j] ^ j) + get2(j, to[i] ^ i)] % mod * pw4[get2(to[i] ^ i, to[j] ^ j)] % mod);if(j == 0) break;}}}for(int i = 0; i < (1 << n); ++i) if((i & vis) == 0) tmp[i] = f[i];dsu = dsu2, memcpy(f, tmp, sizeof(f));
}
void mian() {read(n, m), ans = 0, pw[0] = pw4[0] = 1;memset(f, 0, sizeof(f));for(int i = 1; i <= m; ++i) read(u[i], v[i], w[i]), ln[i] = i;sort(ln + 1, ln + m + 1, [&](const int x, const int y){return w[x] < w[y];});for(int i = 1; i < (1 << n); ++i) pcnt[i] = pcnt[i >> 1] + (i & 1);for(int i = 1; i <= m; ++i) pw[i] = add(pw[i - 1], pw[i - 1]), pw4[i] = 4ll * pw4[i - 1] % mod;dsu.init();for(int i = 1; i <= n; ++i) f[1 << (i - 1)] = 1;int tot = 0;for(int i = 1, j = 1; i <= m; i = j) {int cnt = 0;while(j <= m && w[ln[j]] == w[ln[i]]) {if(dsu.find(u[ln[j]]) != dsu.find(v[ln[j]])) ord[++cnt] = ln[j], ++tot;++j;}if(cnt) solve(1, cnt);}for(int i = 1; i < (1 << n); ++i) Add(ans, f[i]);ans = 1ll * ans * qmi(pw4[tot], mod - 2) % mod;print(ans), putchar('\n');
}