回溯算法小结

目录

回溯算法

基本思想

 子集树与排列树

(1)子集树

 (2)排列树

例题

1.装载问题

2.0-1背包问题


回溯算法

回溯法有“通用的解题法”之称,可以系统地搜索一个问题的所有解或任意解,既有系统性又有跳跃性。

适用范围:当需要找出它的解集。或者回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。

基本做法:搜索或者一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。

这种以深度优先的方式系统地搜索问题的解的方法称为回溯法。

基本思想

  • 问题的解空间应至少包含问题的一个(最优)解。
  • 生成解空间树
  • 在确定了空间的组织结构后,回溯从根结点出发,以深度优先的方式搜索整个解空间。

在回溯法搜索解空间树时,通常采用两种策略(剪枝函数)避免无效搜索,提高回溯法的搜索效率:

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

例如,对于有n种可选择物品的0-1背包问题,其解空间由长度为n的0-1向量组成,该解空间包含了对变量的所有可能的0-1赋值。

 子集树与排列树

有时问题是要从一个集合的所有子集中搜索一个集合,作为问题的解。或者从一个集合的排列中搜索一个排列,作为问题的解。

  • 回溯算法可以很方便地遍历一个集合的所有子集或者所有排列。

(1)子集树

当问题是要计算n个元素的子集,以便达到某种优化目标时,可以把这个解空间组织成一棵子集树。

这类子集树通常有2n个叶结点,结点总数为2^(n+1)-1。

遍历子集树的任何算法,其计算时间复杂度都是Ω(2^n)。

  • n个物品的0-1背包问题相应的解空间树就是一棵子集树。

假设背包容量C=30,w={16,15,15},v={45,25,25}

H、I是可行性剪枝:D不可行,D以下的H、I自然也不可行

D、N、O是最优性剪枝:C左子树的L(叶子)可行,而L比C右子树的价值高

回溯算法搜索子集树的伪代码:

//形参t为树的深度,根为1
void backtrack (int t)
{
  if (t>n) update(x);
  else
    for (int i=0; i<=1; i++)  //每个结点只有两个子树
    {
      x[t]=i;         //即0/1
      if (constraint(t) && bound(t)) backtrack(t+1);
    }
}
  • 约束函数constraint(t)和限界函数bound(t),称为剪枝函数:前者是左子树的,后者是右子树的
  • 函数update(x):与先前最优解比较,用来更新解向量x。

 (2)排列树

当所给的问题是确定n个元素满足某种性质的排列时,可以把这个解空间组织成一棵排列树。

排列树通常有n!个叶子结点。

遍历排列树时,其计算时间复杂度是Ω(n!) 。 

  •  旅行商问题的解就是排列树

旅行商从n个城市中的某一城市出发,经过每个城市仅有一次,最后回到原出发点,在所有可能的路径中求出路径长度最短的一条。

 设G=(V, E)是一个带权图,其每一条边(u, v)∈E的权为正数w(u, v)。目的是要找出G的一条经过每个顶点一次且仅经过一次的回路,即汉密尔顿(Hamilton)回路v1,v2 ,…,vn ,使回路的总权值最小:

回溯法找最小费用周游路线的主要过程

4个城市,因为起始城市以及确定,因此有(4-1)!=6种路线 

 如果从根结点到当前扩展结点的部分周游路线的费用已超过当前找到的最好周游路线费用,则以该结点为根的子树中不包括最优解,就可以剪枝。

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

//形参t为树的深度,根为1
void backtrack (int t)
{
  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]);    //恢复状态
    }
}

例题

1.装载问题

(1)描述

给定n个集装箱要装上一艘载重量为c的轮船,其中集装箱i的重量为wi。要求在不超过轮船载重量的前提下,装入集装箱重量最大。

集装箱问题是从n个集装箱里选择部分集装箱,假设解向量为X(x1, x2, …, xn),其中xi∈{0, 1}, xi =1表示集装箱i装上轮船, xi =0表示集装箱i不装上轮船。

输入 :

第1行有2个整数c和n。C是轮船的载重量(0<c<30000),n是集装箱的个数(n≤20)

第2行有n个整数w1, w2, …, wn,分别表示n个集装箱的重量。

输出:

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

(2)思路

该问题可表示为:

用回溯法解装载问题时,其解空间是一棵子集树。

  •  可行性约束函数:剪去不满足约束条件的子树

  •  令cw(t)表示从根结点到第t个结点(集装箱)为止装入轮船的重量,即部分解(x1, x2 , …, xt)的重量:

  • 当cw(t)>c时,表示该子树中所有结点都不满足约束条件,可将该子树剪去。

