建造军营 NOIP 2022

[NOIP2022] 建造军营

题目: [NOIP2022] 建造军营

这是我第一个自己写出来的组合数学和树形 DP 的题目。

题目内容:

给定 nn 个点 mm 条边的无向联通简单图,可以在任意多个点(至少一个)建立军营,在任意多条边建立士兵。

求有多少种建造方案数使得任意切断一条没有士兵的边后,所有军营依然联通。

n5×105,m106n \le 5\times 10^5,m\le 10^6

时间限制 1.00s1.00s ,内存限制 512.00MB512.00MB ,期望复杂度 O(n+m)O(n+m)

首先,看到这个题目,这其中 所有军营依然联通 的限制让我们想到边双缩点后在树上求解,这是因为在边双中任意断边后边双内任意两点仍然联通。

问题就转变为了在一个树上求解。

设一个边双 ii 内有 aia_i 个点和 bib_i 条边。

如果设 fif_i 表示以 ii 为根的子树内的答案, bi\sum b_i 为以 ii 为根的子树内的边数,那么这里我们需要分情况讨论:

  • 如果不在 ii 建立军营:

那么对于 ii 的每个儿子 uu ,我们都可以在这个子树内选择建立军营(由于 fif_i 的定义,这里会至少建立一个)或者不建立军营。如果建立军营,那么方案数是 fuf_u ;否则不建立军营,那么这个子树内的边和 uu 连向 ii 的边都可以建立或不建立士兵,方案数为 21+bu2^{1+\sum b_u} 。总方案数就是 (21+bu+fu)\displaystyle\prod (2^{1+\sum b_u} + f_u)

然而,由于至少建立一个军营,所以要减去不建立军营的 2bi2^{\sum b_i} 中方案。

并且,当我只在一个子树 uu 内建立军营时, uuii 之间的边其实是可以不建立士兵的。这时我们就要加上这一部分代价,称作补偿贡献。这种情况下,子树 uu 的方案数为 fuf_u ,其余边可以任意选择,方案数为 fu2bibuf_u2^{\sum b_i-\sum b_u}但注意,由于我们原来已经累加了 fu2bibu1f_u2^{\sum b_i-\sum b_u-1} 的代价了,所以这里只累加 fu2bibu1f_u2^{\sum b_i-\sum b_u-1} ,总的补偿贡献就是 fu2bibu1\displaystyle\sum f_u2^{\sum b_i-\sum b_u-1}

这种情况的总方案数就是全方案加补偿贡献减去不合法方案。

  • 如果在 ii 建立军营:

首先,由于 ii 是一个边双,单独ii 建立军营的方案数为 (2ai1)2bi(2^{a_i}-1)2^{b_i} 。然后我们需要考虑子树。

对于子树 uu ,他的贡献和上面是一样的,为 21+bu+fu2^{1+\sum b_u}+f_u 。这样的总贡献为 (2ai1)2bi(21+bu+fu)(2^{a_i}-1)2^{b_i}\displaystyle\prod (2^{1+\sum b_u} + f_u)

其次,由于我们已经在 ii 建立军营了,所以没有不合法方案。

然后,由于所有军营都要保证和 ii 联通,对于子树 uuuuii 的边在 uu 建立军营的情况下必须建立士兵,所以没有补偿贡献

也就是说,总方案数就是全方案。

最后累加这两种情况的答案就行。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <bits/stdc++.h>
using namespace std;
const int N=1000010,P=1000000007;
int n,m,low[N],dfn[N],idx,f[N],s[N],top,dcc[N],cnt,a[N],b[N],book[N],sz[N],pw[N];
vector<int>v[N],g[N];
void dfs(int x,int fa){
low[x]=dfn[x]=++idx;
s[++top]=x;
for(int u:g[x]){
if(!dfn[u]){
dfs(u,x);
low[x]=min(low[x],low[u]);
}
else if(u!=fa)low[x]=min(low[x],dfn[u]);
}
if(low[x]==dfn[x]){
int t;
cnt++;
do{
t=s[top--];
dcc[t]=cnt;
a[cnt]++;
}while(t!=x);
}
}
void getb(int x,int cl){
book[x]=1;
for(int j:g[x]){
if(dcc[j]!=cl)continue;
if(!book[j])getb(j,cl);
b[cl]++;
}
}
void dfs2(int x,int fa){
int cnt1=1;
sz[x]=b[x];
for(int u:v[x]){
if(u==fa)continue;
dfs2(u,x);
sz[x]+=sz[u]+1;
cnt1=1ll*(pw[1+sz[u]]+f[u])%P*cnt1%P;
}
f[x]=(1ll*cnt1*pw[b[x]]%P-pw[sz[x]]+P)%P;
for(int u:v[x]){
if(u==fa)continue;
(f[x]+=1ll*f[u]*pw[sz[x]-sz[u]-1]%P)%=P;
}
(f[x]+=1ll*cnt1*(pw[a[x]]-1+P)%P*pw[b[x]]%P)%=P;
}
int main(){
cin>>n>>m;
pw[0]=1;
for(int i=1;i<=1000000;i++)pw[i]=2ll*pw[i-1];
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
g[x].push_back(y);
g[y].push_back(x);
}
dfs(1,0);
for(int i=1;i<=n;i++){
for(int j:g[i]){
int x=dcc[i],y=dcc[j];
if(x!=y)v[x].push_back(y);
}
}
for(int i=1;i<=n;i++)if(!book[i])getb(i,dcc[i]),b[dcc[i]]>>=1;
dfs2(1,0);
cout<<f[1];
return 0;
}

