P9167 [省选联考 2023] 城市建造 题解
题面
给定一张 \(n\) 个点 \(m\) 条边的无向连通图 \(G = (V, E)\),询问有多少该图的子图 \(G' = (V', E')\),满足 \(E' \ne \varnothing\) 且 \(G - E'\) 中恰好有 \(|V'|\) 个连通块,且任意两个连通块大小之差不超过 \(k\),请输出答案对 \(998,244,353\) 取模的结果。
\(0\le k\le 1,3\le n\le 10^5,n-1\le m\le 2\times 10^5\)
解法
首先建出圆方树,可以转化为删去一个方点连通块,使得剩下各连通块圆点的大小极差不超过 \(k\)。其中两个方点联通是指他们有共同的圆点相连。
由于我们想要把这棵树分成若干个大小近乎相等的块,所以划分的方点的位置应该更靠近树的重心 \(R\),于是可以发现第一个性质——重心或重心的方点应该被包括在那个删除方点的连通块中。
于是令 \(R\) 为根,有等价性质——若一个方点被删除,那么其到根的所有方点都应该被删除,于是变为了一颗有根树。
一种圆方树上DP:圆点表示父亲方点,方点表示自己的设计方案。这是一个较为通用的方法, “圆点表示父亲方点”使得可以简单的处理自己的贡献,“方点表示自己”使得可以将各个子树的信息合并
考虑枚举连通块大小 \(d = 1\sim \lfloor \frac n 2\rfloor\)。
k = 0
以 \(R\) 为根树形 DP。设 \(f_i\) 表示:若 \(i\) 是圆点,能否删去其父亲方点(若 \(i\) 为根,则答案可以视为添加并强制删去 \(i\) 的父亲方点时整棵子树的答案);若 \(i\) 是方点,能否删去其本身。 \(f_R\) 即为所求。
-
对于圆点 \(i\),枚举所有子结点 \(j\)。当 \(sz_j \geq d\) 时,要求 \(f_j = 1\)。否则 \(j\) 不能被删去,因此要求 \(sz_j < d\) 的 \(sz_j\) 之和恰等于 \(d\)。
-
对于方点 \(i\), \(f_i = 1\) 当且仅当其所有子结点 \(j\) 的 \(f_j = 1\)。
k = 1
则 \(k = 0\) 的 \(2\sim \lfloor \frac n 2\rfloor\) 多算了,要减掉。
类似地,设 \(f\_i\) 表示对应方案数, \(f\_R\) 即为所求。
-
对于圆点 \(i\):
-
若 \(sz_j < d\),则 \(j\) 不能被删去。
-
若 \(sz_j > d\),则 \(j\) 必须被删去。
-
否则 \(sz_j = d\)。当 \(f_j = 0\) 时, \(j\) 不能被删去。否则 \(j\) 可以被删去。
设 \(ss\) 为不能被删去的 \(\sum sz_j\) 加上 \(i\) 本身贡献的 \(1\) 表示 \(i\) 的连通块大小最小值,设 \(pd\) 为可以被删去的 \(\prod f_j\):
-
若 \(ss > d + 1\), \(f_i = 0\)。
-
若 \(d\leq ss\leq d + 1\),则 \(f_i\) 加上 \(pd\)。
-
若 \(ss = 1\),则 \(f_i\) 加上对每个可以被删去的 \(sz_j = d\), \(\frac {pd} {f_j}\) 之和。因为 \(sz_j = d\) 且 \(f_j > 0\) 时 \(f_j = 1\),故 \(\sum \frac {pd} {f_j}\) 等于 \(pd\) 乘以 \(sz_j = d 且 f_j > 0\) 的 \(j\) 的数量 \(cnt\)。
注意第二、三个条件可以同时满足。
-
-
对于方点 \(i\), \(f_i\) 等于 \(\prod f_j\)。
观察样例发现真正有值的 \(d\) 很少
\(k=0\) 时\(d|n\)
\(k=1\) 时,\(d=\lfloor \frac n\omega\rfloor\) 或 \(d=\lfloor \frac n\omega\rfloor-1,\omega\in \Z\),表示枚举分成几个连通块。
复杂度为 \(\mathcal O(n^{1.5})\),结合throw
可以剪枝
代码(注意那个throw
)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int NN=2e5+5,MOD=998244353;
int n,m,k,ans,prod=1;
vector<int> ed[NN],rbt[NN];
int low[NN],dfn[NN],num;
int sta[NN],top,ext,root;
void Tarjan(int x,int fa){dfn[x]=low[x]=++num;sta[++top]=x;for(int y:ed[x]){if(y==fa)continue;if(!dfn[y]){Tarjan(y,x);low[x]=min(low[x],low[y]);if(low[y]>=dfn[x]){int z;ext++;do{z=sta[top--];rbt[ext].push_back(z);rbt[z].push_back(ext);}while(z!=y);rbt[ext].push_back(x);rbt[x].push_back(ext);}}else{low[x]=min(low[x],dfn[y]);}}return;
}
int siz[NN],D,f[NN],MX=0x3f3f3f3f;
void DFS(int x,int fa,bool d){siz[x]=x<=n;int mx=0;for(int y:rbt[x]){if(y==fa)continue;DFS(y,x,d);siz[x]+=siz[y];mx=max(mx,siz[y]);}mx=max(mx,n-siz[x]);if(d&&MX>mx){MX=mx,root=x;}return;
}
void DFS0(int x,int fa){if(x<=n){f[x]=1;int sum=1;for(int y:rbt[x]){if(y==fa)continue;DFS0(y,x);if(siz[y]<D)sum+=siz[y];else f[x]&=f[y];} f[x]&=sum==D;}else{f[x]=1;for(int y:rbt[x]){if(y==fa)continue;DFS0(y,x);if(siz[y]<D)f[x]=0;else f[x]&=f[y];}}
}
void Deal(int d,int sign=1){if(d==n)return;D=d;DFS0(root,root);(ans+=f[root]*sign)%=MOD;return;
}
void DFS1(int x,int fa){if(x<=n){//圆点 int sum=1,prod=1,cnt=0;for(int y:rbt[x]){if(y==fa)continue;DFS1(y,x);if(siz[y]<D)sum+=siz[y];else if(siz[y]>D)(prod*=f[y])%=MOD;else if(f[y])(prod*=f[y])%=MOD,cnt++,assert(f[y]==1);else sum+=siz[y];}f[x]=0;if(D<=sum&&sum<=D+1)f[x]=prod;if(sum==1)(f[x]+=prod*cnt%MOD)%=MOD;}else{//方点f[x]=1;for(int y:rbt[x]){if(y==fa)continue;DFS1(y,x);(f[x]*=f[y])%=MOD;}}if(siz[x]>D&&!f[x])throw "Grimgod";//throw大法好 return;
}
void Deal1(int d){D=d;try{DFS1(root,root);(ans+=f[root])%=MOD;}catch(...){}return;
}
signed main(){ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m>>k;ext=n;for(int i=1;i<=m;i++){int u,v;cin>>u>>v;ed[u].push_back(v);ed[v].push_back(u);}Tarjan(1,1);DFS(1,1,1);memset(siz,0,sizeof siz);DFS(root,root,0);if(!k){for(int d=1;d*d<=n;d++){if(n%d==0){Deal(d);if(d*d!=n)Deal(n/d);}}cout<<ans;}else{vector<int> tmp;for(int d=2;d<=n/2;d++)tmp.push_back(n/d),tmp.push_back(n/d-1);sort(tmp.begin(),tmp.end());tmp.erase(unique(tmp.begin(),tmp.end()),tmp.end());for(int o:tmp)Deal1(o);for(int d=2;d<=n/2;d++)if(n%d==0)Deal(d,-1);cout<<(ans+MOD)%MOD;}return 0;
}/*
应该是割方点连通块
----------------------------------------------------
没有想到
性质:那个删除的方点连通块一定想要包括树的重心
一种圆方树上DP:圆点表示父亲方点,方点表示自己的设计方案。这是一个较为通用的方法, “圆点表示父亲方点”使得可以简单的处理自己的贡献,“方点表示自己”使得可以将各个子树的信息合并
k=1的圆点的情况比较复杂,需要好好想才能想清楚。
这样才想出来75分的做法
----------------------------------------------------
75->100是想出来的:
观察样例发现真正有值的d很少,
结合k=0的情况进一步分析,假设此时分为了 k 个连通块,那么d=n/k或d=n/k-1
复杂度为 O(n^{1.5}),结合throw可以剪枝
*/