0x40数据结构进阶(练习)3:真正的骗子(题解)

题意

链接

【题意】
 一个岛上存在着两种居民,一种是天神,一种是恶魔。
 天神永远都不会说假话,而恶魔永远都不会说真话。
 岛上的每一个成员都有一个整数编号(类似于身份证号,用以区分每个成员)。
 现在你拥有n次提问的机会,但是问题的内容只能是向其中一个居民询问另一个居民是否是天神,请你根据收集的回答判断各个居民的身份。 
【输入格式】
 输入包含多组测试用例。
 每组测试用例的第一行包含三个非负整数n,p1,p2,其中n是你可以提问的总次数,p1是天神的总数量,p2是恶魔的总数量。
 接下来n行每行包含两个整数xi,yi以及一个字符串ai,其中xi,yi是岛上居民的编号,你将向编号为xi的居民询问编号为yi的居民是否是天神,
ai是他的回答,如果ai为“yes”,表示他回答你“是”,如果ai为“no”,表示他回答你“不是”。
xi,yi可能相同,表示你问的是那个人自己是否为天神。
 当输入为占据一行的“0 0 0”时,表示输入终止。 
【输出格式】
 对于每组测试用例,如果询问得到的信息足以使你判断每个居民的身份,则将所有天神的编号输出,每个编号占一行,在输出结束后,在另起一行输出“end”,表示该用例输出结束。
 如果得到的信息不足以判断每个居民的身份,则输出“no”,输出同样占一行。 
【数据范围】
1≤xi,yi≤p1+p2,
 1≤n<1000,1≤p1,p2<300。 
【输入样例】
2 1 1
 1 2 no
 2 1 no
 3 2 1
 1 1 yes
 2 2 yes
 3 3 yes
 2 2 1
 1 2 yes
 2 3 no
 5 4 3
 1 2 yes
 1 3 no
 4 5 yes
 5 6 yes
 6 7 no
 0 0 0
【输出样例】
no
 no
 1
 2
 end
 3
 4
 5
 6
 end 

讲解

我们将这个列一列,我们发现问自己是不是天神永远说的是yes,而且这道题目不存在违反解的情况,那么我们就可以先处理出每个人之间的关系。

如果一个人说另外一个人是天神,那么说明这两个人是同种身份,如果说不是天神,那么他们是不同身份,这个列一列就知道了。

那么我们很快发现不就是带权并查集吗,不同的不同就是相同。

那么我们就可以处理出对于每个集合与祖先不同的有多少个数字,就可以推出每个集合内两种不同的身份分别有多少人,对于第 i i i个集合中两种不同的身份人数为 a i , b i a_i,b_i ai,bi,但是我们并不知道 a i a_i ai是哪种身份的,可以 a i a_i ai个人天神, b i b_i bi个人恶魔,反之也可以。

但是我们又发现他给定了天神数量,那么问题就变成了现在有 k k k组数字,每组有两个数字,让你在每组选择一个数字,使得这个数字等于 p 1 p1 p1,问方案数。

方案数为 1 1 1输出方案。

一种很简单的思路是设 f [ i ] [ j ] f[i][j] f[i][j]表示到第 i i i组已经有 j j j个天神了,类似背包一样瞎跑跑。但是我真的没想出来

于是我用了另外一种方法就是对于每组我们都假设取了 m a x ( a i , b i ) max(a_i,b_i) max(ai,bi),那么就有 n o w now now个天神了,很明显我们需要减去 m a x ( a i , b i ) − m i n ( a i , b i ) max(a_i,b_i)-min(a_i,b_i) max(ai,bi)min(ai,bi),那么我们就可以用每组的 m a x ( a i , b i ) − m i n ( a i , b i ) max(a_i,b_i)-min(a_i,b_i) max(ai,bi)min(ai,bi)去填 n o w − q 1 now-q1 nowq1,变成经典的01背包,然后计算方案数,很明显,当 a i = b i a_i=b_i ai=bi时,肯定方案数大于 1 1 1

这里我们把多种方案设为 2 2 2,否则方案不断积累会爆long long。

时间复杂度 O ( n 2 ) O(n^2) O(n2)

当然这种做法在随机数据下会特别的快。

//3ms猛如虎
#include<cstdio>
#include<cstring>
#define  N  1100
using  namespace  std;
int  fa[N],val[N]/*到根的权值*/,n,m,q,p;
int  cnt[N],siz[N],bbk[N];//表示当自己为父亲的时候,子树内总共有多少数字和自己不一样。 
int  findfa(int  x)
{
    if(fa[x]==x)return  x;
    int  y=findfa(fa[x]);val[x]=val[x]*val[fa[x]];fa[x]=y;//表示的就是转移 
    return  fa[x];
}
bool  dp[N],bk[N];int  tot[N],pre[N]/*状态*/,use[N];
int  main()
{
    while(scanf("%d%d%d",&m,&q,&p)!=EOF)
    {
        if(m==0  &&  q==0  &&  p==0)break;
        //初始化 
        n=q+p;
        for(int  i=1;i<=n;i++)fa[i]=i,val[i]=siz[i]=1,cnt[i]=0,bk[i]=0,bbk[i]=1;//永远都是跟自己相等的 
        memset(dp,0,sizeof(dp));dp[0]=0;
        memset(tot,0,sizeof(tot));
        //
        for(int  i=1;i<=m;i++)
        {
            int  x,y,z;char  st[20];scanf("%d%d%s",&x,&y,st+1);
            z=(st[1]=='y'?1:-1);
            int  tx=findfa(x),ty=findfa(y);
            if(tx!=ty)fa[tx]=ty,val[tx]=z*val[x]*val[y],siz[ty]+=siz[tx];/*表示他和fa的关系*/
        }
        for(int  i=1;i<=n;i++)
        {
            int  x=findfa(i);//因为我们要算出每个联通块里面有多少个为-1的,同时计算出每个点的祖先。 
            cnt[x]+=(val[i]==-1);
        }
        int  now=0;
        for(int  i=1;i<=n;i++)
        {
            if(fa[i]==i)
            {
                if(siz[i]-cnt[i]>cnt[i])cnt[i]=siz[i]-cnt[i],bbk[i]=-1/*表示现在的cnt是与fa相等的集合*/;
                now+=cnt[i];
            }
        }
         
        int  k=now-q;dp[0]=1;tot[0]=1;
        for(int  i=1;i<=n;i++)
        {
            if(fa[i]==i)
            {
                int  x=(cnt[i]<<1)-siz[i];
                for(int  j=k;j>=x;j--)
                {
                    if(dp[j-x]==1)
                    {
                        if(dp[j]==0)dp[j]=1,tot[j]=tot[j-x],pre[j]=j-x,use[j]=i;
                        else  tot[j]=2;//多种方案
                    }
                }
            }
        }
        if(tot[k]>1)printf("no\n");
        else
        {
            int  x=k;
            while(x)
            {
                bk[use[x]]=1;//标记是哪个块选的是小的集合
                x=pre[x];
            }
            for(int  i=1;i<=n;i++)
            {
                if((bk[fa[i]]==1  &&  val[i]*bbk[fa[i]]==1)  ||  (bk[fa[i]]==0  &&  val[i]*bbk[fa[i]]==-1))printf("%d\n",i);
            }
            printf("end\n");
        }
    }
    return  0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值