P9167 [省选联考 2023] 城市建造 题解
P9167 [省选联考 2023] 城市建造 题解
题面
给定一张
解法
首先建出圆方树,可以转化为删去一个方点连通块,使得剩下各连通块圆点的大小极差不超过
由于我们想要把这棵树分成若干个大小近乎相等的块,所以划分的方点的位置应该更靠近树的重心
于是令
一种圆方树上DP:圆点表示父亲方点,方点表示自己的设计方案。这是一个较为通用的方法, “圆点表示父亲方点”使得可以简单的处理自己的贡献,“方点表示自己”使得可以将各个子树的信息合并
考虑枚举连通块大小
k = 0
以
-
对于圆点
,枚举所有子结点 。当 时,要求 。否则 不能被删去,因此要求 的 之和恰等于 。 -
对于方点
, 当且仅当其所有子结点 的 。
k = 1
则
类似地,设
-
对于圆点
:-
若
,则 不能被删去。 -
若
,则 必须被删去。 -
否则
。当 时, 不能被删去。否则 可以被删去。
设
为不能被删去的 加上 本身贡献的 表示 的连通块大小最小值,设 为可以被删去的 :-
若
, 。 -
若
,则 加上 。 -
若
,则 加上对每个可以被删去的 , 之和。因为 且 时 ,故 等于 乘以 的 的数量 。
注意第二、三个条件可以同时满足。
-
-
对于方点
, 等于 。
观察样例发现真正有值的
复杂度为 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可以剪枝
*/
参考
作者:lupengheyyds
出处:https://www.cnblogs.com/lupengheyyds/p/18702834
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通