数据结构之回溯

Posted hrnn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之回溯相关的知识,希望对你有一定的参考价值。

回溯

  • 利用回溯算法求解八皇后问题
  • 利用回溯算法求解0-1背包问题

利用回溯算法求解八皇后问题

八皇后问题(eight queens problem)是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。可以把8皇后问题扩展到n皇后问题,即在nxn的棋盘上摆放n个皇后,使任意两个皇后都不能处于同一行、同一列或同一斜线上。

回溯法设计思路

  回溯法从解空间树的根节点出发,按照深度优先策略搜索满足约束条件的解。在搜索至树中节点时,先判断该节点对应的部分解是否满足条件,也就是判断该节点是否包含问题的(最优)解,如果肯定不包含,则跳过以该节点为根的子树,即所谓剪枝。否则进入以该节点给根的子树,继续按照深度优先策略进行搜索。

#include <iostream>
#include<math.h>
using namespace std;
 
int Place(int k,int x[])
{
    for(int i=0;i<k;i++)
        if(x[i]==x[k] || abs(i-k)==abs(x[i]-x[k]))
            return 1;
    return 0;        
}
 
int Queen(int n,int x[],int sum)
{
    int k=0;
    int flag=0;//有无解标志
    
    while (k>=0)
    {
        x[k]++;
    
        while(x[k]<n && Place(k,x)==1)
            x[k]++;
        if(x[k]<n && k==n-1)
        {
            flag=1;//有解
            sum++;
            for(int i=0;i<n;i++)
                cout<<x[i]+1<<"  ";
            cout<<endl;
            if(k<n && x[0]<n)
            {
                x[k--]=-1;//回溯
                continue;
            }
            
        }
        if(x[k]<n && k<n-1)
            k+=1;
        else
            x[k--]=-1;
        
    }
    if(flag==0)
    cout<<"无解。"<<endl;
    return sum;
}
 
int main()
{
    int sum=0;
    int n=8;//皇后个数
    int x[8];
    for(int i=0;i<n;i++)
        x[i]=-1;
    sum=Queen(n,x,sum);
    cout<<"解毕,共有 "<< sum <<" 个解。"<<endl;
    return 0;
}

#include<iostream>
using namespace std;

int Capacity;                //背包容量
bool selected[10000];        //当前选择方案
bool optimal[10000];        //最佳选择方案
int maxTotalValue = 0;        //最大价值
int valueofPackage = 0;        //当前背包价值
int residualCapacity;        //剩余背包价值
int n;
int weight[10000];            //背包重量
int value[10000];            //背包价值

void dfs(int i)
{
    if(i > n){
        if(valueofPackage >= maxTotalValue){
            for(int i = 1 ; i <= n ; i++){
                optimal[i] = selected[i];
            }
            maxTotalValue = valueofPackage;
        }
        return;
    }else{
        residualCapacity -= weight[i];
        if(residualCapacity >= 0){    //遍历左子树
            selected[i] = 1;
            valueofPackage += value[i];
            dfs(i+1);
            selected[i] = 0;
            valueofPackage -= value[i];
            residualCapacity += weight[i];
        }else{//不满足原路返回
            residualCapacity += weight[i];
        }
    }
    //遍历右子树
    dfs(i+1);
}


int main(){
    cout<<"输入背包容量:"<<endl;
    cin>>Capacity;
    residualCapacity = Capacity;
    cout<<"请输入背包个数:"<<endl;
    cin>>n;
    cout<<"请输入每个背包重量:"<<endl;
    for(int i = 1 ; i <= n ; i++){
        cin>>weight[i];
    }
    cout<<"请输入每个背包价值:"<<endl;
    for(int i = 1 ; i <= n ; i++){
        cin>>value[i];
    }
    dfs(1);
    cout<<"最佳方案为:"<<endl;
    for(int i = 1 ; i <= n ; i++){
        if(optimal[i] == 1){
            cout<<i<<" ";
        }
    }
    cout<<endl<<"最大背包价值为:"<<endl<<maxTotalValue;
    
    return 0;
}
回溯法本质是用来搜索问题的解,典型地就是使用深度优先搜索,搜索路径一般沿树形结构进行,在搜索过程中,首先会判断所搜索的树结点是否包含问题的解,如果肯定不包含,则不再搜索以该结点为根的树结点,而向其祖先结点回溯;否则进入该子树,继续按深度优先策略搜索。

       可能这么说不是很容易懂,咱们来的实例吧,那就是经典的0-1背包问题,关于这一问题后边很多算法都会涉及到,咱们一点点深入~
       我们还是使用典型的三背包为例,问题描述如下:设有三个背包,其重量分别为:16,15, 15;价值分别为: 45,25,25;请选择背包,使其重量不超过30,但价值最大。
       可以看出我们的约束条件为总重量不超过30,目标是价值最大,那我们就可以使用回溯法的思想来求解:
       每个背包都可以被选择中或者不选,理论上如果不加任何限制的话一共有八种可能(2×2×2),但我们在搜索的过程中要时刻注意总重量不可超过30 ,在这个基础上使其总价值最大,于是我们可以从第一个背包开始,先选中它,其重量是16,小于30,没有问题,然后在选择第二个背包,其重量为15,二者总重量为31,超过30,所以不能选择第二个背包,同理第三个背包也不能选,也就是说在选中第一个背包的前提下,另外两个背包就都不能再选了,这是八种理论可能情况中的一种;以此类推,不选第一个背包,在此基础上选择第二个背包,总重量为15,没有问题,再选第三个,此时总重量为30,也没有问题;对照这两种情况,前者总价值为45,后者总价值为50,当然,我们以上帝视角可以看出最大价值也就是50,但落实到具体的算法该如何全方位多角度深层次地解决这一问题呢?
        我们来看一个完全二叉树结构图:
