目标和 图解题解
这道题到底在问什么
- 输入
- nums=[1,1,1,1,1], target=3
- 输出
- 5
最优解:为什么这么做
一句话答案:LeetCode 494 目标和的最优解是转化成 0-1 背包计数:设取正号的数之和为 P,由 P = (sum + target) / 2 把「添加减号」翻译成「选出和为 P 的子集有几种」,再用 dp[w] += dp[w - x]、容量倒序的一维背包数方案。时间 O(n·P)、空间 O(P),无解时直接返回 0。
目标和这道题在问什么
给数组 nums 和目标值 target,要给每个数前面添一个加号或减号,使整个表达式的和恰好等于 target,问有多少种不同的添法。注意问的是方案数而不是能不能——每个数都必须用且恰好用一次,只是符号二选一。比如 nums=[1,1,1,1,1]、target=3,共有 5 种添法。
为什么加减号问题能转成子集和计数
直接搜索每个数取正取负,是 2 的 n 次方条路径的暴力枚举。转化的钥匙是一组方程:把取正号的那些数之和记作 P,取负号的数之和(取绝对值)记作 N,那么 P + N = sum(所有数都用了),P - N = target(表达式的值)。两式相加解出 P = (sum + target) / 2。
也就是说,一种符号添法完全由「哪些数取正号」决定,而这个正号集合的和必须恰好是 P。反过来,任何一个和为 P 的子集,把它里面的数标正、其余标负,就是一种合法添法。两边一一对应,所以「添符号凑 target 的方案数」等于「选出和为 P 的子集的个数」——问题从指数搜索变成了背包计数。
这个转化还自带两条判定:P 必须是非负整数,所以 (sum + target) 是奇数、或 sum < |target| 时无解,直接返回 0,不用进主循环。
dp 怎么定义,为什么 dp[0] 初始化为 1
定义 dp[w] 为:用目前已考虑的数,凑出和恰好为 w 的方案数,最终答案是 dp[P]。起点 dp[0] = 1——空集是凑出 0 的唯一方式,算一种方案。这个 1 是全表的种子:所有非零方案数都是从它一层层加出来的,写成 0 整张表恒为 0。
和判断「能不能」的布尔背包(如 LeetCode 416 分割等和子集)相比,这里 dp 存的是「有几种」,逻辑或换成加法,其余框架完全一致。
转移为什么是相加,容量为什么必须倒序
每加入一个数 x,凑出 w 的方案分成两类:不选 x 的方案,个数就是原来的 dp[w];选 x 的方案,等于先凑出 w - x 再放入 x,个数是 dp[w - x]。一种方案不可能既含 x 又不含 x,两类不重叠也不遗漏,按加法原理 dp[w] += dp[w - x]。
内层容量必须从 P 倒序扫到 x:转移引用的 dp[w - x] 必须是本轮更新前的旧值,代表「还没用过 x」的状态。正序扫的话 dp[w - x] 可能已被 x 更新过,等于让同一个数用了两次,方案被重复计数。倒序就是「每个数只用一次」的代码化身。
复杂度多少,含 0 的元素会出问题吗
时间复杂度 O(n·P):n 个数各扫一遍长度为 P 的容量轴,P = (sum + target) / 2。空间 O(P),一维滚动数组。
一个值得想清楚的细节:nums 里若有 0,它取正取负都不改变和,但两种符号是两种不同方案。这套 dp 天然处理正确——x = 0 时转移变成 dp[w] += dp[w],每个状态的方案数恰好翻倍,正好对应 0 的两种添法,不需要任何特判。
▶ 动画逐步走查(共 53 步)——想跟着动画一帧帧对照就展开
- 3加减号问题被翻译成「选哪些数当正号 → 子集和 = P 的方案数」。dp[i][s] = 用前 i 个数凑出和 s 的方案数。
- 4第 0 行 = 一个数都没用。只有「容量 0」有 1 种凑法(空集),其余容量都是 0 种。这是所有递推的起点。
- 5算 dp[1][0]:容量 0 比第 1 个数 1 还小,放不进,只能照搬上方 dp[0][0] = 1 种。
- 6填入 dp[1][0] = 1 种。
- 7算 dp[1][1]:不选第 1 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[0][0] = 1 种。两种填法不重叠,相加。
- 8填入 dp[1][1] = 1 种。
- 9算 dp[1][2]:不选第 1 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[0][1] = 0 种。两种填法不重叠,相加。
- 10填入 dp[1][2] = 0 种。
- 11算 dp[1][3]:不选第 1 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[0][2] = 0 种。两种填法不重叠,相加。
- 12填入 dp[1][3] = 0 种。
- 13算 dp[1][4]:不选第 1 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[0][3] = 0 种。两种填法不重叠,相加。
- 14填入 dp[1][4] = 0 种。
- 15算 dp[2][0]:容量 0 比第 2 个数 1 还小,放不进,只能照搬上方 dp[1][0] = 1 种。
- 16填入 dp[2][0] = 1 种。
- 17算 dp[2][1]:不选第 2 个数(值 1)= 上方 1 种;选它 = 先腾出 1 看左上 dp[1][0] = 1 种。两种填法不重叠,相加。
- 18填入 dp[2][1] = 2 种。
- 19算 dp[2][2]:不选第 2 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[1][1] = 1 种。两种填法不重叠,相加。
- 20填入 dp[2][2] = 1 种。
- 21算 dp[2][3]:不选第 2 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[1][2] = 0 种。两种填法不重叠,相加。
- 22填入 dp[2][3] = 0 种。
- 23算 dp[2][4]:不选第 2 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[1][3] = 0 种。两种填法不重叠,相加。
- 24填入 dp[2][4] = 0 种。
- 25算 dp[3][0]:容量 0 比第 3 个数 1 还小,放不进,只能照搬上方 dp[2][0] = 1 种。
- 26填入 dp[3][0] = 1 种。
- 27算 dp[3][1]:不选第 3 个数(值 1)= 上方 2 种;选它 = 先腾出 1 看左上 dp[2][0] = 1 种。两种填法不重叠,相加。
- 28填入 dp[3][1] = 3 种。
- 29算 dp[3][2]:不选第 3 个数(值 1)= 上方 1 种;选它 = 先腾出 1 看左上 dp[2][1] = 2 种。两种填法不重叠,相加。
- 30填入 dp[3][2] = 3 种。
- 31算 dp[3][3]:不选第 3 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[2][2] = 1 种。两种填法不重叠,相加。
- 32填入 dp[3][3] = 1 种。
- 33算 dp[3][4]:不选第 3 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[2][3] = 0 种。两种填法不重叠,相加。
- 34填入 dp[3][4] = 0 种。
- 35算 dp[4][0]:容量 0 比第 4 个数 1 还小,放不进,只能照搬上方 dp[3][0] = 1 种。
- 36填入 dp[4][0] = 1 种。
- 37算 dp[4][1]:不选第 4 个数(值 1)= 上方 3 种;选它 = 先腾出 1 看左上 dp[3][0] = 1 种。两种填法不重叠,相加。
- 38填入 dp[4][1] = 4 种。
- 39算 dp[4][2]:不选第 4 个数(值 1)= 上方 3 种;选它 = 先腾出 1 看左上 dp[3][1] = 3 种。两种填法不重叠,相加。
- 40填入 dp[4][2] = 6 种。
- 41算 dp[4][3]:不选第 4 个数(值 1)= 上方 1 种;选它 = 先腾出 1 看左上 dp[3][2] = 3 种。两种填法不重叠,相加。
- 42填入 dp[4][3] = 4 种。
- 43算 dp[4][4]:不选第 4 个数(值 1)= 上方 0 种;选它 = 先腾出 1 看左上 dp[3][3] = 1 种。两种填法不重叠,相加。
- 44填入 dp[4][4] = 1 种。
- 45算 dp[5][0]:容量 0 比第 5 个数 1 还小,放不进,只能照搬上方 dp[4][0] = 1 种。
- 46填入 dp[5][0] = 1 种。
- 47算 dp[5][1]:不选第 5 个数(值 1)= 上方 4 种;选它 = 先腾出 1 看左上 dp[4][0] = 1 种。两种填法不重叠,相加。
- 48填入 dp[5][1] = 5 种。
- 49算 dp[5][2]:不选第 5 个数(值 1)= 上方 6 种;选它 = 先腾出 1 看左上 dp[4][1] = 4 种。两种填法不重叠,相加。
- 50填入 dp[5][2] = 10 种。
- 51算 dp[5][3]:不选第 5 个数(值 1)= 上方 4 种;选它 = 先腾出 1 看左上 dp[4][2] = 6 种。两种填法不重叠,相加。
- 52填入 dp[5][3] = 10 种。
- 53算 dp[5][4]:不选第 5 个数(值 1)= 上方 1 种;选它 = 先腾出 1 看左上 dp[4][3] = 4 种。两种填法不重叠,相加。
- 54填入 dp[5][4] = 5 种。
- 55看最后一行的第 4 列 dp[5][4] = 5:用全部 5 个数凑出子集和 4 有 5 种 —— 也就是给 nums 添正负号凑出 3 的方法数 = 5。
⚠️ 容易写错的地方
✗ 错:不判 (sum+target) 的奇偶
✓ 对:是奇数 / sum<|target| 直接返回 0
P 必须是非负整数,否则无解
✗ 错:一维 dp 正序填
✓ 对:容量必须倒序 w 从大到小
正序会让一个数被重复使用,变成完全背包,计数翻倍
✗ 错:dp[0] 初值写错
✓ 对:dp[0]=1(空集 1 种)
它是计数递推的种子,写 0 全表归零
完整代码(Python / C++ / Java)
Python
def findTargetSumWays(nums, target):
s = sum(nums)
if (s + target) % 2 or s < abs(target): return 0
P = (s + target) // 2
dp = [1] + [0] * P # dp[0]=1
for x in nums:
for w in range(P, x - 1, -1):
dp[w] += dp[w - x]
return dp[P]C++
int findTargetSumWays(vector<int>& nums, int target){
int s = 0; for(int x : nums) s += x;
if((s + target) % 2 || s < abs(target)) return 0;
int P = (s + target) / 2;
vector<int> dp(P + 1, 0); dp[0] = 1;
for(int x : nums)
for(int w = P; w >= x; --w)
dp[w] += dp[w - x];
return dp[P];
}Java
public int findTargetSumWays(int[] nums, int target){
int s = 0; for(int x : nums) s += x;
if((s + target) % 2 != 0 || s < Math.abs(target)) return 0;
int P = (s + target) / 2;
int[] dp = new int[P + 1]; dp[0] = 1;
for(int x : nums)
for(int w = P; w >= x; w--)
dp[w] += dp[w - x];
return dp[P];
}复杂度
时间
O(n·P)
n 个数 × 每个数扫一遍容量 P
空间
O(P)
一维滚动数组,P=(sum+target)/2
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 目标和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么能把加减号问题转成子集和计数?+
把取正号的数之和记为 P、取负号的之和为 N,则 P+N=sum、P−N=target,解得 P=(sum+target)/2。每种「凑出 P 的子集」唯一对应一种符号分配,所以方案数相等。
nums 含 0 会怎样?+
0 既可取 + 也可取 −,对子集和没影响但翻倍方案。这套 dp 天然正确:x=0 时内层 dp[w]+=dp[w],会把每个状态数翻倍,正好对应 0 的两种符号。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 目标和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。