for回圈橫向遍歷,遞回縱向遍歷,回溯不斷調整結果集
畫樹,回溯要畫樹
77. 組合
給定兩個整數 n 和 k,回傳范圍 [1, n] 中所有可能的 k 個數的組合,
你可以按 任何順序 回傳答案,
result.add(new ArrayList(path)):開辟一個獨立地址,地址中存放的內容為path鏈表,后續path的變化不會影響到res
res.add(path):將res尾部指向了path地址,后續path內容的變化會導致res的變化,
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backTracking(n,k,1);
return result;
}
void backTracking(int n, int k, int startIndex){
if(path.size()==k){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n; i++) {
path.add(i);
backTracking(n,k,i+1);
path.removeLast();//回溯
}
}
}
剪枝操作優化代碼:
for (int i = startIndex; i <= n; i++) {
剪枝:
for (int i = startIndex; i <=n-(k-path.size())+1; i++) {
path.size() 是當前路徑里元素的個數
k-path.size() 是還需要的元素個數
n-(k-path.size())+1 至多走到哪里

216. 組合總和 III
找出所有相加之和為 n 的 k 個數的組合,且滿足下列條件:
只使用數字1到9
每個數字 最多使用一次
回傳 所有可能的有效組合的串列 ,該串列不能包含相同的組合兩次,組合可以以任何順序回傳,
- 這題和上面一題十分相似,就淺淺改了一下上面的代碼
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
if(k > n){//如果n小于k的話直接回傳
return result;
}
backTracking(k,n,1);
return result;
}
void backTracking(int k, int n, int startIndex){
if(path.size()==k){//陣列的長度達到標準之后
int sum = 0;
for (Integer p : path) {
sum +=p;
}
if(sum == n){//看陣列中數字之和是否達到標準
result.add(new ArrayList<>(path));
}
return;
}
for (int i = startIndex; i <= n; i++) {
path.add(i);
backTracking(k,n,i+1);
path.removeLast();
}
}
}
- 結果超出時間限制,因為沒有剪枝
for (int i = startIndex; i <= Math.min(n-(k*(k-1)/2),9) ; i++) {
- k*(k-1)/2 是對前k-1個數的求和
- n-(k*(k-1)/2) 和 9 取最小是怕n太大,k太小導致path里的數 > 9
17. 電話號碼的字母組合
給定一個僅包含數字 2-9 的字串,回傳所有它能表示的字母組合,答案可以按 任意順序 回傳,
- 把按鍵上對應的數字轉換成字串letters,遍歷的時候要遍歷的是字串
class Solution {
List<String> result = new ArrayList<>();
static String[] letters = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};//下標就是按鍵上的數字
public List<String> letterCombinations(String digits) {
if(digits.equals(""))
return result;//對特殊情況特殊處理
backTracking(digits,0);//從零開始
return result;
}
static StringBuilder sb = new StringBuilder();
void backTracking(String digits, int index){
//index指的是當前對哪個數字進行處理
if(sb.length()==digits.length()){
result.add(sb.toString());
return;
}
String letter = letters[(digits.charAt(index))-'0'];//取當前下標對應的字串
for (int i = 0; i < letter.length(); i++) {
sb.append(letter.charAt(i));
backTracking(digits,index+1);
sb.deleteCharAt(sb.length()-1);
}
}
}
- 感覺慢慢理解回溯了??
39. 組合總和
給你一個 無重復元素 的整數陣列 candidates 和一個目標整數 target ,找出 candidates 中可以使數字和為目標數 target 的 所有 不同組合 ,并以串列形式回傳,你可以按 任意順序 回傳這些組合,
- 對組合問題的變形:允許元素重復使用,不固定路徑陣列的長度
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
combine(candidates, target, 0);
return res;
}
void combine(int[] candidates, int target, int startIndex){
int sum = 0;
//算出當前路徑的結點之和
for (Integer node : path) {
sum += node;
}
if(sum == target){
res.add(new ArrayList<>(path));//得到符合條件的結果,把路徑加到結果集中
return;
}
if(sum > target){
return;//當前路徑上的結點之和已經大于所求,不用再往下加了
}
for (int i = startIndex; i < candidates.length; i++) {
path.add(candidates[i]);//把當前的元素加入path中
combine(candidates,target,i);
path.remove(path.size()-1);
}
}
}
40. 組合總和 II
給定一個候選人編號的集合 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和為 target 的組合,
candidates 中的每個數字在每個組合中只能使用 一次 ,
- 本題也是對組合問題的變形:candidates中的元素 無序 且 重復
- 元素無序 給剪枝操作增加了難度,我在操作前對陣列candidates進行了排序
- 元素重復 我的做法是對結果集res 新建了一個result進行去重,但是有的測驗用例超時
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);//先給陣列排序
int length = candidates.length;
for (int candidate : candidates) {
if(candidate > target){
length--;
}
}
Arrays.copyOf(candidates,length);
combine(candidates, target, 0);
//此時已經拿到結果集res,但是需要對其進行去重操作
List<List<Integer>> result = new ArrayList<>();
for (List<Integer> re : res) {
if (!result.contains(re)){
result.add(re);//如果真正的結果集result中不包括re,就可以把re加入道result
}
}
return result;
}
void combine(int[] candidates, int target, int startIndex){
int sum = 0;
//算出當前路徑的結點之和
for (Integer node : path) {
sum += node;
}
if(sum == target){
res.add(new ArrayList<>(path));//得到符合條件的結果,把路徑加到結果集中
return;
}
if(sum > target){
return;//當前路徑上的結點之和已經大于所求,不用再往下加了
}
for (int i = startIndex; i < candidates.length; i++) {
path.add(candidates[i]);//把當前的元素加入path中
combine(candidates,target,i+1);
path.remove(path.size()-1);
}
}
}
- 就是這個用例超時,仔細瞅瞅這個用例,它說要對重復的資料進行處理,而不能只改結果集,確實,那樣效率太低了??
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
30
- 我在for回圈中加入了如下陳述句,想使重復的元素跳出當前回圈
if(i>0&&candidates[i]==candidates[i-1]){
continue;
}
- 但是他把滿足條件的含有重復元素的結果也給剪掉了,是剪枝的條件出了問題