但是这样是有问题的!

原因是,在转移 ii 的时候, fif_i 包含了两种方案的贡献:一种是正常贡献(即全方案减去不合法方案),一种是补偿贡献

这个对于子树 uu 同样适用。

我们来看一看 uu 的补偿贡献究竟能不能贡献到 fif_i 里。

由于这个是补偿贡献,所以这些方案都是 uu子树的贡献!设为 uu 的子树 vv 的贡献,那么在 uuii 贡献的时候,由于是补偿贡献, uuvv 的连边没有建立士兵!此时根据题意,这种情况是不合法的。

也就是说,补偿贡献不能直接累加到 fif_i 里面

有一个比较直观的例子(样例太简单了,缩点后只有两层,无法体现这个错误):

1
2
3
3 2
1 2
2 3

这时,为了保证转移,我们需要更改 fif_i 的定义为没有补偿贡献的方案数,即,所有军营到 ii 的路径上的边都被建立士兵的方案数。

那这个新的 fif_i 的转移就十分简单了,只需要扔掉原来的转移中的补偿贡献就行了。

但是我们还是要求答案的。所以我们额外定义 FiF_iii 子树内的答案。

我们考虑怎么转移 FF

首先我们是可以直接让 Fi=fiF_i=f_i 的。那么此时 FF 需要累加上补偿贡献。然后,由于当 ii 建造军营时没有补偿贡献,所以我们只考虑 ii 没有建造军营时的情况。

接着,这个时候的方案数也是形如 fu2bibu\displaystyle\sum f_u2^{\sum b_i-\sum b_u} 的。由于原来的 ff 对应现在的 FF ,所以我们需要把贡献改写成 Fu2bibu\displaystyle\sum F_u2^{\sum b_i-\sum b_u}

然后,由于原来的补偿贡献可以写成那样的形式的前提是因为原贡献中已经累加了一部分了。这里已经让 FF 累加了 ff 了,但是 ff 累加的是有关 ff 的式子,而 FF 要累加的是有关 FF 的式子,所以我们还要减去这部分,即 fu2bibu1\displaystyle\sum f_u2^{\sum b_i-\sum b_u-1}

最终就有 Fi=fi+Fu2bibufu2bibu1F_i=f_i+\displaystyle\sum F_u2^{\sum b_i-\sum b_u}-\displaystyle\sum f_u2^{\sum b_i-\sum b_u-1}

答案就是根的 FF 值。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1003010,P=1000000007;
int n,m,low[N],dfn[N],idx,f[N],s[N],top,dcc[N],cnt,a[N],b[N],book[N],sz[N],pw[N],F[N];
vector<int>v[N],g[N];
void dfs(int x,int fa){
low[x]=dfn[x]=++idx;
s[++top]=x;
for(int u:g[x]){
if(!dfn[u]){
dfs(u,x);
low[x]=min(low[x],low[u]);
}
else if(u!=fa)low[x]=min(low[x],dfn[u]);
}
if(low[x]==dfn[x]){
int t;
cnt++;
do{
t=s[top--];
dcc[t]=cnt;
a[cnt]++;
}while(t!=x);
}
}
void getb(int x,int cl){
book[x]=1;
for(int j:g[x]){
if(dcc[j]!=cl)continue;
if(!book[j])getb(j,cl);
b[cl]++;
}
}
void dfs2(int x,int fa){
int cnt1=1;
sz[x]=b[x];
for(int u:v[x]){
if(u==fa)continue;
dfs2(u,x);
sz[x]+=sz[u]+1;
cnt1=1ll*(pw[1+sz[u]]+f[u])%P*cnt1%P;
}
f[x]=(1ll*cnt1*pw[b[x]]%P-pw[sz[x]]+P)%P;
(f[x]+=1ll*cnt1*(pw[a[x]]-1+P)%P*pw[b[x]]%P)%=P;
F[x]=f[x];
for(int u:v[x]){
if(u==fa)continue;
(F[x]+=P-1ll*f[u]*pw[sz[x]-sz[u]-1]%P)%=P;
(F[x]+=1ll*F[u]*pw[sz[x]-sz[u]]%P)%=P;
}
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
pw[0]=1;
for(int i=1;i<=1001110;i++)pw[i]=2ll*pw[i-1]%P;
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
g[x].push_back(y);
g[y].push_back(x);
}
dfs(1,0);
for(int i=1;i<=n;i++){
for(int j:g[i]){
int x=dcc[i],y=dcc[j];
if(x!=y)v[x].push_back(y);
}
}
for(int i=1;i<=n;i++)if(!book[i])getb(i,dcc[i]),b[dcc[i]]>>=1;
dfs2(1,0);
cout<<F[1];
return 0;
}