从0-1背包问题看动态规划的几种方式
0-1 knapsack
Problem Statement
Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack that has a capacity ‘C’. The goal is to get the maximum profit from the items in the knapsack. Each item can only be selected once, as we don’t have multiple quantities of any item.
Items: { Apple, Orange, Banana, Melon }
Weights: { 2, 3, 1, 4 }
Profits: { 4, 5, 3, 7 }
Knapsack capacity: 5
Output: 10
暴力解法
每个 Item 都可以放或者不放,可以对每个元素一次进行递归
export function bruteforce(
profits: number[],
weights: number[],
capacity: number
) {
const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;
return Math.max(
c + weights[i] > capacity
? p
: recursive(i + 1, c + weights[i], p + profits[i]),
recursive(i + 1, c, p)
);
};
const ans = recursive(0, 0, 0);
return ans;
}
时间复杂度:O(2n)
空间复杂度:O(n)
Top-Down DP
稍微变一个 recursive 的逻辑,recursive 函数表示,把 0~i 范围内的 Item 装入 capacity 为 c 的背包中,能够获得的最大的 profit
可以看到有重复的分支,我们需要做的就是在这个基础上记忆
export function topDown(
profits: number[],
weights: number[],
capacity: number
) {
const dp: Record<string, number> = {};
const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;
const key = `${i}_${c}`;
if (dp[key]) {
return dp[key];
}
const ans = Math.max(
c - weights[i] >= 0
? recursive(i + 1, c - weights[i], p + profits[i])
: p,
recursive(i + 1, c, p)
);
return (dp[key] = ans);
};
const ans = recursive(0, capacity, 0);
return ans;
}
时间复杂度:O(n * c),因为记忆化后,最多有 n * c 个子问题
空间复杂度:O(n * c + n)
Bottom-Up DP
dp 记录对于前 i 个元素,当 capacity 为 c 时,能获得的最大 profit,那么
dp[i][c] = max (
// 不取当前元素
dp[i-1][c],
// 取当前元素
profits[i] + dp[i-1][c-weights[i]]
)
图解如下:
export function bottomUp(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[][] = new Array(N)
.fill(0)
.map(() => new Array(capacity + 1).fill(0));
// 初始化
for (let i = 0; i < N; i++) dp[i][0] = 0;
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[0][c] = profits[0];
}
// dp
for (let i = 1; i < N; i++) {
for (let c = 1; c <= capacity; c++) {
dp[i][c] = Math.max(
dp[i - 1][c],
c < weights[i] ? 0 : profits[i] + dp[i - 1][c - weights[i]]
);
}
}
return dp[N - 1][capacity];
}
时间复杂度:O(n * c)
空间复杂度:O(n * c)
Bottom-Up DP 优化
在计算第 i
个元素的过程中,只需要用到前一次的 dp[c]
and dp[c-weight[i]]
,所以,在空间复杂度上我们可以做优化,可以使用同一个数组进行前后两次的记忆
如果 capacity 从 c ~ 0
,而不是 0 ~ c
循环,去修改 dp[i][c ~ capacity]
,可以确保 dp[i][0 ~ c-1]
的值是前一次的,但是,如果按照之前的反过来,计算 dp[i][c]
的时候,dp[i][0 ~ c-1]
已经变成第 i
次的值在存储,所以行不通
export function bottomUp2(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[] = new Array(capacity + 1).fill(0);
// 初始化
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[c] = profits[0];
}
// dp
for (let i = 1; i < N; i++) {
for (let c = capacity; c >= 0; c--) {
dp[c] = Math.max(
dp[c],
c < weights[i] ? 0 : profits[i] + dp[c - weights[i]]
);
}
}
return dp[capacity];
}
作者:sharp_zhou
链接:https://juejin.cn/post/7076458536250638343
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。