- 把 i>0 換成 i>startIndex 后運行通過
if(i>startIndex&&candidates[i]==candidates[i-1]){
131. 分割回文串
給你一個字串 s,請你將 s 分割成一些子串,使每個子串都是 回文串 ,回傳 s 所有可能的分割方案,
回文串 是正著讀和反著讀都一樣的字串,
- 這題是分割問題,但是本質和組合問題思路一樣
- 剪枝的思路是:如果當前切出來的子串已經不是回文串了,就continue進入下一次回圈,這樣可以保證葉子結點都是滿足要求的path

class Solution {
List<List<String>> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
combine(s,0);
return res;
}
void combine(String s, int startIndex){
if(startIndex >= s.length()){
res.add(new LinkedList<>(path));//識訓葉子結點
}
for (int i = startIndex; i < s.length(); i++) {
if (isPalindromes(s,startIndex,i)){
path.add(s.substring(startIndex,i+1));//因為subString()方法是左閉右開的
} else {
continue;//如果當前的子串都不是回文串,已經不滿足題意,不必再往下切了
}
combine(s,i+1);//再往樹的下一層遍歷
path.removeLast();//回溯
}
}
// 判斷是否是回文子串
boolean isPalindromes(String s, int startIndex, int endIndex){
while (true){
if(startIndex > endIndex){
return true;
}
if(s.charAt(startIndex)==s.charAt(endIndex)){
startIndex++;
endIndex--;
} else {
return false;
}
}
}
}
93. 復原 IP 地址
有效 IP 地址 正好由四個整數(每個整數位于 0 到 255 之間組成,且不能含有前導 0),整數之間用 '.' 分隔,
給定一個只包含數字的字串 s ,用以表示一個 IP 地址,回傳所有可能的有效 IP 地址,這些地址可以通過在 s 中插入 '.' 來形成,你 不能 重新排序或洗掉 s 中的任何數字,你可以按 任何 順序回傳答案,
? ? ? 不是自己寫出來的
- 按照自己本來的思路寫,是new了一個StringBuilder,想著這樣操作方便些,但是很難回溯啊,就把自己繞進去了,下面的題解是在String上用subString()的方法操作
- 還有遞回出口,要專門創建一個int去記錄逗點的數量
class Solution {
List<String> result = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
if (s.length() > 12) return result; // 算是剪枝了
backTrack(s, 0, 0);
return result;
}
// startIndex: 搜索的起始位置, pointNum:添加逗點的數量
private void backTrack(String s, int startIndex, int pointNum) {
if (pointNum == 3) {// 逗點數量為3時,分隔結束
// 判斷第四段?字串是否合法,如果合法就放進result中
if (isValid(s,startIndex,s.length()-1)) {
result.add(s);
}
return;
}
for (int i = startIndex; i < s.length(); i++) {
if (isValid(s, startIndex, i)) {
s = s.substring(0, i + 1) + "." + s.substring(i + 1);//在str的后?插??個逗點
pointNum++;
backTrack(s, i + 2, pointNum);// 插?逗點之后下?個?串的起始位置為i+2
pointNum--;// 回溯
s = s.substring(0, i + 1) + s.substring(i + 2);// 回溯掉剛剛插入的逗點
} else {
break;//這里用break而不是continue是因為如果當前情況就不符合條件,再往后遍歷,數字只會越來越大,更不符合條件
}
}
}
// 判斷字串s在左閉?閉區間[start, end]所組成的數字是否合法
private Boolean isValid(String s, int start, int end) {
if (start > end) {
return false;
}
if (s.charAt(start) == '0' && start != end) { // 0開頭的數字不合法
return false;
}
int num = 0;
for (int i = start; i <= end; i++) {
if (s.charAt(i) > '9' || s.charAt(i) < '0') { // 遇到?數字字符不合法
return false;
}
num = num * 10 + (s.charAt(i) - '0');
if (num > 255) { // 如果?于255了不合法
return false;
}
}
return true;
}
}
- 回溯是把當前for回圈里的操作給回溯掉,干干凈凈的進入下一個回圈
- 感覺這一題是挺難的,現在還有點沒迷過來??
78. 子集
給你一個整數陣列 nums ,陣列中的元素 互不相同 ,回傳該陣列所有可能的子集(冪集),
解集 不能 包含重復的子集,你可以按 任意順序 回傳解集,

