[NOIP2022] 建造军营 [题解]
建造军营
\(Problem\)
给定一张 \(n\) 个点 \(m\) 条边的图,你需要从中选出若干个点作为关键点(至少一个),同时选出若干条边使得无论这些边之外的任意一条边被破坏,关键点之间都量量可达。
求方案数,不同的方案当且仅当选点不同或选边不同。
\(n\leq 5\times 10 ^ 5,m\leq 1\times 10 ^ 6\)
\(Solution\)
直接使用 \(Tarjan\) 缩点是显然的,缩点之后整张图就有了一些很好的性质。
首先同一个强连通分量内的点与边我们可以不做考虑,即里面存在的每条边都有选或不选的两种情况,且缩出来的整个点只有是关键点或不是关键点两种情况。而是关键点当且仅当内部存在关键点,内部的这些关键点并不会影响内部边的。
其次整张图就变成了一棵树,在树上考虑这个问题将会更加简单。
考虑如何通过遍历这棵树统计方案数。
设 \(f_u\) 表示考虑以 \(u\) 为根的子树中存在关键点的方案数,对于每个节点 \(u\),我们需要考虑两种情况:
-
当前节点 \(u\) 是关键点,设方案数为 \(x\)。
-
当前节点 \(u\) 不是关键点,设方案数为 \(y\)。
算出这两个方案数后,我们即可统计当且仅当以 \(u\) 为根的子树中存在关键点的方案数,即为 \((x + y)\times 2 ^ {tot - dir_u - 1}\),其中 \(tot\) 为缩点后点的数量,\(dir_u\) 表示以 \(u\) 为根的子树中边的数量。
接下来考虑如何计算 \(x\) 和 \(y\)。
对于 \(x\),首先考虑节点 \(u\) 内部节点选取的情况,若节点 \(u\) 由 \(t\) 个节点组成,则方案数为 \(2 ^ t - 1\),令其为 \(x\) 初值,同时令 \(y\) 初值为 \(1\),在遍历节点 \(u\) 子树的过程中,以 \(v\) 为根的子树中选取情况的方案数为 \(f_v + 2 ^ {dir_v + 1}\),注意在此过程中,我们需要考虑连接 \(u\) 和 \(v\) 的边的情况。
具体来说,\(f_v\) 代表子树 \(v\) 中有关键点,则连接 \(u\) 和 \(v\) 的边则必须选取,而 \(2 ^ {dir_v}\) 表示子树 \(v\) 中没有关键点,里面的边可以随意选取,而连接 \(u\) 和 \(v\) 的边也能随意选取,故而要乘上 \(2\) 的系数。
将这个数同时和 \(x\) 与 \(y\) 相乘,得到初步的 \(x\) 和 \(y\)。这里有一个问题,即对于 \(y\) 来说,\(u\) 节点并非关键点,那么即使子树 \(v\) 中存在关键点,\(u\) 和 \(v\) 之间的边似乎也并非必选。这牵涉到我们对答案的统计,我们每到一个点都统计了当且仅当以该节点为根的子树中含有关键点的方案数,故而我们没有统计到的方案数必然是子树 \(v\) 外仍然存在关键点的方案数,所以这条边实际上也是必选。
而这又引出了另外一个问题,即如果按照这样计算,那将会导致计重,我们需要对 \(y\) 进行一些处理。
首先分析上面计数的方案,不难得出 \(y\) 中包含了一个关键点都不存在的情况,这不符合题意,所以需要先减掉 \(2^{dir_u}\)。
其次 \(y\) 之中还包含只有 \(u\) 的某棵子树中存在关键点的情况,这和之前的计数重复,所以也要容斥掉这种情况,通过遍历子树同时计算另外的子树为空的方案数,这不难做到。
容斥过后再进行计算得到的就是正确答案。
接下来则是更新 \(f_u\)。
首先 \(f_u\) 一定包含 \(x\) 和 \(y\),其次他也包含我们之前容斥掉的情况,全部加上即可。
时间复杂度 \(\mathcal O(n)\)
\(code\)
#include <bits/stdc++.h>
#define st first
#define nd second
#define db double
#define re register
#define pb push_back
#define mk make_pair
#define int long long
#define ldb long double
#define pii pair<int, int>
#define ull unsigned long long
#define mst(a, b) memset(a, b, sizeof(a))
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
int n, m, tot, ans;
int f[N], g[N], dir[N];
int cnt, low[N], dfn[N], bel[N], siz[N], fac[N];
stack<int> s;
vector<int> G[N], T[N];
inline void Tarjan(int u, int fa) //Tarjan 缩点
{
low[u] = dfn[u] = ++cnt, s.push(u);
for(re int to : G[u]){
if(!dfn[to]) Tarjan(to, u), low[u] = min(low[u], low[to]);
else if(to != fa) low[u] = min(low[u], dfn[to]);
}
if(low[u] == dfn[u]){
int x, id = ++tot;
do x = s.top(), bel[x] = id, siz[id] += 1, s.pop();
while(x != u);
}
}
inline int add(int x, int y) { return x + y >= mod ? x + y - mod : x + y; }
inline int del(int x, int y) { return x - y < 0 ? x - y + mod : x - y; }
inline void Sol(int u, int fa)
{
f[u] = del(fac[siz[u]], 1), g[u] = 1;
int sum = 0;
for(re int to : T[u]){
if(to == fa) continue;
Sol(to, u);
f[u] = f[u] * add(f[to], fac[dir[to] + 1]) % mod;
g[u] = g[u] * add(f[to], fac[dir[to] + 1]) % mod;
dir[u] += (dir[to] + 1);
}
g[u] = del(g[u], fac[dir[u]]);
for(re int to : T[u])
if(to != fa) g[u] = del(g[u], f[to] * fac[dir[u] - dir[to] - 1] % mod);
ans = add(ans, add(f[u], g[u]) * fac[tot - dir[u] - 1] % mod);
f[u] = add(f[u], g[u]);
for(re int to : T[u])
if(to != fa) f[u] = add(f[u], f[to] * fac[dir[u] - dir[to] - 1] % mod);
}
signed main()
{
n = read(), m = read(), fac[0] = 1;
for(re int i = 1; i <= m; i++) fac[i] = fac[i - 1] * 2 % mod;
for(re int i = 1, x, y; i <= m; i++)
x = read(), y = read(), G[x].pb(y), G[y].pb(x);
Tarjan(1, 0);
for(re int u = 1; u <= n; u++)
for(re int v : G[u]) if(bel[u] != bel[v]) T[bel[u]].pb(bel[v]);
Sol(1, 0);
printf("%lld\n", ans * fac[m - (tot - 1)] % mod);
return 0;
}
/*
5 4
1 2
2 3
2 4
2 5
*/