从排列到组合——深度优先搜索

前段时间在洛谷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语言全局变量值默认为0

void 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章排列组合中的内容。感觉《程序员的数学》和《啊哈!算法》都是很适合小白入门的书呢,在这里推荐给和我一样的小白们哦~

时间: 2024-10-15 06:00:46

从排列到组合——深度优先搜索的相关文章

有关搜索和深度优先搜索的浅陋见解

祝食用愉快XD 题目链接 (是一道胡乱出的题) U56815 来走迷宫鸭! 解题思路 深度优先搜索,如果能不碰墙地到达右下角的出口,就把旗子立起来表示找到了出口. 什么?你没听过深度优先搜索 没事,且听我道来. 什么是搜索?如何搜索? 简单来说,搜索就是一种特殊的(递归的)枚举.从一种可行的方案进行扩展,并去看这个扩展出来的东西符不符合现有规则.能不能继续扩展. 可是你讲理论我也听不懂啊 那,深度优先搜索又是什么呢? 拿走迷宫这事儿说起.如果你玩过\(MC\),或者无论从哪个去掉了解走迷宫的时候

深度优先搜索(dfs)

关于深度优先搜索的总结: 1 dfs 的基本结构:  void dfs(int x){ if( x 超出边界){ return ; }else{ for(遍历){ if(未访问过){ 访问         ; 打上标记    ; dfs(x + 1) ; 去掉标记    ; //极易忘记 } } } return; } 2 用dfs求全排列: 本来好好的,结果sizeof(pointer) 就完蛋了.神秘的内存错误,而且还能正常的跑出一个不正常的结果出来. 想了解sizeof这个小妖精的看这里

深度优先搜索思想初体验

1.求数字 1~n 的全排列 import java.util.Scanner ; public class Permutation{ //求数字 1~n 的全排列: int[] array ; int[] book ; int n ; public void permutation(int step){ // 深度优先搜索思想: if (step==n+1) { for (int i=1;i<=n;i++) { System.out.print(array[i] + " ")

DFS 深度优先搜索例题

约翰的农场被暴风雨给淹没了,损失很大,他的保险公司将支付给他,但是支付金额取决于被淹没的最大面积.这个农场是一个边长分别为n.m的矩形,包含nm个空间,每个空间要么是干的,要么是被淹没的,一共有k个空间被淹没.求最大的淹没面积. 题目分析:首先建立坐标,标记被淹没的空间,然后从左上角搜索被淹没的空间,并取消标记,再以此为中心向四周搜索是否有被淹没的空间,有的话继续计数并取消标记,以此类推,找出所有被淹没的空间,最后排序,找到被淹没最大的面积. #include <stdio.h>#includ

深度优先搜索(DFS)详解

深度优先搜索(DFS) [算法入门] 1.前言 深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法.它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解,那就返回到上一个节点,然后从另一条路开始走到底,这种尽量往深处走的概念即是深度优先的概念. 你可以跳过第二节先看第三节,:) 2.深度优先搜索VS广度优先搜索 2.1演示深度优先搜索的过程 还是引用上篇文章的样例图,起点仍然是V0,我们修改一下题目意思,只需要让你找出一条V0到V6的道路,而无需

python实现基础的深度优先搜索(DFS, depth first search)解决数的全排列问题

数的全排列,是一个很简单的问题,平时我们用笔用纸就能列出答案,但是数列位多的时候,排列的结果就有非常多了,例如有1,2,3,4,5,6,7,8,9这一个数列,有9个数字,则有9!(9的阶乘)这么多种结果.那是非常大的.今天我就来介绍用深度优先搜索来解决这个数的全排列的问题. 深度优先搜索 首先简单介绍一下深度优先搜索,深度优先搜索的关键在于当下该如何做,至于下一步如何做,就与当下做的一样.深度优先搜索的基本模型为: dfs(step): 判断边界:执行相关操作,返回 尝试每一种可能 for( i

深度优先搜索之全排列

小馨小诺还有小雪和小谢四个人去学校,路上他们走成了一排,爱思考的小馨提出了问题:我们四个人一排共有多少种站法? 数学比较好的小雪说这不是全排列吗,有4的阶乘种4*3*2*1=24种啊. 小馨说对,但是你用编程模拟一下. 小谢自信的回答道四层for循环然后去重即可. 小馨还是不太满意,要是有n个人呢. 小诺说可以用深度优先搜索,但大家都不明白深度优先搜索. 不明白就 关注啊哈磊.小诺也是从那学来的. 1 #include<stdio.h> 2 3 int a[10],book[10],n; 4

图的深度优先搜索及拓扑排序

本文将介绍图的深度优先搜索,并实现基于深度优先搜索的拓扑排序(拓扑排序适用于有向无环图,下面详细介绍). 1. 图的深度优先遍历要解决的问题 图的深度优先搜索与树的深度优先搜索类似,但是对图进行深度优先搜索要解决一个问题,那就是顶点的重复访问,假设图中存在一个环路A-B-C-A,那么对顶点A进行展开后得到B,对B进行展开后得到C,然后对C进行展开后得到A,然后A就被重复访问了... 这显然是不对的!我们需要用一个状态变量来记录一个顶点被访问和被展开的状态.在<算法导论>中,作者使用3种颜色来对

深度优先搜索和广度优先搜索的深入讨论

一.深度优先搜索和广度优先搜索的深入讨论 (一)深度优先搜索的特点是: (1)从上面几个实例看出,可以用深度优先搜索的方法处理的题目是各种各样的.有的搜索深度是已知和固定的,如例题2-4,2-5,2-6:有的是未知的,如例题2-7.例题2-8:有的搜索深度是有限制的,但达到目标的深度是不定的. 但也看到,无论问题的内容和性质以及求解要求如何不同,它们的程序结构都是相同的,即都是深度优先算法(一)和深度优先算法(二)中描述的算法结构,不相同的仅仅是存储结点数据结构和产生规则以及输出要求. (2)深