算法整理八——回溯算法

目录

一、概述

二、素数环问题

三、n个数中取出任意r个数进行排列

四、整数划分问题/自然数拆分(递归方法也可以实现,见递归部分)

五、装载问题(最大装载量)

六、0-1背包问题

七、n皇后问题

八、子集和问题


一、概述

  1. 以深度优先的方式系统地搜索问题的解的方法称为回溯法。
  2. 回溯算法是深度优先策略的典型应用,回溯算法就是沿着一条路向下走,如果此路不通了,则回溯到上一个分岔路,再选一条路走,一直这样递归下去,直到遍历完所有的路径。
  3. 回溯算法的解空间结构可以组成成子集树0-1背包问题、全排列)、排列树。会画出解决某个问题的解空间树,比如会画出描述3个物品的背包问题的解空间树等

旅行商问题就是一颗排列数

4.使用回溯法解题,通常包括的三个步骤

(1)针对所给问题,定义问题的解空间;

(2)确定易于搜索的解空间结构;

(3)以深度优先的方式搜索解空间,并且在搜索过程中使用剪枝函数避免无效搜索。

5.在回溯法搜索解空间树时,通常采用剪枝函数(包括约束函数和限界函数)避免无效搜索以提高回溯法的搜索效率。针对0-1背包问题,可以定义哪些剪枝规则,并能对这些剪枝规则进行简单描述。

  • 用约束函数在扩展结点处剪去不满足约束条件的子树;
  • 用限界函数剪去不能得到最优解的子树。

6.回溯算法搜索子集树的伪代码(之一)

void backtrack (int t){  //形参t为树的深度,根为1

if (t>n)

update(x);   //扩展到叶子结点,得到了一组解决方案

else  

for (int i=0; i<2; i++) //每个结点有2个子树

{

x[t]=i;// 即0/1,表示第  t个元素是否是可选元素

if (constraint(t) && bound(t)) //判断当前结点是否是扩展结点

 backtrack(t+1); //对当前结点进行扩展

}   }

7.回溯算法搜索子集树的伪代码(之二)

void backtrack (int t){  //形参t为树的深度,根为1

for (int i=0; i<2; i++){ //每个结点有n个子树

if (t>n) {

update(x);   //扩展到叶子结点,得到了一组解决方案

break;

           }

x[t]=i;     //即0/1,表示第  t个元素是否是可选元素

if (constraint(t) && bound(t)) //判断当前结点是否是扩展结点

 backtrack(t+1); //对当前结点进行扩展

 }

}

8.回溯算法搜索排列树的伪代码

void backtrack (int t){//形参t为树的深度,根为1

if (t>n)

             update(x); //得到了一个全排列,对排列结果进行更新

else

for (int i=t; i<=n; i++){

//为了保证排列中每个元素不同,通过交换 来实现

swap(x[t], x[i]);

if (constraint(t) && bound(t))   

                         backtrack(t+1);

swap(x[t], x[i]); //恢复状态

}

}

9.回溯算法解决:素数环问题、n个数中取出任意r个数进行排列、自然数拆分(递归方法也可以实现,见递归部分)、装载问题(最大装载量)、0-1背包问题、n皇后问题、子集和问题

知道图的m着色问题、旅行商问题、流水作业调度问题可以使用回溯法解决即可。

二、素数环问题

素数环:从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数。

分析:

从1开始,每个空位有20种可能,只要填进去的数合法:

1、与前面已经填过的数不相同;

2、与左边相邻的数的和是一个素数。

3、第20个数还要判断和第1个数的和是否素数

子集数—全排列

算法流程:

1、数据初始化(布尔数组,标记某个数是否已经填入到素数环中);  

2、递归填数:判断第i个数填入是否合法;

A、如果合法(剪枝条件):

                 填数;判断是否到达目标(20个已填完):

是,打印结果;

                     不是,递归填下一个;

  1. 如果不合法:选择下一种可能;

代码:

  1. 子集数:
void search(int t)//寻找所有解

{

    int i;

    for(i=1;i<=20;i++)

    {

        if(pd(a[t-1],i)&&(!b[i])) //与前一个数是否构成素数及该数是否可用



        {

            a[t]=i;//i进入素数环

            b[i]=1;//标志1

            if(t==20)//到最后了,得到一个解

            {

                if(pd(a[20],a[1]))//判断最后一个和第一个

                    print();

            }

            else

                search(t+1);

            b[i]=0;

        }

    }

}
  1. 排列数:
