C++动态规划——背包问题

1、数学建模

背包问题是一类动态规划的问题,其假设的场景为:有一个容积为b的背包,n个体积分别为ai(i = 1 , 2 , 3, ... , n),价值分别为Ci(i = 1, 2, 3, ... , n)的物品,如何以最大的价值装包。建模为数学问题可以表示为:


背包问题分为3类:0-1背包问题多重背包问题以及完美背包问题

2、0-1背包问题

上述两个公式表示的即为0-1背包问题。xi = {0,1}表示物品只有一件,所以装包的方案只有装包(用1表示)以及不装包(用0表示),0-1背包由此得名。0-1背包问题的解决方案可以采用动态规划以及贪心算法两种方案解决。两者的区别如下所示为

  1. 动态规划中全局最优解一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前所有的局部最优解。而贪心算法每一步的最优解一定包含上一步的最优解,因此,上一步的解是不需要保留的。
  2. 如果把所有问题看成一颗树的话,动态规划是自底向上的,从叶子向根,构造子问题的解,对每一个子树的根,求出下面每一个叶子的值,最后得到一颗完整的树,并且最终选择其中的最优值作为自身的值。贪心算法是从根出发,每次向下遍历最优子树即可,这样的话就不需要知道一个节点的所有子树的情况。
  3. 动态规划本质为遍历(穷举)法,可以保证结果是最优的,复杂度较高。贪心算法一般是趋近于最优,复杂度较低。

本文以动态规划为例,求得背包问题的最优解。背包问题的动态规划的核心思路是在一个二维矩阵中进行遍历搜索。通过比较当前物品是否能够装包(是否超过背包限重),以及在能够装包的前提下其价值是否为最大两个方面进行分析。
背包问题通常需要增加一行和一列存储空间,一是方便物品序号的对应,二是便于起始背包问题寻优的计算。
0-1背包问题只需要考虑该物品装包与不装包的即可。在他人博客中找到了一道比较简单题目进行练习,假设物品数量为5,背包的限重为17,题目如下:


物品情况.png

根据分析可以写出代码

#include <iostream>
#include <vector>

using namespace std;

/*
假设有n个物体,对应n个价值,背包重量是W,如何装包才能有最大的价值问题。
*/

int getMax(int a, int b)
{
    return a > b ? a : b;
}

int main()
{
    int num, W;
    while (cin >> num >> W)
    {
        vector<int> weight(num + 1, 0);
        vector<int> value(num + 1, 0);
        for (int i = 1; i <= num; i++)
            cin >> weight[i] >> value[i];
        cout << " *********** " << endl;
        vector<vector<int> > f(num + 1, vector<int>(W + 1, 0));

        for (int i = 1; i <= num; i++)
        {
            for (int j = W; j >= 1; j--)
            {
                if (j >= weight[i])
                    f[i][j] = getMax(f[i - 1][j], f[i - 1][j - weight[i]] + value[i]);
                else
                    f[i][j] = f[i - 1][j];
            }
        }

        cout << "***********" << endl;
        cout << f[num][W] << endl;
        
    }
    system("pause");
    return 0;
}

核心代码为

        for (int i = 1; i <= num; i++)
        {
            for (int j = W; j >= 1; j--)
            {
                if (j >= weight[i])
                    f[i][j] = getMax(f[i - 1][j], f[i - 1][j - weight[i]] + value[i]);
                else
                    f[i][j] = f[i - 1][j];
            }
        }

即对每一次产生的结果与上一次的最优作比较,产生该次的最优结果。

f[i - 1][j - weight[i]]+value[i];    // 模拟装包过程
f[i - 1][j];                                  // 模拟不装包过程

3、多重背包问题

多重背包问题是指物品不再是只有一件的问题。而是在规定数目下任意一件都行。多重背包问题讨论的情况的变多。同样在他人博客中找到一道例题进行练习。

