之前接了个新项目,忙了一段时间,这一篇拖了好久啊……月末就辞职啦!
今天复习了一下回溯算法,明明是很基础的东西,动手写的时候却感觉很困难,思路不清晰。翻看了一下以前写的全排列问题的代码,思路一下就有了。果然算法套路模板还是很重要的,模板在手什么变体都不怕了,而且必须及时复习,加强记忆。所以决定总结一下经典算法的套路,就从回溯开始吧。
前言
回溯算法是对树形或者图形结构执行一次深度优先遍历,实际上类似枚举的搜索尝试过程,在遍历的过程中寻找问题的解。深度优先遍历有个特点:当发现已不满足求解条件时,就返回并尝试别的路径,我们通常称之为“剪枝”。此时对象类型变量就需要重置成为和之前一样,称为「状态重置」。
一、八皇后问题
八皇后问题可以说是回溯算法的经典例题了,我还记得大二的算法课有一次大作业,给了十几道题目自己选一道来做,我当时就选了八皇后问题。一开始不会写,后来还是参考了网上的代码(这就是经典题目的好处,不会也有得抄哈哈哈),看明白了网上的代码之后,我自己动手改成了两版,一版能画出棋盘的八皇后,另一版只统计数量的N皇后,一起交上去了。
八皇后问题(Eight queens):将 8 个皇后放置在 8×8 的棋盘上,并且使皇后彼此之间不能相互攻击。(国际象棋中皇后攻击的条件是位于同一行、同一列或同一斜线上)
如果使用穷举法,需要尝试8^8种情况,这个时候回溯的特点“剪枝”就体现出优势来了。比如我们先把第一行的Queen放在第一列上,那么第二行的Queen显然不能放在第一列或者第二列了,于是我们把它放在第三列,接下来第三行的Queen就显然不能放在1、2、3、4列上,以此类推,在不符合要求时果断退回上一步,寻找新的路径,就是回溯的核心思想了。
来一个N皇后求解决数量的代码:
class Solution {
public:
int result=0;
int totalNQueens(int n) {
//初始化一个空棋盘
vector<int> board(n,-1);
backtrack(0,board);
return result;
}
void backtrack(int row,vector<int>& board){
//结束条件
if(row==board.size())
{
result++;
return;
}
for(int col=0;col<board.size();col++)
{
if(!isValid(board,row,col)) continue;
//做选择
board[row]=col;
//下一行
backtrack(row+1,board);
//重置
board[row]=-1;
}
}
bool isValid(vector<int> board,int row,int col){
//判断当前位置是否与前几行有冲突
for(int r=0;r<row;r++)
{
int c=board[r];
if(c==col||abs(r-row)==abs(c-col)) return false;
}
return true;
}
};
二、全排列问题
全排列问题:给定一个不含重复数字的数组,返回其所有可能的全排列。
全排列问题可以说是回溯算法的标准模板了,很清晰的实现思路,就是选择下一状态 - 递归 - 状态重置。
全排列代码如下:
class Solution {
public:
vector<vector<int>> result;
vector<vector<int>> permute(vector<int>& nums) {
vector<int> line;
backtrack(line,nums);
return result;
}
void backtrack(vector<int>& line,vector<int> nums){
if(line.size()==nums.size())
{
result.push_back(line);
return;
}
for(int i=0;i<nums.size();i++)
{
bool flag=true;
for(int j=0;j<line.size();j++)
if(line[j]==nums[i]){
flag=false;
break;
}
if(!flag) continue;
line.push_back(nums[i]);
backtrack(line,nums);
line.pop_back();
}
}
};
三、回溯算法模板
代码如下(示例):
//回溯算法的套路
class Solution {
public:
//定义全局变量,例如结果要返回的数量、或者结果集
//(根据需要)定义visited[]
vector<vector<int>> result;
vector<vector<int>> permute(vector<int>& nums) {
//对全局变量做初始化
//定义track(表示当前状态)
vector<int> line;
//执行回溯算法,使用初始参数
backtrack(line,nums);
//返回结果
return result;
}
void backtrack(vector<int>& line,vector<int> nums){
//定义结束状态,回溯到哪里停止
if(line.size()==nums.size())
{
result.push_back(line);
return;
}
//遍历所有可能的下一状态
for(int i=0;i<nums.size();i++)
{
//排除不符合条件的下一状态(剪枝)
bool flag=true;
for(int j=0;j<line.size();j++)
if(line[j]==nums[i]){
flag=false;
break;
}
if(!flag) continue;
//修改track为新状态,修改visited[]
line.push_back(nums[i]);
//对新状态执行回溯,注意修改参数
backtrack(line,nums);
//退回原状态
line.pop_back();
}
}
};
总结
总之回溯算法就是带剪枝的穷举吧,列举所有可能性肯定是非常耗时的,剪枝条件写好了能大幅降低时间复杂度。
有了模板,做题目就轻松多啦,思路也清晰了,就往里套就行了。