01 背包的多种写法

01 背包 OJ 题有:HDU 2602

本文基本来自 挑战程序设计竞赛第二版 P51 以及部分来自 背包九讲

01 背包我觉得是最经典的动态规划问题之一,同时也是背包问题中的最简单的情形之一。所以有效掌握其不同形式的解法无论是对我们理解动态规划思想,还是以后方便阅读别人的背包问题的解法都是大有裨益的。

01 背包问题

问题描述

有 n 个重量和价值分别为 weight i , value i \text{weight}_i, \text{value}_i weighti,valuei 的物品。从这些物品中挑选出总重量不超过 W W W 的物品,求所有挑选方案中价值总和的最大值。

限制条件 :

  • 1 ⩽ n ⩽ 100 1\leqslant n\leqslant100 1n100
  • 1 ⩽ weight i , value i ⩽ 100 1\leqslant \text{weight}_i, \text{value}_i \leqslant100 1weighti,valuei100
  • 1 ⩽ W ⩽ 10000 1\leqslant W \leqslant10000 1W10000

输入

n=4
(w, v) = {(2,3), (1,2), (3,4), (2,2)}
W=5

输出

7(选择 0、1、3 号物品)

解决方法

递归

无优化

不妨先用最朴素的方法,针对每个物品是否放入背包进行搜索试试看。这个想法实现后的结果参考如下代码。

// 输入
int n, W;
int weight[MAX_N], value[MAX_N];

// 从第 i 个物品开始挑选总重小于 j 的部分
int rec(int i, int j) {
   
    int res;
    if (i == n) {
   
        // 已经没有剩余物品了
        res = 0;
    } else if (j < weight[i]) {
   
        // 当前物品的重量大于剩余允许重量,无法挑选这个物品
        res = rec(i+1, j);
    } else {
   
        // 挑选和不挑选两种情况都尝试一下,从中选最优的
        res = max(rec(i+1, j), rec(i+1, j-weight[i])+value[i]);
    }

    return res;
}

void solve() {
   
    printf("%d\n", rec(0, W));
}

分析:这种方法的搜索深度是 n,而且每一层的搜索都需要两次分支,最坏就需要 O ( 2 n ) O(2^n) O(2n) 的时间,当 n 比较大时就没办法解了。所以我们得对原来的算法做些优化。

优化之前,我们先来看下针对样例输入的情形下 rec 递归调用的情况。

如图所示,rec 以 (3,2) 为参数调用了两次。如果参数相同,返回结果也应该相同,所以第二次调用时如果还计算一遍原来计算过的结果就会白白浪费计算时间。

想要用到原来计算出的结果我们就得把第一次计算的结果记录下来,然后每次计算前看看是否已经计算过。

剪枝(记忆化搜索)
int dp[MAX_N+1][MAX_W+1]; // 记忆化数组

int rec(int i, int j) {
   
    if (dp[i][j] >= 0) {
   
        // 已经计算过的话直接使用之前的结果
        return dp[i][j];
    }

    int res;
    if (i == n) {
   
        res = 0;
    } else if (j < weight[i]) {
   
        res = rec(i+1, j);
    } else {
   
        res = max(rec(i+1, j), rec(i+1, j-weight[i])+value[i]);
    }

    // 将结果记录在数组中
    dp[i][j] = res;

    return res;
}

void sole() {
   
    // 用 -1 表示尚未计算过,初始化整个数组
    memset(dp, -1, sizeof(dp));
    printf("%d\n", rec(0, W));
}

这样微小的改进能降低多少复杂度呢?对于同样的参数,只会在第一次被调用到时执行递归部分,第二次之后都会直接返回。参数的组合不过 n W nW nW 种,而函数内只调用 2 次递归,所以只需要 O ( n W ) O(nW) O(nW) 的复杂度就能解决这个问题。

只需要略微改良,可解的问题的规模就可以大幅提高。这种方法一般被称为记忆化搜索

穷竭搜索

如果对记忆化搜索还不是很熟练的话,可能会把前面的搜索写成下面这样

// 目前选择的物品价值总和是 sum,从第 i 个物品之后的物品中挑选重量总和小于 j 的物品
int rec(int i, int j, int sum) {
   
    int res;
    if (i == n) {
   
        // 已经没有剩余物品了
        res = sum;
    } else if (j < weight[i]) {
   
        // 当前物品的重量大于剩余允许重量,无法挑选这个物品
        res = rec(i+1, j, sum);
    } else {
   
        // 挑选和不挑选两种情况都尝试一下,从中选最优的
        res = max(rec(i+1, j, sum), rec(i+1, j-weight[i], sum+value[i]));
    }

    return res;
}

void solve() {
   
    printf("%d\n", rec(0, W, 0));
}

在需要剪枝的情况下,可能会像这样把各种参数都写在函数上,但是在这种情况下会让记忆化搜索难以实现,需要注意。

循环

逆向递推关系的 DP

接下来,我们来仔细研究一下前面的算法利用到的记忆化数组 dp。我们记 dp[i][j] 为 rec 的定义:『从第 i 个物品开始挑选总重小于 j 时,总价值的最大值』。于是,我们有如下递推式(状态转移方程

d p [ n ] [ j ] = 0 (因为已经是最后一个物品,没有多于的物品提供其自身价值了) d p [ i ] [ j ] = { d p [ i + 1 ] [ j ] ( j < w e i g h t [ i ] ) m a x ( d p [ i + 1 ] [ j ] , d p [ i + 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) ( 其 他 ) dp[n][j] = 0 \text{(因为已经是最后一个物品,没有多于的物品提供其自身价值了)} \\ dp[i][j] = \begin{cases} dp[i+1][j] & (j<weight[i]) \\ max(dp[i+1][j], dp[i+1][j-weight[i]]+value[i]) & (其他) \end{cases}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值