彻底搞懂回溯算法

1.回溯算法的核心思想

回溯算法的核心思想是:尝试+记录+回退。
尝试一种选项,在选择该选项的前提下继续寻解,如果最后寻解成功,则记录这个解,否则不用记录,然后再回退到选择该选项前的状态,改为尝试其它选项再继续寻解,判断其它选项是不是解。

2.回溯算法的关键点

回溯算法用于寻找全部解的集合,这些解满足共同的条件。我们先以"N皇后"为例分析下什么是回溯算法。
"N皇后"问题是在一个N*N的棋盘中,将N个棋子放到合适位置,满足任意两个棋子不在同一行、不在同一列、不在主对角线或次对角线上。下图是一个N为4的棋盘,其中Q表示棋子,#表示空格,满足条件。


主对角线是从左上角到右下角的线,次对角线是从右上角到左下角的线,如下图所示

我们可以按照行逐次选择位置放置Q,最终结果肯定是每行有且仅有一个Q。

  • 每行可以选择的列号为[0,1,...,N-1],这就是选项集合
  • 从[0,1,...,N-1]按照先后顺序选择一个位置,记为K,这个K是否满足"所在列无Q、所在主对角线无Q、所在次对角线无Q",这三个条件就是判断选项是否有效的限制条件(因为是按行取,每次只能尝试一个位置,所以每行不可能同时存在多个Q,不用判断"所在行无Q")。
  • 每次尝试一个选项后,棋盘布局和三个限制条件都需要更新。这里棋盘的布局就是动态变化的状态。当该状态满足棋盘行数达到N,则说明找到了一个

如下图所示,展示回溯过程:

  • 首先行0选择(0,0);
  • 行1的(1,0)和(1,1)不满足限制条件,尝试选项(1,2);
  • 行2所有选项都不满足限制条件,说明前面行0和行1那样选择不行,回退到行1取消选项(1,2),尝试
    选项(1,3),行2选择(2,1);
  • 行3所有选项都不满足限制条件,说明前面行0、行1和行2那样选择不行,回退到行2,行2剩余的(2,2)和(2,3)都不行,继续回退到行1,行1没有剩余选项,继续回退到行0,行0尝试选项(0,1),新一轮开始。
  • 行1选择(1,3);
  • 行2选择(2,0);
  • 行3选择(3,2),棋盘行数达到4,所以找到一个解,记录该解。回退最近尝试的选项,回退到行3选择(3,3),不满足限制,故继续回退到行2,通过这种尝试、记录、回退的方法可以找到所有解。


通过上面例子,相信你已经对回溯算法有了自己的理解,回溯算法关键信息包括如下:
1>可以尝试的选项集合
2>用于判断一种选项是否是有效的限制条件
3>尝试新的有效选项后,更新状态
4>继续寻找。
5>状态达到为解需要满足什么解条件。如果是解,需要记录这个解。
6>尝试完成后再回退到该尝试前的状态

3.伪代码

几乎所有的回溯算法代码都可以使用下面的代码框架:

 /*
  *result:所有解的集合.
  *state:状态.
  *choices:选项集合.
  *constraint:限制条件.
  */
void backtrack(vector<State> &result,State &state,vector<Choice>&choices,Constraint &constraint)
{
    if(isSolution(state)) //当前state是解
    {
       recordSolution(result,state);//记录这个解到result
       return;                     //回退
    }
    for(auto choice:choices)       //按先后顺序遍历选项
    {
        if(isValidChoice(choice,constraint)) //选项有效
        {
              tryChoice(state,constraint);                  //尝试该选项
              backtrack(result,state,choices,constraint);   //继续回溯
              roolbackChoice(state,constraint);             //回退该选项
        }
    }
}

4.代码实现

上面"N皇后"问题代码实现如下:

