2、回溯


定义

回溯算法实际上是一个类似枚举的搜索尝试过程,主要是在搜索尝试中寻找问题的解,当发现不能满足条件时,就“回溯”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法” 的美称。

步骤

(1)针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解;
(2)确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间;
(3)以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
基本思想:从一条路往前走,能进则进,不能进则退回来,回一条路再走。

实现

回溯的实现方法一般有两种:递归和递推(迭代)
(1)递归
思路简单,设计容易,但效率较低:

//针对N叉树的递归回溯方法  
void backtrack (int t)  
{  
    if (t>n) output(x); //叶子节点,输出结果,x是可行解  
    else  
       for i = 1 to k//当前节点的所有子节点  
        {  
            x[t]=value(i); //每个子节点的值赋值给x  
            //满足约束条件和限界条件  
          if (constraint(t)&&bound(t))   
                backtrack(t+1);  //递归下一层  
        }  
}  

(2)递推
算法设计较复杂,但效率较高:

//针对N叉树的迭代回溯方法  
void iterativeBacktrack ()  
{  
    int t=1;  
    while (t>0) {  
        if(ExistSubNode(t)) //当前节点的存在子节点  
        {  
            for i = 1 to k  //遍历当前节点的所有子节点  
            {  
                x[t]=value(i);//每个子节点的值赋值给x  
                if (constraint(t)&&bound(t))//满足约束条件和限界条件   
                {  
                    //solution表示在节点t处得到了一个解  
                    if (solution(t)) output(x);//得到问题的一个可行解,输出  
                    else t++;//没有得到解,继续向下搜索  
                }  
            }  
        }  
        else //不存在子节点,返回上一层  
        {  
            t--;  
        }  
    }  
}  

识别回溯

一般回溯的问题有三种:

  • 有没有解
  • 求所有解(个数或具体信息)
  • 求最优解

而对于每种问题有不同的模板
第一种:有没有解

bool solve(Node n){
    if n is a leaf node{
        if the leaf is a goal node , return true;
        else return false;
    }
    else{
        for each child c of n{
            if solve(c) succeeds,return true;
        }
        return false;
    }
}

第二种:求全部解

void solve(Node n){
    if n is a leaf node{
        if the leaf is a goal node,count++,return;
    else return;
    }
    else{
        for each child c of n{
            solve(c);
        }
    }
}

第三种:求最优解

void solve(Node n){
    if n is a leaf node{
        if the leaf is a goal node,update best result,return;
        else return;
    }
    else{
        for each child c of n{
            solve(c);
        }
    }
}

实例一:八皇后问题

八皇后问题是一个古老而著名的问题,是回溯算法的典型例题。该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后(棋子),使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
第一种:有没有解

#include <iostream>
using namespace std;

class ChessBoard{
public:
    ChessBoard();
    ChessBoard(int);
    void findSolution();
private:
    const bool available;
    const int squares,norm;//棋盘边长、设定一个右斜线相加的常数
    bool *column,*leftDiagonal,*rightDiagonal;//定义列、左斜线与右斜线
    int *positionInRow,howMany;//定义行及方法的数量
    char m[10][10];//记录棋盘格
    void putQueen(int);//放置皇后
    void printBoard();//打印输出
    void initBoard();//初始化棋盘
    void Delete();//释放内存
};

ChessBoard::ChessBoard():available(true),squares(8),norm(squares-1){
    initBoard();
}

ChessBoard::ChessBoard(int n):available(true),squares(n),norm(squares-1){
    initBoard()
}

void ChessBoard::initBoard(){
    register int i;//将i寄存,使计算更快
    column = new int[squares];
    positionInRow = new int[squares];
    leftDiagonal = new bool[squares*2-1];//左斜线的数目
    rightDiagonal = new bool[squares*2-1];//右斜线的数目
    for(int i = 0;i < squares;i ++)
        positionInRow[i] = -1;//存储列数
    for(int i = 0;i < squares;i ++)
        column[i] = available;
    for (i = 0; i < squares * 2 - 1; i++)
        leftDiagonal[i] = rightDiagonal[i] = available;
    howMany = 0;
}
void ChessBoard::printBoard() {
    howMany++;//
    cout << howMany << " way is:" << endl;
    //为棋盘格赋值为1
    for (int i = 0;i != squares;i++) {
        for (int j = 0;j != squares;j++)
            m[i][j] = '1';
    }
    //将皇后的位置在棋盘格上用'*'标志出来
    for (int row = 0;row != squares;row++)
        m[row][positionInRow[row]] = '*';
    //打印棋盘格
    for (int i = 0;i != squares;i++) {
        for (int j = 0;j != squares;j++)
            cout << m[i][j];
        cout << endl;
    }
    cout << endl;
}
void ChessBoard::putQueen(int row) {
    for (int col = 0; col < squares; col++) {
        if (column[col] == available &&
            leftDiagonal[row + col] == available &&
            rightDiagonal[row - col + norm] == available)
        {
            positionInRow[row] = col;
            column[col] = !available;
            leftDiagonal[row + col] = !available;
            rightDiagonal[row - col + norm] = !available;
            if (row < squares - 1)
                putQueen(row + 1);
            else printBoard();
            column[col] = available;
            leftDiagonal[row + col] = available;
            rightDiagonal[row - col + norm] = available;
        }
    }
}
void ChessBoard::Delete() {
    delete[]column;
    delete[]positionInRow;
    delete[]leftDiagonal;
    delete[]rightDiagonal;
}
void ChessBoard::findSolutions() {
    putQueen(0);
    cout << howMany << " solutions found.\n";
    Delete();
}
int main() {
    ChessBoard board(7);
    board.findSolutions();
    while (true)
    {

    }
    return 0;
}

实例二:背包问题

给定n种物品和一背包。物品i的重量是wi,其价值为pi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

#include <stdio.h>

#define N 3 //物品的数量
#define C 16;//背包的容量

int w[N]={10,8,5};//每个物品的重量
int v[N]={5,4,1};//每个物品的价值
int x[N]={0,0,0};//代表是否放入背包

int curW=0;//当前背包中物品总重量
int curV=0;//当前背包中物品总价值

int BestV=0;//当前的最大价值
int BestX[N];//最优路径选择

void backTrack(int t){
    //叶子节点,一条路径一条路径的遍历
    if(t>N-1){
        //找到一个更优的解
        if(curV>BestV){
            BestV=CurV;
            for(int i=0;i<N;++i) BestX[i]=x[i];    
        }
    }
    else{
        //主要在此处实现回溯,递归到下一个节点时,价值和重量加;回溯之后,价值和重量减。
        if((CurV+w[t])<=C){
            CurW+=w[t];
            CurV+=v[t];
            backTrack(t+1);
            CurW-=w[t];
            CurV-=v[t];
        }
    } 
}

//主函数
int main(int argc,char* argv[]){
    backTrack(0);
    cout << "最优解:" << BestV << endl;
    for(int i=0;i<N;i++){
        cout << "路径:" << BestX[i]);
    }
    return 0;
}