Werewolf(狼人杀)DFS与回溯算法

回溯算法的一道例题

Werewolf(狼人杀)

  • 题目描述:
    Werewolf(狼人杀) is a game in which the players are partitioned into two parties: the werewolves and the human beings. Suppose that in a game,
    player #1 said: "Player #2 is a werewolf.";
    player #2 said: "Player #3 is a human.";
    player #3 said: "Player #4 is a werewolf.";
    player #4 said: "Player #5 is a human."; and
    player #5 said: "Player #4 is a human.".
    Given that there were 2 werewolves among them, at least one but not all the werewolves were lying, and there were exactly 2 liers. Can you point out the werewolves?

    Now you are asked to solve a harder vertion of this problem: given that there were N players, with M werewolves among them, at least one but not all the werewolves were lying, and there were exactly L liers. You are supposed to point out the werewolves.

    Input Specification:
    Each input file contains one test case. For each case, the first line gives three positive integer N (5 ≤ N ≤ 100), M and L (2 ≤ M < N, 1 ≤ L < N). Then N lines follow and the i-th line gives the statement of the i-th player (1 ≤ i ≤ N), which is represented by the index of the player with a positive sign for a human and a negative sign for a werewolf.

    Output Specification:
    If a solution exists, print in a line in descending order the indices of the M werewolves. The numbers must be separated by exactly one space with no extra spaces at the beginning or the end of the line. If there are more than one solution, you must output the largest solution sequence – that is, for two sequences A = { a[1], …, a[M] } and B = { b[1], …, b[M] }, if there exists 0 ≤ k < M such that a[i] = b[i] (i ≤ k) and a[k+1]>b[k+1], then A is said to be larger than B. In case there is no solution, simply print No Solution.

  • 这道题是一道经典的使用DFS深度优先搜索、外加回溯剪枝的例题,那么我们先来回顾一下回溯算法。

回溯算法


1. 回溯算法的步骤
  • 构建一棵GameTree
    何为GameTree?,其实就是由所有的可能性事件所构建而成的一棵k叉树,我们把找到解的过程分为N步,在每一步上都有k个方向可以进行选择,通过这样的方法构建出来的一棵包含各种解的结果树,不论是我们所需要的解还是我们不需要的解,均包含在其中。
  • 利用深度优先搜索遍历GameTree
    在构建完GameTree以后,剩下的工作就是利用深度优先搜索遍历然后得到我们所需要的解。

2. 回溯体现在哪里

先来观察一下回溯算法的代码模板

  •   bool Backtracking ( int i )
      {   Found = false;
          if ( i > N )
          return true; /* solved with (x1, …, xN) */
          for ( each xi in Si ) { 
          /* check if satisfies the restriction R */
              OK = Check((x1,, xi) , R ); /* pruning */
              if ( OK ) {
                  Count xi in;
                  Found = Backtracking( i+1 );
              if ( !Found )
                  Undo( i ); /* recover to (x1, …, xi-1) */
              }
              if ( Found ) break; 
          }
          return Found;
      }
    
  • 其中的Undo(i);就是回溯的操作,其具体的意义为:如果Found为假,意味着在当前条件或状态下递进到下一层中进行搜索时如果发现当前所在的搜索子树已经无法满足正确解的条件,就返回到上一层(刚刚所在的那层)中去,重新改变方向进行尝试,所以为了取消刚刚为了进入当前子树的操作,我们需要把刚刚的操作取消掉,而这也就是所谓回溯。
  • 说的通俗一点,就相当于你要去一个地方,走着走着发现在现在所在的地方是去不了目的地的,你只有退回上一个路口,换个方向再走看看能不能到达,但是前提是你得回到上一个路口的状态。而这也就是回溯算法的核心。

3.剪枝操作
  • 剪枝操作在DFS中十分常见,其主要目的是减小到达目的解的搜索量,其实就是如果把当前节点的状态加入解集合中,发现不可满足条件就直接改变方向,而不必再去搜索该节点的子树,这样就大大缩小了搜索量。(即代码中的check()函数即为剪枝函数)

该题解法


构建GameTree:
  • 我们可以对N个玩家进行讨论,每个人都可能是两种角色,即狼人或人类,这样可以构建一棵高度为N,每个节点度数为2的一棵二叉树,我们可以定义每个节点的左儿子代表该节点选择狼人状态,右儿子为该节点为人类状态,由于题目要求的是所有可行解中字典序最大的一组,所以我们从最大的玩家开始搜索,并且每次都把每个节点当做狼人的状态进行搜索(可以思考一下为什么)。

