P8867 NOIP2022 建造军营
P8867 NOIP2022 建造军营 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)。
给定一个无向联通图 \(G = (V', E')\),求有多少个二元组 \((V, E)\),满足:
- \(V \subseteq V'\),\(E \subseteq E'\),\(V \ne \varnothing\)。
- 在 \(G\) 上,断开 \(E’ - E\) 中任意一条边后,都有 \(V\) 中所有点在 \(G\) 上仍然联通。
35 pts
枚举 \(2^n - 1\) 种 \(V\) 的情况,再用 \(m(n +m)\) 的复杂度暴力检验将每条边断开后 \(V\) 是否仍然联通,记录【断开该边后 \(V\) 仍然可以联通】的边数 \(M\),答案累加 \(2^{M}\) 即可。
时间复杂度 \(\Theta(m(n+m)2^n)\),期望可以通过前 \(7\) 个数据点(没有实测)。
45 pts
考虑开特殊性质 \(\mathrm{A}\),也就是给定的图是一条链。
我们考虑当 \(V\) 最左面的点为 \(l\),最右面的点为 \(r\) 时,有多少种二元组。
当 \(l = r\),也就是 \(V\) 中只有一个元素 \(l\) 时,所有边都可以随便选,可以有 \(2^{n-1}\) 种取法。这一部分的答案是 \(n \times 2^{n-1}\)。
当 \(l \ne r\) 时,中间的 \(r-l\) 条边必须选入 \(E\),而两头的边都可以随便选取。此时 \(V\) 有 \(2^{r - l - 1}\) 种取法,\(E\) 有 \(2^{n-1-(r-l)}\) 种取法,惊喜发现这一部分答案就是 \(2^{n-2}\),和 \(l\),\(r\) 无关。
\(l\),\(r\) 的取法为 \(\dbinom{n}{2} = \dfrac{n(n-1)}{2}\),所以这部分的答案是 \(n(n-1)2^{n-3}\),总答案为 \(n \times 2 ^ {n-1} + n(n-1)2^{n-3}\)。
到这里都很送。
100 pts
不难发现,对于任意非桥边,删除它后整个图都可联通,因此所有非桥边在任意情况下都可以随便选。
这启发我们对整个图进行边双缩点。边双缩点后形成一棵树,原图非桥边将全部消失(被缩在一个点里),桥边变成新的树边。
因为任何非桥边任意情况下可以任意选,所以最终答案会有一个 \(2^{m - M}\) 的系数,这里 \(M\) 的含义是桥边的数量,也就是缩点后的树边数量。这样以来,我们在讨论选边方案时只讨论 \(M\) 条树边,得到的方案数最后乘上 \(2^{m - M}\) 即可。
当模型变成树后,我们发现,任意两个点之间的路径变得唯一:也就是对于树上的任意两个节点 \(s\) 和 \(t\),如果这两个点均被选入点集,那么对于 \(s \rightsquigarrow t\) 的简单路径上的所有边必须全部选入边集。否则,断开那条没选入边集的边,\(s\) 和 \(t\) 一定不再联通。
为了区分,我们定义 \(s\) 和 \(t\) 是【超联通】的,当且仅当 \(s\) 和 \(t\) 可以仅通过所选的边联通。
计数问题要么是动态规划要么是排列组合,要么两个都占。接下来就不难想到树形 dp 了。我们记 \(T(u)\) 表示 \(u\) 的子树。
考虑设计状态,\(f(u)\) 表示 \(T(u)\) 的答案,也就是在 \(T(u)\) 上,最少选一个点,选任意条边的方案数。我们设 \(e(u)\) 表示 \(T(u)\) 中边的数量。
对于 \(u\) 的子节点 \(v\),如果我们决定不在 \(T(v)\) 上选点,那么 \(T(v)\) 上的边和 \((u, v)\) 这条边都有选和不选两种可能,有 \(2^{e(v) +1}\) 种情况。
若我们在 \(T(v)\) 上选点呢,出现问题:
如果 \(T(v)\) 上存在一个点 \(x\),满足 \(x\) 和 \(v\) 不超联通(注意到这个方案是合法的,因为我们可以不选择点 \(v\))。那么无论我们是否选择 \((u, v)\),都有 \(x\) 和 \(u\) 不超联通。此时点 \(u\),以及 \(v\) 的兄弟的子树上的点一定不可选择,因为无法和 \(x\) 超联通,不合法。
如果 \(T(v)\) 上不存在这样的点 \(x\),也就是 \(T(v)\) 上的点都和 \(v\) 超联通,只要我们选择 \((u, v)\),那么 \(T(v)\) 上选的所有点就能和 \(u\) 超联通。此时,\(u\) 以及 \(v\) 的兄弟的子树上的点就可合理选取。
上面两种情况会造成转移的截然不同,但是由于我们统统放在一个 \(f(v)\) 里,没法转移。所以我们需要分开讨论和转移。
换句话说,我们此时应给状态多加一维:
\(f(u, 1)\) 表示 \(T(u)\) 上至少选了一个点,且选的所有点全部和 \(u\) 超联通的方案数;
\(f(u, 2)\) 表示 \(T(u)\) 上至少选了一个点,且存在一个点不和 \(u\) 超联通的方案数。
从而分开转移。最终答案是 \(f(1, 1) + f(1, 2)\)(令 \(1\) 为根)。
详细的转移方程和代码细节可以参考别的题解,因为在这里我要讲的是一种另外的思路,状态不需要多加一维,更有趣一点。
设 \(f(u)\) 表示 \(T(u)\) 上至少选了一个点,且选的所有点全部和 \(u\) 超联通的方案数。其实就是上边的 \(f(u, 1)\),换句话说,我只需要 \(f(u, 1)\) 一个状态就可完成本题的全部转移和答案统计。
我们加设 \(c(u)\) 表示节点 \(u\) 代表原图上多少个点(即有多少个点被缩进 \(u\) 了)。对于 \(u\) 上的点,可随便选,所以会有个 \(2^{c(u)}\) 的系数。
接着讨论 \(u\) 的子节点 \(v\) 及其子树 \(T(v)\)。
- \(T(v)\) 上不选点:那么 \(T(v)\) 上的边和 \((u, v)\) 这条边都有选和不选两种可能,有 \(2^{e(v) +1}\) 种情况;
- \(T(v)\) 上选点:此时 \((u, v)\) 必选,有 \(f(v)\) 种情况。
最后还有个问题:\(f(u)\) 要保证 \(T(u)\) 上必选点。上面所有情况计算完毕,会包含所有不选点构成的 \(2^{e(u)}\) 种情况,最后减去。
得到 \(f(u)\) 的状态转移方程:
怎么做答案统计?
答案一定不是 \(f(1)\),因为当然会存在:所选点不全部和树根超联通的情况。
我们再考虑,一种选择方案,它选的所有点全都在 \(T(u)\) 上,还都和 \(u\) 超联通,方案数是多少?是 \(f(u)\) 吗?错,我没说所选边都在 \(T(u)\) 上:事实上,上面这个问题的答案是 \(f(u) \times 2^{M - e(u)}\)。
此时我想,答案是否为 \(f(1) +f(2) \times 2^{M - e(2)} + f(3) \times 2^{M- e(3)} + \cdots + f(M + 1) \times 2^{M + 1 - e(M+1)}\)(别忘了 $M +1 $ 是新树的点数,而且 \(M = e(1)\))?然后我构造了两个反例出来:
上面这个选择方案(红点和红边为选择对象,黑色的未不选的对象),会被 \(f(4) \times 2^{M - e(4)}\) 和 \(f(2) \times 2^{M - e(2)}\) 统计两次。
上面这个选择方案,会被 \(f(2) \times 2^{M - e(2)}\) 和 \(f(1)\) 统计两次。
是否有一种办法能让每种方案只被统计一次?
观察上面这两个反例和自己造的一些反例,不难发现,只在所选点集的 LCA 统计答案即可。也就是对于第一个反例,期望在 \(4\) 处统计该情况而不是在 \(2\);对于第二个反例,期望在 \(2\) 处该情况而不是在 \(1\)。如果在上面第二个反例的基础上,再选点 \(3\) 和 \((1, 3)\) 这条边,那就期望在 \(1\) 处统计答案了。
换句话讲,我们的目标:对于节点 \(u\),计算出满足所选点集的 LCA 为点 \(u\) 的方案数。
再换句话讲,就是计算出在 \(f(u)\) 中,要么是 \(u\) 本身被选择,要么是 \(u\) 的两棵及以上子树选了点的方案数,最后乘 \(2^{M - e(u)}\)。
再再换句话讲,就是计算出在 \(f(u)\) 中,\(u\) 没被选而且只有一个子树中选了点的方案数,最后再用 \(f(u)\) 减去它,乘上 \(2^{M - e(u)}\) 的系数。
我们枚举 \(u\) 的子节点 \(v\),\(T(v)\) 上有 \(f(v)\) 种方案,\((u, v)\) 必选(否则就不在 \(f(u)\) 里了),\(T(u)\) 中其他边可选可不选,总共是 \(f(v) \times 2^{e(u) - e(v) - 1}\) 种方案。
因此,\(u\) 作为 LCA 对答案的贡献是:
时间复杂度 \(\Theta(n+m)\)。
/*
* @Author: crab-in-the-northeast
* @Date: 2022-12-07 04:15:55
* @Last Modified by: crab-in-the-northeast
* @Last Modified time: 2022-12-07 05:23:10
*/
#include <bits/stdc++.h>
#define int long long
inline int read() {
int x = 0;
bool f = true;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-')
f = false;
for (; isdigit(ch); ch = getchar())
x = (x << 1) + (x << 3) + ch - '0';
return f ? x : (~(x - 1));
}
inline bool gmi(int &a, int b) {
return b < a ? a = b, true : false;
}
const int maxn = (int)5e5 + 5;
const int maxm = (int)1e6 + 5;
std :: vector <int> G[maxn], T[maxn];
int dfn[maxn], low[maxn], snt = 0, times = 0, sno[maxn];
int c[maxn];
std :: stack <int> s;
void tarjan(int u, int fa) {
low[u] = dfn[u] = ++times;
s.push(u);
for (int v : G[u]) {
if (!dfn[v]) {
tarjan(v, u);
gmi(low[u], low[v]);
} else if (v != fa)
gmi(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++snt;
for (; ;) {
int x = s.top();
s.pop();
sno[x] = snt;
++c[snt];
if (x == u)
break;
}
}
}
const int mod = (int)1e9 + 7;
int f[maxn], ans = 0;
int p[maxm], e[maxn];
void dp(int u, int fa) {
f[u] = p[c[u]];
for (int v : T[u]) {
if (v == fa)
continue;
dp(v, u);
(f[u] *= (p[e[v] + 1] + f[v])) %= mod;
e[u] += e[v] + 1;
}
(f[u] += mod - p[e[u]]) %= mod;
int now = f[u];
for (int v : T[u]) {
if (v == fa)
continue;
((now -= f[v] * p[e[u] - e[v] - 1] % mod) += mod) %= mod;
}
(ans += now * p[snt - 1 - e[u]] % mod) %= mod;
}
signed main() {
int n = read(), m = read();
for (int _ = 1; _ <= m; ++_) {
int u = read(), v = read();
G[u].push_back(v);
G[v].push_back(u);
}
tarjan(1, 0);
for (int u = 1; u <= n; ++u)
for (int v : G[u])
if (sno[u] != sno[v])
T[sno[u]].push_back(sno[v]);
p[0] = 1;
for (int i = 1; i <= m + 3; ++i)
p[i] = (p[i - 1] << 1) % mod;
dp(1, 0);
printf("%lld\n", ans * p[m - e[1]] % mod);
return 0;
}
如果觉得这篇题解写得好,请不要忘记点赞,谢谢!