一和零 图解题解
这道题到底在问什么
- 输入
- strs=["10","0001","1","0"], m=3, n=3
- 输出
- 3
最优解:为什么这么做
一句话答案:LeetCode 474 一和零:把 0、1 的个数当两道容量轴的 0-1 背包。dp[i][j] 记不超过 i 个 0、j 个 1 能选的最多字符串数,每串选或不选,内层双维倒序防重复选。时间 O(L·m·n)、空间 O(m·n)。
两个预算下最多能挑几个二进制串
给一堆二进制字符串 strs,和「最多用 m 个 0、n 个 1」的总预算。每选一个串,就花掉它里面 0 和 1 的个数,问两预算都不超时最多选几个。题面例子 strs=["10","0001","1","0"]、m=3、n=3,答案是 3——全选要 5 个 0、超预算 3,丢掉最贵的 "0001",剩 "10"、"1"、"0" 共 3 个。
为什么把所有子集试一遍数不完
最直接是把 strs 每个子集(子集=从这堆串里任挑几个、含都不挑)列出来,逐个查是否超预算、留最多的。可 L 个串有 2^L 个子集(大 O 记号描述规模变大时操作数怎么涨),L 一大就指数级爆炸数不完,且子集大量重叠被重算。既然「某个 0、1 预算下能选几个」会被反复问到,算一次存下来复用,就是动态规划(DP,把子问题的解记下来别重算)。
为什么这是有两个容量的 0-1 背包
把它看成背包问题(背包=把物品塞进限定容量的包):每个串是一件物品。经典背包只有一维容量,这题一个串同时花掉 0、1 两种资源,两道容量轴一起卡,状态升到二维。定义 dp[i][j] 为「0 用不超过 i 个、1 用不超过 j 个时能选的最多字符串数」,一开始一个串都没选、整张表都是 0。每个串只有选或不选,这是 0-1 背包(每件至多选一次)。
转移式为什么两维一起扣,还得倒序
轮到含 c0 个 0、c1 个 1 的串,每格 dp[i][j] 在两条路里取大:不选它,沿用之前的 dp[i][j];选它,就从「扣掉它消耗后」的 dp[i-c0][j-c1] 接过来加 1。合起来 dp[i][j]=max(dp[i][j], dp[i-c0][j-c1]+1)。
内层两维都要从大到小倒序遍历。这样取的 dp[i-c0][j-c1] 还是「没考虑当前串」的旧值,当前串只计一次;若某一维正序,刚更新过的格子又拿去接自己,同一个串被叠加多次,就成了每件能无限取的完全背包。
拿题面这四个串把表填出 3
四个串的消耗(0 数,1 数):"10"(1,1)、"0001"(3,1)、"1"(0,1)、"0"(1,0)。先处理 "10",0、1 都还剩至少 1 个的格子全变成 1。再处理 "0001",它要 3 个 0,只有 dp[3][j] 付得起,可接的 dp[0][j-1] 全是 0,选不进来。
轮到 "1"(只花 1 个 1),dp[3][3] 从 1 升到 2(接 dp[3][2]=1)。最后处理 "0"(只花 1 个 0),dp[3][3]=max(2, dp[2][3]+1)=max(2, 2+1)=3,选到的正是 "10"、"1"、"0" 这 3 个。
正序会重复选,边界又停在哪
外层扫 L 个串,每个把 (m+1)×(n+1) 的表过一遍,时间 O(L·m·n);只留一张二维 dp 表,空间 O(m·n)。两处最容易写错:一是内层两维忘了倒序、写成正序,同一个串会被重复选、答案偏大;二是内层边界,i 只要减到 c0、j 减到 c1 就该停,预算不够付它就选不了,再减会取到负下标。
▶ 动画逐步走查(共 63 步)——想跟着动画一帧帧对照就展开
- 3把 0 的预算当一维、1 的预算当另一维,两条容量轴一起卡——这就是二维背包。
- 4行标 = 还能用几个 0,列标 = 还能用几个 1。一个字符串都没放,dp 全是 0(一个都选不了)。
- 5轮到 "10",它要吃掉 1 个 0 和 1 个 1。下面从「预算最足」的右下角倒着往回更新——每格问:把它选进来划不划算?
- 6算 dp[3][3](剩 3 个 0、3 个 1):不选 "10" 还是 0;选它就在「扣掉它消耗后」的 dp[2][2]=0 基础上 +1 = 1。
- 7选它更划算,dp[3][3] 升到 1。
- 8算 dp[3][2](剩 3 个 0、2 个 1):不选 "10" 还是 0;选它就在「扣掉它消耗后」的 dp[2][1]=0 基础上 +1 = 1。
- 9选它更划算,dp[3][2] 升到 1。
- 10算 dp[3][1](剩 3 个 0、1 个 1):不选 "10" 还是 0;选它就在「扣掉它消耗后」的 dp[2][0]=0 基础上 +1 = 1。
- 11选它更划算,dp[3][1] 升到 1。
- 12算 dp[2][3](剩 2 个 0、3 个 1):不选 "10" 还是 0;选它就在「扣掉它消耗后」的 dp[1][2]=0 基础上 +1 = 1。
- 13选它更划算,dp[2][3] 升到 1。
- 14算 dp[2][2](剩 2 个 0、2 个 1):不选 "10" 还是 0;选它就在「扣掉它消耗后」的 dp[1][1]=0 基础上 +1 = 1。
- 15选它更划算,dp[2][2] 升到 1。
- 16算 dp[2][1](剩 2 个 0、1 个 1):不选 "10" 还是 0;选它就在「扣掉它消耗后」的 dp[1][0]=0 基础上 +1 = 1。
- 17选它更划算,dp[2][1] 升到 1。
- 18算 dp[1][3](剩 1 个 0、3 个 1):不选 "10" 还是 0;选它就在「扣掉它消耗后」的 dp[0][2]=0 基础上 +1 = 1。
- 19选它更划算,dp[1][3] 升到 1。
- 20算 dp[1][2](剩 1 个 0、2 个 1):不选 "10" 还是 0;选它就在「扣掉它消耗后」的 dp[0][1]=0 基础上 +1 = 1。
- 21选它更划算,dp[1][2] 升到 1。
- 22算 dp[1][1](剩 1 个 0、1 个 1):不选 "10" 还是 0;选它就在「扣掉它消耗后」的 dp[0][0]=0 基础上 +1 = 1。
- 23选它更划算,dp[1][1] 升到 1。
- 24轮到 "0001",它要吃掉 3 个 0 和 1 个 1。下面从「预算最足」的右下角倒着往回更新——每格问:把它选进来划不划算?
- 25轮到 "1",它要吃掉 0 个 0 和 1 个 1。下面从「预算最足」的右下角倒着往回更新——每格问:把它选进来划不划算?
- 26算 dp[3][3](剩 3 个 0、3 个 1):不选 "1" 还是 1;选它就在「扣掉它消耗后」的 dp[3][2]=1 基础上 +1 = 2。
- 27选它更划算,dp[3][3] 升到 2。
- 28算 dp[3][2](剩 3 个 0、2 个 1):不选 "1" 还是 1;选它就在「扣掉它消耗后」的 dp[3][1]=1 基础上 +1 = 2。
- 29选它更划算,dp[3][2] 升到 2。
- 30算 dp[2][3](剩 2 个 0、3 个 1):不选 "1" 还是 1;选它就在「扣掉它消耗后」的 dp[2][2]=1 基础上 +1 = 2。
- 31选它更划算,dp[2][3] 升到 2。
- 32算 dp[2][2](剩 2 个 0、2 个 1):不选 "1" 还是 1;选它就在「扣掉它消耗后」的 dp[2][1]=1 基础上 +1 = 2。
- 33选它更划算,dp[2][2] 升到 2。
- 34算 dp[1][3](剩 1 个 0、3 个 1):不选 "1" 还是 1;选它就在「扣掉它消耗后」的 dp[1][2]=1 基础上 +1 = 2。
- 35选它更划算,dp[1][3] 升到 2。
- 36算 dp[1][2](剩 1 个 0、2 个 1):不选 "1" 还是 1;选它就在「扣掉它消耗后」的 dp[1][1]=1 基础上 +1 = 2。
- 37选它更划算,dp[1][2] 升到 2。
- 38算 dp[0][3](剩 0 个 0、3 个 1):不选 "1" 还是 0;选它就在「扣掉它消耗后」的 dp[0][2]=0 基础上 +1 = 1。
- 39选它更划算,dp[0][3] 升到 1。
- 40算 dp[0][2](剩 0 个 0、2 个 1):不选 "1" 还是 0;选它就在「扣掉它消耗后」的 dp[0][1]=0 基础上 +1 = 1。
- 41选它更划算,dp[0][2] 升到 1。
- 42算 dp[0][1](剩 0 个 0、1 个 1):不选 "1" 还是 0;选它就在「扣掉它消耗后」的 dp[0][0]=0 基础上 +1 = 1。
- 43选它更划算,dp[0][1] 升到 1。
- 44轮到 "0",它要吃掉 1 个 0 和 0 个 1。下面从「预算最足」的右下角倒着往回更新——每格问:把它选进来划不划算?
- 45算 dp[3][3](剩 3 个 0、3 个 1):不选 "0" 还是 2;选它就在「扣掉它消耗后」的 dp[2][3]=2 基础上 +1 = 3。
- 46选它更划算,dp[3][3] 升到 3。
- 47算 dp[3][2](剩 3 个 0、2 个 1):不选 "0" 还是 2;选它就在「扣掉它消耗后」的 dp[2][2]=2 基础上 +1 = 3。
- 48选它更划算,dp[3][2] 升到 3。
- 49算 dp[3][1](剩 3 个 0、1 个 1):不选 "0" 还是 1;选它就在「扣掉它消耗后」的 dp[2][1]=1 基础上 +1 = 2。
- 50选它更划算,dp[3][1] 升到 2。
- 51算 dp[3][0](剩 3 个 0、0 个 1):不选 "0" 还是 0;选它就在「扣掉它消耗后」的 dp[2][0]=0 基础上 +1 = 1。
- 52选它更划算,dp[3][0] 升到 1。
- 53算 dp[2][3](剩 2 个 0、3 个 1):不选 "0" 还是 2;选它就在「扣掉它消耗后」的 dp[1][3]=2 基础上 +1 = 3。
- 54选它更划算,dp[2][3] 升到 3。
- 55算 dp[2][2](剩 2 个 0、2 个 1):不选 "0" 还是 2;选它就在「扣掉它消耗后」的 dp[1][2]=2 基础上 +1 = 3。
- 56选它更划算,dp[2][2] 升到 3。
- 57算 dp[2][1](剩 2 个 0、1 个 1):不选 "0" 还是 1;选它就在「扣掉它消耗后」的 dp[1][1]=1 基础上 +1 = 2。
- 58选它更划算,dp[2][1] 升到 2。
- 59算 dp[2][0](剩 2 个 0、0 个 1):不选 "0" 还是 0;选它就在「扣掉它消耗后」的 dp[1][0]=0 基础上 +1 = 1。
- 60选它更划算,dp[2][0] 升到 1。
- 61算 dp[1][1](剩 1 个 0、1 个 1):不选 "0" 还是 1;选它就在「扣掉它消耗后」的 dp[0][1]=1 基础上 +1 = 2。
- 62选它更划算,dp[1][1] 升到 2。
- 63算 dp[1][0](剩 1 个 0、0 个 1):不选 "0" 还是 0;选它就在「扣掉它消耗后」的 dp[0][0]=0 基础上 +1 = 1。
- 64选它更划算,dp[1][0] 升到 1。
- 65右下角 dp[3][3]=3:在「3 个 0、3 个 1」预算内,最多能选 3 个字符串。
⚠️ 容易写错的地方
✗ 错:两维都正序遍历
✓ 对:i、j 都要倒序
0-1 背包正序会让同一个串被重复选多次
✗ 错:把消耗当成两道独立背包
✓ 对:0 和 1 的预算要同时卡
选一个串同时扣两种容量,必须二维一起转移
✗ 错:边界从 0 起遍历
✓ 对:i 到 cost0、j 到 cost1 就停
预算不够付这个串的消耗时根本选不了它
完整代码(Python / C++ / Java)
Python
def findMaxForm(strs, m, n):
dp = [[0]*(n+1) for _ in range(m+1)]
for s in strs:
z, o = s.count("0"), s.count("1")
for i in range(m, z-1, -1):
for j in range(n, o-1, -1):
dp[i][j] = max(dp[i][j], dp[i-z][j-o] + 1)
return dp[m][n]C++
int findMaxForm(vector<string>& strs, int m, int n){
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for(auto& s : strs){
int z = count(s.begin(), s.end(), '0'), o = s.size() - z;
for(int i = m; i >= z; i--)
for(int j = n; j >= o; j--)
dp[i][j] = max(dp[i][j], dp[i-z][j-o] + 1);
}
return dp[m][n];
}Java
int findMaxForm(String[] strs, int m, int n){
int[][] dp = new int[m+1][n+1];
for(String s : strs){
int z = 0;
for(int t = 0; t < s.length(); t++) if(s.charAt(t)=='0') z++;
int o = s.length() - z;
for(int i = m; i >= z; i--)
for(int j = n; j >= o; j--)
dp[i][j] = Math.max(dp[i][j], dp[i-z][j-o] + 1);
}
return dp[m][n];
}复杂度
时间
O(L·m·n)
L 个串各扫一遍 m×n 表
空间
O(m·n)
只需二维 dp 表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 一和零 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
内层两维为什么必须都从大到小倒序?+
因为每个字符串至多选一次,这是 0-1 背包。倒序遍历保证算 dp[i][j] 时取的 dp[i-c0][j-c1] 还是「没考虑当前串」的旧值,当前串只会被计入一次。若某一维正序,刚被当前串更新过的格子会立刻又拿去接自己,同一个串被叠加多次,就变成了每件物品能无限取的完全背包,答案会偏大。
它和经典 01 背包差在哪,为什么状态要升到二维?+
经典 01 背包只有一维重量容量,dp[w] 一维就够。这题一个字符串要同时花掉 0 和 1 两种资源,等于有两道独立的容量限制、必须同时满足,所以 dp 升到二维 dp[i][j],转移时两维一起扣减到 dp[i-c0][j-c1]。本质还是 01 背包,只是容量从一维变两维。
能不能把 0 和 1 拆成两道独立的背包分别求?+
不能。选一个字符串会同时消耗 0 和 1,两种预算是绑在一起花的,不是各自独立。若拆成两道背包分别算再拼,会算出「0 预算下选 A、1 预算下选 B」这种彼此矛盾的方案。必须把两维放进同一个状态 dp[i][j] 一起转移,才能保证选出的每个串在两种预算里都付得起。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 一和零 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。