P9167 [省选联考 2023] 城市建造 题解

P9167 [省选联考 2023] 城市建造 题解

题面

给定一张 n 个点 m 条边的无向连通G=(V,E),询问有多少该图的子图 G=(V,E),满足 EGE 中恰好有 |V| 个连通块,且任意两个连通块大小之差不超过 k,请输出答案对 998,244,353 取模的结果。

0k1,3n105,n1m2×105

解法

首先建出圆方树,可以转化为删去一个方点连通块,使得剩下各连通块圆点的大小极差不超过 k。其中两个方点联通是指他们有共同的圆点相连。


由于我们想要把这棵树分成若干个大小近乎相等的块,所以划分的方点的位置应该更靠近树的重心 R,于是可以发现第一个性质——重心或重心的方点应该被包括在那个删除方点的连通块中。

于是令 R 为根,有等价性质——若一个方点被删除,那么其到根的所有方点都应该被删除,于是变为了一颗有根树。


一种圆方树上DP:圆点表示父亲方点,方点表示自己的设计方案。这是一个较为通用的方法, “圆点表示父亲方点”使得可以简单的处理自己的贡献,“方点表示自己”使得可以将各个子树的信息合并

考虑枚举连通块大小 d=1n2

k = 0

R 为根树形 DP。设 fi 表示:若 i 是圆点,能否删去其父亲方点(若 i 为根,则答案可以视为添加并强制删去 i 的父亲方点时整棵子树的答案);若 i 是方点,能否删去其本身。 fR 即为所求。

  • 对于圆点 i,枚举所有子结点 j。当 szjd 时,要求 fj=1。否则 j 不能被删去,因此要求 szj<dszj 之和恰等于 d

  • 对于方点 ifi=1 当且仅当其所有子结点 jfj=1

k = 1

k=02n2 多算了,要减掉。

类似地,设 f_i 表示对应方案数, f_R 即为所求。

  • 对于圆点 i

    • szj<d,则 j 不能被删去。

    • szj>d,则 j 必须被删去。

    • 否则 szj=d。当 fj=0 时, j 不能被删去。否则 j 可以被删去。

    ss 为不能被删去的 szj 加上 i 本身贡献的 1 表示 i 的连通块大小最小值,设 pd 为可以被删去的 fj

    • ss>d+1fi=0

    • dssd+1,则 fi 加上 pd

    • ss=1,则 fi 加上对每个可以被删去的 szj=dpdfj 之和。因为 szj=dfj>0fj=1,故 pdfj 等于 pd 乘以 szj=dfj>0j 的数量 cnt

    注意第二、三个条件可以同时满足。

  • 对于方点 ifi 等于 fj


观察样例发现真正有值的 d 很少

k=0d|n

k=1 时,d=nωd=nω1,ω\Z,表示枚举分成几个连通块。

复杂度为 O(n1.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可以剪枝 
*/

参考

作者:lupengheyyds

出处:https://www.cnblogs.com/lupengheyyds/p/18702834

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   lupengheyyds  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
more_horiz
keyboard_arrow_up light_mode palette
选择主题
点击右上角即可分享
微信分享提示