01双肩包、完全背包、多重背包

01背包、完全背包、多重背包

前言

今天花了一下午加一晚上的时间,在九度oj才ac了一道简单的多重背包题目,之前没做过多重背包的题目,导致我做题时复杂化了,虽然是假期但是也不能这么浪费时间,果断总结一下,这里参考了dd_engi大牛的《背包问题九讲》,原文链接:http://love-oriented.com/pack/

01背包


题目

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

思路

这是最基础的背包问题,特点是:每种物品只有一件,可以选择放或者不放

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

dp[i][j] = max{dp[i - 1][j], dp[i - 1][j - c[i] + w[i]]}

这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来。这里详细解释一下:

将前i件物品放入容量为j的背包中这个子问题,若只考虑第i件物品的策略(放或者不放),那么就可以转换为一个只牵扯前i-1件物品的问题。
  • 如果不放第i件物品,那么问题就转换为前i-1件物品放入容量为j的背包中的最大价值,价值为dp[i - 1][j]
  • 如果放入第i件物品,那么问题就转换为前i-1件物品放入容量为j-c[i]的背包中,此时能获得的最大价值是dp[i-1][j-c[i]],再加上放入第i件物品获得的价值w[i]

优化空间复杂度

先考虑一下上面的状态转移方程如何实现,肯定有一个主循环i = 1...N,每次算出来二维数组dp[i][0..V]的所有值。那么如果只用一个数组f[0...V],能不能保证第i次循环结束后f[v]就是我们定义的状态f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v-c[i]]的值呢?事实上,这要求在每次主循环中我们以v=V...0的顺序推f[v],这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值。伪代码如下:

for i  in 0 ... N
    for  v = V ... 0
        f[v] = max{f[v], f[v-c[i]] + w[i]}


练习题目

题目:http://acm.hdu.edu.cn/showproblem.php?pid=2602

代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define N 1010

int value[N], volume[N], dp[N];

// 0-1背包,优化空间
void dpPackage(int n, int v)
{
	int i, j;

	memset(dp, 0, sizeof(dp));

	for (i = 1; i <= n; i ++) {
		for (j = v; j >= volume[i]; j --) {
				dp[j] = dp[j] > dp[j - volume[i]] + value[i] ? dp[j] : dp[j - volume[i]] + value[i];
		}
	}

	printf("%d\n", dp[v]);
}

int main(void)
{
	int i, t, n, v;

	scanf("%d", &t);

	while (t --) {
		// 接收参数
		scanf("%d %d", &n, &v);

		for (i = 1; i <= n; i ++)	scanf("%d", value + i);
		for (i = 1; i <= n; i ++)	scanf("%d", volume + i);

		// 0-1背包
		dpPackage(n, v);
	}

	return 0;
}


完全背包问题


题目

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价格是w[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大

思路

这个问题类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已非取或不取两种,而且右取0件、取1件、取2件...等很多种。如果仍然按照01背包的思路,令dp[i][v]表示前i种物品恰好放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程:

dp[i][v] = max{dp[i-1][v - k * c[i]] + k * w[i] | 0 <= k * c[i]<= v}

转化为01背包求解

最简单的想法是:考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转换为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。但是这样完全没有改进时间复杂度,但这毕竟给了我们将完全背包转换为01背包问题的思路:将一种物品拆成多件物品

O(VN)的算法

这个算法使用一维数组,先看伪代码:

for i = 1 ... N
    for v = 0 ... V
        f[v] = max{f[v], f[v-cost] + weight}

你会发现,这个伪代码与01背包的伪代码只有v的循环次序不同而已。为什么这样一改就行呢?首先,想想为什么01背包问题中要按照v=V...0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰好是每种物品可选无限件,所以在考虑“加选一件dii种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][c-v[i]],所以就可以并且必须采用v=0...V的顺序循环


练习题目

题目链接:http://ac.jobdu.com/problem.php?pid=1454

代码:

/**
 * 完全背包问题
 */

