保鏢(hall定理&&集合動規&&優化)

細雨欣然發表於2017-03-08

【問題描述】

  蒟蒻YxuanwKeith想成為Philisweng的保鏢,但是作為預備隊員的保鏢智商肯定也不能低,至少要回答出下面這個問題:現在有一副若干條邊的二分圖,左邊有N個點ai,右邊有M個點bi,每個點都有一個權值wi。一個合法的子圖滿足以下兩個限制:

  1.選出的點權和大於等於限制t。
  2.並且可以從圖中選出若干條邊,使得二分圖中每個點最多被一條邊覆蓋,而選出的點要恰好被一條邊覆蓋。

  求總方案數。由於YxuanwKeith很弱,所以他找到你來回答這個問題。

【輸入格式】

  第一行包括兩個數N,M,分別表示左邊點的個數和右邊點的個數。
  第2行到第N+1行,第i行一個長度為M的字串Si,第j個字元如果是0表示左邊第i-1個點和右邊第j個點沒有連邊,如果是1表示有連邊。
  第N+2行,N個非負整數,第i個非負整數表示左邊第i個點的權值Wi
  第N+3行,M個非負整數,第i個非負整數表示右邊第i個點的權值vi
  第N+4行,一個正整數t,表示題目中的限制。

【輸出格式】

  輸出共一行,一個數,表示答案。

【輸入樣例】

3 3
010
111
010
1 2 3
8 5 13
21

【輸出樣例】

3

【樣例解釋】

樣例中的二分圖如圖所示:
  
子集{a1,a2,b2,b3}可以通過選擇邊集{(a1,b2),(a2,b3)}滿足條件二,並且權值和是21。
子集{a3,b2,b3}和{a2,a3,b2,b3}都可以通過選擇邊集{(a2,b2),(a3,b2)}滿足兩個條件。
其餘方案都不合法,所以答案是3。

【資料範圍】

對於30%的資料:N,M<=8
對於另外30%的資料:左邊的點和右邊的點兩兩間都有連邊。
對於100%的資料:N,M<=20,t<=4*10^8,wi,vi<=10^8

【來源】

https://jzoj.net

一道很神奇的題,開始用的網路流,結果全錯。
正解是集合動規,主要是要用hall定理(請自行百度)
根據hall定理我們可以用動規的思想來檢視一個集合是否成立,然後把權值和記錄下來,這樣我們就可以得到2個陣列。(不成立的權值應該是負無窮)
然後一個排序,在另一個上進行二分查詢就可以了。
一些必要的優化請自行檢視程式碼

完整程式碼如下:

#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=45;
const ll inf=20000000000000ll;

vector<int>g[maxn];
ll d[1<<21]={0},q[1<<21]={0};
int m,n,a[maxn];
bool vis[21];
ll b[maxn];
ll t;
char s[maxn];

ll work(int x)
{
    int num1=0,num2=0;
    memset(vis,0,sizeof(vis));
    for(int i=1;i<=n;i++) if((a[i]&x)==a[i])
    {
        num1++;
        for(int k=0;k<g[i].size();k++) if(!vis[g[i][k]])
        vis[g[i][k]]=1,num2++;
        if(num2>=n-i+num1) return 1;
    }
    return num1<=num2;
}
ll work2(int x)
{
    int num1=0,num2=0;
    memset(vis,0,sizeof(vis));
    for(int i=1;i<=m;i++) if((a[i]&x)==a[i])
    {
        num1++;
        for(int k=0;k<g[i+n].size();k++) if(!vis[g[i+n][k]])
        vis[g[i+n][k]]=1,num2++;
        if(num2>=m-i+num1) return 1;
    }
    return num1<=num2;
}
int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%s",s);
        for(int k=0;k<m;k++) if(s[k]=='1')
        {
            g[i].push_back(k+1);
            g[k+1+n].push_back(i);
        }
    }
    for(int i=1;i<=n+m;i++) cin>>b[i];
    cin>>t;
    a[1]=1;
    for(int i=2;i<=max(n,m);i++)a[i]=a[i-1]*2;
    int k1=1<<n,k2=1<<m;

    for(int i=1;i<k1;i++)
    {
        if(d[i]!=-inf&&!work(i)) //根據hall定理進行判斷
        for(int j=i;j<k1;j++) if((i&j)==i)
        d[j]=-inf;
    }
    for(int i=1;i<k2;i++)
    {
        if(q[i]!=-inf&&!work2(i))
        for(int j=i;j<k2;j++) if((i&j)==i)
        q[j]=-inf;
    }

    for(int i=1;i<k1;i++) if(d[i]!=-inf)
    for(int j=1;j<=n;j++) if((a[j]&i)==a[j])//算權值和的時候的優化
    {
        d[i]=b[j]+d[i-a[j]];
        break;
    }
    for(int i=1;i<k2;i++) if(q[i]!=-inf)
    for(int j=1;j<=m;j++) if((a[j]&i)==a[j])
    {
        q[i]=b[j+n]+q[i-a[j]];
        break;
    }

    sort(d,d+k1);
    ll ans=0;
    for(int i=0;i<k2;i++) if(q[i]!=-inf)//二分找答案
    {
        ans+=k1-(lower_bound(d,d+k1,t-q[i])-d);
    }
    cout<<ans;
    return 0;
}

相關文章