• 张菖蒲点数计算--深搜(dfs)解决


    这是啥?

    先上链接

    exe附源码一份

    链接: https://pan.baidu.com/s/1VQ6zlxCDqsGMtEuMc7bynA 提取码: fx6z

    C版本

    Python版,GUI界面

    https://github.com/Wfo-12/zcp

    img

    问题来了,标题是什么意思

    (以下皆C语言版本,python版本源码在github,上面已给出)

    严教:简单的说就是 拿n张牌,从这n张牌里分出两组任意数量的牌,加起来总和相等的话就能给玩家

    前几天玩三国杀,朋友抽了个张菖蒲出来,一开始还好,就拿4张牌让我算,差不多看一眼就出答案了

    直到有一次他掏出8张牌,1分钟的技能持续时间,我硬是一种都没算出来。一气之下写了个程序来算

    把这个技能转换成算法题就是这个意思:

    img-3

    在游戏里追求牌多,所以同样的点数和就只输出了一种

    算法分析

    因为是写来在游戏里用的,以轻量级为主,我这里使用C语言编写

    1. 不多说,上来就是导入stdio
    #include<stdio.h>
    
    1. 这题第一眼看过去就能想到是一种排列组合。不过先别急,分析一下先。要在n张牌里分出两组牌,令第一组牌为A,第二组为B

      首先要搞清楚,并不是所有的牌都参与到分组中,也就是说可以有多余的牌,那么A与B的关系就不是简单的A+B=n了,所以A和B两组牌是分别进行的排列组合。

      解决排列组合最方便的算法应该就是深搜了,如果对深搜不熟悉或者没听过的话可以去CSDN搜索(dfs、深度优先搜索)。CSDN上有很多关于深搜的讲解

    2. 既然清楚了是两个排列组合,那么就决定使用两个深搜。先定义几个全局变量

      #define MAXLENTH 16
      // 全局变量
      int a[MAXLENTH] = { 0 }; // 输入数组
      int b[MAXLENTH] = { 0 }; // 0-未出现  1-第一组  2-第二组
      int c[10000] = { 0 }; // 记录sum
      int sum1 = 0; 
      int sum2 = 0;
      int n;
      int k = 0; // c数组下标
      int t = 1; // 深度1
      int t2 = 1; // 深度2
      
      // 函数声明
      void dfs(int i, int x);  // 深搜-第一组
      void dfs2(int i, int x);  // 深搜-第二组
      void pt(); // 输出
      
      • MAXLENTH -> 自定义的输入最大长度
      • a数组 -> 输入数据
      • b数组 -> a数组中各个数据的状态
      • c数组 -> 已出现过的牌堆和
      • sum1 -> 牌堆1的点数和
      • sum2 -> 牌堆2的点数和
      • n -> 输入数据个数
      • k -> 当前c数组长度
      • t / t2 -> 两个深搜函数的最大到达深度

      这里先不解释这些变量的作用,能看懂当我没说,我也觉得我写的好乱,应该是那时候太急了。

    3. 先来看输入函数pt

      void pt() {
          int x = 1;
          for (int i = 0; i < 2; i++) {
      	   for (int j = 0; j < n; j++) 
      	       if (b[j] == x) printf("%d ", a[j]);
      	   printf("
      ");
      	   x++;
      	}
          printf("
      ");
      }
      

      为什么i是0到1? (*`д´*) 这都看不出来是输出两组数吗

      j呢? j不就是遍历数组吗

      x是什么, x是状态,定义变量的时候注释了b数组的3中状态,x就是特定的一种状态,当x等于1的时候, 也就是i等于0的那次循环,输出b[j]等于1的a[j],也就是第一组数据

      然后x++,在输出第二组数

      应该不难理解,这里提一下就是先知道b[j]的3种状态分别表示什么

    4. 接下来看看深搜部分。理解题目之后应该清楚,在例子1 2 3 4 5 6中,31 2是一对,4 62 3 5也是一对,所以两组数各自的长度是未知的,可以是一个数、两个数、三个数……

      那么我们每次排列组合当然需要一个深度咯

      int t = 1; // 深度1
      int t2 = 1; // 深度2
      

      开头定义的两个全局变量t t2分别就是两个深搜的深度

      假设有n个数,那么开始t t2都是1,也就是只有一层深度,两组数都是只有一个数,分别都是遍历了整个a数组,而且不能重复

      还是看图直观吧

    第一个深搜一个一个慢慢遍历,深搜1每次指向一个数的时候深搜2都要跑一遍整个数组,每一次都判断总和是否相等,当然如果某个数已经被选中了就不选了(也就是b数组对应的下标不是0了)。

    否则的话就选中(更改b数组中的这个位置的数为1或者2,1就是第一组,2就是第二组)

    当然图里红线蓝线都经过了a[0],不是画错了,因为遍历可不管你这个位置b是多少,只有判断才管

    像这样蓝线走完一次之后,红色箭头像下走一格,然后蓝线继续老操作。

    是不是很简单,但是这才第一层

    如果我第二组要两个数呢

    还是画个图

    第一组还是一个数,所以还是老的做法,红色箭头每指向一个新的数,绿色箭头要依次遍历a数组,并取出这个数来作为第二组的其中一个数,然后进行第二次深搜,也就是蓝线

    这么说可能有点难理解,换个形式就是

    for(){
        for(){
    	
        }
    }
    

    这样的嵌套遍历,第一个for取一个数,第二for也取一个数

    这两个不同的数共同作为第二组数

    如果你不懂深搜可能要问了,那为什么不直接用for?深搜是什么?

    别急,这里还只是1-2(下面都这样表示两组数分别的数量,左边表示第一组的数,右边表示第二组)

    首先我们要明确一点,for循环是定死的,获取可以用break来减少循环,但你总不能增加循环吧。

    写了4个for运行能来5个for?

    其次,基数小的时候确实写for方便,但数量一大呢,例如我开头说的给我了8张牌,4-4甚至1-7的时候呢,写7个循环?不现实吧。

    所以我们要用深搜

    直接上代码

    void dfs(int i, int x) {
        if(x == t){
           return;
        }
        while (i < n) {
           if (b[i] == 0) {
              b[i] = 1;
    	     sum1 += a[i];
    	     dfs(i + 1, x + 1);
    	     b[i] = 0;
    	     sum1 -= a[i];
    	  }
    	  i++;
        }
    };
    

    这是一个最典型的深搜,当然我还没加一些别的条件

    仅仅是对一组数排列组合

    第一个if不用说了,就是深度(也就是递归跳出条件)

    i就是当前遍历的下标

    while中如果当前下标没被选择那么就选中它并且表明是1(第一组),同时使这一组数的和sum1加上这个数,然后继续调用自身进行搜索,出来之后也就是要换目标了,于是让这个位置的b重置为0,表示未选中,再让sum1减去这个数

    这就是个简单的排列组合,所以第二组的深搜也同样可以调用这个模块

    void dfs(int i, int x) {
        if (x == t) {
            for (int j = 1; j <= n - t; j++) {
                sum2 = 0;
                t2 = j;
                dfs2(0, 0);
           }
       return;
    }
    	
    // 模块
    while (i < n) {
        if (b[i] == 0) {
            b[i] = 1;
            sum1 += a[i];
            dfs(i + 1, x + 1);
            b[i] = 0;
            sum1 -= a[i];
        }
        i++;
    }
    };
    
    
    void dfs2(int i, int x) {
        if (x == t2) {
            if (sum1 == sum2) {
                int m;
                for (m = 0; m <= k; m++) 
                    if (c[m] == sum1) break;
                if (m >= k) {
                    c[k] = sum1;
                    k++;
                    printf("第%d种
    ", k);
                    pt();
                }
        }
        return;
    }
    	
    // 模块
    while (i < n) {
        if (b[i] == 0) {
            b[i] = 2;
            sum2 += a[i];
            dfs2(i + 1, x + 1);
            b[i] = 0;
            sum2 -= a[i];
        }
        i++;
    }
    	
    }
    

    上面我已经把相同的模块标注出来了,要注意的是里面的部分变量要修改成对应的变量

    下面来看他们不同的部分

    dfs ( 第一组 ) 中:

    if (x == t) {
        for (int j = 1; j <= n - t; j++) {
            sum2 = 0;
            t2 = j;
            dfs2(0, 0);
        }
        return;
    }
    

    x 等于t时也就是到达当前最大深度时执行这些代码

    for循环从1到n-tn-t就是剩下几个数,所以第二个深搜的深度可以从1到n-t,每一次循环重置sum2并且改变t2,调用第二组的深搜。

    应该不难理解,就是以第一组为主,第一组每找到一种排列组合,就要让第二组找出他的所有排列组合情况

    dfs2 ( 第二组 ) 中:

    if (x == t2) {
        if (sum1 == sum2) {
            int m;
            for (m = 0; m <= k; m++) 
                if (c[m] == sum1) break;
           if (m >= k) {
                c[k] = sum1;
                k++;
                printf("第%d种
    ", k);
                pt();
            }
        }
    return;
    }
    

    x等于t2,第二组的深搜到达当前最大深度,要知道我们前置条件就是第一组已经到达最大深度,所以这时候就可以开始比较了。

    如果两个sum相等的话,首先当然要遍历c数组看看有没有这个和,如果有就直接跳过了,没有的话继续执行下面的代码。

    注意k是全局变量,我们不动他的时候他就是个定值,用来表示c数组的长度在合适不过了

    话题回来,如果还没有过这个和,那么就向c数组中添加这个值,并且输出。

    1. 两个函数部分就到这里结束了,细心的话能发现目前为止我们的第一组始终只有一个数,因为我们的t没变过

      这个实现方法很多,我是写在主函数中,所以放到了最后

      int main() {
          while(1){
              printf("输入n ( 0结束 ): ");
              scanf("%d", &n);
              if (!n) break;
              printf("输入数字序列:");
              for (int i = 0; i < n; i++) {
                  scanf("%d", &a[i]);
              }
              /************************/
              for (int i = 1; i <= n; i++) {
                  sum1 = 0;
                  t = i;
                  dfs(0, 0);
              }
              /************************/
      		// 初始化
              for (int i = 0; i <= k; i++) {
                  c[i] = 0;
                  k = 0;
              }
              for (int i = 0; i <= n; i++) {
                  a[i] = 0;
                  b[i] = 0;
              }
          }
      }
      

      *号包围起来的部分就是不断的改变t的值来使第一组数的数量发生变化,不多讲了

      其他部分的代码应该也不难懂。

      讲解就到这了,下面附上完整代码

    完整代码

    #include<stdio.h>
    int a[16] = { 0 }; // 输入数组
    int b[16] = { 0 }; // 0-未出现  1-第一组  2-第二组
    int c[10000] = { 0 }; // 记录sum
    int sum1 = 0;
    int sum2 = 0;
    int n;
    int k = 0; // c数组下标
    int t = 1; // 深度1
    int t2 = 1; // 深度2
    
    void dfs(int i, int x);  // 深搜-第一组
    void dfs2(int i, int x);  // 深搜-第二组
    void pt(); // 输出
    
    int main() {
           while(1){
               printf("输入n ( 0结束 ): ");
               scanf("%d", &n);
               if (!n) break;
               printf("输入数字序列:");
               for (int i = 0; i < n; i++) {
                   scanf("%d", &a[i]);
               }
               for (int i = 1; i <= n; i++) {
                   sum1 = 0;
                   t = i;
                   dfs(0, 0);
               }
       		// 初始化
               for (int i = 0; i <= k; i++) {
                   c[i] = 0;
                   k = 0;
               }
               for (int i = 0; i <= n; i++) {
                   a[i] = 0;
                   b[i] = 0;
               }
           }
    }
    void dfs(int i, int x) {
       if (x == t) {
           for (int j = 1; j <= n - t; j++) {
               sum2 = 0;
               t2 = j;
               dfs2(0, 0);
           }
           return;
       }
       while (i < n) {
           if (b[i] == 0) {
               b[i] = 1;
               sum1 += a[i];
               dfs(i + 1, x + 1);
               b[i] = 0;
               sum1 -= a[i];
           }
           i++;
       }
    };
    void dfs2(int i, int x) {
        if (x == t2) {
    	if (sum1 == sum2) {
    	    int m;
    	    for (m = 0; m <= k; m++) 
    		if (c[m] == sum1) break;
    	    if (m >= k) {
    	        c[k] = sum1;
    		k++;
    		printf("第%d种
    ", k);
    		pt();
    	    }
    	}
    	return;
        }
        while (i < n) {
            if (b[i] == 0) {
    	    b[i] = 2;
    	    sum2 += a[i];
    	    dfs2(i + 1, x + 1);
    	    b[i] = 0;
    	    sum2 -= a[i];
    	}
    	i++;
        }
    }
    void pt() {
        int x = 1;
            for (int i = 0; i < 2; i++) {
                for (int j = 0; j < n; j++) 
    		if (b[j] == x) printf("%d ", a[j]);
    	    printf("
    ");
    	    x++;
            }
        printf("
    ");
    }
    
    
  • 相关阅读:
    QT信号槽传递自定义结构体
    Qt5MVC模式(一)
    深层次理解MVC
    代码重构与单元测试——测试项目(二)
    代码重构与单元测试(一)
    在Visual Studio 中使用git——同步到远程服务器-下(十二)
    在Visual Studio 中使用git——同步到远程服务器-上(十一)
    在Visual Studio 中使用git——标记(Tag)管理(十)
    在Visual Studio 中使用git——分支管理-下(九)
    在Visual Studio 中使用git——分支管理-上(八)
  • 原文地址:https://www.cnblogs.com/jvav/p/13546360.html
Copyright © 2020-2023  润新知