技术图片
对于上述的背包问题,在此二叉树结构中可以简单地理解为:从A出发,往左子树方向走说明选中了A,往右子树方向走说明没有选中A,即“左选右不选”,落实到上图中就是1代表选中0代表未选中;我们上边说道的第一种情况,即只选中第一个背包的情况对应上图的A->B->E->K;那这里有朋友可能会问了为啥二叉树会有四层,不是一共三个背包嘛,对的,因为我们每一层所代表的背包选与不选都得由下一层所决定,比如节点E代表第三个背包,如果我们走到K就说明不选E,反之,若走到J则说明选中了E,因此三个背包我们需要四层完全二叉树,同理,若有N个背包则需要N+1层。
        当我们的搜索路径到达K后,得到了一组值,即总重量为16,总价值为45,此时,由于已经到了树的叶子结点,因此需要回溯直到根,再继续进行后续的搜索,在后续搜索过程中,一方面要进行结点的判断,另一方面,一旦得到了一个合符要求的价值,则与前一次搜索所得到的结果进行比较,如果比前一次搜索得到的值大,则取代,反之,继续搜索直到整个树搜索结束所得到的最大值即为问题的解。
       总的来说,上述的求解过程在程序实现过程中可以这样来理解:我们把二叉树的每一层看成是某一个物品。当我们选择物品时总是从第一个物品开始进行选择,可能选,也可能不选。如果选中,则从二叉树的左边子树开始搜索,如果未选中,则从二叉树的右边子树开始搜索。以此类推即可~ 
       如果将每个物品的重量对应每一层的一个节点,在每次选择每一个物品时进行重量的判断,并记录权值,则可决定是否应该继续搜索下一层的子树。     
       如果设物品的重量存放在W数组中, W[i]为第i个物品的重量,P[i]表示第i个物品的价值。C表示背包能够承受的最大重量。cw表示当前物品的重量,cp表示当前物品的价值(稍微记忆一下这几个符号量的意义)。在选中情况下(即搜索左子树)执行下面的操作:     
( 1 )首先判断加上该物品重量后是否满足最大重量不超过C的要求,如果不超过,则:       cw+=w[i];       cp+=p[i];   反之则搜索右子树;  
( 2 )继续搜索下一层,执行相同的操作;      
( 3 )当搜索到最后一层时,显然可得到从根到该结点所选择的所有物品的价值,如果该价值大于前一次得到的最大价值,则替代前一次的价值,反之,则不取代;    
( 4 )  退回到上一层,即其双亲结点所在的一层,显然此时应执行:        cw-=w[i];        cp-=p[i];       
执行上述语句的目的在于为访问右子树做准备。在访问右子树时,显然不需要计算其重量和价值,因为右子树表示未选中该物品
      最后这几步是我在一些资料中看到的...总感觉有问题,尝试做了一些修改,还是有些别扭,其实大家也可以理解,算法这东西变幻莫测,再基本的原理也可以有很多表现形式,可以进一步优化,我们不要以上帝视角看待这个过程,这样会想当然地人为进行条件约束;

 

以上是关于数据结构之回溯的主要内容,如果未能解决你的问题,请参考以下文章

Java数据结构之回溯算法的递归应用迷宫的路径问题

一文通数据结构与算法之——回溯算法+常见题型与解题策略+Leetcode经典题

Flink 保存点之回溯时间

Flink 保存点之回溯时间

五大算法之回溯算法

17图的搜索算法之回溯法