BZOJ5287 HNOI2018毒瘤

Problem

BZOJ

Solution

顾名思义,如果不是暴力分比较多,的确是一道很毒瘤的题。。

题面中所谓的联通操作就是保证互斥的连边后,是一个连通图。我们从树的状态考虑起。不妨设f[x][1/0]表示子树x中x选/不选时合法的方案数。
容易得到转移方程 f [ x ] [ 1 ] = f [ s o n ] [ 0 ] , f [ x ] [ 0 ] = ( f [ s o n ] [ 0 ] + f [ s o n ] [ 1 ] )

那么对于加入的非树边应该怎么办呢,我们可以去掉环上的一边,使得它变成树,将这个边的作用看做约束条件,即x,y不能同时选中。注意到此时有合法的三种状态即(1,0),(0,1),(0,0),那么我们对每条非树边枚举状态再DP算贡献,时间复杂度为 O ( 3 m n + 1 n )

我们可以把状态合并成两个状态,改成枚举dfs序更小的点选不选,因为当它不选时,另一个点可以选也可以不选,合并起来转移即可做到 O ( 2 m n + 1 n ) 这个时候的暴力分已经很多了。

如果你大力推公式,你可以发现,除了非树边约束的两个点,其它的dp都可以预处理转移的系数。那么我们考虑建立一棵虚树,然后dfs暴力转移出系数,并把系数关系化成虚树上边的权值,然后就可以做了。时间复杂度是 s = m n + 1 , O ( n + s 2 s )

Code

#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int maxn=100100,mod=998244353;
inline int pls(int x,int y){return x+y>=mod?x+y-mod:x+y;}
template <typename Tp> inline void read(Tp &x)
{
    x=0;int f=0;char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9')) ch=getchar();
    if(ch=='-') f=1,ch=getchar();
    while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
    if(f) x=-x;
}
struct data{int v,nxt;}edge[maxn<<1];
struct factor{
    int x,y;
    factor(){}
    factor(int _x,int _y){x=_x;y=_y;}
    factor operator + (const factor &t){return factor(pls(x,t.x),pls(y,t.y));}
    factor operator * (const int t){return factor((ll)x*t%mod,(ll)y*t%mod);}
}k[maxn][2];
struct data2{int v,nxt;factor a,b;}ed[maxn];
int n,m,ep,edp,ans,dfc,cnt,head[maxn],h[maxn],dfn[maxn],mark[maxn],sz[maxn];
int vis[maxn],p[maxn][2],st[maxn][2],f[maxn][2];
pii e[20];
inline void insert(int u,int v){edge[++ep]=(data){v,head[u]};head[u]=ep;}
inline void insert(int u,int v,factor a,factor b)
{
    ed[++edp]=(data2){v,h[u],a,b};h[u]=edp;
}
int dfs(int x,int pre)
{
    dfn[x]=++dfc;
    for(int i=head[x];i;i=edge[i].nxt)
      if(edge[i].v^pre)
      {
        if(!dfn[edge[i].v]) sz[x]+=dfs(edge[i].v,x);//sz表示有几棵子树中有虚树节点
        else
        {
            mark[x]=1;//mark标记虚树节点
            if(dfn[edge[i].v]>dfn[x]) e[++cnt]=make_pair(x,edge[i].v);
        }
      }
    mark[x]|=(sz[x]>1);//这说明x是虚树上的一个lca
    return sz[x]||mark[x];
}
int build(int x)
{
    int pos=0,w,v;
    p[x][0]=p[x][1]=1;vis[x]=1;//p是自己的dp,k用于累加虚树边的系数
    for(int i=head[x];i;i=edge[i].nxt)
      if(!vis[edge[i].v])
      {
        v=edge[i].v;w=build(v);
        if(!w)
        {
            p[x][0]=(ll)p[x][0]*pls(p[v][0],p[v][1])%mod;
            p[x][1]=(ll)p[x][1]*p[v][0]%mod;
        }
        else if(mark[x]) insert(x,w,k[v][0]+k[v][1],k[v][0]);
        else k[x][0]=k[v][0]+k[v][1],k[x][1]=k[v][0],pos=w;//此时只会有一个虚树节点,否则mark[x]=1
      }
    if(mark[x]) k[x][0]=factor(1,0),k[x][1]=factor(0,1),pos=x;
    else k[x][0]=k[x][0]*p[x][0],k[x][1]=k[x][1]*p[x][1];
    return pos;//子树中深度最小的虚树节点
}
void input()
{
    int x,y;
    read(n);read(m);
    for(int i=1;i<=m;i++)
    {
        read(x);read(y);
        insert(x,y);insert(y,x);
    }
    dfs(1,0);mark[1]=1;build(1);
}
void dp(int x)
{
    int f0,f1;
    f[x][0]=st[x][1]?0:p[x][0];f[x][1]=st[x][0]?0:p[x][1];
    for(int i=h[x];i;i=ed[i].nxt)
    {
        dp(ed[i].v);f0=f[ed[i].v][0];f1=f[ed[i].v][1];
        f[x][0]=(ll)f[x][0]*pls((ll)ed[i].a.x*f0%mod,(ll)ed[i].a.y*f1%mod)%mod;
        f[x][1]=(ll)f[x][1]*pls((ll)ed[i].b.x*f0%mod,(ll)ed[i].b.y*f1%mod)%mod;
    }
}
int main()
{
    #ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    #endif
    input();
    int lim=1<<cnt;
    for(int i=0;i<lim;i++)
    {
        for(int j=1;j<=cnt;j++)
        {
            if(i&(1<<j-1)) st[e[j].first][1]=st[e[j].second][0]=1;
            else st[e[j].first][0]=1;
        }
        dp(1);
        ans=pls(ans,pls(f[1][0],f[1][1]));
        for(int j=1;j<=cnt;j++)
        {
            if(i&(1<<j-1)) st[e[j].first][1]=st[e[j].second][0]=0;
            else st[e[j].first][0]=0;
        }
    }
    printf("%d\n",ans);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/as_a_kid/article/details/80678823