#include <stdio.h>
#include <stdlib.h>

#define INF 50000000

typedef struct coin {
	int price, weight;
} coin;

void dynamicPackage(coin *coins, int n, int v)
{
	if (v < 0) {
		printf("This is impossible.\n");
		return;
	}

	int i, j, *dp;

	// 动态分配内存
	dp = (int *)malloc(sizeof(int) * (v + 1));

	// 初始化
	dp[0] = 0;
	for (i = 1; i <= v; i ++)	dp[i] = INF;

	// 完全背包问题
	for (i = 1; i <= n; i ++) {
		for (j = coins[i].weight; j <= v; j ++) {
			dp[j] = (dp[j] < dp[j - coins[i].weight] + coins[i].price) ? dp[j] : dp[j - coins[i].weight] + coins[i].price;
		}
	}

	if (dp[v] >= INF)
		printf("This is impossible.\n");
	else
		printf("The minimum amount of money in the piggy-bank is %d.\n", dp[v]);


	// 清理内存
	free(dp);
	dp = NULL;
}


int main(void)
{
	int t, e, f, n, i;
	coin *coins;

	scanf("%d", &t);

	while (t --) {
		scanf("%d %d", &e, &f);
		scanf("%d", &n);

		// 接收货币
		coins = (coin *)malloc(sizeof(coin) * (n + 1));
		if (coins == NULL)	exit(-1);

		for (i = 1; i <= n; i ++) {
			scanf("%d %d", &coins[i].price, &coins[i].weight);
		}

		// 完全背包
		dynamicPackage(coins, n, f - e);


		free(coins);
		coins = NULL;	
	}	

	return 0;
}


多重背包问题


题目

有N种物品和一个容量为V的背包。第i种物品最多有n[i]件,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大

思路

多重背包问题的思路跟完全背包的思路非常类似,只是k的取值是有限制的,因为每件物品的数量是有限制的,状态转移方程为:

dp[i][v] = max{dp[i - 1][v - k * c[i]] + w[i] | 0 <=k <= n[i]}


练习题目

题目:http://ac.jobdu.com/problem.php?pid=1455

代码:

#include <stdio.h>
#include <stdlib.h>
 
typedef struct rice {
    int price, weight, num;
} rice;
 
void dynamic(rice *rices, int m, int n)
{
    int i, j, cur, k, **dp;
 
    // 动态申请二维数组
    dp = (int **)malloc(sizeof(int *) * (m + 1));
    for (i = 0; i <= m; i ++)
        dp[i] = (int *)malloc(sizeof(int) * (n + 1));
 
    // 初始化
    for (i = 0; i <= m; i ++)
        for (j = 0; j <= n; j ++)
            dp[i][j] = 0;
 
    // 动态规划
    for (i = 1; i <= m; i ++) {
        for (j = 1; j <= n; j ++) {
            for (k = 0; k <= rices[i].num; k ++) {
                if (j - k * rices[i].price >= 0) {
                    cur = dp[i - 1][j - k * rices[i].price] + k * rices[i].weight;
                    dp[i][j] = dp[i][j] > cur ? dp[i][j] : cur;
                } else {
                    break;
                }
            }
        }
    }
 
    printf("%d\n", dp[m][n]);
 
    for (i = 0; i <= m; i ++)
        free(dp[i]);
}
 
 
int main(void)
{
    int i, c, n, m;
     
    rice rices[2010];
 
    scanf("%d", &c);
 
    while (c --) {
        scanf("%d %d", &n, &m);
 
        // 接收数据
        for (i = 1; i <= m; i ++) {
            scanf("%d %d %d", &rices[i].price, &rices[i].weight, &rices[i].num);
        }
 
        // 多重背包问题
        dynamic(rices, m, n);
    }
 
 
    return 0;
}
 

后记

主要还是为了巩固01背包问题并且多做点题目,所以记录了一下学习《背包九讲》的过程,大家真想搞清楚背包问题,建议还是参考原文链接:http://love-oriented.com/pack/