474. 一和零

考点

  • 01背包
  • 精髓题

思路

二维01背包的精髓题,必须搞懂

暴力DP

每个字符串都有若干个0和1,每个字符串选或不选两种状态

总的01个数相同的情况下,选字符串个数最大的,这就是经典的01背包,很容易写出下面的三维DP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
public:
int findMaxForm(vector<string>& strs, int M, int N) {
int c0[605], c1[605];
memset(c0, 0, sizeof(c0)), memset(c1, 0, sizeof(c1));
// 预处理
for (int i = 0; i < strs.size(); ++i) {
for (int j = 0; j < strs[i].size(); ++j) {
if (strs[i][j] == '0')
c0[i]++;
else
c1[i]++;
}
}
// 01背包, f[i][j][k]: 前i个, 记录0的个数为j、1的个数为k的序列长度,取最大值
int f[605][105][105];
memset(f, 0xcf, sizeof(f));
f[0][0][0]=0;
int n = strs.size();
for (int i = 0; i < n; ++i) {
for (int j = 0; j <= M; ++j) {
for (int k = 0; k <= N; ++k) {
if(f[i][j][k] == 0xcfcfcfcf) continue;
f[i + 1][j][k] = max(f[i + 1][j][k], f[i][j][k]);
if (j + c0[i] <= M && k + c1[i] <= N)
f[i + 1][j + c0[i]][k + c1[i]] =
max(f[i + 1][j + c0[i]][k + c1[i]], f[i][j][k] + 1);
}
}
}
int ans = 0;
for (int j = 0; j <= M; ++j) {
for (int k = 0; k <= N; ++k) {
ans = max(ans, f[n][j][k]);
}
}
return ans;
}
};

正序逆序问题

第一维可以滚动数组去掉,但是这里jk都必须逆序

尽管jk隔离开了,但是可以发现在c0 = 0的情况下,状态转移方程退化为

1
f[j + 0][k + c1] = max(f[j + 0][k + c1], f[j][k] + 1);

实际上就是在同一层做01背包,所以k也必须进行倒序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
int findMaxForm(vector<string>& strs, int M, int N) {
// 01背包, f[i][j][k]: 前i个, 记录0的个数为j、1的个数为k的序列长度,取最大值
int f[105][105], n = strs.size();
memset(f, 0xcf, sizeof(f));
f[0][0] = 0;
for (int i = 0; i < n; ++i) {
int c0 = 0, c1 = 0;
for (char chr : strs[i]) chr == '0' ? c0++ : c1++;
for (int j = M - c0; j >= 0; --j) {
for (int k = N - c1; k >=0 ; --k) {
if (f[j][k] < 0) continue;
f[j + c0][k + c1] = max(f[j + c0][k + c1], f[j][k] + 1);
}
}
}
int ans = 0;
for (int j = 0; j <= M; ++j) {
for (int k = 0; k <= N; ++k) {
ans = max(ans, f[j][k]);
}
}
return ans;
}
};

当然如果你不信的话,单独把c0 == 0这一层剥开就行了,发现正序也能AC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
int findMaxForm(vector<string>& strs, int M, int N) {
// 01背包, f[i][j][k]: 前i个,
// 记录0的个数为j、1的个数为k的序列长度,取最大值
int f[105][105], n = strs.size();
memset(f, 0xcf, sizeof(f));
f[0][0] = 0;
for (int i = 0; i < n; ++i) {
int c0 = 0, c1 = 0;
for (char chr : strs[i])
chr == '0' ? c0++ : c1++;
for (int j = M - c0; j >= 0; --j) {
if (c0) {
for (int k = 0; k <= N - c1; ++k) {
if (f[j][k] < 0)
continue;
f[j + c0][k + c1] = max(f[j + c0][k + c1], f[j][k] + 1);
}
} else {
for (int k = N - c1; k >= 0; --k) {
if (f[j][k] < 0)
continue;
f[j + c0][k + c1] = max(f[j + c0][k + c1], f[j][k] + 1);
}
}
}
}
int ans = 0;
for (int j = 0; j <= M; ++j) {
for (int k = 0; k <= N; ++k) {
ans = max(ans, f[j][k]);
}
}
return ans;
}
};

背包容量从“刚好”变成“小于等于”

先考虑经典的背包问题

迄今为止,我们定义的背包容量都是“刚好”,这样好想也好写

f[i][j]为前i个物品刚好达到容量j,这也是为什么最后需要遍历所有的f[n][0 ~ m]来找极值,因为不一定能装满

现在改成f[i][j]为前i个物品的容量小于等于j,情况变成如下:

1
2
3
初始值, memset(f, 0 ,sizeof(f)), f[0][0 ~ m] = 0
f[i][j] = max(f[i-1][j], f[i-1][j - w[i]] + v[i])
答案, f[n][m]

对比我们经典的模型,发现只有初值和结果不同而已,方程竟然是没变的:

1
2
3
初始值, memset(f, 0xcf ,sizeof(f)), f[0][0] = 0
f[i][j] = max(f[i-1][j], f[i-1][j - w[i]] + v[i])
答案, max(f[n][0 ~ m])

很容易理解,举个例子,和 <= 5,但当前物品重量为3,两边同时减去3,得到和 - 3 <= 5 - 3,即和 <= 2

如果实在觉得荒谬,我也做了数学推导,可以看图:

那么这题也可以用新的定义压缩代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int findMaxForm(vector<string>& strs, int M, int N) {
// 01背包, f[i][j][k]: 考虑前i个,
// 记录0的个数小于等于j、1的个数小于等于k的序列长度,取最大值
int f[105][105], n = strs.size();
memset(f, 0, sizeof(f));
for (int i = 0; i < n; ++i) {
int c0 = 0, c1 = 0;
for (char chr : strs[i]) chr == '0' ? c0++ : c1++;
for (int j = M - c0; j >= 0; --j) {
for (int k = N - c1; k >= 0; --k) {
f[j + c0][k + c1] = max(f[j + c0][k + c1], f[j][k] + 1);
}
}
}
return f[M][N];
}
};