01背包
题目描述
有
n
n
n 个重量和价值分别为
w
i
w_i
wi,
v
i
v_i
vi 的物品。从这些物品中挑选出总重量不超过
W
W
W 的物品,求所有挑选方案中价值总和的最大值。
数据范围:
1
≤
n
≤
100
1\le n\le100
1≤n≤100
1
≤
w
i
,
v
i
≤
100
1\le w_i,v_i\le100
1≤wi,vi≤100
1
≤
W
≤
10000
1\le W\le10000
1≤W≤10000
优化前
- 递推式:
d p [ 0 ] [ j ] = 0 d p [ i + 1 ] [ j ] = { d p [ i ] [ j ] , j < w i m a x { d p [ i ] [ j ] , d p [ i ] [ j − w i ] + v i } , j ≥ w i \begin{split} dp[0][j]&=0 \\ dp[i + 1][j]&=\begin{cases} dp[i][j]&,j<w_i \\ max\{dp[i][j],dp[i][j-w_i]+v_i\}&,j\ge w_i \end{cases} \end{split} dp[0][j]dp[i+1][j]=0={dp[i][j]max{dp[i][j],dp[i][j−wi]+vi},j<wi,j≥wi
- 代码:
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX][MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= W; j++)
{
if (j < w[i])
dp[i + 1][j] = dp[i][j];
else
dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
}
}
printf("%d\n", dp[n][W]);
}
优化一:二维数组
由 d p [ i + 1 ] [ j ] = m a x { d p [ i ] [ j ] , d p [ i ] [ j − w i ] + v i } dp[i + 1][j]=max\{dp[i][j],dp[i][j-w_i]+v_i\} dp[i+1][j]=max{dp[i][j],dp[i][j−wi]+vi},我们可以发现,虽然 d p [ M A X ] [ M A X ] dp[MAX][MAX] dp[MAX][MAX] 有 M A X MAX MAX 行,但是每次只访问 i i i 和 i + 1 i+1 i+1行。因此,我们只要用一个 d p [ 2 ] [ M A X ] dp[2][MAX] dp[2][MAX] 就能实现原来的功能。
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[2][MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= W; j++)
{
if (j < w[i])
dp[(i + 1) & 1][j] = dp[i & 1][j];
else
dp[(i + 1) & 1][j] = max(dp[i & 1][j], dp[i & 1][j - w[i]] + v[i]);
}
}
printf("%d\n", dp[n & 1][W]);
}
&
按位与是一个双目操作符,对数字的二进制位进行操作。规则是:两个数字相对应的二进制位相同,运算结果为1;不同,则为0。- 由十进制与二进制的转换规则可知,从右往左看,偶数的第一个二进制为0;奇数的第一个二进制位为1。
所以,我们可以得到:
i
&
1
=
{
1
,
i
=
2
∗
k
+
1
0
,
i
=
2
∗
k
k
=
0
,
1
,
2...
i\&1=\begin{cases} 1&, i=2\ast k+1\\ 0&,i=2\ast k \end{cases} k=0,1,2...
i&1={10,i=2∗k+1,i=2∗kk=0,1,2...
优化二:一维数组
动态规划的核心是先记录,再访问。如果访问过后,数据就不要再需要了,那么覆盖这条记录也不会对结果造成影响。因此,我们只使用一个一维数组
d
p
[
M
A
X
]
dp[MAX]
dp[MAX] 也能达到目的。
我们再来看递推式:
d p [ 0 ] [ j ] = 0 d p [ i + 1 ] [ j ] = { d p [ i ] [ j ] , j < w i m a x { d p [ i ] [ j ] , d p [ i ] [ j − w i ] + v i } , j ≥ w i \begin{split} dp[0][j]&=0 \\ dp[i + 1][j]&=\begin{cases} dp[i][j]&,j<w_i \\ max\{dp[i][j],dp[i][j-w_i]+v_i\}&,j\ge w_i \end{cases} \end{split} dp[0][j]dp[i+1][j]=0={dp[i][j]max{dp[i][j],dp[i][j−wi]+vi},j<wi,j≥wi
- 当
j
<
w
i
j<w_i
j<wi 时,从二维的视角来看,数据从
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 拷贝到
d
p
[
i
+
1
]
[
j
]
dp[i+1][j]
dp[i+1][j];切换到一维,数据其实没有发生变化,也就是说不需要对原来的数据进行操作。
- 当
j
≥
w
i
j\ge w_i
j≥wi 时,从二维的视角来看,计算
d
p
[
i
+
1
]
[
j
]
dp[i+1][j]
dp[i+1][j] 需要访问
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 和
d
p
[
i
]
[
j
−
w
i
]
dp[i][j-w_i]
dp[i][j−wi]。再到一维,就可以发现,需要的数据是总是在该位置的左半部分(要计算第
j
j
j 列,需要访问第
j
j
j 列和第
j
−
w
i
j-w_i
j−wi 列的数据)。也就是说,右半部分的数据是可以覆盖的)。
原来的代码是
for (int j = 0; j <= W; j++)
因为是从左向右的循环,所以肯定影响后来的计算。若改为从右往左计算,则能避免这样的问题。
d p [ j ] = m a x { d p [ j ] , d p [ j − w i ] + v i } , j ≥ w i dp[j]=max\{dp[j],dp[j-w_i]+v_i\},j\ge w_i dp[j]=max{dp[j],dp[j−wi]+vi},j≥wi
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = W; j >= w[i]; j--)
{
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
printf("%d\n", dp[W]);
}
完全背包
题目描述
有
n
n
n 个重量和价值分别为
w
i
w_i
wi,
v
i
v_i
vi 的物品。从这些物品中挑选出总重量不超过
W
W
W 的物品,求所有挑选方案中价值总和的最大值。在这里,每种物品可以挑选任意多件。
数据范围:
1
≤
n
≤
100
1\le n\le100
1≤n≤100
1
≤
w
i
,
v
i
≤
100
1\le w_i,v_i\le100
1≤wi,vi≤100
1
≤
W
≤
10000
1\le W\le10000
1≤W≤10000
优化前
- 递推式
d p [ 0 ] [ j ] = 0 d p [ i + 1 ] [ j ] = { d p [ i ] [ j ] , j < w i m a x { d p [ i ] [ j ] , d p [ i + 1 ] [ j − w i ] + v i } , j ≥ w i \begin{split} dp[0][j]&=0 \\ dp[i + 1][j]&=\begin{cases} dp[i][j]&,j<w_i \\ max\{dp[i][j],dp[i+1][j-w_i]+v_i\}&,j\ge w_i \end{cases} \end{split} dp[0][j]dp[i+1][j]=0={dp[i][j]max{dp[i][j],dp[i+1][j−wi]+vi},j<wi,j≥wi
- 代码
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX][MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= W; j++)
{
if (j < w[i])
dp[i + 1][j] = dp[i][j];
else
dp[i + 1][j] = max(dp[i][j], dp[i + 1][j - w[i]] + v[i]); // 与01背包唯一的不同
}
}
printf("%d\n", dp[n][W]);
}
优化一:二维数组
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[2][MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= W; j++)
{
if (j < w[i])
dp[(i + 1) & 1][j] = dp[i & 1][j];
else
dp[(i + 1) & 1][j] = max(dp[i & 1][j], dp[(i + 1) & 1][j - w[i]] + v[i]); // 与01背包唯一的不同
}
}
printf("%d\n", dp[n & 1][W]);
}
优化二:一维数组
- 当
j
<
w
i
j<w_i
j<wi 时,与01背包中的分析一样。
- 当
j
≥
w
i
j\ge w_i
j≥wi 时,与01背包唯一的不同是,其中一个数据由
d
p
[
i
]
[
j
−
w
i
]
dp[i][j-w_i]
dp[i][j−wi] 变成了
d
p
[
i
+
1
]
[
j
−
w
i
]
dp[i+1][j-w_i]
dp[i+1][j−wi]。虽然都是访问计算点左半部分的数据,但是,需要注意的是,访问的是新数据(
i
+
1
i+1
i+1),不是老数据(
i
i
i)。因此,第二个循环必须是从左往右的。
d p [ j ] = m a x { d p [ j ] , d p [ j − w i ] + v i } , j ≥ w i dp[j]=max\{dp[j],dp[j-w_i]+v_i\},j\ge w_i dp[j]=max{dp[j],dp[j−wi]+vi},j≥wi
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = w[i]; j <= W; j++)
{
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
printf("%d\n", dp[W]);
}
与01背包比较,通过该方法优化后,二者代码的区别仅在第二个循环的方向:
- 01背包:
for (int j = W; j >= w[i]; j--) \\ 从右往左
- 完全背包:
for (int j = w[i]; j <= W; j++) \\ 从左往右
完