(3)代码

#include <iostream>
#include <stdio.h>
using namespace std;
#define NUM 100

int n;//集装箱数
    int c;//轮船可载重
    int w[NUM];//各个集装箱的重量
    int x[NUM];//当前搜索的解向量
    int r;//剩余集装箱重量
    int cw;//当前轮船装了多重
    int bestw;//当前最优载重
    int bestx[NUM];//当前最优解

//考虑t号集装箱
void BackTrack(int t){
    if(t>n){//集装箱考虑完了,当前是最后一个
        if(cw>bestw){
            for(int i=1;i<=n;i++)
                bestx[i]=x[i];
            bestw=cw;
        }
        return;
    }

    r-=w[t];//正要考虑t号集装箱装不装,先把t从r中减去

    //搜索左子树,想装入t号集装箱
    if(cw+w[t]<=c){
        x[t]=1;
        cw+=w[t];
        BackTrack(t+1);
        cw-=w[t];//恢复左总枝
    }

    //搜索右子树,不想装入t号集装箱
    //如果cw+r<=bestw,就没有必要再看t+1个结点了
    if(cw+r>bestw){
        //r=t+1~n号集装箱的总重
        x[t]=0;
        BackTrack(t+1);
    }

    r+=w[t];//考虑好了装不装t号集装箱,再把r恢复

}
int main()
{
    r=0;
    scanf("%d%d",&c,&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&w[i]);
        r+=w[i];
    }

    BackTrack(1);
    printf("%d\n",bestw);
    for(int i=1;i<=n;i++)
        if(bestx[i])
            printf("%d ",i);
    return 0;
}

2.0-1背包问题

(1)描述

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

输入:

第一个数据是背包的容量为c(1≤c≤1500),第二个数据是物品的数量为n(1≤n≤50)。接下来n行是物品i的重量是wi,其价值为vi。所有的数据全部为整数,且保证输入数据中物品的总重量大于背包的容量。 当c=0时,表示输入数据结束。

输出:

对每组测试数据,输出装入背包中物品的最大价值。

(2)思路

  • 令cw(i)表示目前搜索到第i个已经装入背包的物品总重量,即部分解(x1, x2 , …, xi)的重量:
  •  对于左子树,xi =1,其约束函数为:
      • 若constraint(i)>W,则停止搜索左子树,否则继续搜索
  • 对于右子树,采用上界函数Bound(i)剪枝。
    • cv(i)表示目前到第i层结点已经装入背包的物品价值
    • r(i)表示剩余物品的总价值

假设当前最优值为bestv,若Bound(i)<bestv,则停止搜索第i层结点及其子树,否则继续搜索。r(i)越小, Bound(i)越小,剪掉的分支就越多。

为了构造更小的r(i) ,按性价比di=vi/wi递减的顺序进行排列

对于第i个物品,背包的剩余容量为W-cw(i),采用贪心算法把剩余的物品放进背包。

(3)代码

#include <iostream>
#include <stdio.h>
#include <algorithm>
using namespace std;
#define NUM 100

int c;  //背包容量
int n;  //物品数量
int cw; //已经装入背包的总重量
int cv; //已经装入背包的总价值
int bestv;//当前最优价值

struct Object{
    int w;
    int v;
    double d;//性价比
}q[NUM];

bool cmp(Object a,Object b){
    if(a.d>=b.d)
        return true;
    return false;
}

int Bound(int i){
    int cleft=c-cw;//背包剩余容量
    int b=cv;

    while(i<n&&q[i].w<=cleft){
        cleft-=q[i].w;
        b+=q[i].v;
        i++;
    }

    if(i<n)
        b+=1.0*cleft*q[i].v/q[i].w;
    return b;
}

void BackTract(int i){
    if(i+1>n){
        if(cv>bestv)
            bestv=cv;
        return;
    }
    //搜索左子树
    if(cw+q[i].w<=c){
        cw+=q[i].w;
        cv+=q[i].v;
        BackTract(i+1);
        //恢复现场
        cw-=q[i].w;
        cv-=q[i].v;
    }
    //搜索右子树
    if(Bound(i+1)>bestv)
        BackTract(i+1);

}


int main()
{
    scanf("%d%d",&c,&n);
    for(int i=0;i<n;i++){
        scanf("%d%d",&q[i].w,&q[i].v);
        q[i].d=1.0*q[i].v/q[i].w;
    }
    sort(q,q+n,cmp);

    for(int i=0;i<n;i++)
        BackTract(i);
    return 0;
}

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值