• 关于01背包的动态规划,回溯和分支限界法的一些分析和代码


    首先,说一下什么是0-1背包问题。

      有N件物品和一个容量为V的背包。(每种物品均只有一件)第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

      特点是:每种物品仅有一件,可以选择放或不放。 

    然后,我分别用了动态规划法,分支限界法和回溯法来解决这个问题。

    //=======================================

    以下是测时间的模板:
     

        LARGE_INTEGER BegainTime ;
    LARGE_INTEGER EndTime ;
    LARGE_INTEGER Frequency ;
    QueryPerformanceFrequency(&Frequency);
    QueryPerformanceCounter(&BegainTime) ;

    //要测试的代码放在这里
    DP();

    QueryPerformanceCounter(&EndTime);
    //输出运行时间(单位:s)

    cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl;


        

     随机数据的测量结果:

    当数据量为20的物品数和200的背包容量的时候。动态规划快于排序优化的回溯法快于回溯法快于分支限界法。

    (可以根据输出的随机数据判断时候得到正确的算法)

    当数据量加大到40的物品数和400的背包容量的时候。此时分支限界法和动态规划法的效率依旧不错,时间增长的并不是太多。而回溯法和排序加速的回溯法则时间上升很快。

    (为截图方便,此次将随机生成的数据不输出)

     

    以下进行算法分析

    //========================================

    首先说下动态规划法:

    用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是: 

    f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

    把这个过程理解下:在前i件物品放进容量v的背包时, 

    它有两种情况: 

    第一种是第i件不放进去,这时所得价值为:f[i-1][v] 

    第二种是第i件放进去,这时所得价值为:f[i-1][v-c[i]]+w[i] 

    (第二种是什么意思?就是如果第i件放进去,那么在容量v-c[i]里就要放进前i-1件物品) 

    最后比较第一种与第二种所得价值的大小,哪种相对大,f[i][v]的值就是哪种。 

    (这是基础,要理解!) 

          当然,这只是比较容易理解的一种,时间上似乎没有什么办法继续优化了,但是空间上却可以进一步优化。可以用一维数组实现。

    关于一维数组的实现和讲解在这篇blog里有讲,讲的非常详细易懂。

    本文为了输出所选择的物品的编号,还是选择了二维数组。实现起来非常方便(详见代码及注释)。

    //========================================

    接着说回溯法:

          首先说一下解空间树(也称状态空间树):

     

    树的根节点位于第1层,表示搜索的初始状态。

    第2层的节点表示对解向量的第一个分量做出选择后达到的状态,第1层到第2层的边上标出对第一个分量选择的结果。

    ……

    所有从根到叶子节点的路径就构成了解空间的一个可能解。

    例子:

    三个物品重量:20, 15, 10。

    三个物品价值:20, 30, 20。

     

    参照下图理解:  0表示不选第i个物品,1表示选择(i表示层数)。

     

     

    图和文字选自这里

    这不重点,只是为了引出回溯法,若还有些不理解的,可以自己去Google下(个人更喜欢用这个),后面的理解要建立在这个基础上。

    在回溯法之前,还有一点准备活动——蛮力法。

    所谓蛮力法,是对整个解空间树中的所有可能的解进行穷举搜索的一种方法(即每个叶子都代表一种解)。

    但是,我们可以对其进行剪枝。

    剪枝,就是在搜索至树中的任意一个结点时,先判断该结点对应的部分是否满足约束条件,或者是否超出目标函数的值,也就是判断该结点是否包含问题的(最优)解,如果肯定不包含,则跳过对以该结点为根的子树的搜索。

    以下对照图和上图对比理解。

    其实,把整个解空间树当成一棵真正的树(倒过来看),剪枝这个词的由来和含义就比较好理解了。

    好了,这就是回溯法,也就是深度优先搜索代(码中递归实现和输出选择的物品数)。

    其实想想,回溯法也就是对一颗搜索树进行剪枝而来的。

    回溯法关于0-1背包的再次优化

    当然,最朴素的回溯法已经包含了一定的剪枝,那有没有什么办法,让回溯法在不进行大幅度改动的情况下进一步提高效率呢——答案是肯定的,是的,排序。

    在理解上面回溯法的基础上,其实可以比较容易的想到,对体积进行从小到大的排序,注意,是体积,后面的分支限界法用的是单位价值。

    对体积排序之后,当小体积的放入已经超出背包容量的时候,自然在其后的稍微大一点或者相等体积的都不用再次尝试了。因此,可以进一步的减少运算量,而实际生成的随机数据此种优化方法速度快了一半以上(由于回溯法时间复杂度的原因,未能进行大一些的随机检查,但是,很明显的趋势是,数据越多,体积排序后的时间优化效果越明显(从代码中也能看出,几乎和普通的代码量和操作难度一样)。

    //========================================

    最后出场的是分支限界法:

    虽然回溯法求0-1背包问题,运用剪枝已经极大的减少了搜索空间,但是整个空间都是按深度优先搜索策略机械地进行,这种搜索太过盲目,能不能让电脑像人一样的思考呢?

    这里,我推荐一个讲A*算法的blog

     

    懂分支限界法思想的朋友可以继续看这个例子,不懂的可以点这里

     

    其实分支限界法理解了也很好实现,举一个《算法设计与分析》上的例子。

     

    例:0/1背包问题。假设有4个物品,其重量分别为(4, 7, 5, 3),价值分别为(40, 42, 25, 12),背包容量W=10。首先,将给定物品按单位重量价值从大到小排序,结果如下:

     

     

    我们使用的启发式函数为

     

    通过这个启发式函数得到的一个解空间树如下图:

     

     

    可以对照一下步骤具体的搜索过程如下:(红色表示我的代码实现)

     

    1)在根结点1,没有将任何物品装入背包,因此,背包的重量和获得的价值均为0,根据限界函数计算结点1的目标函数值为10×10=100

      (计算完之后推入队列,作为起始点)。

    2)在结点2,将物品1装入背包,因此,背包的重量为4,获得的价值为40,目标函数值为40 + (10-4)×6=76,将结点2加入待处理结点表PT中;在结点3,没有将物品1装入背包,因此,背包的重量和获得的价值仍为0,目标函数值为10×660,将结点3加入表PT中;

      (推出结点1,对选择和不选择物品1分别计算ub值,并推入队列)

    3)在表PT中选取目标函数值取得极大的结点2优先进行搜索;

      (出队ub值大的点)

    4)在结点4,将物品2装入背包,因此,背包的重量为11,不满足约束条件,将结点4丢弃;在结点5,没有将物品2装入背包,因此,背包的重量和获得的价值与结点2相同,目标函数值为40 + (10-4)×5=70,将结点5加入表PT中;

      (重复结点1的操作并入队新结点)

    5)在表PT中选取目标函数值取得极大的结点5优先进行搜索;

    6)在结点6,将物品3装入背包,因此,背包的重量为9,获得的价值为65,目标函数值为65 + (10-9)×4=69,将结点6加入表PT中;在结点7,没有将物品3装入背包,因此,背包的重量和获得的价值与结点5相同,目标函数值为40 + (10-4)×464,将结点6加入表PT中;

    7)在表PT中选取目标函数值取得极大的结点6优先进行搜索;

    8)在结点8,将物品4装入背包,因此,背包的重量为12,不满足约束条件,将结点8丢弃;在结点9,没有将物品4装入背包,因此,背包的重量和获得的价值与结点6相同,目标函数值为65

    9)由于结点9是叶子结点,同时结点9的目标函数值是表PT中的极大值,所以,结点9对应的解即是问题的最优解,搜索结束。

      (判断结束并跳出)

     

    我实现的方法是先按单位密度排序优先队列,每次踢出ub值最大的,对排在这个点之后的点选择或者不选择分别进行一次计算,得出相应的ub,放入优先队列中。

    跳出的条件设定了两个:

    1、当踢出的最大ub在叶子结点上时,结束。

    2、当踢出的最大ub和v值相等时,结束。

    第一点显而易见,现在说说第二点。

    当ub和v相等的时候,可以这么理解,此时背包已经被完全装满了,因此完全不用再继续试下去了,对于剩下的物品,直接全都不选即可,不用再进行计算。

     

    //==============================================
    //author: FreeAquar
    //data:2011-12
    //==============================================
    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    #include <ctime>
    #include <windows.h>
    #include <queue>
    #include <time.h>
    #include <cmath>
    #define eps 1e-7
    #define nMax 1000
    using namespace std;
    int n; //总共的物品数量
    int W; //背包容量
    int cw; //背包中物品总容量
    int cp; //背包中物品总价值
    double bestP;//0-1背包中最大价值
    int record[nMax][nMax]; //0-1背包中当前最大值
    bool x[nMax];
    bool y[nMax];

    struct Node
    {
    int cw; //背包体积
    int cv; //背包总价值
    int cnt; //背包中试过多少种物品,当cnt==n时为叶子结点
    bool x[nMax]; //选中的物品
    Node()
    {
    memset(x, 0, sizeof(x));
    cnt=1;
    cw=0;
    cv=0;
    }
    double ub; //启发函数的结果
    friend bool operator< (Node n1, Node n2) //按启发函数建立大顶堆
    {
    return n1.ub < n2.ub;
    }
    }node[nMax], maxn;

    struct Item
    {
    int w; //物品的重量
    int v; //物品的价值
    int i; //物品的编号
    double p; //物品的单位价值
    }item[nMax];

    priority_queue<Node> Q; //优先队列
    double Max_ub;
    double Min_ub;

    int Rand(int x); //随机数生成函数
    void input(); //读入数据
    bool cmp1(Item a, Item b); //按密度排序
    bool cmp2(Item a, Item b); //按体积排序

    //动态规划法
    void DP(); //动态规划
    void get_DP(); //找路径

    //分支限界法
    double calculate_ub(int v, int w, double p); //启发函数
    void Insert(bool flag, int i, int Cv, int Cw, double ub, Node temp);//判断和入队
    void bfs(); //优先队列搜索

    //回溯法
    void BackTrack1(int i); //体积排序回溯
    void BackTrack2(int i); //回溯

    //输出函数
    void output_way(bool x[]);

    int main()
    {
    input();
    LARGE_INTEGER BegainTime ;
    LARGE_INTEGER EndTime ;
    LARGE_INTEGER Frequency ;
    QueryPerformanceFrequency(&Frequency);
    QueryPerformanceCounter(&BegainTime) ;
    //要测试的代码放在这里
    DP();
    QueryPerformanceCounter(&EndTime);
    printf("动态规划: %d\n", record[n][W]);
    printf(" DP背包选择的物品编号:\n ");
    get_DP();
    output_way(x);
    //输出运行时间(单位:s)
    cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl;


    maxn.cv=0;
    QueryPerformanceFrequency(&Frequency);
    QueryPerformanceCounter(&BegainTime) ;
    //要测试的代码放在这里
    sort(item+1, item+n+1, cmp1);
    bfs();

    QueryPerformanceCounter(&EndTime);
    printf("分支限界法:%d\n", maxn.cv);
    printf(" 分支限界法选择的物品编号:\n ");
    output_way(maxn.x);
    //输出运行时间(单位:s)
    cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl;

    bestP=0;
    cp=cw=0;
    memset(x, 0, sizeof(x));
    memset(y, 0, sizeof(y));
    QueryPerformanceFrequency(&Frequency);
    QueryPerformanceCounter(&BegainTime) ;
    //要测试的代码放在这里
    sort(item+1, item+n+1, cmp2);
    BackTrack1(1);


    QueryPerformanceCounter(&EndTime);
    printf("按体积排序的回溯法:%.0lf\n", bestP);
    printf(" 回溯法选择的物品编号:\n ");
    output_way(x);
    //输出运行时间(单位:s)
    cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl;

    bestP=0;
    cp=cw=0;
    memset(x, 0, sizeof(x));
    memset(y, 0, sizeof(y));
    QueryPerformanceFrequency(&Frequency);
    QueryPerformanceCounter(&BegainTime) ;
    //要测试的代码放在这里
    BackTrack2(1);


    QueryPerformanceCounter(&EndTime);
    printf("回溯法:%.0lf\n", bestP);
    printf(" 回溯法选择的物品编号:\n ");
    output_way(x);
    //输出运行时间(单位:s)
    cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl;

    return 0;
    }
    //=====================================
    //随机数,比较函数和读入数据
    //=====================================
    int Rand(int x)
    {
    int t=x,sum=0;
    while (t>0)
    {
    sum+=rand()%t;
    t-=32767;
    }
    return sum;
    }
    bool cmp1(Item a, Item b)
    {
    return a.p>b.p;
    }

    bool cmp2(Item a, Item b)
    {
    return a.w<b.w;
    }
    void input()
    {
    n=40; W=400;
    //scanf("%d%d", &n, &W);
    cout<<"物品数目为:"<<n<<endl;
    cout<<"背包容量为:"<<W<<endl;
    cout<<"------------------------------------------"<<endl;
    double Min=1e20;
    double Max=-1;
    for(int i=1; i<=n; i++)
    {
    //每个物品的状态
    //scanf("%d%d", &item[i].w, &item[i].v);
    item[i].w=Rand(100);
    item[i].v=Rand(1000);
    item[i].p=1.0*item[i].v/item[i].w;
    item[i].i=i;
    Min=min(Min, item[i].p);
    Max=max(Max, item[i].p);
    //启发值的边界
    }
    //for(int i=1; i<=n; i++)
    //cout<<item[i].w<<" "<<item[i].v<<endl;
    Max_ub=W*Max;
    Min_ub=W*Min;
    }
    //=====================================
    //读入结束
    //动态规划法开始
    //=====================================
    void DP()
    {
    for(int i=0; i<=n; i++)
    record[i][0]=0;
    for(int j=0; j<=W; j++)
    record[0][j]=0;
    for(int i=1; i<=n; i++)
    for(int j=1; j<=W; j++)
    {
    if(j<item[i].w)
    record[i][j]=record[i-1][j];
    else
    record[i][j]=max(record[i-1][j], record[i-1][j-item[i].w]+item[i].v);
    }
    }
    void get_DP()
    {
    int j=W;
    for(int i=n; i>0; i--)
    {
    if(record[i][j]>record[i-1][j])
    {
    x[i]=1;
    j=j-item[i].w;
    }
    else
    x[i]=0;
    }
    }
    //=====================================
    //动态规划结束
    //分支限界法开始
    //=====================================
    double calculate_ub(int v, int w, double p) //计算启发函数
    {
    return v+(W-w)*p;
    }
    void Insert(bool flag, int i, int Cv, int Cw, double ub, Node temp)
    {
    if((ub<Max_ub+eps && ub>Min_ub-eps) && (Cw<=W)) //结点符合要求
    {
    //cout<<ub<<endl;
    Node cm;
    cm.cw=Cw;
    cm.cv=Cv;
    cm.ub=ub;
    for(int j=1; j<=n; j++)
    {
    cm.x[j]=temp.x[j];
    }
    if(flag)
    cm.x[item[i].i]=1;
    cm.cnt=i+1;
    Q.push(cm);
    }
    }
    void bfs() //广搜
    {
    Node node;
    node.cw=0;
    node.cv=0;
    node.ub=calculate_ub(0, 0, item[1].p);
    Q.push(node);
    while(!Q.empty())
    {
    Node temp=Q.top();
    Q.pop();
    //当前值与启发式函数结果相同或者叶子结点启发式结果最大
    if(abs(temp.ub-temp.cv)<eps || temp.cnt>n)
    {
    if(maxn.cv<temp.cv+eps)
    maxn=temp;
    break;
    }

    int Cw=temp.cw;
    int Cv=temp.cv;
    for(int flag=0; flag<2; flag++)
    //flag=0时,计算不放第temp.cnt+1件物品
    //flag=1时,计算放入第temp.cnt+1件物品
    {
    int i=temp.cnt;
    if(flag)
    {
    Cw+=item[i].w;
    Cv+=item[i].v;
    }
    double ub=calculate_ub(Cv, Cw, item[i+1].p);
    Insert(flag, i, Cv, Cw, ub, temp);
    }
    }
    return ;
    }
    //=====================================
    //分支限界法结束
    //体积排序回溯开始
    //=====================================
    void BackTrack1(int i)
    {
    if(i>n || (W<cw+item[i].w))
    {
    if(bestP<cp)//更新
    {
    bestP=cp;
    for(int i=1; i<=n; i++)
    x[i]=y[i];
    }
    return ;
    }

    //cout<<cw<<" "<<item[i].c<<" "<<item[i].w<<" "<<cp<<endl;
    cw=cw+item[i].w; //搜索
    cp=cp+item[i].v;
    y[item[i].i]=1;
    BackTrack1(i+1); //左子树
    y[item[i].i]=0;
    cw=cw-item[i].w; //回溯
    cp=cp-item[i].v;
    BackTrack1(i+1); //右子树
    }
    //=====================================
    //体积排序回溯结束
    //回溯法开始
    //=====================================
    void BackTrack2(int i)
    {
    if(i>n)
    {
    if(bestP<cp) //更新
    {
    bestP=cp;
    for(int i=1; i<=n; i++)
    x[i]=y[i];
    }
    return ;
    }

    //cout<<cw<<" "<<item[i].c<<" "<<item[i].w<<" "<<cp<<endl;
    if(W>=cw+item[i].w) //判断背包是否能放下
    {
    cw=cw+item[i].w; //搜索
    cp=cp+item[i].v;
    y[item[i].i]=1;
    BackTrack2(i+1); //左子树
    y[item[i].i]=0;
    cw=cw-item[i].w; //回溯
    cp=cp-item[i].v;
    }
    BackTrack2(i+1); //右子树
    }
    //=====================================
    //所有方法结束
    //输出路径
    //=====================================
    void output_way(bool x[])
    {
    for(int i=1; i<=n; i++)
    {
    if(x[i])
    printf("%d ", i);
    }
    cout<<endl;
    }
    作者:FreeAquar
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    我来说说博客评论的事
    SWFUpload+Javascript仿163邮件上传文件
    如何暂停和终止线程
    分享我的数据处理类库,欢迎拍砖
    求数列两两之差,再求和
    poj 1006 中国剩余定理
    Poj算法做题顺序
    poj 1328
    ZOJ 3279
    poj 2352 树状数组
  • 原文地址:https://www.cnblogs.com/FreeAquar/p/2291436.html
Copyright © 2020-2023  润新知