class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
res.add(new LinkedList<>(path));//把空的path進去
combine(nums,0);
return res;
}
void combine(int[] nums, int startIndex){
if(!path.isEmpty()&&path.getLast()==nums[nums.length-1]){
return;//遞回出口
}
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]);
res.add(new LinkedList<>(path));//每一個結點都要收集
combine(nums,i+1);
path.removeLast();//回溯
}
}
}
- 這題比較簡單,子集的特點就是每一個結點都要收集,那我們在添加路徑時收集就可以了
- 遞回的出口是當path最后一個數是陣列中的最后一個數時,不再向下遞回
90. 子集 II
給你一個整數陣列 nums ,其中可能包含重復元素,請你回傳該陣列所有可能的子集(冪集),
解集 不能 包含重復的子集,回傳的解集中,子集可以按 任意順序 排列,
- 這題和之前相比就多了一個去重操作,先排序后去重
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
res.add(new LinkedList<>(path));//把空的path進去
Arrays.sort(nums);//先排序
combine(nums,0);
return res;
}
void combine(int[] nums, int startIndex){
for (int i = startIndex; i < nums.length; i++) {
if (i > startIndex && nums[i] == nums[i - 1]) {
continue;//去重
}
path.add(nums[i]);
res.add(new LinkedList<>(path));//每一個結點都要收集
combine(nums,i+1);
path.removeLast();//回溯
}
}
}
491. 遞增子序列
給你一個整數陣列 nums ,找出并回傳所有該陣列中不同的遞增子序列,遞增子序列中 至少有兩個元素 ,你可以按 任意順序 回傳答案,
陣列中可能含有重復元素,如出現兩個整數相等,也可以視作遞增序列的一種特殊情況,
class Solution {
List<List<Integer>> res = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
combine(nums, 0);
return res;
}
void combine(int[] nums, int startIndex) {
if (path.size() > 1) {
res.add(new ArrayList<>(path));
}
Map<Integer, Integer> map = new HashMap();//每層一個map
for (int i = startIndex; i < nums.length; i++) {
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)) {
continue;//剪枝
}
if(map.getOrDefault(nums[i],0)>0){//如果該元素的value不是0,說明之前添加過,直接進下一次回圈
continue;//去重
}
map.put(nums[i],map.getOrDefault(nums[i],0)+1);//第一次遍歷到該元素時,以陣列元素為key,value賦1
path.add(nums[i]);
combine(nums, i + 1);
path.remove(path.size() - 1);//回溯
}
}
}
- 這里去重是用map實作的
default V getOrDefault ?( Object key , V defaultValue ) 回傳指定key映射到的value,如果當前map不包含當前key的映射,則回傳 defaultValue ,
46. 全排列
給定一個不含重復數字的陣列 nums ,回傳其 所有可能的全排列 ,你可以 按任意順序 回傳答案,
-
全排列問題 還是要畫樹

-
回圈遍歷當前path中沒有的數
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
combine(nums);
return res;
}
void combine(int[] nums){
if (path.size()== nums.length){
res.add(new ArrayList<>(path));//收集結果
}
for (int num : nums) {
if (path.contains(num)) {
continue;//如果路徑里有num,跳出此次回圈
}
path.add(num);
combine(nums);
path.remove(path.size()-1);
}
}
47. 全排列 II
給定一個可包含重復數字的序列 nums ,按任意順序 回傳所有不重復的全排列,
只做樹層的去重,不做樹枝的去重
-
nums[i]=nums[i-1]并且used[i-1]=true說明nums[i-1]已經被加進了path中,這是數枝的重復
-
nums[i]=nums[i-1]并且used[i-1]=false說明nums[i-1]已經被回溯成了false,這是樹層的重復,要剪掉?

-
新建了一個used陣列來表示nums陣列的使用情況
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
used = new boolean[nums.length];
for (boolean b : used) {
b=false;
}
Arrays.sort(nums);
comine(nums,used);
return res;
}
void comine(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));//收集結果
}
for (int i = 0; i < nums.length; i++) {
if (i>0&&nums[i]==nums[i-1]&&used[i-1]==false) {
continue;//當前數和前面的數相等并且前面的數還未使用時
}
if(used[i]==false){//當前元素還未使用時
path.add(nums[i]);
used[i]=true;//在path中添加元素,該位置標記為true
comine(nums,used);
path.remove(path.size() - 1);//回溯
used[i]=false;//回溯
}
}
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/528812.html
標籤:其他
下一篇:「組合數學」隔離區
