【NOIP2022】 建造军营

题目链接

点击打开链接

题目解法

第一眼可以发现这道题要按照边双缩点
在一个边双联通分量内的点即使去掉一条边也可以互相到达
于是我们可以把这道题变成一个树上的问题,最后答案只要乘上 2 边双内的边数 2^{边双内的边数} 2边双内的边数 的系数就可以了

下文把双联通过后的图中的边双成为点,把看守的边称为连接的边
我们知道每个作为军营的点之间必须有边连接
考虑树形 D P DP DP
首先一个显然的式子是 d p [ u ] [ 0 / 1 ] dp[u][0/1] dp[u][0/1] 表示在 u u u 的子树中,是(1)否(0)选点的方案数,后文用 v v v 来表示 u u u 的一个儿子
后面一维 0 / 1 0/1 0/1 是为了转移时是否一定需要取 u u u v v v 之间的边
考虑转移
d p [ u ] [ 0 ] = 2 u 的子树中包含的树边 dp[u][0]=2^{u的子树中包含的树边} dp[u][0]=2u的子树中包含的树边
这个比较好理解
主要是 d p [ u ] [ 1 ] dp[u][1] dp[u][1]
如果不考虑必须选一个点,方案数是 2 s i z [ u ] ∗ ( d p [ v ] [ 0 ] ∗ 2 + d p [ v ] [ 1 ] ) 2^{siz[u]}*(dp[v][0]*2+dp[v][1]) 2siz[u](dp[v][0]2+dp[v][1])
其中 s i z [ u ] siz[u] siz[u] 表示边双 u u u 的大小,这个式子也不难理解
于是我们考虑必须选点
可以从前往后考虑必须选 v v v v v v 之前的点需要被固定为不选,这样可以做到不重不漏

考虑最终的答案是什么
我们发现如果在 l c a lca lca 处统计会算重,为什么?
考虑下面的情况:
【NOIP2022】 建造军营_第1张图片
我们发现 u u u l c a lca lca 处会多算
所以我们可以换一种统计的方法
即在第一条不与上面相连的边处统计
这样答案就是 ∑ i = 1 n d p [ i ] [ 1 ] ∗ 2 不在 i 中的树边的数量 − 1 \sum_{i=1}^{n} dp[i][1]*2^{不在 i 中的树边的数量-1} i=1ndp[i][1]2不在i中的树边的数量1
i i i f a t h e r [ i ] father[i] father[i] 的边必须不连,这里根要特判一下

#include 
typedef long long LL;
using namespace std;
const int N(500100),M(2000100),P(1e9+7);
int n,m,cnt,bs[M],dp[N][2];
int treeE[N],totV[N];
int e[M],ne[M],h[N],idx;
int dfn[N],low[N],dfs_clock,scc_num[N],scc_cnt;
int stk[N],top;
vector<int> G[N];
inline int read(){
	int FF=0,RR=1;
	char ch=getchar();
	for(;!isdigit(ch);ch=getchar())
		if(ch=='-')
			RR=-1;
	for(;isdigit(ch);ch=getchar())
		FF=(FF<<1)+(FF<<3)+ch-48;
	return FF*RR;
}
void tarjan(int u,int from){
	dfn[u]=low[u]=++dfs_clock,stk[++top]=u;
	for(int i=h[u];~i;i=ne[i]){
		if(i==(from^1)) continue;
		int v=e[i];
		if(!dfn[v]){
			tarjan(v,i);
			low[u]=min(low[u],low[v]);
		}
		else
			low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		scc_cnt++;
		while(stk[top]!=u)
			scc_num[stk[top--]]=scc_cnt,totV[scc_cnt]++;
		scc_num[stk[top--]]=scc_cnt,totV[scc_cnt]++;
	}
}
int qmi(int a,int b,int p){
	int res=1;
	for(;b;b>>=1){
		if(b&1) res=(LL)res*a%p;
		a=(LL)a*a%p;
	}
	return res;
}
void dfs(int u,int fa){
	int tot=bs[totV[u]];
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v==fa) continue;
		dfs(v,u);
		treeE[u]+=treeE[v]+1;
		tot=(LL)tot*(dp[v][0]*2%P+dp[v][1])%P;
	}
	tot=(LL)tot*qmi(bs[totV[u]],P-2,P)%P;
	dp[u][1]=(LL)tot*(bs[totV[u]]-1+P)%P; 
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v==fa) continue;
		tot=(LL)tot*qmi(dp[v][0]*2%P+dp[v][1],P-2,P)%P;
		dp[u][1]=(dp[u][1]+(LL)tot*dp[v][1]%P)%P;
		tot=(LL)tot*dp[v][0]*2%P;
	}
	dp[u][0]=bs[treeE[u]];
}
void add(int a,int b){
	e[idx]=b,ne[idx]=h[a],h[a]=idx++; 
}
int main(){
	bs[0]=1;
	for(int i=1;i<M;i++) bs[i]=bs[i-1]*2%P;
	n=read(),m=read();
	memset(h,-1,sizeof(h));
	for(int i=1,a,b;i<=m;i++){
		a=read(),b=read();
		add(a,b),add(b,a);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i]) tarjan(i,-1);
	int inE=0;
	for(int i=1;i<=n;i++)
		for(int j=h[i];~j;j=ne[j]){
			int a=scc_num[i],b=scc_num[e[j]];
			if(a!=b) G[a].push_back(b);
			else inE++;
		}
	dfs(1,-1);
	int ans=0;
	for(int i=1;i<=scc_cnt;i++){
		int left=max(0,scc_cnt-2-treeE[i]);
		ans=(ans+(LL)bs[left]*dp[i][1]%P)%P;
	}
	printf("%d",(LL)ans*bs[inE>>1]%P);
	return 0;
}

你可能感兴趣的:(Luogu,算法)