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

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(i?1,j),d(i?1,j?V[i?1])+W[i?1]}

注意讨论前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]

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-08 10:44:35

背包问题:动态规划和贪心算法的相关文章

动态规划和贪心算法的区别

这是转别人的,待会我会自己总结 动态规划和贪心算法的区别动态规划和贪心算法都是一种递推算法 均有局部最优解来推导全局最优解 不同点: 贪心算法: 1.贪心算法中,作出的每步贪心决策都无法改变,因为贪心策略是由上一步的最优解推导下一步的最优解,而上一部之前的最优解则不作保留. 2.由(1)中的介绍,可以知道贪心法正确的条件是:每一步的最优解一定包含上一步的最优解. 动态规划算法: 1.全局最优解中一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前的所有最优解 2.动态规划的关键

从 活动选择问题 看动态规划和贪心算法的区别与联系

这篇文章主要用来记录我对<算法导论> 贪心算法一章中的“活动选择问题”的动态规划求解和贪心算法求解 的思路和理解. 主要涉及到以下几个方面的内容: ①什么是活动选择问题---粗略提下,详细请参考<算法导论> ②活动选择问题的DP(Dynamic programming)求解--DP求解问题的思路 ③活动选择问题的贪心算法求解 ④为什么这个问题可以用贪心算法求解? ⑤动态规划与贪心算法的一些区别与联系 ⑥活动选择问题的DP求解的JAVA语言实现以及时间复杂度分析 ⑦活动选择问题的Gr

动态规划与贪心算法区别以及如何思考动态规划

动态规划和贪心算法的区别 动态规划和贪心算法都是一种递推算法 均有局部最优解来推导全局最优解 不同点: 贪心算法: 1.贪心算法中,作出的每步贪心决策都无法改变,因为贪心策略是由上一步的最优解推导下一步的最优解,而上一部之前的最优解则不作保留. 2.由(1)中的介绍,可以知道贪心法正确的条件是:每一步的最优解一定包含上一步的最优解. 动态规划算法: 1.全局最优解中一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前的所有最优解 2.动态规划的关键是状态转移方程,即如何由以求出

算法复习笔记(分治法、动态规划、贪心算法)

分治法 动态规划 贪心算法 分治法 分治法的基本思想是将一个规模为n的问题分解为k个规模较小的问题,这些子问题互相独立且与原问题相同(所以可以递归).递归地解这些子问题,然后将各个子问题的解合并得到原问题的解.它的一般算法设计模式如下: divide-and-conquer(P) { //|P|表示问题的规模,n0表示阈值,当规模不超过n0时,问题容易解出,不必分解 if(|P|<=n0) adhoc(P); //将P分解成子问题 divide P into smaller subinstanc

分治法、动态规划、贪心算法区别

1.分治法 算法思想:将原问题划分成若干个规模较小而结构与原问题相似的子问题,递归的解决这些子问题,然后再合其结果,就得到原问题的解 特征: 该问题的规模缩小到一定的程度就很容易解决 该问题可以分解为若干个规模较小的相同问题,即改问题具有最优子结构性质 利用该问题分解出的子问题的解可以合并为该问题的解: 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题 2.动态规划 算法思想:与分治法相似,也是通过组合子问题的解而解决整个问题.区别是,动态规划适用于分解得到的子问题往往不是

活动选择问题 (动态规划 与 贪心算法)

问题描述: 设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源.每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si <fi.如果选择了活动i,则它在半开时间区间[si, fi)内占用资源.若区间[si, fi)与区间[sj, fj)不相交,则称活动i与活动j是相容的.也就是说,当si≥fj或sj≥fi时,活动i与活动j相容.活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合. . 从图中

2016-6-19 动态规划,贪心算法练习

DP:1.codevs 1493 糖果 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 黄金 Gold 题目描述 Description 最近小修很高兴,因为她的k个外甥就要来她家里玩了.她上超市买了一大堆糖果,总共有n颗. 小修准备把所有的糖果分成k堆(当然每一堆至少要有一颗糖果).可是到底有多少种分法呢? 她冥思苦想不得其解,你能帮助她吗? 输入描述 Input Description 两个用空格分开的整数n, k(n<=60000, k<=100). 输出描述 Outp

【算法复习】分治算法、动态规划、贪心算法

Notes ## 分治思想和递归表达式 [分治思想] 将一个问题分解为与原问题相似但规模更小的若干子问题,递归地解这些子问题,然后将这些子问题的解结合起来构成原问题的解.这种方法在每层递归上均包括三个步骤: divide(分解):将问题划分为若干个子问题 conquer(求解):递归地解这些子问题:若子问题Size足够小,则直接解决之 Combine(组合):将子问题的解组合成原问题的解 [分治递归表达式] 设T(n)是Size为n的执行时间,若Size足够小,如n ≤ C (常数),则直接求解

关于贪心算法和动态规划的学习 - 背包问题为例

说到背包问题,我看到了很多分类,不同的类似问题,有不一样的解法,看到的最多的两种方式是贪心算法和动态规划,于我来说,更迫切的应该是要认识一下,这些算法的区别和相同的地方,所以这节就来找资料学习一下贪心算法和动态规划的区别. 这是找到的一个对我帮助最大的,源地址:https://www.cnblogs.com/Renyi-Fan/p/7741089.html 看之前先摘一下这几个解决方案的区别:摘自网上 "所以一个问题,该用递推.贪心.搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定