回溯法-八皇后问题 vs. 枚举法-1~n的全排列

下面的两个题都用到了递归的思想:
将原问题分解,每次递归扩大解的规模,直到和原问题规模一致。
注意一开始都是给出了递归终止条件,解的规模和原问题一和原问题一样了,递归就是终止了

回溯法

当用递归对问题求解时,如果当前步骤没有合法选择,则递归函数返回到上一级调用中,这种现象称为回溯

1.回溯法的思路框架

维护【路径】和【选择列表】

//一般路径就是一个解,用path[]维护
//选择列表用一个flag[]数组维护所有选择

//选择的时候:
path.push_back(i);//表示将i加入路径
flag[i]=false//表示i被从选择列表中删除了,后面不能再选了

//回溯的时候:
path.pop_bakc();//将i从路径中删除
flag[i]=true;//表示i被重新加入选择列表

回溯的时候修改flag[]数组就是下面,八皇后问题中的,把辅助变量恢复原状
path.pop_back()没有在下中体现,是因为下文直接在递归中path[i]进行赋值,相当于是path.pop_back()后再进行path.push_back()

2.结合图片理解

这是完整的回溯树

在根节点处的时候:路径=[],选择列表=[1,2,3]
在这里插入图片描述

做选择

当选择1的时候,如下图蓝色箭头所示:

做选择,更改【选择列表】和【路径】的状态:

将1从选择列表中删除(表示已经被选了,后面不能再选了)flag[“1”]=false
将1加入路径 res.push_back(“1”)

在这里插入图片描述

回溯

如上图红色箭头所示

撤销选择,更改【选择列表】和【路径】的状态:(这一步就是回溯)

将1重新加入选择列表(表示又可以选了)flag[“1”]=true
将1从路径中删除 res.pop_back()

3.伪代码
void backtrack(当前路径,选择列表)
{
	for 选择 in 选择列表:
	{
		flag[选择]=false;
		当前路径.push_back(选择);
		
		backtrack(当前路径,选择列表);
		
		flag[选择]=true;
		当前路径.pop_back();
	}
}
八皇后问题

递归求第cur行的皇后应该放在哪一列

cur: 当前行
c[ i ]: 第i行的皇后放在第几列
tot:  解的个数

对应上面的框架
第一列,第二列,第三列。。。组成选择列表
c[i]对应path[i],c[cur]=i表示将选择i加入路径
ok对应flag[],ok=1对应flag[i]=true,ok=0对应flag[i]=false
下面的方法就是专门用一个vis[][]维护对应flag[]的功能

void search(int cur,int n)
{
	if(cur==n) tot++;//递归边界
	else for(int i=0;i<n;i++)//判断放在每一列的可行性
	{
		int ok=1;//初始认为放在第i列可行
		c[cur]=i;
		
		for(int j=0;j<cur;j++)//前0-cur-1行有没有皇后会和放在第i列冲突
		{
			if(c[j]==i||cur+c[cur]==c[j]+j||cur-c[cur]==j-c[j])
				{ok=0;break;}//放在第i列不可行
		}
		
		if(ok) search(cur+1);
	
	}//else for
}//search

这里不需要修改全局变量是因为当前步骤没有合法选择时(遍历i=0~n,ok全部=0),回溯到上一个递归中,上一个递归此时正在某个i值的循环中,说明这个i值不可行,那么i++到下一个,会直接在c[cur]=i这一步更新数组,所以不需单独修改。


如果用辅助全局变量记录当前皇后所在的列和对角线是否有其他皇后

vis[2][]:用二维数组标记,有=1,无=0

vis[0][i]记录当前列是否有其他皇后
vis[1][cur+i]记录住对角线上是否在其他皇后
vis[2][n+cur-i]记录副对角线上是否有其他皇后

上面是用ok来标记当前步是否可行,每列尝试的时候(每个i)都先把ok置为1,如果发现不可行,把ok改为0

这里是需要及时恢复vis数组的值

回溯中,如果修改了辅助全局变量,则需要及时把他们恢复原状,后面分析原因

void search(int cur,int n)
{
	if(cur==n) {}tot++;return;}
	else for(int i=0;i<n;i++)
	{
		if(!vis[0][i]&&!vis[1][i+cur]&&!vis[2][n+i-cur])//这里代替了前面的 ok=1/0配合for(j循环)检查是否冲突
		{
			c[cur]=i;
			vis[0][i]=vis[1][i+cur]=vis[2][n+i-cur]=1;
			search(cur+1);
 
 			vis[0][i]=vis[1][i+cur]=vis[2][n+i-cur]=0;
 		}
 	}
 }

