01 背包问题的三种写法

不妨先用最朴素的方法,针对每个物品是否放入背包进行搜索:

// 输入
int n, W;
int w[MAX_N], v[MAX_N];

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

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

只不过,这种方法的搜索深度是 n,而且每一层的搜索都需要两次分支,最坏的就需要 O(2^{n}) 的时间,当 n 比较大时就没办法解了。由于 rec 在递归调用时存在重复调用的情况,所以我们可以把第一次计算时的结果记录下来,省略掉第二次以后的重复计算试试看。

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 < w[i]) {
        res = rec(i + 1, j);
    } else {
        res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
    }

    // 将结果储存在数组中
    return dp[i][j] = res;
}

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

对于同样的参数,只会在第一次被调用到时执行递归部分,第二次之后都会直接返回。参数的组合不过 nW 种,而函数内只调用两次递归,所以只需要 O(nW) 的复杂度就能解决问题。这种方法一般被称为记忆化搜索

如果对记忆化搜索还不是很熟练的话,可以写成穷竭搜索的写法:

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

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

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

dp[n][j] = 0

dp[i][j] = \left\{\begin{matrix} dp[i + 1][j] & & (j < w[i]) \\ max(dp[i+1][j], dp[i+1][j - w[i]] + v[i]) & & (else) \end{matrix}\right.

这样不用写递归函数,直接利用递推式将各项的值计算出来,简单的二重循环也能解决这一问题。

int dp[MAX_N + 1][MAX_W + 1];  // DP 数组

void solve() {
    for (int i = n - 1; i >= 0; i--) {
        for (int j = 0; j <= W; j++) {
            if (j < w[i]) {
                dp[i][j] = dp[i + 1][j];
            } else {
                dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i]);
            }
        }
    }
    printf("%d\n", dp[0][W]);
}

这个算法的复杂度与前面的相同,也是 O(nW) ,但是简洁了很多。以这种方式一步步按顺序求出问题的解的方法被称作动态规划法,也就是常说的 DP

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

laugh12321

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值