哈喽大家好!今天小鹅来给大家讲一讲C++的算法——搜索与回溯。
目录:
一、今日干货?
1.了解并掌握搜索和回溯算法的内涵、解题步骤和代码框架✅
2.搜索树和剪枝优化✅
3.全排列与N皇后问题✅
二、正文
1.引入
先看一个问题:
给定一个正整数N(1≤N≤15)请你从小到大生成1-N的全排列。
举个栗子:
N=3时,1-3的全排列为:
[1,2,3] [1,3,2] [2,1,3] [2,3,1] [3,2,1] [3,1,2]
面对这种问题,很多人或许第一反映是用for循环枚举:
for(int i=1;i<=3;i++){//枚举第一位
for(int j=1;j<=3;j++){//枚举第二位
for(int k=1;k<=3;k++){//枚举第三位
if(i!=j&&j!=k&&i!=k){//如果互不相同
cout<<i<<' '<<j<<' '<<k<<endl;//输出
}
}
}
}
这种方法虽然可以,但有以下缺点:
1.如果N很大,容易超时!
2.N不一定就是3。如果N是4、5、6呢?不可能每个N都写一个枚举。
所以我们引入一个算法——搜索与回溯
2.搜索与回溯
1️⃣算法分析
上面题目出现一个问题:
因为N不是一定的,所以循环次数和元素数量也是不确定的,所以无法进行for循环。
由于每一位都要进行一次遍历,所以我们可以把遍历的内容写成一个子程序。进行递归求解
所以我们来分析一下递归的内容:
我们可以定义一个叫cur的变量表示当前放置数字的位置,定义一个ans数组存储全排列。
1.递归边界:
当cur>n时,说明已经构造好了一个全排列。
Q:为什么不是cur==n?
A:因为第n个位置也要枚举。
此时就要输出ans数组并退出。
2.内容
由于要枚举第cur个位置上的数字,所以要对1到n进行for循环。
for循环里要存储枚举的数:ans[cur]=i;
随后遍历下一位:solve(cur+1);
实现代码:
const int N=20; int n,ans[N]; void solve(int cur){ if(cur>n){//说明已经填完一个排列 for(int i=1;i<=n;i++)cout<<ans[i]<<' '; printf("\n"); } for(int i=1;i<=n;i++){//否则遍历 ans[cur]=i; solve(cur+1); } }
这时运行会出现问题:
无限输出1 1 1??
分析代码,我们发现原来没有判断重复。该如何解决?
这时就要引入一个小技巧——剪枝优化
2️⃣剪枝优化
通过分析代码运行过程,我们会发现过程很像一颗树!
如图,当n=3时:
所以全排列的搜索其实就是解树的dfs,即从根节点到叶子节点构成一个解。
上述代码有N^N种可能,然而全排列其实只有N!(N*(N-1)*(N-2)*……*1)种可能!
所以在遍历过程中遇到不可能的解就可以停止搜索,降低搜索量!
这就是——剪枝优化。
所以我们可以定义一个bool类型的vis数组,用来标记此数字是否已经使用。
算法框架代码:
const int N=20;
int n,ans[N];
bool vis[N];
void solve(int cur){
if(cur>n){//说明已经填完一个排列
for(int i=1;i<=n;i++)cout<<ans[i]<<' ';
printf("\n");
}
for(int i=1;i<=n;i++){//否则遍历
if(!vis[i]){//如果此数字没使用过
vis[i]=true;//标记成已使用
ans[cur]=i;
solve(cur+1);
vis[i]=false;//记得恢复
}
}
}
运行结果:
3.N皇后问题
题目:
N皇后问题,即在N×N格的国际象棋上摆放N个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,请问有多少种摆法。图所示即是摆法的一种。3<N≤12
1️⃣分析
按照平常的思路,看到题目第一个思路就是从N*N个格子中挑出N个格子,使得任意两个格子都不能处于同一行、同一列或同一斜线上。当然这肯定很麻烦。
通过观察题目,由于N个皇后不可能处于同一行,所以一定是一行放一个皇后!
那么这就变成一个搜索问题:搜索每一行的每一列,尝试放置皇后,通过判断突进行剪枝。
2️⃣实现
定义一个cur变量,表示当前在第cur行放置皇后,ans数组存储每个皇后处于的列。
同时还要写一个check函数,用来检查此解是否成立。
什么时候成立呢?任意两个皇后都不能处于同一行、同一列或同一斜线上。
同一行:
不需要判断,因为递归程序已经控制每个皇后单独一行。
同一列:
传进来两个参数x,y,表示当前皇后的位置。遍历1到x-1,即之前所有皇后的列。如果y==ans[i],说明处于同一列。
同一斜线:
斜线分左斜线和右斜线。该如何判断?
左斜线:
由图可知,左斜线特点为行下标-列下标相等
所以如果x-y==i-ans[i],说明同处于左斜线
右斜线:
由图可知,右斜线特点为行下标+列下标相等
所以如果x+y==i+ans[i],说明同处于右斜线
check函数代码:
bool check(int x,int y){
for(int i=1;i<x;i++){//遍历所有列
if(y==ans[i]||x-y==i-ans[i]||x+y==i+ans[i])return false;
}
return true;
}
总代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
#include<queue>
using namespace std;
const int N=15;
int n,ans[N],cnt=0;
bool check(int x,int y){
for(int i=1;i<x;i++){
if(y==ans[i]||x-y==i-ans[i]||x+y==i+ans[i])return false;
}
return true;
}
//cur--当前在第cur行放置皇后
void dfs(int cur){
if(cur>n){
cnt++;
return ;
}
for(int j=1;j<=n;j++){
if(check(cur,j)){
ans[cur]=j;
dfs(cur+1);
}
}
}
int main()
{
cin>>n;
dfs(1);
cout<<cnt;
return 0;
}