void search(int t)//寻找所有解

{

    int i;

    for(i=t;i<=20;i++)

    {

        swap(a[t],a[i]);

        if(pd(a[t-1],a[t]))

        {

            if(t==20)

            {

                if(pd(a[20],a[1]))

                    print();

            }

            else

                search(t+1);

        }

        swap(a[t],a[i]);

    }

}

完整代码:

#include <iostream>

#include<algorithm>

#include<cstring>

#include<cmath>

using namespace std;

bool b[21]={0};//标志i是否出现在素数环中

int total=0,a[21]={0};//a记录素数环中的每一个数

void search(int t);//回溯过程。形参表示素数环中的数的编号

void print();//输出方案

bool pd(int,int);//判断两数之和是不是素数



void search(int t)//寻找所有解

{

    int i;

    for(i=t;i<=20;i++)

    {

        swap(a[t],a[i]);

        if(pd(a[t-1],a[t]))

        {

            if(t==20)

            {

                if(pd(a[20],a[1]))

                    print();

            }

            else

                search(t+1);

        }

        swap(a[t],a[i]);

    }

}

void print()

{

    total++;

    cout<<"<"<<total<<">";

    for(int j=1;j<=20;j++)

        cout<<a[j]<<" ";

    cout<<endl;

}

bool pd(int x,int y)

{

    int k=2,i=x+y;

    while(k<=sqrt(i)&&i%k!=0)

        k++;

    if(k>sqrt(i))

        return 1;

    else

        return 0;

}

int main()

{

    for(int i=1;i<=20;i++)

        a[i]=i;

    search(1);

    cout<<total<<endl; //输出总方案数

    return 0;



}

三、n个数中取出任意r个数进行排列

设有n个整数【1-n】的集合{1,2,…,n},从中取出任意r个数进行排列(r<n),试列出所有的排列

输入:n r

输出:方案数

代码:

#include <iostream>

#include<algorithm>

#include<cstring>

#include<cmath>

using namespace std;

int num=0,a[101]={0},n,r;//从1-n,取r个数

bool b[101]={0};//标志

//void search(int ,int);//回溯过程

void print();//输出方案

int search(int k,int start) //两个参数。k代表组合中的第几个数,start从哪个位置开始选择数字

{

for(int i=start;i<=n;i++)

 {

  a[k]=i;//保存结果

  if (k==r)

            print();

        else

            search(k+1,i+1);

 }

}

void print()

{

    num++;

    for(int j=1;j<=r;j++)

        cout<<a[j]<<" ";

    cout<<endl;

}

int main()

{

   cin>>n>>r;

   search(1,1);

   cout<<num<<endl;//输出方案总数

}

四、整数划分问题/自然数拆分(递归方法也可以实现,见递归部分)

任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。

#include <iostream>

#include<algorithm>

#include<cstring>

#include<cmath>

using namespace std;

int total,a[101]={1},n;

int search(int s,int t);//表待拆分的数; 序列中t 代表拆分序列中的数的编号

int print(int);//输出方案

int search(int s,int t)

{

int i;

for(i=a[t-1];i<=s;i++)

        if(i<n)当前数i要大于等于前1位数,且不过n

    {

        a[t]=i;//保存当前拆分的数i

        s-=i;//s减去数i, s的值将继续拆分

        if(s==0)

            print(t);  //当s=0时,拆分结束输出结果

        else

            search(s,t+1);//当s>0时,继续递归

        s+=i;//回溯:加上拆分的数,以便产生所有可能的拆分

    }

}

int print(int t)

{

    cout<<n<<"=";

    for(int i=1;i<=t-1;i++)

        cout<<a[i]<<"+";

    cout<<a[t]<<endl;

    total++;

}

int main()

{

   cin>>n;

   search(n,1);//将要拆分的数n传递给s

   cout<<total<<endl;//输出方案总数

}

五、装载问题(最大装载量)

给定n个集装箱要装上一艘载重量为c的轮船,其中集装箱i的重量为wi。集装箱装载问题要求确定在不超过轮船载重量的前提下,将尽可能多的集装箱装上轮船(贪心算法中的装载问题讨论的是装载件数;本题讨论的是最大装载重量。)

输入:

每组测试数据:第1行有2个整数c和n。c是轮船的载重量(0<c<30000),n是集装箱的个数(n≤20)。第2行有n个整数w1, w2, …, wn,分别表示n个集装箱的重量。