题目描述:
为了挽救灾区同胞的生命,心系灾区同胞的你准备自己采购一些粮食支援灾区,现在假设你一共有资金n元,而市场有m种大米,每种大米都是袋装产品,其价格不等,并且只能整袋购买。请问:你用有限的资金最多能采购多少公斤粮食呢?
输入: 输入数据首先包含一个正整数C,表示有C组测试用例,每组测试用例的第一行是两个整数n和m(1<=n<=100, 1<=m<=100),分别表示经费的金额和大米的种类,然后是m行数据,每行包含3个数p,h和c(1<=p<=20,1<=h<=200,1<=c<=20),分别表示每袋的价格、每袋的重量以及对应种类大米的袋数。

解决方案的代码是在0-1背包核心代码中再加一层循环。作为遍历采用M件物品的所有情况(M = 0,1,2, ... M)。可以得到第i件物品的装包情况一共有M+1种。代码如下:

#include <iostream>
#include <vector>

using namespace std;

int getMax(int a, int b)
{
    return a > b ? a : b;
}

int main()
{
    int money;
    int kind;
    while (cin >> money >> kind)
    {
        vector<int> wei(kind + 1, 0);
        vector<int> val(kind + 1, 0);
        vector<int> amount(kind + 1, 0);
        for (int i = 1; i <= kind; i++)
            cin >> val[i] >> wei[i] >> amount[i];
        cout << "***************" << endl;

        vector<int> weight(kind + 1, 0);
        vector<int> value(kind + 1, 0);

        for (int i = 1; i < weight.size(); i++)
        {
            weight[i] = val[i];
            value[i] =  wei[i];
        }

        vector<vector<int> > f(kind + 1, vector<int>(money + 1));
        for (int i = 1; i <= kind; i++)
        {
            for (int j = money; j >= 1; j--)
            {
                for (int k = 0; k <= amount[i]; k++)
                {
                    if (j >= k*weight[i])
                        f[i][j] = getMax(f[i][j], f[i - 1][j - k*weight[i]] + k* value[i]);
                }
            }
        }
        cout << f[kind][money] << endl;

    }
    system("pause");
    return 0;
}

相比于0-1背包,多重背包仅仅在核心代码处多了一层循环。

        for (int i = 1; i <= kind; i++)
        {
            for (int j = money; j >= 1; j--)
            {
                for (int k = 0; k <= amount[i]; k++)
                {
                    if (j >= k*weight[i])
                        f[i][j] = getMax(f[i][j], f[i - 1][j - k*weight[i]] + k* value[i]);
                }
            }
        }

其思路与0-1背包的思路基本一致,但是需要考虑的情况变多。


4、完全背包问题

完全背包问题是多重背包问题的再拓展,它不再规定每件物品的数量,可以在不超过限重的情况下,无限制地装入某种物品。因此,对比多重背包问题中考虑的是0M个物品装包的M+1种情况。那么假设背包限重为W的情况下,完全背包问题中考虑的是0floor(W/M)的所有情况。因此,代码可能只需要在最内层的循环中修改循环条件即可。
同样在网路上找到一道完全背包问题进行练习。

输入为第一行:两个整数,M(背包容量,M<=200)和N(物品数量,N<=30);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。

解决方案的代码为:

#include <iostream>
#include <vector>

using namespace std;

int getMax(int a, int b)
{
    return a > b ? a : b;
}

int main()
{
    int num;        // 物品数量
    int capacity;   // 背包数量
    while (cin >> num >> capacity)
    {
        vector<int> volumn(num + 1);
        vector<int> value(num + 1);
        for (int i = 1; i <= num; i++)
            cin >> volumn[i] >> value[i];
        vector<vector<int> > f(num + 1, vector<int>(capacity + 1, 0));

        for (int i = 1; i <= num; i++)
        {
            for (int j = capacity; j >= 1; j--)
            {
                for (int k = 0; k <= capacity / volumn[i]; k++)
                {
                    if (j >= k*volumn[i])
                        f[i][j] = getMax(f[i][j], f[i - 1][j - k*volumn[i]] + k*value[i]);
                }
            }
        }

        cout << f[num][capacity] << endl;
    }
    system("pause");
    return 0;
}

可以看到,完全背包问题的内层循环条件为

k <= capacity / volumn[i]

其思路与上述的0-1背包问题和多重背包问题完全相似。


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容