class Solution 
{
private:
	int s_row;					 //当前处理的行号
	int m_size;					 //棋盘大小
	vector<int> m_colPostionS;   //表示可选择的列
	vector<bool> m_colConstrain; //表示每列是否有Q
	vector<bool> m_mDiagonalConstrain;//表示每个主对角线上是否有Q
	vector<bool> m_sDiagonalConstrain;//表示每个次对角线上是否有Q
public:
  int solveNQueues(int n) 
  {
    s_row=0;
    m_size=n;
    m_colPostionS.resize(m_size);
    for(int i=0;i<m_size;++i)
        m_colPostionS[i]=i;
     m_mDiagonalConstrain.resize(2*n-1,false);
     m_sDiagonalConstrain.resize(2*n-1,false);
     m_colConstrain.resize(n,false);
    //
    vector<vector<string>> chessboard;
    vector<vector<vector<string>>> res;
    backtrackMy(chessboard,res,m_colPostionS,m_colConstrain,m_mDiagonalConstrain,m_sDiagonalConstrain);
    return res.size();
  }
//判断解
  bool isSolution(vector<vector<string>> &state)
  {
	if (!state.empty() && state.size()==m_size)//解条件
		return true;
	return false;
  }
//记录解
void recordSolution(vector<vector<string>> &state,vector<vector<vector<string>>> &res)
{
	res.emplace_back(state);
}
//判断选项有效
bool isValidChoice(int row,int col,vector<bool> &colConstrain,vector<bool> &mDiagonalConstrain,vector<bool> &sDiagonalConstrain)
{
	if(colConstrain[col]==true || mDiagonalConstrain[row+col]== true || sDiagonalConstrain[row-col+m_size-1]==true)
		return false;
	return true;
}
//尝试选项
void tryChoice(vector<vector<string>> &state,int row,int col,vector<bool> &colConstrain,vector<bool> &mDiagonalConstrain,vector<bool> &sDiagonalConstrain)
{
	vector<string>tempRow(m_size,"#");
	tempRow[col]="Q";
	state.emplace_back(tempRow);
	colConstrain[col]=true;
	sDiagonalConstrain[row+col]=true;
	mDiagonalConstrain[row-col+m_size-1]=true;
}
//回退选项
void roolbackChoice(vector<vector<string>> &state,int row,int col,vector<bool> &colConstrain,vector<bool> &mDiagonalConstrain,vector<bool> &sDiagonalConstrain)
{
	state.pop_back();
	colConstrain[col]=false;
	sDiagonalConstrain[row+col]=false;
	mDiagonalConstrain[row-col+m_size-1]=false;

}
//使用按行放置的策略
//棋盘上"Q"表示皇后,"#"表示空位置
//colPostionS是choices
//state表示棋盘
//res表示最终结果
void backtrack(vector<vector<string>> &state, vector<vector<vector<string>>> &res,vector<int> &colS,vector<bool> &colConstrain,vector<bool> &mDiagonalConstrain,vector<bool> &sDiagonalConstrain) 
{
   
	
	if (isSolution(state))//检查是否为解 
	{
		recordSolution(state, res);//记录解
		return;
	}
	for (int col : colS)    // 遍历所有选择
	{
		if (isValidChoice(s_row,col,colConstrain,mDiagonalConstrain,sDiagonalConstrain))//有效 
		{
			// 尝试:做出选择,更新状态
			tryChoice(state,s_row++,col,colConstrain,mDiagonalConstrain,sDiagonalConstrain);//注意对s_row的处理
			// 进行下一轮选择
			backtrack(state, res,colS,colConstrain,mDiagonalConstrain,sDiagonalConstrain);
			// 回退:撤销选择,恢复到之前的状态
			roolbackChoice(state,--s_row,col,colConstrain,mDiagonalConstrain,sDiagonalConstrain);//注意对s_row的处理
		}
	}
}

};
说明:
大小为N的棋盘,其主对角线和次对角线个数都为2N+1条。如下图所示N为3为例:

  • 每条主对角线上元素的行号减去列号是一样的,即row-col分别为[-2,-1,0,1,2]。即row-col+N-1对应[0,1,2,3,4]。
  • 每条次对角线上元素的行号加上列号是一样的,即row+col分别为[0,1,2,3,4]

posted @ 2024-09-24 22:01  MadAdam  阅读(764)  评论(0)    收藏  举报