输出:

对每个测试例,输出两行:第1行是装载到轮船的最大载重量,第2行是集装箱的编号。

分析:

假设解向量为X(x1, x2, …, xn),其中xi∈{0, 1}, xi =1表示集装箱i装上轮船, xi =0表示集装箱i不装上轮船。

用回溯法解装载问题时,其解空间是一棵子集树,与0 - 1背包问题的解空间树相同。

int cw,   当前的轮船的荷载

int bestcw ,  当前的最大荷载,荷载到达叶子结点才会更新

int r ;  剩余的未考察的集装箱重量和

约束函数剪枝:cw>c

限界函数剪枝:cw+r<bestw

不明白为什么关于这个问题的的解决方案大都用了类

方法一:课上方法,函数参数太多,有点乱

#include <iostream>

#include<algorithm>

#include<cstring>

#include<cmath>

using namespace std;



class goods{

    int weight;//集装箱重量

public:

    goods(int w=0):weight(w)

    {}

    int get_w()

    {

        return weight;

    }

    void set(int w)

    {

        weight=w;

    }

};

void load(goods *g, int *x, int t, int n,int cw, int &bestcw ,int *best,int r,int c)//轮船载重

//goods *g,集装箱列表,*x,满足当前最大荷载的装载方案,t,子集树的层号,n,集装箱的总数

// cw, 当前的轮船的荷载,bestcw ,当前的最大荷载, *best,待求解的最优装载方案;r,剩余的未考察的集装箱重量和

{

    if(t==n)//已经遍历的到叶子结点

    {

        if(cw>bestcw)//当前的轮船的荷载>当前的最大荷载

        {

            for(int i=0;i<n;i++)

            {

                best[i]=x[i];

            }

             bestcw=cw;

        }

    }

    else

        {

            r=r-g[t].get_w();//剩余未处理的物品的重量和,与是否选取当前物品无关

            if(cw+g[t].get_w()<=c)//约束条件进行剪枝

            {

                x[t]=1;

                cw=cw+g[t].get_w();//当前装入的物品的重量和

                load(g,x,t+1,n,cw,bestcw,best,r,c);

                cw=cw-g[t].get_w(); //回溯的需要

            }

            if(cw+r>bestcw)//限界规则

            {

                x[t]=0;

                load(g,x,t+1,n,cw,bestcw,best,r,c);

            }

            r=r+g[t].get_w(); //回溯的需要

        }

    }





int main()

{

    int n,c,bestcw=0;

int *x,*best, r=0;

cout<< "请输入轮船的装载重量和物品的件数:";

cin>>c>>n;

goods *g;

g=new goods[n];

x=new int [n];

best=new int[n];

cout<<"请输入每件物品的重量:";

for(int i=0;i<n;i++)

    {

int w;

        cin>>w;

        g[i].set(w);

        r=r+w;

}

load(g,x,0,n,0,bestcw,best,r,c);

cout<<bestcw<<endl;

for(int i=0;i<n;i++)

cout<<best[i]<<"  ";

cout<<endl;

return 0;

}

方法二:如果用类,为什么不直接全部使用类,方法单独定义,这样就不用传那么多参数了,两个方法其实是一样的,但是这样看起来是不是简洁好理解一点了

#include<bits/stdc++.h>

using namespace std;



int bestx[101];//满足当前最大荷载的装载方案

class Loading

{

int MaxLoading(int[],int,int);//初始化的函数

public:

void Backtrack(int i);//回溯

int n;//集装箱数

int* w,//集装箱重量数组

c,//轮船的载重量

cw,//当前载重量,当前的轮船的荷载

bestw,//当前的最大荷载,当前最优载重量

r;//剩余未考察的集装箱重量

};



void Loading::Backtrack(int i)

{

//搜索第i层结点

if(i>n)//到达叶节点

{

if(cw>bestw)

bestw=cw;//更新最优解

return;

}

r-=w[i];//修改剩余集装箱重量

if(cw+w[i]<=c)//约束函数进行剪枝

{

cw+=w[i];//装入

bestx[i]=1;

Backtrack(i+1);

cw-=w[i];//回溯

}

if(cw+r>bestw) //限界规则

{

Backtrack(i+1);

bestx[i]=0;

r+=w[i];//修改剩余集装箱重量

}

}



int MaxLoading(int w[],int c,int n)

