树形 \(dp\) 好题。
观察题目发现, 如果B国袭击后,导致A国两个军营不联通,那么B国袭击的一定是一条割边,反之,如果袭击的不是割边,那么不会导致任何影响。
所以先进行边双缩点,变成一棵树,记每个联通块(缩完后)内的点数为 \(wa\),边数为 \(wb\),不妨先考虑对于树的情况如何处理。
将问题进行转化,即在树上选一些点和边,使得所有点可以通过选的边相互到达,询问选点和边的方案数。
首先设 \(f[i][0/1]\) 表示以 \(i\) 为根的子树内没有/有 军营的方案数,那么答案显然是 \(f[1][1]\),但我们发现一种情况很难转移:对于节点 \(i\),有些子树内有军营,有些没有,很难对边进行计数。
出现难以转移的情况,有两种解决办法:
- 增加 \(dp\) 状态,更加具体地描述每个状态;
- 不增加状态,但是对状态作出更严格的限制。
经过尝试,即使增加 \(dp\) 状态,仍然难以解决上述问题,所以我们采用第二种方法。
设 \(f[i][0/1]\),其中 \(f[i][0]\) 表示以 \(i\) 为根的子树内没有选点,\(f[i][1]\) 表示以 \(i\) 为根的子树内选了点,且选出的边满足选出的点与 \(i\) 点连通(注意:\(i\) 不一定要选)。
先来考虑答案是什么,显然不是 \(f[1][1]\),需要对每个点单独考虑,具体地,设整棵树除去 \(i\) 为根的子树的边数为 \(cnt[i]\),那么答案就是:
\[\sum_{i=1}^{siz} f[i][1]\times (2^{cnt[i]}-1) \]解释一下:依次考虑每个点,记当前点为 \(i\),并且钦定只有 \(i\) 子树内选了点,于是 \(i\) 子树外面的边怎么选就无所谓(反正没有点,乱选边也不会有影响),所以每个点选或不选就是 \(2^{cnt[i]}\),但直接这样求会算重,原因如下图所示:
原因是在考虑 \(i\) 时,在选择 \(i\) 外面的边时选择了 \(i\) 头上的边,这种情况恰好包含在 \(f[fa][1]\) 中,因为正好通过这条边与 \(fa\) 相连。
所以考虑 \(i\) 时强制不选 \(i\) 与 \(fa\) 的边,这样就不会导致算重。也就是上述式子中的 \(2^{cnt[i]}-1\)。
那么这种算法能将所有情况统一下来不漏吗?换一个角度考虑,发现任意一种选点方案,会在选的点的 \(\rm LCA\) 及其祖先处进行统计,由于我们规定不选与父亲相连的边,所以在所以祖先处考虑时边集不会重复,且覆盖所有,所以没有问题。
下面来考虑如何 \(dp\)。
\[\begin{cases} f[nd][1]=2\times \prod_{x\in son[nd]} (f[x][1]+2\times f[x][0])-\prod_{x\in son[nd]} 2\times f[x][0]\\ f[nd][0]=\prod_{x\in son[nd]} f[x][0]\times 2 \end{cases} \]解释:\(f[x][0] \times 2\) 代表 \(nd\) 与 \(x\) 相连的边有选/不选两种可能,\(f[nd][1]\) 后面减的东西表示除去子树全部为 \(0\) 的情况,前面的 \(2\times\) 系数表示 \(nd\) 自己选/不选两种可能,如果 \(nd\) 选了,那么子树选不选都无所谓,但如果 \(nd\) 没选,那么子树必须要选。
这样我们就会了单纯是树的情况。
下面来考虑边双缩点成的树的情况,多了 \(wa\) 和 \(wb\) 两个权值。
其实几乎只有边界条件的不同,若选择点 \(i\),那么有 \((2^{wa[i]}-1)\times 2^{wb[i]}\),若不选,就是\(2^{wb[i]}\),然后剩下按照转移方程计算即可。
时间复杂度:\(\rm O(n)\)。
代码解释:f
同上,dp
表示子树内的边,wa
表示联通块内点数,wb
表示联通块内边数,p
表示 \(2\) 的次幂。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
typedef long long LL;
typedef unsigned long long ULL;
LL read() {
char c=getchar(); LL sum=0,flag=1;
while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();}
while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();}
return sum*flag;
}
const int N=5e5+10,M=1e6+10;
const LL MOD=1e9+7;
int n,m;
int low[N],dfn[N],siz,tmst,id[N];
int wa[N],wb[N];
vector<PII> g[N];
struct Edge {
int l,r;
}a[M];
stack<int> st;
LL f[N][2],p[M],dp[N];
void dfs(int nd,int lst) {
dfn[nd]=low[nd]=++tmst; st.push(nd);
for(auto t:g[nd]) {
int x=t.first,id=t.second;
if(lst==id) continue;
if(!dfn[x]) {
dfs(x,id);
low[nd]=min(low[nd],low[x]);
}
else low[nd]=min(low[nd],dfn[x]);
}
if(low[nd]==dfn[nd]) {
int y=0; ++siz;
do {
y=st.top(); st.pop();
id[y]=siz; wa[siz]++;
} while(y!=nd);
}
}
void dfs2(int nd,int fa) {
f[nd][0]=p[wb[nd]];
f[nd][1]=(p[wa[nd]]-1)*p[wb[nd]]%MOD;
dp[nd]=wb[nd];
if(g[nd].size()==1&&g[nd][0].first==fa) return ;
LL s1=p[wb[nd]];//自己不选
LL s2=(p[wa[nd]]-1)*p[wb[nd]]%MOD;//自己选
LL s0=p[wb[nd]];//计算子树全部不选的数量
for(auto t:g[nd]) {
int x=t.first; if(x==fa) continue;
dfs2(x,nd);
dp[nd]+=dp[x]+1;
f[nd][0]*=f[x][0]*2; f[nd][0]%=MOD;
s1*=f[x][1]+f[x][0]*2; s1%=MOD;
s2*=f[x][1]+f[x][0]*2; s2%=MOD;
s0*=f[x][0]*2%MOD; s0%=MOD;
}
f[nd][1]=s2+s1-s0; f[nd][1]=(f[nd][1]%MOD+MOD)%MOD;
}
int main(){
n=read(); m=read();
p[0]=1; for(int i=1;i<=m;i++) p[i]=p[i-1]*2ll%MOD;
for(int i=1;i<=m;i++) {
int x=read(),y=read();
a[i]={x,y};
g[x].push_back({y,i});
g[y].push_back({x,i});
}
dfs(1,0);
for(int i=1;i<=n;i++) {
g[i].clear();
}
for(int i=1;i<=m;i++) {
int x=id[a[i].l],y=id[a[i].r];
if(x==y) wb[x]++;
else {
g[x].push_back({y,0});
g[y].push_back({x,0});
}
}
dfs2(1,0);
LL ans=0;
for(int i=1;i<=siz;i++) {
ans=(ans+f[i][1]*p[max(dp[1]-dp[i]-1,0ll)]%MOD)%MOD;
}
cout<<ans;
return 0;
}
/*
*/
标签:wb,int,题解,nd,times,军营,NOIP2022,dp,MOD
From: https://www.cnblogs.com/zhangyuzhe/p/18494370