这里要修改会全局变量是因为,当上一行的search()调用返回的时候,只有两种可能:
1.cur==n,已经找到了一组可行的解,此时需要回溯到cur=7(n-1),把最后一行原来放的皇后拿走,看是否可以放在其他列如果能则又得到一组可行解,然后再回溯;如果不能,再回溯到cur=6,把第六行原来放置的皇后拿走,看是否可以放在其他列,然后再看第七行是否又可以得到一组可行解,如果不行,再回溯到cur=5,把第五行原来放置的皇后拿走。。。
把vis恢复的意义就是把当前行原来放的皇后拿走

2.当前cur遍历所有i都不能得到可行步,回溯到cur-1中,把cur-1行原来放置的皇后拿走,然后i++,看是否可以把cur-1行的皇后放在其他列

1~n的全排列

枚举法

枚举法使用的时候
(1)如果原题有两个变量需要枚举,先找二者的关系,用其中一个来计算另一个
(2)缩小要枚举的变量的范围

用递归解决

1~n的全排列

A[maxn] 存储排列
cur 当前要确定放哪个数字的位置

void fun(int n,int cur,int* A)
{
	if(cur==n) //递归边界
		for(int i=0;i<n;i++) cout<<A[i]<<endl;
	else for(int i=1;i<=n;i++)//1~n中每个数字都可以尝试放在cur这个位置
	{
		int ok=1;
		//判断A[0~cur-1]是否用过数字i了
		for(int j=0;j<cur;j++) if(A[j]==i)	{ok=0;break;}

		if(ok){
			A[cur]=i;
			//八皇后问题把这个赋值放ok=1后面for循环检测是否合法前面因为要通过cur值判断皇后是否冲突
			fun(n,cur+1,A);
		}
	}
}
包含重复数字的数组全排列

输入:数组P[n](长度为n)
输出:按照字典排序输出A(即所有可能的P中元素构成的全排列)
基本思路和上述一样:
递归函数void pailie(vector<int> a,int cur,int n)
其中cur就是当前是给A第cur个位置上的值尝试
其实每个位置上值都有可能是P[0~n-1]中的任何一个数

原来的思路是:
(1)判断是否到递归边界 cur==n
(2)for循环遍历P中所有元素,判断A的前0~cur-1位是否出现过,出现过则换下一个数,没出现递归尝试cur+1位可以选的元素

但是现在因为P中有重复元素,如果通过是否在A[0~cur-1]位出现过判断,则使得重复的数字只能用一次

解决方案:假设现在A的cur位上要是用P[i]这个元素,我们可以统计一下A[0~cur-1]中P[i]这个元素出现的次数c1,P数组中P[i]这个元素出现的次数c2,如果c1<c2,那么就说明A[cur]这个位置可以用P[i]

但是又出现了问题了,比如P数组是1,2,3,3,5
比如我们现在选cur=0即A的第一个位置的元素,我们遍历P数组,当i=2的时候,P中第一个三被选为A中第一个元素,然后继续递归,,得到一组3开头的全排列
当i=3的时候,P中第二个3倍选为A的第一个位置的元素,然后递归,,得到一组3开头的全排列
其实这两组3开头的排列是一样的

解决方案:
我们把P数组从小到大排列后,在对P遍历用来确定A的cur位置可能的值的时候,先加一个判断,如果P[i]==p[i-1]的话,说明是重复元素,不要重复选择只有当i==0||p[i]!=p[i-1]的时候,才继续for循环中的内容

void pailie(vector<int> a,int cur,int n)
{
	if(cur==n) //说明已经获得了一个排列,可以打印了
		for(int i=0;i<n;i++) cout<<a[i]<<" ";
	
	else for(int i=0;i<n;i++)//开始遍历P中的每个元素看是否能放在A[cur]处
	{
		int c1=0,c2=0;
		if(i==0||P[i]!=P[i-1])
		{
			for(int j=0;j<cur;j++) if(P[i]==A[j]) c1++;
			for(int j=0;j<n;j+=) if(P[i]==P[j]) c2++;
			
			if(c1<c2)
			{
				A[cur]=P[i];
				pailie(A,cur+1,n);
			}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值