1. 程式人生 > >【Codeforces】【圖論】【數量】【哈密頓迴路】Fake bullions (CodeForces - 804F)

【Codeforces】【圖論】【數量】【哈密頓迴路】Fake bullions (CodeForces - 804F)

題意

有n個黑幫(gang),每個黑幫有siz[i]個人,黑幫與黑幫之間有有向邊,並形成了一個競賽完全圖(即去除方向後正好為一個無向完全圖)。在很多年前,有一些人蔘與了一次大型搶劫,參與搶劫的人都獲得了一個真金條。
在這些年間,不同的黑幫之間進行了交易。具體過程是:
在時刻i,假如有一條邊是u->v,那麼u幫派中的i mod siz[u]號如果有金條(無論真假),並且v幫的i mod siz[v]沒有任何金條,那麼u中的這個人就會向v中的這個人一個假金條。

經過無數年的交易之後,各個幫派的金條擁有情況就確定了。他們開始向外面的世界傾銷金條。真金條一定能夠賣出去,而假金條可能買的出去,也可能賣不出去。定義一個幫派的力量值(strength)為成功賣出去的金條的數量。將所有的幫派按照力量值從大到小進行排序,從前a個幫派中選b個幫派。求所有可能的選出來的集合的個數。答案模1e9+7。
1<=b<=a<=n<=510^3
siz[i]<=2

10^6,\(\sum{siz[i]}<=2*10^6\)

輸入格式

第一行為n,a,b。
接下來是一個n*n的鄰接矩陣,無自環,(i,j)為1表示有邊i->j,且G[i][j]+G[j][i]=1.
接下來有n行,每行開頭是一個整數siz[i],接下來一個長為siz[i]的二進位制串,如果第j(0 <=j< siz[i])位是1表示第j個人最開始有真金條,否則最開始沒有金條。

輸出格式

一個整數表示答案。

思路

首先原題的題目打錯了,並不是0表示有金條,而是1表示有金條...雖然CF發了更正通知,但是題目並沒有更改過來...

這道題顯然是兩部分:一個是求最後的金條擁有情況,一個是根據幫派的最大銷售量mx[i]和最小銷售量mn[i]求答案。

第一部分

首先有幾個性質:

性質一

對於u->v這條邊而言,u中的i能夠對v中的j造成貢獻,當且僅當\(i \equiv j(mod\) \(gcd(siz[u],siz[v]))\)(具體的證明博主表示不想說了,大概就是歐幾里得搞一下)。

並由此能夠推出一個引理,就是對於一條路徑:u->p1->p2->p3->...->v,u中的i能夠對v中的j造成貢獻,當且僅當\(i \equiv j(mod\) \(gcd(siz[u],siz[p1],siz[p2]...siz[v]))\)(具體的證明等我看懂了再補吧...)
因此一個黑幫對另一個黑幫造成貢獻時,當然是經過的中間點越多越好。

性質二

對於一條鏈的情況上面已經說過了,不難想到對於一個強連通分量而言,假設其中所有的黑幫的siz的gcd為bgcd(block gcd),那麼其中的某一個黑幫的i號有金條當且僅當\(i \equiv j(mod\) \(bgcd)\),且j(另外一個黑幫的成員)有金條。

那麼我們就可以將整個強連通分量看做是一個整體(即一個黑幫),一共有bgcd個人,其中第i個人有金條當且僅當\(i \equiv j(mod\) \(bgcd)(j是這個塊中的某一個黑幫中的成員且j有金條)\)

這樣縮點之後我們就得到了一個有向無環的競賽完全圖。

性質三

假設我們處理完之後,怎麼通過一個強連通分量在外部得到的資訊來更新塊內的資訊呢??

這個還是比較好想的。
假設num[i]為第i個黑幫最後擁有的真假金條的總和。設它屬於第k個塊。這個塊的虛擬黑幫最後有blonum[k]個真假金條。那麼有:
\[num[i]=blonum[k]*\dfrac{siz[i]}{bgcd[k]}\]
這樣就可以只看最後的那個DAG了。

性質四

