前段时间在洛谷3.0上刷到一个题,让本人挠头了一段时间,RT:

题目描述

已知 n 个整数 x1,x2,…,xn,以及一个整数 k(k<n)。从 n 个整数中任选 k 个整数相加,可分别得到一系列的和。例如当 n=4,k=3,4 个整数分别为 3,7,12,19 时,可得全部的组合与它们的和为:

3+7+12=22  3+7+19=29  7+12+19=38  3+12+19=34。

现在,要求你计算出和为素数共有多少种。

例如上例,只有一种的和为素数:3+7+19=29。


首先解决这个问题显然需要把输入的所有组合罗列出来,求和再判读素数就好啦。这篇文章主要是解决组合排列问题,所以判断素数这里就忽略啦o(^▽^)o,但对于我这个算法小白来说这个题可不太容易,如何用通俗易懂的方式理解呢?

在这里先向大家摘取《啊哈!算法》书中第四章第一节中用深度优先搜索解决全排列的方法。从书中的全排列例子,我们再自己推到排列,再由此推及到组合。

例·1、2、3的全排列是:

123、132、213、231、312、321。

求1、2、3的全排列:

for(a=1; a<=3; a++)for(b=1; b<=3; b++)for(c=1; c<=3; c++)if(a!=b && a!=c && b!=c)cout << a << b << c << endl;

这个很简单,三重循环嵌套就可以搞定,这里用for a循环来枚举第1位,用for b循环来枚举第2位,用for c循环来枚举第3位。再用一个if语句来判断,只有当a、b、c互不相等的时候才能输出。

OK,要是输入一个指定的数n,输出1~n的全排列,又该怎么办呢?这样的话循环的嵌套层数是个动态的值,似乎用循环不太好解决,下面让我们用深度优先搜索试一试。

例·输入一个数n,输出1~n的全排列。

我们将问题形象化,假如你手里有编号为1、2、3的3张扑克牌和编号为1、2、3的三个盒子。现在需要将这3张扑克牌分别放入3个盒子里,并且每个盒子有且只有一张扑克牌。总共有几种放法呢?

[box_1]   [box_2]  [box_3]  [box_4]

首先你来到了1号盒子面前,你现在手里有3张扑克牌,先放哪一张好呢?很显然三者都要尝试,那就姑且约定一个顺序:每次到一个盒子面前,都先放1号,再放2号,最后放3号。于是你在一号盒子里放入了编号为1的扑克牌。来到2号盒子面前,由于之前的1号扑克牌已经不在手中,按照之前约定的顺序,你将2号牌放到了2号盒子里。3号也是同样。你又往后走当你来到第4个盒子面前,诶,没有第四个盒子,其实我们不需要第4个盒子,因为手中的扑克牌已经放完了。

你发现了吗?当你走到第四个盒子前的时候,已经完成了一种排列,即“1 2 3”。然而并没有到此结束,产生了一种排列之后,你需要立即返回。现在你已经退到了3号盒子面前,你需要取回之前放在3号盒子中的扑克牌,再去尝试看看还能否放别的扑克牌,从而产生一个新的排列。于是你取回了3号牌,但由于你手中只有3号牌,你只能再次退回到2号盒子面前。

你回到2号盒子后,收回了2号牌。现在你的手中有2张牌了,分别是2号和3号牌。按照之前的约定,现在需要往2号盒子中放3号扑克牌(上次放的是2号牌)。放好后,你来到3号盒子面前,将手中仅剩的2号牌放入了3号盒子。又来到了4号盒子面前,当然没有4号盒子。此时又产生了一个新的排列“1 3 2”。

接下来按照刚才的步骤去模拟,便会依次生成所有排列:“2 1 3”、“2 3 1”、“3 1 2”和“3 2 1”。

明白了基本思路,到了用程序实现的时候了。首先解决最基本的问题:如何往小盒子中放入扑克牌?这里用一个for循环解决:

for(i = 1; i <= n; i++)
{a[step]=i;//将i号扑克牌放入到第step个盒子中
}

数组a用来表示小盒子,变量step表示当前正处在第step个小盒子面前。这里还需要考虑,如果一张扑克牌已经放到别的小盒子中了,那么此时就不能放入同样的扑克牌到当前小盒子中,因为此时手中已经没有这张牌了。因此还需要一个数组book来标记哪些牌已经使用过了。

for(i = 1; i <= n; i++)
{if(book[i] == 0)//表示i号扑克牌仍然在手上{a[step]= i;//将i号扑克牌放入到第step个盒子中book[i] = 1;//表示i号扑克牌已经不在手上了}
}

