HDU 6151 Party(状压DP+Hall定理)

256 篇文章 0 订阅
40 篇文章 1 订阅

Description

给出一个二部图,左边 n 个点,标记分别为a1,a2,...,an,右边 m 个点,标记分别为b1,b2,...,bm,给出一个 n×m 的矩阵表示二部图的边, 1 表示连边0表示不连边, q 次查询,每次查询给出一个t,在标记被 t 整除的点中选取一个非空子集,使得该子集中每一个点都属于原图中一条边,且这些边不相交,问满足条件的子集数

Input

第一行输入一整数T表示用例组数,每组用例首先输入三个整数 n,m,q 表示两边点的数量和查询数,之后输入一个 n×m 01 矩阵表示该二分图的边,然后输入 n 个整数a1,a2,...,an表示左边点的编号,然后输入 m 个整数b1,b2,...,bm表示右边点的编号,最后输入 q 个整数ti表示查询

(1T100,1n,m20,1q10,1ai,bi,ti109)

Output

对于每次查询,输出满足条件的子集数量

Sample Input

1
3 3 2
010
111
010
4 2 6
8 12 5
2 3

Sample Output

Case #1: 23 3

Solution

根据广义 Hall 引理,只需要从左边选取一个子集 X 使得其被一个覆盖盖住,从右边选取一个子集Y使得其被一个覆盖盖住,那么这两个子集组成的子集 V=X+Y 就可以被一个覆盖盖住,根据 Hall 定理,一个二分图中,一侧的点集 X 被一个覆盖盖住当且仅当X中任意 k 个点至少与另一侧的k个点相邻,用状压 DP 解决这个问题,预处理左侧点连向右侧点的状态 L[i] ,和右侧点连向左侧点的状态 R[i] ,对于一个查询 t ,求出两侧所有编号被t整除的点的状态 A,B ,下面只说求 A 的合法子集个数,即枚举A的一个子集 s ,假设其中有nums个点,要从 B 中找到至少nums个点与这些点相邻,以 dp[s] 表示取 A 的子集s是否满足条件,首先如果 s 的一个真子集不满足条件那么dp[s]=0, 如果 s 的真子集都满足条件,那么只需判断右侧B中是否有 nums 个点与 s 相邻,这个通过R[i]记录的状态可以判断,如果 B 侧第i个点和 s 状态中某一点相邻,即R[i]&s0则说明 s 满足条件,以此统计出A中合法子集的数量 ansl ,同理求出 B 中合法子集的数量ansr,答案即为 anslansr1 ,减一是减去两边都是空集的情况

Code

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<ctime>
using namespace std;
typedef long long ll;
typedef pair<int,int>P;
const int INF=0x3f3f3f3f,maxn=(1<<20)+5;
int T,n,m,q,a[22],b[22],L[maxn],R[maxn],dp[maxn];
char s[22];
int Solve(int *S,int n,int m,int A,int B)
{
    int ans=0,N=1<<n;
    for(int s=0;s<N;s++)
    {
        if((A&s)!=s)continue;
        dp[s]=1;
        int numa=0;
        for(int i=0;i<n;i++)
            if(s&(1<<i))
            {
                numa++,dp[s]&=dp[s^(1<<i)];
                if(!dp[s])break;
            }
        if(!dp[s])continue;
        int numb=0;
        for(int i=0;i<m;i++)
            if((B&(1<<i))&&(S[i]&s))numb++;
        if(numb<numa)dp[s]=0;
        if(dp[s])ans++;
    }
    return ans;
}
int main()
{
    int Case=1;
    scanf("%d",&T);
    while(T--)
    {
        memset(L,0,sizeof(L));
        memset(R,0,sizeof(R));
        scanf("%d%d%d",&n,&m,&q);
        for(int i=0;i<n;i++)
        {
            scanf("%s",s);
            for(int j=0;j<m;j++)
                if(s[j]=='1')L[i]|=(1<<j),R[j]|=(1<<i);
        }
        for(int i=0;i<n;i++)scanf("%d",&a[i]);
        for(int i=0;i<m;i++)scanf("%d",&b[i]);
        printf("Case #%d:",Case++);
        while(q--)
        {
            int t,A=0,B=0;
            scanf("%d",&t);
            for(int i=0;i<n;i++)
                if(a[i]%t==0)A|=(1<<i);
            for(int i=0;i<m;i++)
                if(b[i]%t==0)B|=(1<<i);
            int ansl=Solve(R,n,m,A,B);
            int ansr=Solve(L,m,n,B,A);
            printf(" %I64d",(ll)ansl*ansr-1);
        }
        printf("\n");
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值