题解 [NOIP2022] 建造军营
树形 好题。
观察题目发现, 如果B国袭击后,导致A国两个军营不联通,那么B国袭击的一定是一条割边,反之,如果袭击的不是割边,那么不会导致任何影响。
所以先进行边双缩点,变成一棵树,记每个联通块(缩完后)内的点数为 ,边数为 ,不妨先考虑对于树的情况如何处理。
将问题进行转化,即在树上选一些点和边,使得所有点可以通过选的边相互到达,询问选点和边的方案数。
首先设 表示以 为根的子树内没有/有 军营的方案数,那么答案显然是 ,但我们发现一种情况很难转移:对于节点 ,有些子树内有军营,有些没有,很难对边进行计数。
出现难以转移的情况,有两种解决办法:
- 增加 状态,更加具体地描述每个状态;
- 不增加状态,但是对状态作出更严格的限制。
经过尝试,即使增加 状态,仍然难以解决上述问题,所以我们采用第二种方法。
设 ,其中 表示以 为根的子树内没有选点, 表示以 为根的子树内选了点,且选出的边满足选出的点与 点连通(注意: 不一定要选)。
先来考虑答案是什么,显然不是 ,需要对每个点单独考虑,具体地,设整棵树除去 为根的子树的边数为 ,那么答案就是:
解释一下:依次考虑每个点,记当前点为 ,并且钦定只有 子树内选了点,于是 子树外面的边怎么选就无所谓(反正没有点,乱选边也不会有影响),所以每个点选或不选就是 ,但直接这样求会算重,原因如下图所示:
原因是在考虑 时,在选择 外面的边时选择了 头上的边,这种情况恰好包含在 中,因为正好通过这条边与 相连。
所以考虑 时强制不选 与 的边,这样就不会导致算重。也就是上述式子中的 。
那么这种算法能将所有情况统一下来不漏吗?换一个角度考虑,发现任意一种选点方案,会在选的点的 及其祖先处进行统计,由于我们规定不选与父亲相连的边,所以在所以祖先处考虑时边集不会重复,且覆盖所有,所以没有问题。
下面来考虑如何 。
解释: 代表 与 相连的边有选/不选两种可能, 后面减的东西表示除去子树全部为 的情况,前面的 系数表示 自己选/不选两种可能,如果 选了,那么子树选不选都无所谓,但如果 没选,那么子树必须要选。
这样我们就会了单纯是树的情况。
下面来考虑边双缩点成的树的情况,多了 和 两个权值。
其实几乎只有边界条件的不同,若选择点 ,那么有 ,若不选,就是,然后剩下按照转移方程计算即可。
时间复杂度:。
代码解释:f
同上,dp
表示子树内的边,wa
表示联通块内点数,wb
表示联通块内边数,p
表示 的次幂。
#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; } /* */
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!