OK,现在已经处理完第step个小盒子了,接下来还要再往下走一步,继续处理第step+1个小盒子。那么如何处理呢?处理方法其实和我们刚刚处理第step个小盒子的方法是一样的。因此这里我们可以想到把刚刚处理第step个小盒子的代码封装成一个函数,如下:

void dfs(int step)//step表示现在站在第几个盒子面前
{for(i = 1; i <= n; i++){if(book[i] == 0)//表示i号扑克牌仍然在手上{a[step]= i;//将i号扑克牌放入到第step个盒子中book[i] = 1;//表示i号扑克牌已经不在手上了}}
}

把这个过程写成函数后,刚才的问题就好办了。在处理完第step个小盒子后,紧接着处理第step+1个小盒子,处理的方法就是dfs(step+1)。

void dfs(int step)//step表示现在站在第几个盒子面前
{for(i = 1; i <= n; i++){if(book[i] == 0)//表示i号扑克牌仍然在手上{a[step]= i;//将i号扑克牌放入到第step个盒子中book[i] = 1;//表示i号扑克牌已经不在手上了dfs(step+1);//这里通过函数的递归调用来实现(自己调用自己)book[i] = 0;//这里非常重要,一定要将刚才尝试的扑克牌收回,才能进行下一次尝试</strong>}   }
}

还剩下一个问题,就是什么时候该输出一个满足要求的序列呢?其实当我们处理到第n+1个小盒子的时候(即step等于n+1),那么说明前n个盒子都已经放好扑克牌了,这里就将1~n个小盒子中的扑克牌打印出来就好啦。要注意的是,打印完毕后一定要return,不然程序就无休止地运行下去了!

完整代码如下。

#include <iostream>
using namespace std;int a[10],book[10],n;//C语言全局变量值默认为0void dfs(int step)//step表示现在站在第几个盒子面前
{int i;if(step == n+1)//如果站在第n+1个盒子面前,则表示前n个盒子已经放好了扑克牌{//输出一种排列for(i=1;i<=n;i++)cout << a[i];cout << endl;return;//返回之前的一步(最近一次调用dfs函数的地方)}//站在第step个盒子的面前,按照1、2、3...n的顺序一一尝试for(i = 1; i <= n; i++){if(book[i] == 0)//表示i号扑克牌仍然在手上{a[step]= i;//将i号扑克牌放入到第step个盒子中book[i] = 1;//表示i号扑克牌已经不在手上了dfs(step+1);book[i] = 0;}    }return;
}int main()
{cin >> n;//由于数组大小的限制,输入的时候要注意为1~9之间的整数dfs(1);//首先站在1号小盒子面前return 0;}

这个核心代码不超过20行的例子,饱含深度优先搜索(Depth First Search,DFS)的基本模型。理解深度优先搜索的关键在于解决“当下该如何做”。至于“下一步如何做”则与“当下该如何做”是一样的。比如我们这里写的dfs(step)函数的主要功能就是解决当你在用step个盒子的时候你该怎么办。通常的方法就是把每一种可能都去尝试一遍(一般用for循环遍历)。当前这一步解决后便进入下一步dfs(step+1)。下一步的解决方法和当前这一步的解决方法是完全一样的。下面的代码就是深度优先搜索的基本模型。

void dfs(int step)
{判断边界尝试每一种可能 for(i=1; i<=n; i++){继续下一步 dfs(step+1);}返回
}

每一种尝试就是一种“扩展”。每次站在一个盒子面前的时候,其实都有n种扩展方法,但是并不是每种扩展都能够扩展成功。

下面,我们考虑一下n个数中选k个排列如何实现呢?例如从1、2、3中选2个排列的结果是:12、13、23

在这里k是小于等于n的,那么这就意味这每个箱子放一张扑克牌,所有的箱子都放上牌,手里的牌可能刚好全部用掉,也可能将会剩下来一些牌。也就是说,原先箱子数和牌数是正好相等的,而现在箱子数和牌数由用户指定,可以相等也可以不相等(相等时即为全排列,注意这里讨论的是排列,可以存在箱子数和牌数相等的情况,而本文章最先提到的的题目讨论的是组合,不考虑两者相等的情况,实际上两者相等时组合将只有一种)。对于每个盒子的处理办法其实和之前是一样的,变化的无非是两个。一个是排列的数字不是从1~n了,而是用户输入的一组数据(整数),这样的话,我们引入数组储存用户输入的数据,将原来的1~n作为数组下标即可;另一个是盒子和牌的数量关系变了,之前已经讨论过了。下面我们就看一下修改过的代码,请注意一下加粗的部分。

#include <iostream>
using namespace std;int a[10],book[10],b[10],n,k;//a表示小盒子,b表示手中的牌,牌上的数字由用户指定,n表示牌数,k表示盒子数void dfs(int step)//step表示现在站在第几个盒子面前
{int i;if(step == k+1)//如果站在第k+1个盒子面前,则表示前k个盒子已经放好了扑克牌{//输出一种排列for(i=1;i<=k;i++)//注意这里只输出cout << a[i];cout << endl;return;//返回之前的一步(最近一次调用dfs函数的地方)}//站在第step个盒子的面前,按照1、2、3...n的顺序一一尝试for(i = 1; i <= n; i++){if(book[i] == 0)//表示i号扑克牌仍然在手上{a[step]= b[i];//第i个扑克牌放入到第step个盒子中book[i] = 1;//表示i号扑克牌已经不在手上了dfs(step+1);//走到下一个小盒子面前book[i] = 0;//收回盒子中的牌}   }return;
}int main()
{cin >> n >> k;for(int i=1; i<=n; i++)cin >> b[i];dfs(1);//首先站在1号小盒子面前return 0;
}

现在排列问题也顺利解决啦,我们已经离成功越来越近啦!下面我们就来看一下组合到底怎么解决呢?

刚开始小白我也是伤透了脑筋,但通过比较排列和组合的关系,似乎有了一些头绪。让我们观察一下从5张牌中取3张的排列。

123 132 213 231 312 321
124 142 214 241 412 421
125 152 215 251 512 521
134 143 314 341 413 431
135 153 315 351 513 531
145 154 415 451 514 541
234 243 324 342 423 432
235 253 325 352 523 532
245 254 425 452 524 542
345 354 435 453 534 543

由表格我们可以看出第一列的10个组合即是我们所需要的组合,与排列相比我们需要考虑其重复度。在这种组合中,每一行的6个组合被视为一种情况。我们在设计程序时,就要考虑如何防止多余的情况产生呢?

让我们再观察一下第一列的10种组合。组合是“不考虑顺序的方法”,相对应排列是“考虑顺序的方法”。在组合中,你同样来到了第一个箱子面前,放入了1号牌,按照之前的逻辑,你又在第二个箱子里放入了2号牌,再到第三个箱子放了3号牌,来到4号箱子,实际上是发现没箱子了,然后得到了一个组合后再回到3号箱子...如此反复。不同的是什么呢?如果现在一号箱子中已经有了1号牌,二号箱子中也放入了2号牌,符合一号和二号箱子里的牌仍分别是1、2号牌的条件的所有情况都已经尝试过了,即123、124、125,那么接下来我们就不能再考虑当一号箱子中是1号牌时,在剩下的箱子中再放入2号牌的情况了。如果仍然要固执地使用2号牌呢?按照之前的约定,我们按照牌号从小到大的顺序来放牌,这时候二号箱子不能再放入2号牌,而应放入3号牌(因为在二号箱子里是2号牌的情况我们已经考虑过了,不过请注意在这一前提是一号箱子里一直都是1号牌)。这时我们又来到了三号箱子面前,按照从小号到大号的顺序放牌,我们应该放1号,但别忘了1号牌已经在1号箱子中用过啦!接下来我们把手头上还有的2号牌放进去。一个我们不愿意看到的情况发生了:产生了132组合!很明显它和我们之前已经得到的123组合重复啦!在此我们也可以理解为当三张牌中两张已经相同了,在剩余的牌中选择一张作为第三张牌,一定会出现1次组合重复的情况(如123和132)。所以说我们不能如此任性哦~这里有点绕,毕竟没有人真的会这么闲,来回倒腾纸牌玩(●◡●)。如果没读懂请好好理解一下哦,这里实际上是由排列到组合的一个关键。

假设你已经读懂了上段文字我在扯什么,请往下看(如果没读懂,我表示深深的歉意^-^)。

1-2-x的所有情况我们都考虑完后,我们就可以在排除2的情况下考虑所有1-3-x的情况。然后是1-4-x,但会出现1-5-x吗?不会啦~因为一共只有5张牌哦~而所有的牌此时都被标记为已用哦,即book为1,所以第三个箱子里是没有可以放的牌的!程序会直接跳过滴,我们就不用担心啦。此时带有1的所有组合我们都考虑完毕啦,于是给它对应的book标记上1。于是我们顺利退回到一号箱子,在一号箱子中放入了2号牌,接下来在不考虑1号的情况下排列出2-x-x的组合。思路已经和上面完全一样啦!我们将会得到234、235和245。得到的最后一个组合就是345了,上代码。

void dfs(int step)//step表示现在站在第几个盒子面前
{int i;if(step == k+1)//如果站在第k+1个盒子面前,则表示前k个盒子已经放好了扑克牌{//输出一种排列for(i=1;i<=k;i++)//注意这里只输出cout << a[i];cout << endl;return;//返回之前的一步(最近一次调用dfs函数的地方)}//站在第step个盒子的面前,按照1、2、3...n的顺序一一尝试for(i = 1; i <= n; i++){if(book[i] == 0)//表示i号扑克牌仍然在手上{a[step]= b[i];//第i个扑克牌放入到第step个盒子中book[i] = 1;//表示i号扑克牌已经不在手上了dfs(step+1);//走到下一个小盒子面前book[i] = 0;//收回盒子中的牌if(flag == 1){book[i] = 1;flag = 0;}}if(i == n)//表示在某个箱子上已经遍历完了从1到n号的所有扑克牌flag = 1;}return;
}

在for循环的最后我们加入了一个判断,若i与牌数n相等,则表示 在某个箱子上已经完成了所有的遍历,然后我们给它做个标记。由于该循环执行的条件是i<=n,这样做完标记后,i++,i已经大于n了,函数返回了,即退回到了上一个箱子(最近的dfs()),然后收回盒子中的牌。这时当标号为i的牌在我们手中时,我们我们给当前牌标上1,表示这种情况我们已经全部考虑完了,这张牌暂时不能再用了,注意是暂时哦。然后把flag再变回0,以便之后重复使用。

大功告成了吗?No!这样就造成了一个问题:比如说在6选4的组合中,得到1234、1235、1236后,3号牌被标记成了1后,就不会再得到1345、1346、1356这三个组合。所以我们需要将部分数字恢复成可用状态。我们用一个for循环消除从当前牌号的下一位到最后一张牌的标记,以便以后再次使用。我把代码拿出来,至于为什么这样实现相信大家能思考得出来。这里是终极代码。

#include <iostream>
using namespace std;int a[10],book[10],b[10],n,k,flag;//a表示小盒子,b表示手中的牌,牌上的数字由用户指定,n表示牌数,k表示盒子数void dfs(int step)//step表示现在站在第几个盒子面前
{int i;if(step == k+1)//如果站在第k+1个盒子面前,则表示前k个盒子已经放好了扑克牌{//输出一种排列for(i=1;i<=k;i++)//注意这里只输出cout << a[i];cout << endl;return;//返回之前的一步(最近一次调用dfs函数的地方)}//站在第step个盒子的面前,按照1、2、3...n的顺序一一尝试for(i = 1; i <= n; i++){if(book[i] == 0)//表示i号扑克牌仍然在手上{a[step]= b[i];//第i个扑克牌放入到第step个盒子中book[i] = 1;//表示i号扑克牌已经不在手上了dfs(step+1);//走到下一个小盒子面前book[i] = 0;//收回盒子中的牌if(flag == 1){book[i] = 1;flag = 0;for(int j=i+1; j<=n; j++)//消除从当前牌号的下一位到最后一张牌的标记,以便以后再次使用book[j] = 0;}}if(i == n)//表示在第step个箱子上已经遍历完了从1到n号的所有扑克牌flag = 1;}return;
}int main()
{cin >> n >> k;for(int i=1; i<=n; i++)cin >> b[i];dfs(1);//首先站在1号小盒子面前return 0;
}

我们用深度优先搜索的方法解决了这个问题,如果有对排列组合不太明白的,可以参考结城浩的《程序员的数学》中第5章排列组合中的内容。感觉《程序员的数学》和《啊哈!算法》都是很适合小白入门的书呢,在这里推荐给和我一样的小白们哦~

从排列到组合——深度优先搜索相关推荐

  1. 【算法】蓝桥杯dfs深度优先搜索之排列组合总结

    [导航] 上一篇文章 → <[算法]蓝桥杯dfs深度优先搜索之凑算式总结>   为了重申感谢之意,再次声明下文的大部分灵感均来自于[CSDN]梅森上校<JAVA版本:DFS算法题解两 ...

  2. 面试题 08.08. 有重复字符串的排列组合-快速排序+回溯深度优先搜索

    面试题 08.08. 有重复字符串的排列组合+快速排序加回溯深度优先搜索 有重复字符串的排列组合.编写一种方法,计算某字符串的所有排列组合. 示例1: 输入:S = "qqe" 输 ...

  3. 深度优先搜索(DFS) + DFS的应用:字符串的排列问题

    文章目录 深度优先搜索 基本思路: 举例: 穷举: 深度优先算法思路: DFS应用:全排列问题 深度优先搜索 深度优先搜索属于图算法的一种,是一个针对图和树的遍历算法.其过程简要来说是对每一个可能的分 ...

  4. 一文搞懂深度优先搜索、广度优先搜索(dfs、bfs)

    前言 你问一个人听过哪些算法,那么深度优先搜索(dfs)和宽度优先搜索(bfs)那肯定在其中,很多小老弟学会dfs和bfs就觉得好像懂算法了,无所不能,确实如此,学会dfs和bfs暴力搜索枚举确实利用 ...

  5. LeetCode算法总结-回溯法与深度优先搜索

    转载自  LeetCode算法总结-回溯法与深度优先搜索 回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标.但当探索到某一步时,发现原先选择并不优或达不到目标,就退 ...

  6. 深度优先搜索——选数(洛谷 P1036)

    今天的题是一道深度优先搜索的题 题目链接 选自洛谷(P1036) 是一道很经典的DFS问题 首先我们先看一下题目是怎么描述的,读完题目我会在后面给出详细的思路! 题目描述 已知 nn 个整数 x_1, ...

  7. 图的深度优先搜索(DFS)

    今天又复习了图的深度优先搜索,深深感觉了深搜就是外挂,岂止是外挂,简直就是外挂,动态规划做不出来的,深搜搜出来了,贪心贪不出来的深搜搜出来了,连并查集,拓扑排序做不出来的,深搜都做出来了,很遗憾以前深 ...

  8. 蓝桥杯笔记:DFS(深度优先搜索)解决问题

    DFS: 深度优先搜索: 访问当前的元素后,递归访问元素的邻接元素(找出所有下一步的可能元素,判断是否能访问),撤回当前元素访问 dfs(当前状态) {//递归终止条件if(当前状态==结束状态) { ...

  9. 【蓝桥杯C/C++】专题五:DFS深度优先搜索

    专题五:DFS深度优先搜索 目录 专题五:DFS深度优先搜索 前言 什么是回溯法 如何理解回溯法 回溯法解决的问题 回溯法模板 1 .回溯函数模板返回值以及参数 2. 回溯函数终止条件 3 .回溯搜索 ...

最新文章

  1. 步进电机加减速算法介绍和基于AVR446_Linear speed control of stepper motor的步进电机加减速实现
  2. Winform开发框架重构总结
  3. 全栈工程师之路(二)—— JavaScript(网页前端脚本语言)
  4. Flash 检测摄像头是否被占用
  5. Windows终端利器Cmder
  6. YbtOJ#732-斐波那契【特征方程,LCT】
  7. checkboxlist详细用法、checkboxlist用法、checkboxlist
  8. 十年磨一剑!腾讯QQ Linux版 2.0.0 Beta重磅发布!
  9. CSS命名规范--BEM
  10. 人脸对齐—3DDFA
  11. 点云纹理映射 matlab,一种点云模型纹理映射方法与系统与流程
  12. mm1排队系统仿真matlab实验报告,matlab仿真实验报告_mm1排队系统仿真matlab实验报告资料...
  13. python怎么关闭浏览器_/python里webbrowser怎么关闭游览器
  14. 星环科技:坚持国产自主路线,突破大数据的细分应用场景
  15. 计算机网络知识汇总(十万字超详细)
  16. Win7共享设置(xp访问win7的共享)
  17. 疯子坐飞机,第1个人疯了,随便坐下,第n个人能够坐在自己位置上的概率
  18. monkey自定义脚本
  19. isis学不到looback口的路由_干货 | ONU、机顶盒、路由器常见网络问题及处理方法...
  20. java发送邮件,多人单人发送,抄送,密送,附件

热门文章

  1. 教大家做一个酒驾杀手-酒精呼吸器
  2. 手机批发业务—产品选购
  3. 多线程系列教材 (五)- Java 演示多线程死锁
  4. 毕设路线—pytorch环境下的深度学习的高光谱图像分类问题
  5. java抽象语法树_抽象语法树(AST)
  6. MySQL之查看数据库(SHOW DATABASES语句)
  7. Java基础考试题,来测试下自己能拿多少分?提供答案
  8. python docx提取word中的目录及文本框中的文本
  9. 腾讯云布置SVN服务器
  10. 云发单详细使用视频教程