bzoj3812 主旋律(容斥原理+状压dp)

本文介绍了一种计算图中强联通子图数量的方法。通过将问题转化为计算不强联通子图的数量,利用容斥原理和动态规划技巧,最终在O(3^n)的时间复杂度内求解。代码实现部分详细展示了关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接

分析:

dada的题解

有多少个边的子集删去之后整个图仍然强联通=>整个图有多少个强联通子图
强联通子图的个数比较难计算,我们考虑进行进一步的转化:
强联通子图的个数=>子图个数-不强联通的子图个数

不强联通的子图有什么特点呢?将强连通分量缩点后会形成一个节点数>=2的DAG
那么我们就考虑这个子问题:一些点构成 DAG 的方案数

设原图为 G G ,点集为V,边集为 E E ,子集的点集为T

一个DAG肯定是由一些没有出度的点组成的,我们可以枚举这些点组成的集合 T T
那么VT内的边和 VT V − T T T 的边随便要不要都是可以的,但是这样有可能会出现重复的情况
因为在枚举T的时候这样计算不能保证 VT V − T 内没有不存在出度的点,也就是说两个点 u,v u , v T=u T = u 时会计算一次没有出度的点是 u,v u , v 的情况, T=v T = v 时也会计算一次,因此需要用容斥原理

f(S) f ( S ) 表示点集 S S 构成的DAG的方案数,那么可以得到转移

f(S)=TS,T(1)|T|12ways(ST,T)+h(ST)f(ST)

这里 ways(ST,T) w a y s ( S − T , T ) 表示 ST S − T T T 的边数
h(ST)表示 ST S − T 中的边数

然后这样由于要枚举强连通分量,复杂度十分高,这题肯定没办法过

换个角度,可以枚举所有没有出度的强连通分量缩成的点的集合 T T
根据上面的容斥,如果T内的点组成奇数个强连通分量,那么对答案的贡献将是1,如果是偶数个那就是−1

维护 g(t) g ( t ) 表示集合的点缩点后成为奇数个彼此没有边的点的方案数
c(t) c ( t ) 表示集合的点缩点后成为偶数个彼此没有边的点的方案数
重新定义 f(t) f ( t ) 表示 t t 是强联通子图的方案数
(奇数=偶数+1,偶数=奇数+1)

g(t)=jt,jf(j)c(tj)

c(t)=jt,jf(j)g(tj) c ( t ) = ∑ j ⊆ t , j ≠ ∅ f ( j ) ∗ g ( t − j )

由此得到递推式:

f(S)=2h(S)TS,T(c(T)g(T))2ways(ST,T)+h(ST) f ( S ) = 2 h ( S ) − ∑ T ⊆ S , T ≠ ∅ ( c ( T ) − g ( T ) ) ∗ 2 w a y s ( S − T , T ) + h ( S − T )

关于 ways w a y s h h 的计算可以在计算f的时候顺便计算,这样就可以在 O(3n) O ( 3 n ) 时间内计算出答案了


这道题连理解起来都有点困难,所以还是看看代码吧

for (int i=0;i<(1<<n);i++)
    for (int j=0;j<n;j++) {
        int t=i&(-i);
        cnt[j][i]=(cnt[j][t]+cnt[j][i-t])%p;
    }

一开始我们把边都存储成这个形式: cnt[i][j],ij c n t [ i ] [ j ] , i 点 的 后 继 状 态 为 j
在这个循环中,就是在维护 i i 点的后继状态j中有多少个1
i&(-i) 就是把状态 i i 分成了两个子集
举个小例子:

i=             i&(-i)=
    4   (100)           4   (100)
    5   (101)           1   (1)
    12  (1100)          4   (100)
    14  (1110)          2   (10)
for (int i=0;i<(1<<n);i++) 
    for (int j=0;j<n;j++) 
        if (i&(1<<j)) h[i]=(h[i]+cnt[j][i])%p;

h表示点集 i i 内的边数

dp卡着式子来就好了

f(S)=2h(S)TS,T(c(T)g(T))2ways(ST,T)+h(ST)

for (int i=0;i<(1<<n);i++) {                        //枚举点集 S
    if (i==(i&(-i))) {
        f[i]=g[i]=1;
        continue;
    }
    f[i]=mi[h[i]];                                  //2^h(S)
    for (int j=i&(i-1);j;j=i&(j-1)) {               //枚举S的子集 T
        int t=i-j;                                  //t=S-T
        int tmp=0;
        for (int k=0;k<n;k++) 
            if (t&(1<<k)) tmp=(tmp+cnt[k][i])%p;    //way(S-T,S)
        f[i]=(f[i]+mi[tmp]*(c[j]-g[j]))%p;          //递推式 2^way(S-T,S)*(c(T)-g(T))
        if (j&(i&(-i))) {
            c[i]=(c[i]+f[j]*g[t])%p;
            g[i]=(g[i]+f[j]*c[t])%p;
        } 
    }
    f[i]=(f[i]+c[i])%p;                   //按照递推式维护g,f,h
    f[i]=(f[i]-g[i])%p;
    g[i]=(g[i]+f[i])%p;
}

#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long long

using namespace std;

const ll p=1e9+7;
const int N=16;
ll cnt[N][1<<16],mi[405],h[1<<16],g[1<<16],c[1<<16],f[1<<16];
int n,m;

int main() 
{
    scanf("%d%d",&n,&m);
    mi[0]=1;
    for (int i=1;i<=300;i++) mi[i]=mi[i-1]*2%p;
    for (int i=1;i<=m;i++) {
        int x,y;
        scanf("%d%d",&x,&y);
        x--; y--; cnt[x][1<<y]++;        //后继结点 
    }
    for (int i=0;i<(1<<n);i++)
        for (int j=0;j<n;j++) {
            int t=i&(-i);                //子集
            cnt[j][i]=(cnt[j][t]+cnt[j][i-t])%p;
        } 
    for (int i=0;i<(1<<n);i++) 
        for (int j=0;j<n;j++) 
            if (i&(1<<j)) h[i]=(h[i]+cnt[j][i])%p;

    c[0]=1;
    for (int i=0;i<(1<<n);i++) {
        if (i==(i&(-i))) {
            f[i]=g[i]=1;
            continue;
        }
        f[i]=mi[h[i]];
        for (int j=i&(i-1);j;j=i&(j-1)) {     //枚举子集
            int t=i-j;
            int tmp=0;
            for (int k=0;k<n;k++) 
                if (t&(1<<k)) tmp=(tmp+cnt[k][i])%p;
            f[i]=(f[i]+mi[tmp]*(c[j]-g[j]))%p;
            if (j&(i&(-i))) {
                c[i]=(c[i]+f[j]*g[t])%p;
                g[i]=(g[i]+f[j]*c[t])%p;
            } 
        }
        f[i]=(f[i]+c[i])%p;
        f[i]=(f[i]-g[i])%p;
        g[i]=(g[i]+f[i])%p;
    }
    int t=(1<<n)-1;
    f[t]=(f[t]%p+p)%p;
    printf("%lld\n",f[t]);

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值