uoj37 【清华集训2014】主旋律 做题心得

uoj37 【清华集训2014】主旋律 做题心得

前言

好一个毒瘤数数题

以前集训课上,听老师讲过;但是当时完全没听懂,所以今天来补一补

思维过程

算是我做这个题的完整心路历程吧,前面的错误想法可以选择性的看,对正解影响不大

这个 \(n\) 非常小,只有 \(15\);但是这个 \(m\) 却非常大,最大200余。看来我们的算法需要和 \(n\) 有关,而且可以猜到是指数的,毕竟这个问题一看就很np

那我们考虑点。发现直接考虑,所有点都在一大块SCC里面,能考虑个寂寞。于是,容易想到反面考虑:算多少种边集使得图 强联通

不强联通,那可以考虑缩点,然后就会变成一堆SCC块,它们组成DAG。

发现DAG有一个性质,就是我们可以把点集划一刀,分成 \(A,B\) 两块,使得只存在 \(A\)\(B\) 的边。

然后我们设 \(f(S)\) 表示 \(S\) 点集强联通的方案,而 \(g(S)\) 是不强联通。那么 \(f(S)+g(S)=2^{E(S)}\),其中 \(E(S)\) 表示 \(S\) 内部边数。也就是说,这俩算一个就行

看来是 \(g\) 比较好算。根据上面的 “划一刀” 理论,机智的想到:我们枚举它一个子集 \(T\),然后

\(g(S)=\sum\limits_{T} 2^{E(T)}\times 2^{E(S/T)}\times 2^{E(T\rightarrow S/T)}\)

\(S/T\) 表示 \(T\)\(S\) 中的补集,因为我不知道正规写法怎么用 \(\LaTeX\) 打,就暂且这样写

\(E(A,B)\) 表示从 \(A\) 集合到 \(B\) 集合的边数,即 \(\# (u,v)|u\in A,v\in B\)

一看怎么这么简单,肯定不对。拿脑子一想,肯定得重,因为我们这个“切一刀”的方法不唯一,一个缩点的方案会被算很多遍

能不能去重呢?考虑序列的去重,我们任意划一刀肯定会重,但如果每次只切掉第一个位置,就不会重

再来考虑这个问题,同样是找一个 唯一 的 “开始” 位置。容易想到,就是那些入度为 \(0\) 的点。

那我们如果要计算 \(g\) ,可以枚举一个集合 \(T\),强制它作为"开始"点。然后搞一搞 \(T\rightarrow S/T\) 的边就行了

\(G(S)\) 表示,把点集 \(S\) 分成若干部分,使得每个部分是一个SCC,且SCC间两两没有边,方案数。

然后我们枚举 \(T\) 作为“开始”,内部划分方案数就是 \(G(T)\)。然后 \(T\rightarrow S/T\)\(S/T\) 内部,这些边都任选。得到式子:

\[g(S)=\sum\limits_{T} G(T)\times 2^{E(T\rightarrow S/T)+E(S/T)} \]

这回脑子转的久一点,但是又发现重了:因为我们没有保证 \(S/T\) 里面没有“开始”点!

我自己想就到这,想了好久发现不知道咋整,就去听老师讲回放了

我一看,嗷,原来我的容斥已经完全不熟练了,尽管看出来要容斥,也不知道咋搞系数了

假设最终缩成的DAG上有 \(k\) 个“开始”点,那么它的 \(2^k-1\) 个非空子集都会被我们枚举,并且把它算一次。也就是说,它一共会被算 \(2^k-1\) 次。

关于子集的去重,大家应该都很熟悉。假设 \(T\) 里面分了 \(k\) 块 SCC,乘一个 \((-1)^{k+1}\) 作为容斥系数就行了。

形象的说,就是:分一块方案数,-分两块,+分三块,-分四块...

为啥:

假设最后是 \(k\) 块,考虑它被算了几次:

对于 \(T\) 里面分了 \(k'\) 块的时候,会把这个东西算 \(\binom{k}{k'}\) 次。那一共被算:

