背包问题:动态规划和贪心算法

1. 动态规划

以下关于动态规划的文字描述来源

动态规划之背包问题(一)
作者:Hawstein
出处:http://hawstein.com/posts/dp-knapsack.html

一切都要从一则故事说起。

话说有一哥们去森林里玩发现了一堆宝石,他数了数,一共有n个。 但他身上能装宝石的就只有一个背包,背包的容量为C。这哥们把n个宝石排成一排并编上号: 0,1,2,…,n-1。第i个宝石对应的体积和价值分别为V[i]和W[i] 。排好后这哥们开始思考: 背包总共也就只能装下体积为C的东西,那我要装下哪些宝石才能让我获得最大的利益呢?

OK,如果是你,你会怎么做?你斩钉截铁的说:动态规划啊!恭喜你,答对了。 那么让我们来看看,动态规划中最最最重要的两个概念: 状态和状态转移方程在这个问题中分别是什么。

我们要怎样去定义状态呢?这个状态总不能是凭空想象或是从天上掉下来的吧。 为了方便说明,让我们先实例化上面的问题。一般遇到n,你就果断地给n赋予一个很小的数, 比如n=3。然后设背包容量C=10,三个宝石的体积为5,4,3,对应的价值为20,10,12。 对于这个例子,我想智商大于0的人都知道正解应该是把体积为5和3的宝石装到背包里, 此时对应的价值是20+12=32。接下来,我们把第三个宝石拿走, 同时背包容量减去第三个宝石的体积(因为它是装入背包的宝石之一), 于是问题的各参数变为:n=2,C=7,体积{5,4},价值{20,10}。好了, 现在这个问题的解是什么?我想智商等于0的也解得出了:把体积为5的宝石放入背包 (然后剩下体积2,装不下第二个宝石,只能眼睁睁看着它溜走),此时价值为20。 这样一来,我们发现,n=3时,放入背包的是0号和2号宝石;当n=2时, 我们放入的是0号宝石。这并不是一个偶然,没错, 这就是传说中的“全局最优解包含局部最优解”(n=2是n=3情况的一个局部子问题)。 绕了那么大的圈子,你可能要问,这都哪跟哪啊?说好的状态呢?说好的状态转移方程呢? 别急,它们已经呼之欲出了。

我们再把上面的例子理一下。当n=2时,我们要求的是前2个宝石, 装到体积为7的背包里能达到的最大价值;当n=3时,我们要求的是前3个宝石, 装到体积为10的背包里能达到的最大价值。有没有发现它们其实是一个句式!OK, 让我们形式化地表示一下它们, 定义d(i,j)为前i个宝石装到剩余体积为j的背包里能达到的最大价值。 那么上面两句话即为:d(2, 7)和d(3, 10)。这样看着真是爽多了, 而这两个看着很爽的符号就是我们要找的状态了。 即状态d(i,j)表示前i个宝石装到剩余体积为j的背包里能达到的最大价值。 上面那么多的文字,用一句话概括就是:根据子问题定义状态!你找到子问题, 状态也就浮出水面了。而我们最终要求解的最大价值即为d(n, C):前n个宝石 (0,1,2…,n-1)装入剩余容量为C的背包中的最大价值。状态好不容易找到了, 状态转移方程呢?顾名思义,状态转移方程就是描述状态是怎么转移的方程(好废话!)。 那么回到例子,d(2, 7)和d(3, 10)是怎么转移的?来,我们来说说2号宝石 (记住宝石编号是从0开始的)。从d(2, 7)到d(3, 10)就隔了这个2号宝石。 它有两种情况,装或者不装入背包。如果装入,在面对前2个宝石时, 背包就只剩下体积7来装它们,而相应的要加上2号宝石的价值12, d(3, 10)=d(2, 10-3)+12=d(2, 7)+12;如果不装入,体积仍为10,价值自然不变了, d(3, 10)=d(2, 10)。记住,d(3, 10)表示的是前3个宝石装入到剩余体积为10 的背包里能达到的最大价值,既然是最大价值,就有d(3, 10)=max{ d(2, 10), d(2, 7)+12 }。好了,这条方程描述了状态d(i, j)的一些关系, 没错,它就是状态转移方程了。把它形式化一下:

d(i,j)=max{d(i1,j),d(i1,jV[i1])+W[i1]}

注意讨论前i个宝石装入背包的时候, 其实是在考查第i-1个宝石装不装入背包(因为宝石是从0开始编号的)。至此, 状态和状态转移方程都已经有了。

2. 贪心算法

如上述是一个 0-1 背包问题,即对一个宝石只能拿或不拿。还有一种是分数背包问题,即可以拿走宝石的一部分,因此可以优先拿走所有单位价值最高的宝石。故此处是一个贪心算法问题。

3. C++代码实现

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Good {
public:
    int num;
    int weight;
    int value;
    float unit_value;
    Good(int n, int w, int v) : num(n), weight(w), value(v) {
        if (w == 0) unit_value = 0;
        else    unit_value = (float)v / (float)w;
    };
};

class Bag{

public:
    // 0-1背包问题:对一件物品拿或不拿得到最大价值
    void OneZeroBag(vector<Good>goods, const int W) {
        int n = goods.size();
        // d[i][j] 表示在前 i 个(0 ~ i-1)宝石中,剩余体积为 j 能达到的最大价值
        int **d = New2DMat(n + 1, W + 1);
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= W; j++) {
                d[i][j] = (i == 0) ? 0 : d[i - 1][j]; // 0 个宝石价值为0
                if (i > 0 && j >= goods[i-1].weight) {
                    int q = d[i - 1][j - goods[i - 1].weight] + goods[i - 1].value; // 拿第 i 个
                    d[i][j] = max(d[i][j], q); // 根据价值选择拿或不拿
                }
            }
        }
        cout << "最大价值:" << d[n][W] << endl;
        // 输出选择结果
        cout << "所拿商品编号:";
        int j = W;
        for (int i = n; i > 0; i--) {
            if (d[i][j] > d[i - 1][j]) {
                j -= goods[i - 1].weight;
                cout << goods[i - 1].num << " ";
            }
        }
        cout << endl;
    }
    // 分数背包问题:假设物品可以被分割
    void FractionalBag(vector<Good> &goods, const int W) {
        SortGood(goods, 0, goods.size() - 1); // 按单位价值从高到低排序
        int m = 0;
        for (int i = 0; i < goods.size(); i++) {
            int take_weight = min(goods[i].weight, W - m);
            cout << "拿走商品 "<< goods[i].num<<" 的重量是 "<< take_weight << endl;
            m += take_weight;
            if (m == W)
                break;
        }
    }

private:
    void SortGood(vector<Good> &goods, int l, int r) {
        if (l >= r)
            return;
        int m = (l + r) / 2;
        SortGood(goods, l, m);
        SortGood(goods, m + 1, r);
        MergeAB(goods, l, m, r);
    };
    void MergeAB(vector<Good> &goods, int l, int m, int r) {
        vector<Good>temp;
        int i, j, k;
        for (i = l; i <= m; i++)
            temp.push_back(goods[i]);
        for (j = r; j > m; j--)
            temp.push_back(goods[j]);
        i = l, j = r;
        k = l;
        while (i <= j) {
            if (temp[i].unit_value > temp[j].unit_value)
                goods[k++] = temp[i++];
            else
                goods[k++] = temp[j--];
        }
    };
    int** New2DMat(int row, int col) {
        int **Mat = new int*[row];
        for (int i = 0; i < row; i++)
            Mat[i] = new int[col];
        return Mat;
    }
};
int main()
{
    Good g1(1, 5, 20);
    Good g2(2, 4, 10);
    Good g3(3, 3, 12);
    Good good_array[] = { g1, g2, g3 };
    vector<Good> goods(good_array, good_array+3);
    const int W = 10;
    Bag bag;

    cout << "分数背包问题:" << endl;
    bag.FractionalBag(goods, W);

    cout << "0-1 背包问题(拿或不拿):" << endl;
    bag.OneZeroBag(goods, W);
}

输出结果为:

分数背包问题:
拿走商品 3 的重量是 3
拿走商品 1 的重量是 5
拿走商品 2 的重量是 2
0-1 背包问题(拿或不拿):
最大价值:32
所拿商品编号:1 3 
[Finished in 0.9s]
    原文作者:贪心算法
    原文地址: https://blog.csdn.net/quzhongxin/article/details/46654039
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