True Liars POJ - 1417 (带权并查集+01背包)

题目链接

题意:
正义阵营只会说真话,邪恶阵营只会说假话

共有n句话,形如x y a
表示:x说y属于正义或邪恶(x可以等于y)

其中a为yes或no(yes表示正义,no表示邪恶)

题目保证不会出现矛盾的情况。

分析:
①如果a是yes,那么x y一定是同一个阵营,如果是no,一定是相反阵营
②据此我们可以用带权并查集维护1~p1+p2所有节点之间的相对关系,
0表示同阵营,1表示相反阵营。

最终维护得到若干个连通块。每个连通块分别有若干个正义/邪恶阵营,统计信息,存进数组bag[i][0] / bag[i][1] 当中 。同时给每个连通块标号
(表示以第i个连通块中,包括根节点在内和根节点同阵营的节点有bag[i][0]个,和根节点不同阵营的有bag[i][1]个)

③到这里问题就简化成:对于每个连通块,有2个阵营进行二选一,问所有的连通块选一遍之后,得到p1人数有多少种选法,如果只有1种,那么就有解,否则答案是no。
到这里就是一个朴素的01背包问题了,dp[i]j[j]表示前i个连通块选到j个人的选法个数。 转移方程如下:
在这里插入图片描述

最后再逆序判断对于每一个连通块,最优解是选择了哪一个阵营。细节见代码。

#include<iostream>
#include<cstdio>
#include<set>
#include<vector>
#include<map>
#include<algorithm>
#include<string.h>
using namespace std;
#define ll long long
#define ull unsigned long long 
const int maxn = 8e2+7;
const ll inf = 34359738370;
const int has = 99959;
//p1 p2 2个阵营 p1说真话 p2说假话 
//a b yes/no 表示a说b是p1/p2
//判断能否找出哪些说真话并输出
//分析:yes表示a b同阵营  no表示a b反阵营 用带权并查集最终处理得到若干个连通块 每个连通块有2个阵营
//01背包 dp[i][j]表示前i个连通块选中j个人的方案数
int fa[maxn],v[maxn];//v[i]==0 表示i 和fa[i]同阵营 1 反
int dp[maxn][305];//前i个连通块 选到j个人的方案数
int p1,p2,m;
int bag[maxn][2];//第i个连通块 第0/1阵营的人数 0表示与根节点同阵营
int num[maxn];//以i为根的连通块的编号
int cnt=0;
inline int find(int x)
{
    if(fa[x] == x) return x;
    int f=fa[x];
    fa[x]=find(fa[x]);
    v[x]=(v[x]+v[f])%2;
    return fa[x];
}
inline void hb(int x,int y,int k)
{
    int r1=find(x),r2=find(y);
    if(x == y) return ;
    fa[r1]=r2;
    v[r1]=(v[y]+k-v[x]+2)%2;
    return ;
}
void init()
{
    for(int i=0;i<maxn;i++) fa[i]=i;
    memset(dp,0,sizeof(dp));
    memset(v,0,sizeof(v));
    memset(num,0,sizeof(num));
    memset(bag,0,sizeof(bag));
    cnt=0;
}
void get_cnt()
{
    for(int i=1;i<=p1+p2;i++)
    {
        int f=find(i);
        if(!num[f]) num[f]=++cnt;
        bag[num[f]][v[i]]++;
    }
}
void dp_solve()
{
    dp[1][bag[1][0]]++;
    dp[1][bag[1][1]]++;//不能赋值为1 因为可能bag[1][0]==bag[1][1]
    for(int i=2;i<=cnt;i++)
    {
        //第i个阵营只能选0 / 1 两种情况 
        for(int j=0;j<=p1;j++)
        {
            if(j>=bag[i][0]) dp[i][j]=dp[i-1][j-bag[i][0]];//第i个阵营选0
            if(j>=bag[i][1]) dp[i][j]+=dp[i-1][j-bag[i][1]];//第i个阵营选1
        }
    }
}
int main()
{
   while(~scanf("%d %d %d",&m,&p1,&p2) && m+p1+p2)
   {
       init();
       char s[10];
       while(m--)
       {
           int a,b;
           scanf("%d %d %s",&a,&b,s);
           if(s[0]=='n') hb(a,b,1);
           else hb(a,b,0);
       }
       //得到若干个连通块
        get_cnt();
        dp_solve();//01背包
        if(dp[cnt][p1] == 1)//方案唯一 可以知道p1是哪些
        {
            //先逆推得到每个连通块是选了哪个阵营
            int chos[maxn],j=p1;
            fill(chos,chos+maxn,-1);
            for(int i=cnt;i>1;i--)//i==1的时候 j就是bag[1][0/1]的数目
            {
                if(1 == dp[i-1][j-bag[i][0]]) //逆推过程中都是等于1
                {
                    chos[i]=0;
                    j-=bag[i][0];
                }
                else //如果不是取的0  那就是取的1
                {
                    chos[i]=1;
                    j-=bag[i][1];
                }
            }
            if(j == bag[1][0]) chos[1]=0;
            else chos[1]=1;
            for(int i=1;i<=p1+p2;i++)
            {
                int f=find(i);
                if(chos[num[f]]==v[i]) printf("%d\n",i);
            }
            printf("end\n");
        }
        else printf("no\n");
   }
   return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值