\(\sum\limits_{k'=1}^{k} \binom{k}{k'}\times (-1)^{k'+1}\)

我们发现这玩意就是二项式定理的形式,瞎几把搞一搞,最后发现它就是 \(1\)

那现在似乎式子对了,设 \(G_k(T)\) 表示把 \(T\) 分成 \(k\) 块 SCC,SCC间没有边的方案数。则:

\[g(S)=\sum\limits_{T} 2^{E(T\rightarrow S/T)+E(S/T)}\times \sum\limits_{k} (-1)^{k+1}G_k(T) \]

我们发现我们并不需要知道具体的 \(G\) 值,只需要分奇偶就行了。那我们给它换个定义:\(G_{0/1}(T)\) 表示把 \(T\) 分成偶数/奇数块SCC,SCC间没有边的方案数。则:

\[g(S)=\sum\limits_{T} 2^{E(T\rightarrow S/T)+E(S/T)}\times (G_1(T)-G_0(T)) \]

然后我们就能得到 \(f\),然后有了 \(f\) 之后,\(G_0,G_1\) 也可以交叉着互推

\[f(S)=2^{E(S)}-G(S)\\G_0(S)=\sum\limits_{T} f(T)\times G_1(S/T)\\G_1(S)=\sum\limits_{T} f(T)\times G_0(S/T) \]

然后注意一些边界问题,就ok了。

代码实现啥的并不是本题的重点,直接参考代码即可

总结

  • 容斥的套路:奇数+,偶数-,解决子集算重的情况

  • 脑子里先搞一个大概想法,然后再往这个想法上面凑

    比如这个题,就可以先看出来是个容斥,然后想想咋容斥

代码

#include <bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
    #define N   15
    #define int long long
    #define mod 1000000007
    #define i2  500000004
    #define F(i,l,r) for(int i=l;i<=r;++i)
    #define D(i,r,l) for(int i=r;i>=l;--i)
    #define Fs(i,l,r,c) for(int i=l;i<=r;c)
    #define Ds(i,r,l,c) for(int i=r;i>=l;c)
    #define MEM(x,a) memset(x,a,sizeof(x))
    #define FK(x) MEM(x,0)
    #define Tra(i,u) for(int i=G.st(u),v=G.to(i);~i;i=G.nx(i),v=G.to(i))
    #define p_b push_back
    #define sz(a) ((int)a.size())
    #define all(a) a.begin(),a.end()
    #define iter(a,p) (a.begin()+p)
    int I() {char c=getchar(); int x=0; int f=1; while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar(); while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return ((f==1)?x:-x);}
    template <typename T> void Rd(T& arg){arg=I();}
    template <typename T,typename...Types> void Rd(T& arg,Types&...args){arg=I(); Rd(args...);}
    void RA(int *p,int n) {F(i,1,n) *p=I(),++p;}
    int n,m,U;
    bool e[N][N];
    bitset<300> so[1<<N],si[1<<N]; // 搞两个bitset, 快速支持E(A,B)的查询
    void Input()
    {
        n=I(),m=I(); U=(1<<n)-1;
        F(i,1,m)
        {
            int u=I()-1,v=I()-1;
            F(s,1,U)
            {
                if ((s>>u)&1) so[s][i]=1;
                else so[s][i]=0;

                if ((s>>v)&1) si[s][i]=1;
                else si[s][i]=0;
            }
        }
    }
    int lg[1<<N],pw2[300];
    void init()
    {
        pw2[0]=1;
        F(i,1,250) pw2[i]=pw2[i-1]*2%mod;
        F(i,0,n-1) lg[1<<i]=i;
        F(i,1,U) if (!lg[i]) lg[i]=lg[i-1];
    }
    int E(int S,int T)
    {
        int ans=(so[S]&si[T]).count();
        return ans;
    }
    int f[1<<N],g[1<<N],G[2][1<<N];
    void Sakuya()
    {
        init();
        g[0]=0;
        G[0][0]=1; G[1][0]=0;
        F(s,1,U)
        {
            for(int t=(s-1)&s;t;t=(t-1)&s)
            {
                g[s]+=pw2[E(t,s^t)+E(s^t,s^t)]*(G[1][t]-G[0][t]%mod+mod)%mod;
                g[s]%=mod;
                if (t&(s&(-s)))
                {
                    G[0][s]+=f[t]*G[1][s^t]%mod;
                    G[1][s]+=f[t]*G[0][s^t]%mod;
                    G[0][s]%=mod; G[1][s]%=mod;
                }
            }
            g[s]=(g[s]+G[1][s]+mod-G[0][s]%mod)%mod; 
            f[s]=(pw2[E(s,s)]-g[s]%mod+mod)%mod;
            G[1][s]=(G[1][s]+f[s])%mod;
        }
        printf("%lld\n",f[U]); 
    }
    void IsMyWife()
    {
        Input();
        Sakuya();
    }
}
#undef int //long long
int main()
{
    Flandre_Scarlet::IsMyWife();
    getchar();
    return 0;
}
posted @ 2021-07-31 21:36  Flandre-Zhu  阅读(128)  评论(0编辑  收藏  举报