在得到的DAG中,一定存在著一條哈密頓迴路。因為假設不存在,那麼一定有大於等於2個點,它們的出度為0。但是由於這是一個競賽完全圖,兩兩之間都有連邊,因此這是不可能的。那麼就存在了一條哈密頓迴路。
其次,在這條哈密頓迴路上,從起點開始,出度遞減,入度遞增。
這是因為加入存在一個點的出度大於它之前的那個點,那麼它必然能夠指向之前那個點的一條入邊的來源點(因為當前這個點連向的點的數量大於前面的點的入邊的來源點的數量),這樣就形成環了(眾所周知,環是一個強連通分量)。
又因為入邊+出邊=n-1,所以對於入邊也是成立的。

性質五

這是和那個DAG有關的。沿著那條哈密頓迴路,其中的點i只能夠對之後的點造成貢獻,而不能夠先回到之前的點,因為這樣子也有環了。
又因為一個黑幫對另一個黑幫造成貢獻時,經過的中間點越多越好,所以只能夠沿著哈密頓迴路進行貢獻。
並且哈密頓迴路的起點到終點的新分配的強聯通編號是遞減的(從dfs樹上就能看出)。

做法

有了以上的性質之後,就好搞了。
首先進行Tarjan縮點,對於每一個黑幫的強連通分量建立一個虛擬黑幫。然後將第i個黑幫的金條資訊利用性質二放進去。
然後就得到了一個DAG。我們在這個DAG中找出一條哈密頓迴路。然後從起點開始,不斷地將當前強聯通分量的資訊向著下一個點進行轉移(記住每一次轉移的剩餘系都是在mod gcd(blo[i],blo[i-1]下的)),然後依次做下來,就是O(n^2)的了。
這樣子就得到了最終的金條分別情況了。心累

第二部分

假設i號黑幫的最大銷售量為mx[i],最低為mn[i]。那麼我們列舉i,並且考慮i號黑幫是選出來的b個黑幫中最差勁(力量值最低)的那個。
首先,對於mn[j]>mx[i]的j號,他肯定是要排在i號前面的,先統計起來為cnt1.然後就是mn[j]<=mx[i]的,由於i號是最差的為了防止重複,我們定義“最差”為:力量值為唯一最小或(不唯一最小,但是編號最小)。那麼我們就統計這些mn[j]<=mx[i]且(mx[j]>mx[j]||(mx[j]==mx[i]&&j>i))的數量cnt2.
然後我們考慮列舉b中有j個人來自cnt2,那麼來自cnt1的就有b-j-1個人。
然後對於j有一些限制:
1.j<=cnt2
2.j<=a-1-cnt1
3.j>=0
4.j+1+cnt1>=b
限制的具體原因就不提及的,請聰明的讀者自行死尻思考。
然後將C(cnt2,j)*C(cnt1,b-1-j)累加起來就好了。