{

//初始化

Loading X;

X.w = w;

X.c = c;

X.n = n;

X.bestw = 0;

X.cw = 0;

//初始化

X.r = 0;

for(int i=1;i<=n;i++)

{

X.r+=w[i];

}

//初始时的r为全体物品重量和

X.Backtrack(1);//从第一层开始搜索

return X.bestw;

}



int main()

{

int c,n,m;

cin>>c>>n;

int w[101];

for(int i=1;i<=n;i++)

        cin>>w[i];

    m=MaxLoading(w, c, n);

cout<<"最大装载量为:"<<endl;

cout<<m<<endl;

for(int i=1;i<=n;i++)

{

cout<<"x["<<i<<"]:"<<bestx[i]<<endl;

}



return 0;

}



六、0-1背包问题

给定一个物品集合s={1,2,3,…,n},物品i的重量是wi,其价值是vi,背包的容量为W,即最大载重量不超过W。在限定的总重量W内,我们如何选择物品,才能使得物品的总价值最大。

输入:

第一行是背包容量 c,物品个数n

接下来n行是物品i的重量是wi,其价值为vi

输出:

输出装入背包中物品的最大价值。

分析:0-1背包问题(子集数)

  • 令cw(i)表示目前搜索到第i层已经装入背包的物品总重量
  • 令cv(i)表示目前到第i层结点已经装入背包的物品价值
  • 对于左子树, xi =1 ,其约束函数为:construction(i)=cw(i-1)+wi。若constraint(i)>W,则停止搜索左子树,否则继续搜索。
  • 对于右子树,r(i)表示剩余物品的总价值,限界函数Bound(i)=cv(i)+r(i)
  • r(i)越小, Bound(i)越小,剪掉的分支就越多(越靠近树根剪枝,剪掉的分支越多。从而能加快搜索速度)。
  • 为了构造更小的r(i) ,将物品以单位重量价值比di=vi/wi递减的顺序进行排列(贪心策略)

代码:

#include<bits/stdc++.h>

using namespace std;



int c,n;//背包容量、物品数量

int cw,cv;//背包当前重量,当前价值

int bestv;//当前最优价值

struct bag

{

    int w; //物品的重量

int v; //物品的价值

double d; //物品的单位重量价值比

}a[101];

bool cmp(bag a,bag b)//按照价值比从大到小排序

{

    if(a.d>=b.d)

        return true;

    else

        return false;

}

int Bound(int i)//限界函数

{

    int cleft=c-cw;//背包剩余的容量

    int b=cv;//上界

    //尽量装满背包

    while(i<n&&a[i].w<=cleft)

    {

        cleft-=a[i].w;

        b+=a[i].v;

        i++;

    }

    /*剩余的部分空间也装满。0-1是可能装不满的。

    但此处主要计算最大值,所以,需要从装满的角度考虑该问题

    (尽管这不是一个可行解,但可以证明其价值是最优值的上界)

    */

    if(i<n)

        b+=1.0*cleft*a[i].v/a[i].w;

    return b;

}

int ff(int i)//形参i是回溯的深度,从0开始.商品编号从0开始编号

{

    if(i==n)

    {

         bestv=cv;//当前最优价值=当前价值

        // return ;

    }

    //进入左子树搜索, 表示选择第i件物品

    if(cw+a[i].w<=c)//满足约束条件

    {

        cw+=a[i].w;//选择第i件物品

        cv+=a[i].v;

        ff(i+1);

        cw-=a[i].w;//回溯的需要

        cv-=a[i].v;//回溯需要

    }

    //进入右子树搜索,表示不选择第i件物品,相关的cw, cv不改变

    if(Bound(i+1)>bestv)//大于当前最优价值,满足条件不剪枝

    {

        ff(i+1);

    }

}

int main()

{

    cin>>c>>n;

    for(int i=0;i<n;i++)

    {

        cin>>a[i].w>>a[i].v;

        a[i].d=1.0*a[i].v/a[i].w;

    }

    sort(a,a+n,cmp);

    ff(0);

    cout<<bestv<<endl;

}

七、n皇后问题

在n×n格的棋盘上放置彼此不受攻击的n个皇后。

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n皇后问题等价于在n×n格的棋盘上放置n个皇后,任何两个皇后不放在同一行或同一列或同一斜线上。

编程要求:找出一个n×n格的棋盘上放置n个皇后并使其不能互相攻击的所有方案。

输入样例:5【皇后个数】

输出样例:

