`
wbj0110
  • 浏览: 1613284 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

动态规划之背包问题

阅读更多

话说有一哥们去森林里玩发现了一堆宝石,他数了数,一共有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开始编号的)。至此, 状态和状态转移方程都已经有了。接下来,直接上代码。

for(int i=0; i<=n; ++i){
    for(int j=0; j<=C; ++j){
        d[i][j] = i==0 ? 0 : d[i-1][j];
        if(i>0 && j>=V[i-1])  d[i][j] >?= d[i-1][j-V[i-1]]+W[i-1];
    }
 }

i=0时,d(i, j)为什么为0呢?因为前0个宝石装入背包就是没东西装入,所以最大价值为0。 if语句里,j>=V[i-1]说明只有当背包剩余体积j大于等于i-1号宝石的体积时, 我才考虑把它装进来的情况,不然d[i][j]就直接等于d[i-1][j]。i>0不用说了吧, 前0个宝石装入背包的情况是边界,直接等于0,只有i>0才有必要讨论, 我是装呢还是不装呢。简单吧,核心算法就这么一丁点,接下来上完整代码knapsack.cpp。

/**0-1 knapsack d(i, j)表示前i个物品装到剩余容量为j的背包中的最大重量**/
#include<cstdio>
using namespace std;
#define MAXN 1000
#define MAXC 100000

int V[MAXN], W[MAXN];
int d[MAXN][MAXC];

int main(){
    freopen("data.in", "r", stdin);//重定向输入流
    freopen("data.out", "w", stdout);//重定向输出流
    int n, C;
    while(scanf("%d %d", &n, &C) != EOF){
        for(int i=0; i<n; ++i)   scanf("%d %d", &V[i], &W[i]);
        
        for(int i=0; i<=n; ++i){
            for(int j=0; j<=C; ++j){
                d[i][j] = i==0 ? 0 : d[i-1][j];
                if(i>0 && j>=V[i-1])  d[i][j] >?= d[i-1][j-V[i-1]]+W[i-1];
            }
        }
        printf("%d\n", d[n][C]);//最终求解的最大价值
    }
    fclose(stdin);
    fclose(stdout);
    return 0;
}

其中freopen函数将标准输入流重定向到文件data.in, 这比运行程序时一点点手输要方便许多,将标准输出流重定向到data.out。 data.in中每组输入的第一行为宝石数量n及背包体积C,接下来会有n行的数据, 每行两个数对应的是宝石的体积及价值。本测试用例data.in如下:

5 10
4 9
3 6
5 1
2 4
5 1
4 9
4 20
3 6
4 20
2 4
5 10
2 6
2 3
6 5
5 4
4 6

data.out为算法输出结果,对应该测试用例,输出结果如下:

19
40
15

好,至此我们解决了背包问题中最基本的0/1背包问题。等等,这时你可能要问, 我现在只知道背包能装入宝石的最大价值,但我还不知道要往背包里装入哪些宝石啊。嗯, 好问题!让我们先定义一个数组x,对于其中的元素为1时表示对应编号的宝石放入背包, 为0则不放入。让我们回到上面的例子,对于体积为5,4,3,价值为20,10,12的3个宝石 ,如何求得其对应的数组x呢?(明显我们目测一下就知道x={1 0 1}, 但程序可目测不出来)OK,让我们还是从状态说起。如果我们把2号宝石放入了背包, 那么是不是也就意味着,前3个宝石放入背包的最大价值要比前2个宝石放入背包的价值大, 即:d(3, 10)>d(2, 10)。再用字母代替具体的数字 (不知不觉中我们就用了不完全归纳法哈),当d(i, j)>d(i-1, j)时,x(i-1)=1;OK, 上代码:

//输出打印方案
int j = C;
for(int i=n; i>0; --i){
    if(d[i][j] > d[i-1][j]){
        x[i-1] = 1;
        j = j - V[i-1];//装入第i-1个宝石后背包能装入的体积就只剩下j - V[i-1]
    }
}
for(int i=0; i<n; ++i)  printf("%d ", x[i]);

好了,加入这部分内容,knapsack.cpp变为如下:

/**0-1 knapsack d(i, j)表示前i个物品装到剩余容量为j的背包中的最大重量**/
#include<cstdio>
using namespace std;
#define MAXN 1000
#define MAXC 100000

int V[MAXN], W[MAXN], x[MAXN];
int d[MAXN][MAXC];

int main(){
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
    int n, C;
    while(scanf("%d %d", &n, &C) != EOF){
        for(int i=0; i<n; ++i)   scanf("%d %d", &V[i], &W[i]);
        for(int i=0; i<n; ++i)   x[i] = 0; //初始化打印方案
        
        for(int i=0; i<=n; ++i){
            for(int j=0; j<=C; ++j){
                d[i][j] = i==0 ? 0 : d[i-1][j];
                if(i>0 && j>=V[i-1])  d[i][j] >?= d[i-1][j-V[i-1]]+W[i-1];
            }
        }
        printf("%d\n", d[n][C]);
        
        //输出打印方案
        int j = C;
        for(int i=n; i>0; --i){
            if(d[i][j] > d[i-1][j]){
                x[i-1] = 1;
                j = j - V[i-1];
            }
        }
        for(int i=0; i<n; ++i)   printf("%d ", x[i]);
        printf("\n");
    }
    fclose(stdin);
    fclose(stdout);
    return 0;
}

data.out输出结果变为:

19
1 1 0 1 0
40
1 0 1 0
15
1 1 0 0 1

至此,好像该解决的问题都解决了。当一个问题找到一个放心可靠的解决方案后, 我们往往就要考虑一下是不是有优化方案了。为了保持代码的简洁, 我们暂且把宝石装包方案的求解去掉。该算法的时间复杂度是O(nC), 即时间都花在两个for循环里了,这个应该是没办法再优化了。再看看空间复杂度, 数组d用来保存每个状态的值,空间复杂度为O(nC); 数组V和W用来保存每个宝石的体积和价值,空间复杂度为O(n)。程序总的空间复杂度为 O(nC),这个是可以进一步优化的。首先,我们先把数组V和W去掉, 因为它们没有保存的必要,改为一边读入一边计算:

int V = 0, W = 0;
for(int i=0; i<=n; ++i){
    if(i>0) scanf("%d %d", &V,&W);
    for(int j=0; j<=C;++j){
        d[i][j] = i==0 ? 0 : d[i-1][j];
        if(j>=V && i>0) d[i][j] >?= d[i-1][j-V]+W;
    }
}

好了,接下来让我们继续压榨空间复杂度。保存状态值我们开了一个二维数组d, 在看过把一维数组V和W变为一个变量后,我们是不是要思考一下, 有没有办法将这个二维数组也压榨一下呢?换言之, 这个二维数组中的每个状态值我们真的有必要都保存么? 让我们先来看一下以下的一张示意图(参照《算法竞赛入门经典》P169的图画的)

由上面那一小段优化过后的代码可知,状态转移方程为:d(i, j)=max{ d(i-1, j), d(i-1, j-V)+W },也就是在计算d(i, j)时我们用到了d(i-1,j)和d(i-1, j-V)的值。 如果我们只用一个一维数组d(0)~d(C)来保存状态值可以么?将i方向的维数去掉, 我们可以将原来二维数组表示为一维数据:d(i-1, j-V)变为d(j-V), d(i-1, j)变为d(j)。当我们要计算d(i, j)时,只需要比较d(j)和d(j-V)+W的大小, 用较大的数更新d(j)即可。等等,如果我要计算d(i, j+1),而它恰好要用到d(i-1, j)的值, 那么问题就出来了,因为你刚刚才把它更新为d(i, j)了。那么,怎么办呢? 按照j递减的顺序即可避免这种问题。比如,你计算完d(i, j), 接下来要计算的是d(i,j-1),而它的状态转移方程为d(i, j-1)=max{ d(i-1, j-1), d(i-1, j-1-V)+W },它不会再用到d(i-1,j)的值!所以, 即使该位置的值被更新了也无所谓。好,上代码:

memset(d, 0, sizeof(d));
for(int i=0; i<=n; ++i){
    if(i>0) scanf("%d %d", &V,&W);
    for(int j=C;j>=0; --j){
        if(j>=V && i>0) d[j] >?= d[j-V]+W;
    }
}

优化后的完整代码如下,此时空间复杂度仅为O(C)

/**0-1 knapsack d(i, j)表示前i个物品装到剩余容量为j的背包中的最大重量**/
#include<cstdio>
#include<cstdlib>
#include<cstring>
using namespace std;

int main(){
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
    int n, C, V = 0, W = 0;
    while(scanf("%d %d", &n, &C) != EOF){
        int* d = (int*)malloc((C+1)*sizeof(int));
        memset(d, 0, (C+1)*sizeof(int));
        
        for(int i=0; i<=n; ++i){
            if(i>0)   scanf("%d %d", &V, &W);
            for(int j=C; j>=0; --j){
                if(j>=V && i>0)    d[j] >?= d[j-V]+W;
            }
        }
        printf("%d\n", d[C]);
        free(d);
    }
    fclose(stdin);
    fclose(stdout);
    return 0;
}
http://www.hawstein.com/posts/dp-knapsack.html
分享到:
评论

相关推荐

    利用动态规划解决01背包问题

    01背包问题是动态规划的经典模型之一,它可以用于解决许多实际问题,例如最短路线、库存管理、资源分配、设备更新、排序、装载等问题。 动态规划算法的基本思想是将待求解的问题分解成若干个子问题,然后从这些子...

    动态规划求解背包问题

    本篇文章将深入探讨这两个背包问题以及如何使用动态规划来解决它们。 **0-1背包问题**: 0-1背包问题是一个典型的优化问题,它假设有一个容量有限的背包(背包容量为W),以及一系列物品,每件物品都有一个重量w[i]...

    动态规划解背包问题 C语言

    在本场景中,我们关注的是使用C语言实现动态规划解决背包问题。背包问题是一类经典的组合优化问题,通常涉及到在给定容量的背包中选择物品以最大化价值或满足特定目标。 背包问题的基本形式可以描述为:给定一组...

    动态规划的背包问题

    动态规划的背包问题,是不是一度让你很苦恼?不急,有了这个PPT,能让你做背包问题易如反掌。

    算法-动态规划- 背包问题 P04- 混合背包(包含源程序).rar

    动态规划是一种强大的算法思想,广泛应用于解决复杂优化问题,其中包括著名的背包问题。在这个问题中,我们通常面临一个有限容量的背包和一系列物品,每个物品都有自己的重量和价值。目标是选择物品,使得装入背包的...

    背包九讲 背包问题 动态规划

    《背包九讲》是关于背包问题和动态规划算法的一份深度学习资料,它涵盖了完全背包、0-1背包以及多重背包等经典的优化问题。在计算机科学和算法设计中,动态规划是一种有效解决这类问题的方法,它通过将大问题分解为...

    动态规划求解0-1背包问题的改进算法完整解释

    在计算机算法设计与分析中,动态规划法是解决背包问题的常用方法之一。所谓背包问题,是指在有限的背包容量下,如何选择物品来达到最大价值的问题。在本文中,我们将对动态规划法求解0-1背包问题的改进算法进行详细...

    C++动态规划之背包问题解决方法

    本文实例讲述了C++动态规划之背包问题解决方法。分享给大家供大家参考。具体分析如下: 问题描述: 背包的最大容量为W,有N件物品,每件物品重量为w,价值为p,怎样选择物品能使得背包里的物品价值最大? 输入: 10 3...

    动态规划法求解0-1背包问题实验报告.pdf

    0-1背包问题是一个经典的优化问题,主要涉及动态规划算法的运用。在这个实验报告中,学生使用Java语言解决了一个0-1背包问题的实例。以下是关于这个问题和解决方案的详细解释。 一、问题描述: 0-1背包问题的核心是...

    动态规划解决01背包问题

    01背包是在M件物品取出若干件放在空间为W的背包里,每件物品的体积为W1...动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。

    动态规划解决背包问题 MATLAB

    本资源为 MATLAB 代码,代码中用动态规划解决了0-1背包问题。具体问题为:物品价值:v=[90 75 83 32 56 31 21 43 14 65 12 24 42 17 60];物品重量:w=[30 27 23 24 21 18 16 14 12 10 9 8 6 5 3]; 背包容量:120。...

    动态规划背包问题讲解

    ### 动态规划背包问题讲解 #### 一、动态规划概览 动态规划(Dynamic Programming,简称DP)是一种在计算机科学与数学优化中常见的算法思想。它主要用于解决那些具有重叠子问题和最优子结构的问题。动态规划的核心...

    动态规划解决0-1背包问题

    动态规划是一种有效的算法设计策略,尤其适用于解决具有重叠子问题和最优子结构的问题,而0-1背包问题就是这种策略的一个典型应用。0-1背包问题是一个经典的组合优化问题,它源自实际生活中的资源分配问题,比如在...

    动态规划背包问题.cpp

    动态规划背包问题.cpp

    回溯法和动态规划法解01背包问题

    在提供的压缩包文件"Knapsack"中,可能包含了用某种编程语言实现的回溯法或动态规划法解01背包问题的源代码。由于代码未编译完成,你需要自行完成编译和运行,以验证和理解算法的具体实现。在调试和运行代码时,注意...

    0-1背包问题 动态规划法——C语言代码

    对于0-1背包问题,动态规划的解决方案通常包括以下步骤: 1. 初始化:创建一个二维数组`dp`,其中`dp[i][j]`表示在前`i`件物品中选择总重量不超过`j`的物品所能得到的最大价值。初始化时,`dp[0][j] = 0`,因为没有...

    动态规划01背包问题

    基于动态规划的01背包问题,代码比较规范,注释比较详细。

    c语言实现动态规划求01背包问题

    用c语言实现的基于动态规划求解01背包问题,,其中2.txt中的内容为: 4 5 2 1 3 2 12 10 20 15

    动态规划专题之多重背包问题1

    在本文中,我们将深入探讨一个具体的动态规划问题,即多重背包问题。多重背包问题与经典的0-1背包问题有所不同,它允许每种物品可以被选择多次,直至达到其最大数量限制。 题目描述了一个动态规划专题,其中涉及一...

Global site tag (gtag.js) - Google Analytics