进行搜索:
  • 在搜索时我们根据GameTree的构建方法进行DFS,跳出递归搜索的条件为当前所记录的狼人数量num==M或者对玩家的搜索完毕,我们就对状态进行检验,复合要求的就作为解,实现检验的就是check函数,check函数的具体实现如下:
  •   int check(int *a,int num);
      int check(int *a,int num)
      {
          int humanlie=0;//记录人类的谎言数量
          int wolflie=0;//记录狼人的谎言数量
          int i;
          for(i=1;i<=N;i++)
          {
              if(playerstage[abs(statement[i])]*statement[i]<0)//如果某人谎言
              {
                  if(playerstage[i]>0)//判断本人身份
                  {
                      humanlie++;
                  }else{
                      wolflie++;
                  }
              }
          }
          if(wolflie>=1&&wolflie<M&&(wolflie+humanlie==L)&&(num==M))//判断符合题目约束与否
              return 1;
          else
              return 0;
      }
    
  • 在dfs中,我们首先对可否跳出递归进行判断,如果出现num==M或step<1的情况,直接跳出搜索,如果check通过,则找到一组解,直接让flag=1,意为成功找到解。话不多说,上代码:
  •   int dfs(int step,int num)
      {
          int isfound = 0;
          if((num==M)||step<1)//出现num==M或step<1的情况,直接跳出搜索
          {
              int haveans = check(playerstage,num);
              if(haveans)
              {
                  flag=1;
                  return 1;
              }else{
                  return 0;
              }
          }
          if(num>M)return 0;//已不会成为可行解
          for(int i=0;i<2;i++)
          {
              if(playerbook[step][i]==0)
              {
                  playerstage[step]=playerstage[step]-2+2*i;//为第step个玩家的身份赋值
                  playerbook[step][i]=1;//标记第step个玩家的身份
                  isfound = dfs(step-1,num+1-i);//递进搜索
                  if(!isfound)
                  {
                      playerbook[step][i]=0;
                      playerstage[step]=playerstage[step]+2-2*i;
                  }   //如果不可能成为可行解,回溯操作
              }  
              if(isfound)break;//找到解则退出
          }
          return isfound;
      }
    
  • 在整个过程中需要记录每位玩家的发言statement,每个玩家的身份数组playerstage,,一个用来标记玩家身份的book数组,但是需要是二维数组,playerbook[max][2]
  • 最终代码如下:
  •   #include<stdio.h>
      #include<stdlib.h>
      #include<math.h>
      #define max 102
      int N;
      int M;
      int L;
    
      int playerstage[max];
      int playerbook[max][2];
      int statement[max];
      int ans[max];
      int rear=0;
      int flag=0;
    
      int dfs(int step,int num);
      int check(int *a,int num);
      int main()
      {
          scanf("%d%d%d",&N,&M,&L);
          int i;
          for(i=1;i<=N;i++)
          {
              char c;
              scanf("%c%d",&c,&statement[i]);
              if(c=='-')statement[i] *= -1;
              playerstage[i]=1;
          }
          int stage = dfs(N,0);
          if(stage)
          {
              for(i=N;i>=1;i--)
              {
                  if(playerstage[i]==-1)
                  {
                      ans[rear++]=i;
                  }
              }         
              for(i=0;i<rear;i++)
              {   
                  if(i==0)printf("%d",ans[i]);
                  else printf(" %d",ans[i]);
              }
          }else{
              printf("No Solution\n");
          }
          return 0;
      }
      int dfs(int step,int num)
      {
          int isfound = 0;
          if((num==M)||step<1)
          {
              int haveans = check(playerstage,num);
              if(haveans)
              {
                  flag=1;
                  return 1;
              }else{
                  return 0;
              }
          }
          if(num>M)return 0;
          for(int i=0;i<2;i++)
          {
              if(playerbook[step][i]==0)
              {
                  playerstage[step]=playerstage[step]-2+2*i;
                  playerbook[step][i]=1;
                  isfound = dfs(step-1,num+1-i);
                  if(!isfound)
                  {
                      playerbook[step][i]=0;
                      playerstage[step]=playerstage[step]+2-2*i;
                  }//undo(i);
              }  
              if(isfound)break;
          }
          return isfound;
      }
      int check(int *a,int num)
      {
          int humanlie=0;
          int wolflie=0;
          int i;
          for(i=1;i<=N;i++)
          {
              if(playerstage[abs(statement[i])]*statement[i]<0)
              {
                  if(playerstage[i]>0)
                  {
                      humanlie++;
                  }else{
                      wolflie++;
                  }
              }
          }
          if(wolflie>=1&&wolflie<M&&(wolflie+humanlie==L)&&(num==M))
              return 1;
          else
              return 0;
      }
    

最后附上PTA结果:

在这里插入图片描述
看来结果还是很准确的()


评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值