【干啥啥不行枪毙正解第一名】NOIP2022 T3 建造军营(barrack) 题解
2020kanade · · 题解
前言:目前(2022.12.7)题解区有一篇与笔者状态设计相同的题解,但那位神犇对于某个状态的计算好像有点繁琐(笔者个人认为),因此本题解大概不算重复解法,请各位至少看完状态转移方程导出部分,谢谢。
题意:给定一张无重边与自环的无向图,选出一个点集和一个边集(点集不得为空),定义一种选择方案是合法的当且仅当删除边集外任意一条边时点集中的所有点在原图中仍然联通,求总方案数对
看到“删除一条边后仍联通”之后非条件反射般想到边双缩点建边双树,至于为什么,大概是因为熟练吧,无向图中如果删掉一条边会对原图的子图的连通性造成影响,那条边一定是割边,而边双树的树边都是割边,并且树形结构具有很多优秀的性质。
发现定边之后选点比定点之后选边要麻烦很多(因为要保证合法性质),考虑讨论一个点的状态,之后对边决策。
发现边双点中的边选不选无所谓,因为随便割一条不会对连通性产生影响,它们都有两种选择,因此要决策的只剩下割边了。
给边双树钦定一个根(这里为一号结点),从子树角度考虑,设
对于 其实笔者当时根本没想这么多,这种东西百分之八九十都是 DP ,经验问题吧,大概。
继续讨论结点状态,发现
令
对于
对于
对于
这时候拿样例 1(没错就是那个只有两个点一条边的样例 1)试一下会发现
容易发现这个不合法部分我们已经定义过了,就是
至于初始状态,
综上,转移方程为:
按照定义,
其中
预处理模数意义下
注意取模,以及 Tarjan 算法求边双联通分量的实现,笔者就是因为这里写炸挂了 20 分。
官方数据也 A 了。
code:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e5+10,p=1e9+7;
struct edge
{
int to,nxt;
}T[N*2];int cnt2,H2[N];
int n,m;
vector<int> G[N];
inline void add_Edge(int u,int v)
{
G[u].emplace_back(v);
}
inline void add_T(int u,int v)
{
T[++cnt2]=edge{v,H2[u]};H2[u]=cnt2;
}
#define RAPE(x) for(auto v:G[x])
#define Rape(x) for(int i=H2[x];i;i=T[i].nxt)
int dp[N][3],p2[N*3+1];
//dp[u][0]=dp[u][0]*dp[v][0]+dp[u][0]*dp[v][1]+dp[u][0]*2*dp[v][2]
//dp[u][1]=dp[u][1]*dp[v][0]+dp[u][1]*dp[v][1]+dp[u][1]*2*dp[v][2]
//dp[u][2]=dp[u][2]*2*dp[v][2]
//dp[u][1]-=dp[u][2]
int stk[N],top;
int ebcsz[N],ebcc,ebc[N],dfc,dfn[N],low[N];
int tarjan(int u,int f)
{
low[u]=dfn[u]=++dfc;stk[++top]=u;
RAPE(u) if(v^f)
{
if(!dfn[v]) low[u]=min(low[u],tarjan(v,u));
else low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
++ebcc;int x;
do
{
x=stk[top--];
ebc[x]=ebcc,++ebcsz[ebcc];
}while(x^u);
}
return low[u];
}
int ts[N];
inline int add(int x,int y) {return (1ll*x+y>=p)?x-p+y:x+y;}
inline int sb(int x,int y) {return add(x,p-y);}
inline int kake(int x,int y) {return 1ll*x*y%p;}
int treedp(int u,int f)
{
dp[u][0]=sb(p2[ebcsz[u]],1);dp[u][1]=dp[u][2]=ts[u]=1;
Rape(u) if(T[i].to^f)
{
int v=T[i].to;ts[u]+=treedp(v,u);
dp[u][0]=add(add(kake(dp[u][0],dp[v][0]),kake(dp[u][0],dp[v][1])),kake(kake(2,dp[u][0]),dp[v][2]));
dp[u][1]=add(add(kake(dp[u][1],dp[v][0]),kake(dp[u][1],dp[v][1])),kake(kake(2,dp[u][1]),dp[v][2]));
dp[u][2]=kake(kake(2,dp[u][2]),dp[v][2]);
}
dp[u][1]=sb(dp[u][1],dp[u][2]);
return ts[u];
}
int u,v,ans;
int main()
{
freopen("barrack.in","r",stdin);
freopen("barrack.out","w",stdout);
ios::sync_with_stdio(0);
cin>>n>>m;p2[0]=1;
for(int i=1;i<=m*2;++i) p2[i]=kake(p2[i-1],2);//,cout<<p2[i]<<" ";cout<<endl;
for(int i=1;i<=m;++i) cin>>u>>v,add_Edge(u,v),add_Edge(v,u);
tarjan(1,0);//for(int i=1;i<=ebcc;++i) cout<<ebcsz[i]<<endl;
for(u=1;u<=n;++u) RAPE(u) if(ebc[v]^ebc[u]) add_T(ebc[u],ebc[v]);
treedp(1,0);//for(int i=1;i<=ebcc;++i) cout<<dp[i][0]+dp[i][1]<<endl;
for(int i=1;i<=ebcc;++i) ans=(1ll*ans+1ll*((dp[i][0]+dp[i][1])%p)*p2[max(0,ts[1]-ts[i]-1)]%p)%p;
ans=1ll*ans*p2[m-ts[1]+1]%p;
cout<<ans;
return 0;
}
后记:
按照上面的状态设计,在不关闭流同步时因常数问题可能在某些 OJ 会被卡掉 5-15pts。
实际上,上面的
转移方程其实差不太多,留给读者思考。
改过之后的 Code 在此。