保镖(hall定理&&集合动规&&优化)

【问题描述】

  蒟蒻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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值