从回溯到记忆化搜索再到递推
从回溯到记忆化搜索再到递推
一、回溯(递归暴力搜索)
回溯就是尝试所有可能的选择。
遇到一个问题时,递归地做选择,直到到达终点,然后回到上一步换一种选择继续试。
- 特点:不管有没有用过之前的计算结果,每次都重新算一遍。
- 优点:简单,直观。
- 缺点:效率极低,重复计算,指数级爆炸。
形象理解:在一棵决策树上,暴力走遍所有路径。
二、记忆化搜索(递归 + 记忆)
记忆化搜索是在回溯的基础上优化的。
就是在递归的时候,把算过的子问题结果记下来,下次遇到同样的子问题就直接用,不用再算。
- 特点:还是递归,但是每个子问题只算一次。
- 优点:大量避免重复计算,时间复杂度从指数级降到多项式级。
- 核心思想:记录状态 → 剪枝
形象理解:在走决策树的过程中,遇到同一棵子树,直接抄答案。
三、递推(动态规划,自底向上)
递推是去掉递归的做法。
明确每一个子问题的定义(也叫做状态),
然后根据小的状态一步步算出大的状态,按一定顺序推进。
- 特点:不需要函数调用栈,节省开销。
- 优点:效率最高,容易进行空间优化。
- 核心思想:找到状态转移关系,控制计算顺序
形象理解:像搭积木一样,一块块从底部搭上去。
题目1:打家劫舍(House Robber)
-
递归(回溯)
思路:对第 i 间房可“偷”或“不偷”,暴力枚举所有方案,时间复杂度 O(2^n)。
int rob(int[] nums, int i) {if (i >= nums.length) return 0;return Math.max(nums[i] + rob(nums, i + 2), rob(nums, i + 1)); }
-
记忆化搜索
在递归基础上用
memo[i]
缓存已算结果,避免重复调用,时间复杂度降为 O(n)。int[] memo; int rob(int[] nums, int i) {if (i >= nums.length) return 0;if (memo[i] != -1) return memo[i];int res = Math.max(nums[i] + rob(nums, i + 2), rob(nums, i + 1));return memo[i] = res; }
-
自底向上 DP
定义
dp[i]
为从第 i 间房到末尾能偷到的最大金额,则:dp[n] = dp[n+1] = 0 for (i = n-1; i >= 0; i--) {dp[i] = Math.max(nums[i] + dp[i+2], dp[i+1]); }
最终返回
dp[0]
。
题目2:目标和(Target Sum)
-
递归(回溯)
对每个元素选择“+”或“-”,暴力枚举,时间 O(2^n)。
int findTargetSumWays(int[] nums, int target) {return dfs(nums, 0, 0, target); } int dfs(int[] nums, int i, int sum, int target) {if (i == nums.length) return sum == target ? 1 : 0;return dfs(nums, i + 1, sum + nums[i], target)+ dfs(nums, i + 1, sum - nums[i], target); }
-
记忆化搜索
缓存
(i, sum)
状态到 Map 或二维数组,避免重复,复杂度 O(n·sum)。Map<String,Integer> memo; int dfs(int[] nums, int i, int sum, int target) {String key = i + "," + sum;if (i == nums.length) return sum == target ? 1 : 0;if (memo.containsKey(key)) return memo.get(key);int ways = dfs(nums, i + 1, sum + nums[i], target)+ dfs(nums, i + 1, sum - nums[i], target);memo.put(key, ways);return ways; }
-
自底向上 DP(子集和转化)
令 P = (sum(nums) + target) / 2,统计凑成 P 的子集方案数,定义
dp[j]
为和为 j 的方案数:dp[0] = 1 for (int num : nums) {for (int j = P; j >= num; j--) {dp[j] += dp[j - num];} }
最后返回
dp[P]
。
题目3:零钱兑换 II(Coin Change II)
-
递归(回溯)
枚举每种面额取与不取,时间复杂度指数级。
int change(int amount, int[] coins) {return dfs(coins, amount, 0); } int dfs(int[] coins, int rem, int i) {if (rem == 0) return 1;if (i == coins.length || rem < 0) return 0;return dfs(coins, rem - coins[i], i)+ dfs(coins, rem, i + 1); }
-
记忆化搜索
用
memo[i][rem]
缓存子问题,时间降为 O(n·amount)。int[][] memo; int dfs(int[] coins, int rem, int i) {if (rem == 0) return 1;if (i == coins.length || rem < 0) return 0;if (memo[i][rem] != -1) return memo[i][rem];int ans = dfs(coins, rem - coins[i], i)+ dfs(coins, rem, i + 1);return memo[i][rem] = ans; }
-
自底向上 DP
定义
dp[j]
为凑成金额 j 的组合数:dp[0] = 1 for (int coin : coins) {for (int j = coin; j <= amount; j++) {dp[j] += dp[j - coin];} }
返回
dp[amount]
。
题目4:最长公共子序列(LCS)
-
递归(暴力)
int lcs(String s1, String s2, int i, int j) {if (i == s1.length() || j == s2.length()) return 0;if (s1.charAt(i) == s2.charAt(j)) {return 1 + lcs(s1, s2, i + 1, j + 1);}return Math.max(lcs(s1, s2, i + 1, j),lcs(s1, s2, i, j + 1)); }
-
记忆化搜索
int[][] memo; int lcs(String s1, String s2, int i, int j) {if (i == s1.length() || j == s2.length()) return 0;if (memo[i][j] != -1) return memo[i][j];int res;if (s1.charAt(i) == s2.charAt(j)) {res = 1 + lcs(s1, s2, i + 1, j + 1);} else {res = Math.max(lcs(s1, s2, i + 1, j),lcs(s1, s2, i, j + 1));}return memo[i][j] = res; }
-
自底向上 DP
for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (s1.charAt(i) == s2.charAt(j)) {dp[i+1][j+1] = dp[i][j] + 1;} else {dp[i+1][j+1] = Math.max(dp[i][j+1], dp[i+1][j]);}} }
题目5:编辑距离(Edit Distance)
-
递归(暴力)
int edit(String w1, String w2, int i, int j) {if (i == 0) return j;if (j == 0) return i;if (w1.charAt(i - 1) == w2.charAt(j - 1)) {return edit(w1, w2, i - 1, j - 1);}return 1 + Math.min(Math.min(edit(w1, w2, i - 1, j), // 删除edit(w1, w2, i, j - 1)), // 插入edit(w1, w2, i - 1, j - 1) // 替换); }
-
记忆化搜索
int[][] memo; int edit(String w1, String w2, int i, int j) {if (i == 0) return j;if (j == 0) return i;if (memo[i][j] != -1) return memo[i][j];int res;if (w1.charAt(i - 1) == w2.charAt(j - 1)) {res = edit(w1, w2, i - 1, j - 1);} else {res = 1 + Math.min(Math.min(edit(w1, w2, i - 1, j),edit(w1, w2, i, j - 1)),edit(w1, w2, i - 1, j - 1));}return memo[i][j] = res; }
-
自底向上 DP
for (int i = 0; i <= m; i++) dp[i][0] = i;for (int j = 0; j <= n; j++) dp[0][j] = j;for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {if (w1.charAt(i - 1) == w2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1];} else {dp[i][j] = 1 + Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]),dp[i - 1][j - 1]);}} }
题目6:最长递增子序列(LIS)
-
递归(暴力)
int lis(int[] nums, int prev, int i) {if (i == nums.length) return 0;int taken = nums[i] > prev ? 1 + lis(nums, nums[i], i + 1) : 0;int notTaken = lis(nums, prev, i + 1);return Math.max(taken, notTaken); }
-
记忆化搜索
int[] memo; int lis(int[] nums, int i) {if (memo[i] != -1) return memo[i];int max = 1;for (int j = 0; j < i; j++) {if (nums[j] < nums[i]) {max = Math.max(max, lis(nums, j) + 1);}}return memo[i] = max; }
-
自底向上 DP
for (int i = 0; i < n; i++) {dp[i] = 1;for (int j = 0; j < i; j++) {if (nums[j] < nums[i]) {dp[i] = Math.max(dp[i], dp[j] + 1);}} }
方法 | 特点 | 效率 | 难度 |
---|---|---|---|
回溯 | 全部暴力尝试,不记结果 | 最低 | 最低 |
记忆化搜索 | 递归 + 记录,避免重复 | 中等 | 中等 |
递推(动态规划) | 明确状态,自底向上推进 | 最高 | 略高 |