1 3 5 2 4

1 4 2 5 3

2 4 1 3 5

2 5 3 1 4

3 1 4 2 5

3 5 2 4 1

4 1 3 5 2

4 2 5 3 1

5 2 4 1 3

5 3 1 4 2

Total= 10

【输出样例含义:拿第一个来说,第一例的1,第二列的3,第二列的5.......】

分析:

由于棋盘的每列/行只有一个皇后,所以可以用一维向量X( x1, x2, …, xn),其中xi∈{1, 2, …, n},表示第i列皇后所在的行x[i]。解空间的每个结点都有n个儿子,因此解空间的大小为nn,这是一棵子集树

同一对角线判断:

#include<bits/stdc++.h>

using namespace std;



int n;//棋盘大小

int x[101];//解向量

int sum=0;//当前已经找到的可行方案数



inline bool Place(int t)//判断是否在同一行,同一列,同一对角线

{

    int i;

    for(i=1;i<t;i++)

    {

        if ((abs(t-i) == abs(x[i]-x[t])) || (x[i] == x[t]))

            //判断是否在同一对角线、同一行

            return false;

    }

    return true;

}



void Back(int t)//t是回溯的深度,从1开始

{

    int i;

    if(t>n)//到达叶子结点

    {

        sum++;//方案数+1

        for(i=1;i<=n;i++)

            cout<<x[i]<<" ";

        cout<<endl;

    }

    else

    {

        for(i=1;i<=n;i++)

        {

            x[t]=i;

            if(Place(t))//判断是否符合条件

                Back(t+1);

        }

    }

}



int main()

{

    cin>>n;

    Back(1);

    cout<<sum<<endl;

}

八、子集和问题

子集和问题的一个实例为<S,c>。其中,S={w1, w2, …, wn}是一个正整数的集合,c是一个正整数。子集和问题判定是否存在S的一个子集S1,使得S1的和为c。

编程任务:对于给定的正整数集合S={w1, w2, …, wn}和正整数c,编程计算S的一个子集S1,使得S1的和为c。

输入:

第一行有2个正整数n和c,n表示S的大小,c是子集和的目标值。接下来的一行,有n个正整数(1≤n≤10000),表示集合S中的元素。

输出:

输出子集和问题的全部解,每个数据后面都有一个空格。当问题无解时,输出“No Solution!”

分析:用回溯法求解子集和问题,与0-1背包问题类似,解空间树是一棵子集树。

代码:

#include<bits/stdc++.h>

using namespace std;



class Sum{

    int n; //集合中数据的个数

int *set;//集合

int *x;//解向量,当x[i]=1表示第i 个元素属于子集,否则,x[i]=0;

int c;//输入的条件。要计算的子集和

int s;//正在构造的子集中元素的和

int r;//不在子集中的未考察的其他数据元素之和;

int total;//记录符合条件的子集的个数

public:

    Sum(int n=0,int c=0);//构造函数

    void calculate(int t);//计算

    void display();

    int get_T()//方案总数

    {

        return total;

    }

};

Sum::Sum(int n,int c)

{

    this->n=n;

    this->c=c;

    x=new int [n+1];

    set=new int [n+1];

    cout<<"请输入集合中的元素"<<endl;

    r=0;

    total=0;

    for(int i=1;i<=n;i++)

    {

        cin>>set[i];

        x[i]=0;

        r=r+set[i];

    }

    s=0;

}

void Sum::calculate(int t)

{

    if(t>n)

    {

        if(c==s)

        {

            display();

            total++;

        }

    }

    else{

        r=r-set[t];

        if(s+set[t]<=c)//约束规则

        {

            x[t]=1;

            s=s+set[t];

            calculate(t+1);

            s=s-set[t];

            x[t]=0;

        }

        if(s+r>=c)//限界函数

            calculate(t+1);

        r=r+set[t];

    }

}

void Sum::display()

{

    int i;

for(int i=1;i<=n;i++)

if(x[i]==1)

        cout<<set[i]<<" ";

cout<<endl;

}

int main()

{

int n,c;

cout<<"输入n 和c 的值:";

cin>>n>>c;

Sum s(n,c);

s.calculate(1);

if(s.get_T()==0)

cout<<"NO Solution!!"<<endl;

else

cout<<"方案数="<<s.get_T()<<endl;



return 0;

}

这几章不好懂,大家一起加油呀!!! 

  • 11
    点赞
  • 125
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

季沐晴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值