【回溯】子集树和排列树(装载/最大团/n皇后/旅行商)

来源:互联网 发布:剑灵正太捏脸数据 编辑:程序博客网 时间:2024/06/05 18:07

之前一直分不清哪种递归属于回溯法,当然回溯法也不一定用递归来做,上了算法课有了一点感悟,记录一下。这四道题是算法的作业,在OJ上可以测试通过,感觉解空间这个概念真的是很帮助思考的一个东西。

解空间

解空间就是所有解的可能取值构成的空间, 一个解往往包含了得到这个解的每一步,往往就是对应解空间树中一条从根节点到叶节点的路径。
子集树和排列树都是一种解空间,它们不是真实存在的数据结构,也就是说并不是真的有这样一棵树,只是抽象出的解空间树。

约束条件和限界条件

约束条件Constraint是问题中就限定好的条件,比如在装载问题中装入第i个物体后不能超过背包总容量时才考虑装入它,即搜索左子树的情况。
限界条件Bound是需要自己挖掘的一个界,可能是上界或者下界,当问题中的某个值在走向某棵子树时会超出这个界限时,就可以放弃这棵子树了。
用这两个条件可以对解空间树进行剪枝,这样回溯法才有别于枚举。

子集树

当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树。子集树通常有2^n个叶结点(完全二叉树)。
这里写图片描述
回溯法搜索子集树的一般算法:

void backtrack(int i){    if(i>t)    {        //找到了一个解,记录一下        return ;    }    if(Constraint1() && Bound1())//左子树剪枝    {        x[i]=1;        //考虑搜索左子树        backtrack(i+1);        //回退了!维护好    }    if(Constraint0() && Bound0())//右子树剪枝    {        x[i]=0;        //考虑搜索右子树        backtrack(i+1);    }return ;}

一般来说,子集树是一棵完全二叉树,这是因为子集往往只能取或者不取原集合中的元素,也就是取=1(左子树),不取=0(右子树)。
因为树的根节点只有一个,但从第一个物体开始就要考虑选择还是不选择,所以选择序列数组应该开t+1大小的,然后从1~t层(根节点算0层)即是考虑这个物体选择/不选择的层。
要注意搜索完左子树,往往要维护好这个i节点,因为相当于回退到了上一层,再去考虑是否要搜索右子树。

排列树

当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有n!个叶节点。也就是说n个元素的每一个排列都是解空间中的一个元素。
这里写图片描述
回溯法搜索排列树的一般算法:

void backtrack(int i){    if(i>t)    {        //找到了一个解,记录一下        return ;    }    else        for(int j=i;j<=t;j++)        {            swap(x[j],x[i]);            if(Constraint() && Bound())//剪枝                backtrack(i+1);            swap(x[j],x[i]);        }return ;}

对于i层及其后的所有层,都和第i层交换值然后再搜索交换后的下一层,并在回退后交换回来(维护好本层),这样能够搜索完全排列(可以想象一下2个节点的情况,3个节点的情况…)。
因为树的根节点只有一个,但从第一个位置开始就要考虑排这t个物品中的哪一个,所以选择序列数组应该开t+1大小的,然后从1~t层(根节点算0层)即是考虑这个位置放t个物品中的哪一个。

装载问题(子集树)

这里写图片描述

#include<iostream>using namespace std;int count=0;//全局计数int t;//箱子数int *w;//重量数组int c1,c2;//两艘船载重量int cw;//当前载重量int bestw;//当前找到过的最优成功载重量int *c;//当前的放入情况int *b;//当前找到过的最优的放入情况bool ok=false;void backtrack(int i)//回溯法:在一棵子集树解空间中深度搜索{    if(i>t)//超出子集树的叶节点了    {        if(cw>bestw)//仅当这次搜索严格大于当前最优时        {            bestw=cw;//才做更新(这样能保证一样的值取按字典序最大者)            for(int j=1;j<=t;j++)//更新最优选择序列                b[j]=c[j];            ok=true;//记录找到过成功的解        }        return ;//回退    }    //考虑搜索左子树(即放入第i个的情况)    if(cw+w[i]<=c1)//约束条件剪枝:不能超过c1载重量才可以放入i物品(走左子树)    {        c[i]=1;//记录当前第i个装进来了        cw+=w[i];//把当前载重量加进来        backtrack(i+1);//然后搜索下一层(左子树)        cw-=w[i];//搜索完以后,要退回到本层,所以把刚才加上的左子树的重量减掉        //而w[i]是不需要动的,因为每次向下走一层时总是会按左走还是右走更新c[i]        //也就是说目前c[i]里的值只是废的而已    }    //考虑搜索右子树(即不放入第i个的情况)    int sumend=0;    for(int j=i+1;j<=t;j++)        sumend+=w[j];//sumend里存的是从第i+1个到第t个全部放进来的重量    if(cw+sumend>bestw)//限界条件剪枝:如果剩下的全放进来都没法更重,就不用看右子树了    {        c[i]=0;//记录第i个物体不放入(从第i-1层到第i层走了右子树)        backtrack(i+1);//然后搜索下一层(右子树)    }}int main(){    while(cin>>t)    {        w=new int[t+1];//分配空间,从1开始计数        c=new int[t+1];        b=new int[t+1];        int sum=0;//用于求总和        for(int i=1;i<=t;i++)//读入物体重量        {            cin>>w[i];            sum+=w[i];//加到总和里        }        cin>>c1>>c2;//读入两船载重量        //输入检查:如果c1+c2都没有所有物体加和大,不可能装得下        if(c1+c2<sum)        {            cout<<"Case "<<++count<<endl;            cout<<"No"<<endl;            //释放空间            delete[] w;            delete[] c;            delete[] b;            continue;//直接进入下次循环        }        //每次初始化        cw=bestw=0;        ok=false;        //从第0层向第1层开始搜索        backtrack(1);        cout<<"Case "<<++count<<endl;        if(true==ok && sum-bestw<=c2)//如果对于c1而言能找到ok的解,并且剩下的物体不超过c2容量        {            cout<<bestw<<" ";            for(int i=1;i<=t;i++)                cout<<b[i];//把左1右0的子树选择序列输出,也就是c1的物品选择情况            cout<<endl;        }        else//否则,并不能放下            cout<<"No"<<endl;        //释放空间        delete[] w;        delete[] c;        delete[] b;    }return 0;}

这里写图片描述

最大团问题(子集树)

这里写图片描述

#include<iostream>using namespace std;int count=0;//全局计数char **v;//用来存二维邻接方阵int t;//方阵行列数int nown,maxn;//当前节点数和当前找到过的最大节点数bool *k;//选择序列:用来存搜索过程中的节点i是否放在了当前的图的节点集中void backtrack(int i)//回溯法:在一棵子集树解空间中深度搜索{    if(i>t)//超过叶节点层,说明找到了一个解    {        if(nown>maxn)//看一下会不会更大            maxn=nown;//更大就更新一下        return ;//返回    }    //考虑搜索左子树(即第i个节点放入的情况)    //约束条件剪枝:放入第i个节点后所得的图仍然是个团(完全图),才搜索左子树    bool ok=true;    for(int j=1;j<i;j++)//对i以前的每个节点j        if(true==k[j] && '0'==v[i][j])//如果出现了i和在当前团中的节点j没有边的情况        {            ok=false;//那么就不必考虑左子树了,因为放入i会破坏当前的'团'的性质            break;        }    if(true==ok)//如果可以搜索左子树,就搜索之    {        k[i]=true;//记录第i个放入了        nown++;//团的规模+1        backtrack(i+1);//向下一层进发考虑        nown--;//当回退到本层时,因为后面还要考虑搜索右子树,团的规模要减回来(即此时还没放入第i个)    }    //考虑搜索右子树(即第i个节点不放入的情况)    //边界条件剪枝:如果剩下的节点全都可以和当前构成团,都不会有更大的团,右子树就不必看了    if(t-i+nown>maxn)//仅当可能有更大的团时才做    {        k[i]=false;//记录第i个不放入        backtrack(i+1);//向下一层进发考虑    }return ;//左右子树都考虑完,该回退到上一层了}int main(){    while(cin>>t)    {        //不妨从1开始计数,分配1~t的方阵空间        v=new char*[t+1];        for(int i=1;i<=t;i++)            v[i]=new char[t+1];        //分配选择序列的空间        k=new bool[t+1];        //读入方阵        for(int i=1;i<=t;i++)            for(int j=1;j<=t;j++)                cin>>v[i][j];        /*        for(int i=1;i<=t;i++)        {            for(int j=1;j<=t;j++)                cout<<v[i][j]<<" ";            cout<<endl;        }*/        //初始化        nown=maxn=0;        //从第0层向第1层开始搜索        backtrack(1);        cout<<"Case "<<++count<<": "<<maxn<<endl;        //释放空间        for(int i=1;i<=t;i++)            delete[] v[i];        delete[] v;    }return 0;}

这里写图片描述

n皇后问题(排列树)

这里写图片描述

//约束①行不相同:直接让i号皇后就保持在第i行//约束②列不相同:让t个x[i]的值都不相同,且都在1~t之间,因为只做swap()所以约束②也自然满足//约束③不在同一斜线:abs(x[i]-x[j])!=abs(i-j)即斜率不为1或-1,该约束需要手动剪枝#include<iostream>#include<algorithm>//含有swap()#include<cmath>//含有abs()using namespace std;int t;//每次输入的规模int *x;//x[i]表示i号皇后放在第i行的第x[i]列,这样自然地约束了①int total;//解的个数int c=0;bool Place(int i)//关于③的可行性约束:看从i以前的(算是对后续而言暂时摆好了的皇后)是否都符合③{    for(int k=1;k<i;k++)//对i以前的每个(它们之间肯定不用检查了,因为之前的层已经检查过了)        if(abs(x[i]-x[k])==abs(i-k))//如果有和i号皇后在同一斜线的            return false;//说明i号皇后列在这里不可行,返回falsereturn true;//否则,i号皇后的列在这里是可行的}void backtrack(int i)//回溯法:在一棵排列树解空间中深度优先搜索{    if(i>t)//说明超过了叶节点,即找到了一个可行解        total++;//记录多了这个解    else//否则,还要继续往下搜索        for(int j=i;j<=t;j++)//对它和它后面行上的所有皇后        {            swap(x[j],x[i]);//两个皇后列列交换,这样这个位置(这一层)上就有了前面层的基础上的所有可能列            if(true==Place(i))//手动剪枝,去掉不满足约束③的                backtrack(i+1);//向下一层进发考虑            swap(x[j],x[i]);//回退到这一层时,需要把列号交换回来        }return ;//返回上一层}int main(){    while(cin>>t)    {        x=new int[t+1];//分配t+1个空间,即从1开始标号        for(int i=1;i<=t;i++)            x[i]=i;//初始化,第i个皇后放在第i行,这是一种符合①②的摆法        //初始化        total=0;        backtrack(1);//从第1个皇后,即第1行开始搜索排列树解空间        cout<<"Case "<<++c<<": n="<<t<<endl;        cout<<"Total: "<<total<<endl<<endl;        delete[] x;    }return 0;}

这里写图片描述

旅行商问题(排列树)

这里写图片描述

#include<iostream>#include<algorithm>using namespace std;int coun=0;//全局计数int t;//矩阵维度int **a;//邻接矩阵int *x;//x数组用来作排列树层次,存矩阵的游标int nowf;//当前费用int minf;//当前已经找到的最小费用bool Place(int i,int j)//可行性条件{    if(a[x[i-1]][x[j]]!=-1)//如果从i-1能进入j        if(-1==minf || nowf+a[x[i-1]][x[j]]<minf)//限界条件:剪枝            return true;return false;}void backtrace(int i)//回溯法遍历排列树解空间{    if(i==t)//如果到了最后一层,那么要看看能不能回来(上一层并没有加上至第t层的值,更没有考虑回到第1层的事)    {        if(a[x[t-1]][x[t]]!=-1 && a[x[t]][x[1]]!=-1)//如果从第n-1层能到n层,并且从第n层能到第1层            if(-1==minf || nowf+a[x[t-1]][x[t]]+a[x[t]][x[1]]<minf)//如果还没找到过最小,或者当前比之前的还小                minf=nowf+a[x[t-1]][x[t]]+a[x[t]][x[1]];//更新最小    }    else//如果没到最后一层    {        for(int j=i;j<=t;j++)//对于i及其后的x里存的所有游标            if(true==Place(i,j))//如果满足可行性条件            {                swap(x[j],x[i]);//交换,这样这个位置(这一层)上就有了前面层的基础上的所有可能                nowf+=a[x[i-1]][x[i]];//因为这次选择了x[i-1]->x[i]这条路,把它加进来                backtrace(i+1);//搜索下一层                nowf-=a[x[i-1]][x[i]];//回退以后减回来                swap(x[j],x[i]);//回退之后交换回来            }    }return ;//返回上一层}int main(){    while(cin>>t)    {        //开空间:下标从1开始计数        a=new int*[t+1];        for(int i=1;i<=t;i++)            a[i]=new int[t+1];        x=new int[t+1];        //初始化        nowf=0;        minf=-1;        for(int i=1;i<=t;i++)            for(int j=1;j<=t;j++)                cin>>a[i][j];        //就以第1个为起始点即可,毕竟要形成一个环,以谁开始都一样        for(int j=1;j<=t;j++)            x[j]=j;//先1,2,3,4,..这样排列        //回溯法遍历排列树解空间        backtrace(2);//从1->2层开始考虑        //输出        cout<<"Case "<<++coun<<endl;        if(-1==minf)            cout<<"No loop"<<endl;        else            cout<<minf<<endl;        //释放空间        delete[] x;        for(int i=1;i<=t;i++)            delete[] a[i];        delete[] a;    }return 0;}

这里写图片描述

原创粉丝点击