程式碼

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define MAXN 10000
#define MAXL 2000000
#define MO 1000000007
using namespace std;
int n,a,b,c;
char str[MAXL+5];
vector<int> G[MAXN+5];
vector<int> per[MAXN+5];
int siz[MAXN+5],dfn[MAXN+5],low[MAXN+5],dcnt;
int blo[MAXN+5],bgcd[MAXN+5],blocnt;
int stk[MAXN+5],t;
int num[MAXN+5],mx[MAXN+5],mn[MAXN+5];
bool instk[MAXN+5];
int fact[MAXN+5],inv[MAXN+5];
int PowMod(int x,int y)
{
    int ret=1;
    while(y)
    {
        if(y&1)
            ret=1LL*ret*x%MO;
        x=1LL*x*x%MO;
        y>>=1;
    }
    return ret;
}
int gcd(int x,int y)
{
    if(y==0)
        return x;
    return gcd(y,x%y);
}
void InPut()
{
    scanf("%d %d %d",&n,&a,&b);
    for(int i=1;i<=n;i++)
    {
        scanf("%s",str+1);
        for(int j=1;j<=n;j++)
            if(str[j]=='1')
                G[i].push_back(j);
    }
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&siz[i]);
        per[i].resize(siz[i]+1);
        scanf("%s",str);
        for(int j=0;j<siz[i];j++)
            if(str[j]=='1')
                per[i][j]=1;
            else if(str[j]=='0')
                per[i][j]=0;
    }
}
void Tarjan(int u,int fa)
{
    dfn[u]=low[u]=++dcnt;
    instk[u]=true;stk[t++]=u;
    for(int i=0;i<(int)G[u].size();i++)
    {
        int v=G[u][i];
        if(dfn[v]==0)
        {
            Tarjan(v,u);
            low[u]=min(low[u],low[v]);
        }
        else if(instk[v]==true)
            low[u]=min(low[u],dfn[v]);
    }
    if(low[u]==dfn[u])
    {
        int fro=0;blocnt++;
        do
        {
            fro=stk[--t];instk[fro]=false;
            blo[fro]=blocnt;
            bgcd[blocnt]=gcd(bgcd[blocnt],siz[fro]);
        }while(fro!=u);
    }
}
void Part1()
{
    for(int i=1;i<=n;i++)
        if(dfn[i]==0)
            Tarjan(i,-1);
    for(int i=1;i<=blocnt;i++)
        per[i+n].resize(bgcd[i]+1);//預先設定大小
    for(int i=1;i<=n;i++)
        for(int j=0;j<siz[i];j++)
            per[blo[i]+n][j%bgcd[blo[i]]]|=per[i][j];//存到虛擬黑幫中
    int nw;
    for(int i=blocnt;i>1;i--)//自然形成了哈密頓迴路
    {
        nw=gcd(bgcd[i],bgcd[i-1]);//當前的剩餘系
        for(int j=0;j<bgcd[i];j++)
            if(per[i+n][j])
            {
                per[i-1+n][j%nw]|=1;//對下一個點進行更新
                num[i]++;//num就是總的金條數
            }
    }
    for(int j=0;j<bgcd[1];j++)
        num[1]+=per[1+n][j];//記得一定要統計block1的值
    for(int i=1;i<=n;i++)
    {
        mx[i]=mn[i]=0;
        mx[i]=1LL*num[blo[i]]*siz[i]/bgcd[blo[i]];根據性質三進行還原
        for(int j=0;j<siz[i];j++)
            mn[i]+=per[i][j];//直接利用初始資訊算
    }
}
void Prepare()
{
    fact[0]=1;
    for(int i=1;i<=MAXN;i++)
        fact[i]=1LL*fact[i-1]*i%MO;
    inv[MAXN]=PowMod(fact[MAXN],MO-2);
    for(int i=MAXN-1;i>=0;i--)
        inv[i]=1LL*inv[i+1]*(1LL*i+1LL)%MO;
}
inline int C(int x,int y)
{
    return 1LL*fact[x]*inv[y]%MO*inv[x-y]%MO;
}
int Part2()
{
    Prepare();
    int cnt1=0,cnt2=0,ret=0;
    for(int i=1;i<=n;i++)
    {
        cnt1=cnt2=0;
        for(int j=1;j<=n;j++)
        {
            if(i!=j&&mn[j]>mx[i])
                cnt1++;
            if(i!=j&&mn[j]<=mx[i]&&(mx[j]>mx[i]||(mx[j]==mx[i]&&j<i)))//避免重複
                cnt2++;
        }
        if(cnt1>a-1)
            continue;
        for(int j=min(b,min(a-1-cnt1,cnt2));j>=0&&j+1+cnt1>=b;j--)//一堆限制
            ret=(1LL*ret+1LL*C(cnt2,j)*C(cnt1,b-j-1)%MO)%MO;
    }
    return ret;
}
int main()
{
    InPut();
    Part1();
    int ans=Part2();
    printf("%d\n",ans);
    return 0;
}

值得一提的是,為了迎合題目的限制,這裡我使用了vector(賴皮)

可能有些地方並非原創,部分借鑑了網上的題解和CF的官方題解。
但是題解(除了性質一)絕對是詳細的。

祝您玩的愉快