zoukankan      html  css  js  c++  java
  • 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)=sumlimits_{T} 2^{E(T)} imes 2^{E(S/T)} imes 2^{E(T ightarrow S/T)})

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

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

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

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

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

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

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

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

    [g(S)=sumlimits_{T} G(T) imes 2^{E(T ightarrow S/T)+E(S/T)} ]

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

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

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

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

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

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

    为啥:

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

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

    (sumlimits_{k'=1}^{k} inom{k}{k'} imes (-1)^{k'+1})

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

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

    [g(S)=sumlimits_{T} 2^{E(T ightarrow S/T)+E(S/T)} imes sumlimits_{k} (-1)^{k+1}G_k(T) ]

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

    [g(S)=sumlimits_{T} 2^{E(T ightarrow S/T)+E(S/T)} imes (G_1(T)-G_0(T)) ]

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

    [f(S)=2^{E(S)}-G(S)\G_0(S)=sumlimits_{T} f(T) imes G_1(S/T)\G_1(S)=sumlimits_{T} f(T) imes 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
    ",f[U]); 
        }
        void IsMyWife()
        {
            Input();
            Sakuya();
        }
    }
    #undef int //long long
    int main()
    {
        Flandre_Scarlet::IsMyWife();
        getchar();
        return 0;
    }
    
  • 相关阅读:
    【解决】Word 在试图打开文件时遇到错误 请尝试下列方法:* xxx * xxx * xxx
    【开源】简单4步搞定QQ登录,无需什么代码功底【无语言界限】
    [经验]无线鼠标和无线键盘真的不能用了?——雷柏的重生之路~
    Git技巧:右键菜单怎么去除?
    07.GitHub实战系列~7.Git之VS2013团队开发(如果不想了解git命令直接学这篇即可)
    Git异常:fatal: V1.0 cannot be resolved to branch.
    06.GitHub实战系列~6.过滤器过滤掉的文件如何上传
    Git异常:Cannot delete the branch 'test1' which you are currently on
    ElasticSearch查询 第四篇:匹配查询(Match)
    字符串的长度,是字符数量,还是字节数量?
  • 原文地址:https://www.cnblogs.com/LightningUZ/p/15085344.html
Copyright © 2011-2022 走看看