39.組合總和
給你一個?無重復元素?的整數數組?
candidates
?和一個目標整數?target
?,找出?candidates
?中可以使數字和為目標數?target
?的 所有?不同組合?,并以列表形式返回。你可以按?任意順序?返回這些組合。
candidates
?中的?同一個?數字可以?無限制重復被選取?。如果至少一個數字的被選數量不同,則兩種組合是不同的。?對于給定的輸入,保證和為?
target
?的不同組合數少于?150
?個。示例?1:
輸入:candidates =[2,3,6,7]
, target =7
輸出:[[2,2,3],[7]] 解釋: 2 和 3 可以形成一組候選,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一個候選, 7 = 7 。 僅有這兩種組合。示例?2:
輸入: candidates = [2,3,5],
target = 8 輸出: [[2,2,2,2],[2,3,3],[3,5]]示例 3:
輸入: candidates =[2],
target = 1 輸出: []提示:
1 <= candidates.length <= 30
2 <= candidates[i] <= 40
candidates
?的所有元素?互不相同1 <= target <= 40
解題思路
這個問題要求從給定的無重復整數數組 candidates 中找出所有和為目標值 target 的不同組合,允許每個數字無限制重復使用。我們使用 回溯法 來解決這個問題,核心思路如下:
- 回溯法:
- 遍歷 candidates 中的每個數字,嘗試將其加入當前組合。
- 遞歸調用繼續選擇數字(可以選擇當前數字或后續數字),更新當前和 sum。
- 如果當前和等于 target,將組合加入結果集;如果超過 target,終止當前分支。
- 使用回溯撤銷當前選擇,嘗試其他可能性。
- 避免重復組合:
- 通過參數 index 限制每次選擇從當前索引開始,確保組合按順序生成(如 [2,3] 不會重復為 [3,2])。
- 允許重復使用同一個數字,因此遞歸時傳遞相同的 index(backtrack(..., i))。
- 剪枝優化:
- 如果當前和加上當前數字超過 target(sum + candidates[i] > target),終止當前分支。
- 預先對 candidates 排序,確保數字按升序處理,進一步優化剪枝(當和超過 target 時,后續更大數字無需嘗試)。
- 終止條件:
- 當當前和 sum == target 時,將當前組合加入結果集 ans。
- 特殊情況:
- 如果 target 小于 candidates 中最小值,或數組為空,則無解,返回空列表。
時間和空間復雜度
- 時間復雜度:O(N * 2^N),其中 N 是 candidates 的長度。在最壞情況下,每個數字可以選或不選,生成所有可能的組合。實際復雜度因剪枝而低于理論值。
- 空間復雜度:O(target / min(candidates)),用于遞歸棧的最大深度(最壞情況下,組合由最小數字組成)。結果集空間不計入復雜度。
代碼?
import java.util.*;class Solution {public List<List<Integer>> combinationSum(int[] candidates, int target) {// 初始化結果集,存儲所有有效組合List<List<Integer>> ans = new ArrayList<>();// 初始化臨時列表,存儲當前組合List<Integer> temp = new ArrayList<>();// 排序數組,便于剪枝Arrays.sort(candidates);// 開始回溯,從索引 0 開始backtrack(candidates, target, 0, temp, ans, 0);return ans;}/*** 回溯方法,生成所有和為 target 的組合* @param candidates 候選數字數組* @param target 目標和* @param sum 當前組合的和* @param temp 當前正在構建的組合* @param ans 結果集,存儲所有有效組合* @param index 當前選擇的起始索引*/private void backtrack(int[] candidates, int target, int sum, List<Integer> temp, List<List<Integer>> ans, int index) {// 終止條件:當前和等于 target,加入結果集if (sum == target) {ans.add(new ArrayList<>(temp));return;}// 從 index 開始遍歷候選數字for (int i = index; i < candidates.length; i++) {// 剪枝:如果當前和加上當前數字超過 target,終止(因數組已排序,后續數字更大)if (sum + candidates[i] > target) {break;}// 選擇當前數字temp.add(candidates[i]);// 遞歸調用,允許重復選擇當前數字(傳遞 i)backtrack(candidates, target, sum + candidates[i], temp, ans, i);// 回溯:移除最后一個數字,恢復狀態temp.remove(temp.size() - 1);}}
}
40.組合總和II
給定一個候選人編號的集合?
candidates
?和一個目標數?target
?,找出?candidates
?中所有可以使數字和為?target
?的組合。
candidates
?中的每個數字在每個組合中只能使用?一次?。注意:解集不能包含重復的組合。?
示例?1:
輸入: candidates =?[10,1,2,7,6,1,5]
, target =?8
, 輸出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]示例?2:
輸入: candidates =?[2,5,2,1,2], target =?5, 輸出: [ [1,2,2], [5] ]提示:
1 <=?candidates.length <= 100
1 <=?candidates[i] <= 50
1 <= target <= 30
解題思路
這個問題要求從給定的候選數字數組 candidates 中找出所有和為目標值 target 的組合,每個數字只能使用一次,且結果不能包含重復組合。由于可能存在重復數字,我們需要特別處理去重邏輯。使用 回溯法 解決,核心思路如下:
- 回溯法:
- 遍歷 candidates,將每個數字嘗試加入當前組合。
- 遞歸選擇后續數字,更新當前和 sum,直到和達到 target 或超過。
- 使用回溯撤銷選擇,嘗試其他可能性。
- 去重邏輯:
- 為了避免重復組合(如 [1,2,5] 和 [1,2,5]),對 candidates 預先排序,將相同數字放在一起。
- 在同一樹層中,跳過重復的數字(通過 i > start && candidates[i] == candidates[i-1] 判斷)。
- 使用 start 參數確保每個數字只在當前組合中選擇一次(遞歸時傳遞 i+1)。
- 剪枝優化:
- 如果當前和加上當前數字超過 target(sum + candidates[i] > target),終止當前分支。
- 排序后,當和超過 target 時,后續更大數字無需嘗試。
- 終止條件:
- 當當前和 sum == target 時,將當前組合加入結果集 res。
- 數據結構:
- 使用 LinkedList 存儲當前組合 path,便于尾部操作。
- 使用全局變量 sum 跟蹤當前和,減少參數傳遞。
時間和空間復雜度
- 時間復雜度:O(2^N),其中 N 是 candidates 的長度。最壞情況下,每個數字選或不選,生成所有可能組合。去重和剪枝降低實際復雜度。
- 空間復雜度:O(N),用于遞歸棧和當前組合 path。結果集空間不計入復雜度。
代碼?
import java.util.*;class Solution {// 結果集,存儲所有有效組合private List<List<Integer>> res = new ArrayList<>();// 當前組合,使用 LinkedList 便于尾部操作private LinkedList<Integer> path = new LinkedList<>();// 當前組合的和private int sum = 0;public List<List<Integer>> combinationSum2(int[] candidates, int target) {// 排序數組,便于去重和剪枝Arrays.sort(candidates);// 開始回溯,從索引 0 開始backTracking(candidates, target, 0);return res;}/*** 回溯方法,生成所有和為 target 的組合* @param candidates 候選數字數組(已排序)* @param target 目標和* @param start 當前選擇的起始索引,避免重復使用同一數字*/private void backTracking(int[] candidates, int target, int start) {// 終止條件:當前和等于 target,加入結果集if (sum == target) {res.add(new ArrayList<>(path));return;}// 遍歷從 start 開始的候選數字for (int i = start; i < candidates.length && sum + candidates[i] <= target; i++) {// 去重:跳過同一樹層的重復數字if (i > start && candidates[i] == candidates[i - 1]) {continue;}// 選擇當前數字sum += candidates[i];path.add(candidates[i]);// 遞歸調用,選擇下一個數字(i+1 確保每個數字只用一次)backTracking(candidates, target, i + 1);// 回溯:移除最后一個數字,恢復狀態sum -= path.remove(path.size() - 1);}}
}
131.分割回文串
給你一個字符串?
s
,請你將?s
?分割成一些?子串,使每個子串都是?回文串?。返回?s
?所有可能的分割方案。示例 1:
輸入:s = "aab" 輸出:[["a","a","b"],["aa","b"]]示例 2:
輸入:s = "a" 輸出:[["a"]]提示:
1 <= s.length <= 16
s
?僅由小寫英文字母組成
解題思路
這個問題要求將字符串 s 分割成若干子串,使得每個子串都是回文串,并返回所有可能的分割方案。由于需要探索所有可能的分割方式,我們使用 回溯法 來解決,核心思路如下:
- 回溯法:
- 從字符串的起始位置 start 開始,嘗試所有可能的子串(從 start 到 i)。
- 如果當前子串是回文串,將其加入當前分割方案 cur,然后遞歸處理剩余部分(從 i+1 開始)。
- 使用回溯撤銷當前子串,嘗試其他可能的子串。
- 回文檢查:
- 編寫一個輔助方法 check,用于判斷子串是否為回文。
- 回文檢查通過比較字符串兩端的字符是否相等實現。
- 終止條件:
- 當起始索引 start 達到字符串長度 s.length(),說明當前分割方案完整,將其加入結果集 res。
- 去重與優化:
- 由于輸入字符串僅由小寫英文字母組成,且分割是連續的,無需特別處理重復組合。
- 使用 StringBuilder 構建子串,但可以優化為直接截取子串,減少額外空間。
- 特殊情況:
- 如果輸入字符串為空(題目保證長度至少為 1),無需處理。
時間和空間復雜度
- 時間復雜度:O(N * 2^N),其中 N 是字符串 s 的長度。分割方案的數量為 O(2^N)(每個字符可以選擇是否分割),每次檢查回文和復制組合需要 O(N)。
- 空間復雜度:O(N),用于遞歸棧(最大深度為 N)和當前分割方案 cur。結果集空間不計入復雜度。
代碼
import java.util.*;class Solution {// 結果集,存儲所有有效分割方案private List<List<String>> res = new ArrayList<>();// 當前分割方案private List<String> cur = new ArrayList<>();public List<List<String>> partition(String s) {// 從索引 0 開始回溯backtracking(s, 0);return res;}/*** 回溯方法,生成所有可能的回文分割方案* @param s 輸入字符串* @param start 當前分割的起始索引*/private void backtracking(String s, int start) {// 終止條件:如果起始索引達到字符串長度,當前方案有效if (start == s.length()) {res.add(new ArrayList<>(cur));return;}// 嘗試從 start 到 i 的子串for (int i = start; i < s.length(); i++) {// 檢查子串 s[start:i+1] 是否為回文if (isPalindrome(s, start, i)) {// 添加當前回文子串到方案cur.add(s.substring(start, i + 1));// 遞歸處理剩余部分backtracking(s, i + 1);// 回溯:移除最后一個子串cur.remove(cur.size() - 1);}}}/*** 輔助方法,檢查子串 s[left:right+1] 是否為回文* @param s 輸入字符串* @param left 子串起始索引* @param right 子串結束索引* @return 是否為回文*/private boolean isPalindrome(String s, int left, int right) {while (left < right) {if (s.charAt(left) != s.charAt(right)) {return false;}left++;right--;}return true;}
}
10.復原IP地址
有效 IP 地址?正好由四個整數(每個整數位于?
0
?到?255
?之間組成,且不能含有前導?0
),整數之間用?'.'
?分隔。
- 例如:
"0.1.2.201"
?和"192.168.1.1"
?是?有效?IP 地址,但是?"0.011.255.245"
、"192.168.1.312"
?和?"192.168@1.1"
?是?無效?IP 地址。給定一個只包含數字的字符串?
s
?,用以表示一個 IP 地址,返回所有可能的有效 IP 地址,這些地址可以通過在?s
?中插入?'.'
?來形成。你?不能?重新排序或刪除?s
?中的任何數字。你可以按?任何?順序返回答案。示例 1:
輸入:s = "25525511135" 輸出:["255.255.11.135","255.255.111.35"]示例 2:
輸入:s = "0000" 輸出:["0.0.0.0"]示例 3:
輸入:s = "101023" 輸出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]提示:
1 <= s.length <= 20
s
?僅由數字組成
解題思路
這個問題要求將給定的數字字符串 s 分割成四個部分,形成有效的 IP 地址(每個部分為 0 到 255 的整數,且無前導零)。我們使用 回溯法 來嘗試所有可能的分割方案,核心思路如下:
- 回溯法:
- 將字符串 s 分割成四段,每段長度為 1 到 3 位。
- 使用遞歸嘗試從起始索引 start 開始的每種可能段長度,檢查是否有效。
- 如果當前段有效,加入當前組合,繼續遞歸處理剩余部分。
- 使用回溯撤銷當前段,嘗試其他長度。
- 有效性檢查:
- 每段必須是 0 到 255 的整數。
- 禁止前導零(例如 "01" 或 "001" 無效)。
- 每段長度為 1 到 3 位。
- 使用輔助方法 isValid 檢查段是否符合規則。
- 剪枝優化:
- 輸入字符串長度必須在 4 到 12 之間(少于 4 位無法形成四段,超過 12 位無法都滿足 1-3 位限制)。
- 剩余字符數量必須滿足剩余段數的需求:remaining >= 4 - num(每段至少 1 位)且 remaining <= 3 * (4 - num)(每段最多 3 位)。
- 如果段數超過 4 或起始索引超出字符串長度,終止。
- 終止條件:
- 當分割出 4 段且用完所有字符(num == 4 && start == s.length()),將當前組合加入結果集。
- 去掉結果中末尾的點(.)以符合 IP 地址格式。
- 數據結構:
- 使用 StringBuilder 高效構建當前 IP 地址,減少字符串拼接開銷。
- 使用 List<String> 存儲所有有效 IP 地址。
時間和空間復雜度
- 時間復雜度:O(3^4 * N) = O(N),其中 N 是字符串長度。每個段有最多 3 種長度選擇(1、2、3),共 4 段,生成組合數量固定(3^4 = 81),每次處理涉及字符串操作 O(N)。
- 空間復雜度:O(N),用于遞歸棧(最大深度 4)和 StringBuilder 存儲當前組合。結果集空間不計入復雜度。
代碼?
import java.util.*;class Solution {// 結果集,存儲所有有效 IP 地址private List<String> ans = new ArrayList<>();public List<String> restoreIpAddresses(String s) {// 邊界檢查:字符串長度必須在 4 到 12 之間if (s == null || s.length() < 4 || s.length() > 12) {return ans;}// 開始回溯,從索引 0 開始,初始段數為 0backtracking(s, new StringBuilder(), 0, 0);return ans;}/*** 回溯方法,生成所有可能的有效 IP 地址* @param s 輸入數字字符串* @param sb 當前構建的 IP 地址* @param start 當前處理的字符串起始索引* @param segmentCount 當前已分割的段數*/private void backtracking(String s, StringBuilder sb, int start, int segmentCount) {// 終止條件:分割出 4 段且用完所有字符if (segmentCount == 4 && start == s.length()) {ans.add(sb.toString().substring(0, sb.length() - 1)); // 去掉末尾的點return;}// 剪枝:段數超限或字符不足/過多if (segmentCount > 4 || start >= s.length()) {return;}int remaining = s.length() - start;if (remaining < (4 - segmentCount) || remaining > 3 * (4 - segmentCount)) {return;}// 嘗試長度為 1 到 3 的段for (int len = 1; len <= 3 && start + len <= s.length(); len++) {String segment = s.substring(start, start + len);// 檢查當前段是否有效if (isValid(segment)) {int sbLen = sb.length();// 添加當前段和點sb.append(segment).append('.');// 遞歸處理下一段backtracking(s, sb, start + len, segmentCount + 1);// 回溯:恢復 StringBuilder 到之前狀態sb.setLength(sbLen);}}}/*** 輔助方法,檢查段是否為有效 IP 段* @param segment 待檢查的段* @return 是否有效*/private boolean isValid(String segment) {int len = segment.length();// 長度必須為 1 到 3if (len < 1 || len > 3) {return false;}// 禁止前導零(除了單個 0)if (len > 1 && segment.charAt(0) == '0') {return false;}// 轉換為整數,檢查是否在 0 到 255 之間try {int value = Integer.parseInt(segment);return value >= 0 && value <= 255;} catch (NumberFormatException e) {return false;}}
}
78.子集
給你一個整數數組?
nums
?,數組中的元素?互不相同?。返回該數組所有可能的子集(冪集)。解集?不能?包含重復的子集。你可以按?任意順序?返回解集。
示例 1:
輸入:nums = [1,2,3] 輸出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]示例 2:
輸入:nums = [0] 輸出:[[],[0]]提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums
?中的所有元素?互不相同?
解題思路
這個問題要求返回整數數組 nums 的所有可能子集(冪集),且數組元素互不相同,解集不能包含重復子集。我們可以使用 回溯法 來生成所有子集,核心思路如下:
- 回溯法:
- 子集是數組中元素的任意組合(包括空集),可以通過遞歸選擇或不選擇每個元素來生成。
- 使用 start 參數控制當前選擇的起始索引,確保子集按順序生成(避免重復)。
- 每次遞歸可以選擇當前元素并繼續遞歸,或跳過當前元素遞歸后續部分。
- 生成所有子集:
- 為了生成所有子集,我們可以:
- 枚舉子集長度(從 0 到 nums.length),為每種長度調用回溯。
- 或者在回溯中允許任意長度,將每次遞歸的當前組合加入結果集(更簡潔)。
- 由于 nums 元素互不相同,無需額外去重。
- 優化:
- 直接在回溯中收集所有子集,無需顯式枚舉長度。
- 使用 start 確保每個元素只在后續子集中考慮,避免重復。
- 終止條件:
- 回溯中無需顯式終止條件,每次遞歸的當前組合(cur)都可加入結果集。
- 當索引超出數組長度時,停止遞歸。
- 特殊情況:
- 題目保證數組非空(1 <= nums.length),無需處理空輸入。
時間和空間復雜度
- 時間復雜度:O(N * 2^N),其中 N 是 nums 的長度。數組有 2^N 個子集,每個子集復制到結果集需要 O(N)。
- 空間復雜度:O(N),用于遞歸棧(最大深度 N)和當前組合 cur。結果集空間不計入復雜度。
代碼
import java.util.*;class Solution {// 結果集,存儲所有子集private List<List<Integer>> ans = new ArrayList<>();// 當前子集private List<Integer> cur = new ArrayList<>();public List<List<Integer>> subsets(int[] nums) {// 開始回溯,從索引 0 開始backtracking(nums, 0);return ans;}/*** 回溯方法,生成所有可能的子集* @param nums 輸入數組* @param start 當前選擇的起始索引*/private void backtracking(int[] nums, int start) {// 每次遞歸都將當前子集加入結果集(包括空集)ans.add(new ArrayList<>(cur));// 從 start 開始遍歷剩余元素for (int i = start; i < nums.length; i++) {// 選擇當前元素cur.add(nums[i]);// 遞歸處理后續元素backtracking(nums, i + 1);// 回溯:移除最后一個元素cur.remove(cur.size() - 1);}}
}