日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

LeetCode算法题14:递归和回溯2

發布時間:2025/6/17 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 LeetCode算法题14:递归和回溯2 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • 前言
  • 一、全排列II
    • 仿照全排列(n 叉樹)
    • 剪枝(去掉重復的結果)
  • 二、組合總和
    • 一、初始解法(n 叉樹):
      • 1,采用 Set 去重
      • 2,在遞歸搜索的時候去重(推薦解法)
        • 初始解法的遍歷過程:
        • 新方式的遍歷過程:
      • 引申(組合問題)
        • 組合問題的解法-n 叉樹
        • 組合問題的解法-二叉樹
        • 組合問題總結
    • 二、另一種解法(二叉樹):
  • 三、組合總和II
    • 解法1(n 叉樹)
    • 解法2(二叉樹)
  • 四、子集
    • 解法1(二叉樹)
    • 解法2(n 叉樹)
  • 五、子集II
    • 解法1(二叉樹)
    • 解法2(n 叉樹)
  • 總結


前言

??????遞歸和回溯續:包括有全排列II 、組合總和、組合總和II、子集、子集II、第 k 個排列、復原IP地址。

一、全排列II

??????題目鏈接:https://leetcode-cn.com/problems/permutations-ii/

??????題目描述:給定一個可包含重復數字的序列 nums ,按任意順序 返回所有不重復的全排列。

仿照全排列(n 叉樹)

??????和之前的全排列相比,這里的區別是數字序列包含了重復元素。一個很無腦的方法是將之間全排列解法中的 List 轉換為 Set,去掉最后排列結果中的重復序列即可。參考算法如下:

LinkedList<Integer> tmp=new LinkedList<>();Set<List<Integer>> ans=new HashSet<>();//改為 Set 存儲boolean[] flag;public List<List<Integer>> permuteUnique(int[] nums) {int len=nums.length;flag=new boolean[len];//由于每次選擇一個元素之后,該元素就不能再被選取了,所以需要標記狀態solve(nums,len-1);return new ArrayList<>(ans);}public void solve(int[] nums,int end) {if(tmp.size()==end+1){//得到一次排列時,保存結果ans.add(new LinkedList<>(tmp));return;}for(int i=0;i<=end;i++){//依次選擇每一個元素,由于有標記數組存在,所以此處循環每次都從下標 0 開始。if(flag[i]==false){tmp.addLast(nums[i]);flag[i]=true;solve(nums,end);flag[i]=false;tmp.removeLast();}}}

剪枝(去掉重復的結果)

??????以數字序列 【1 1 3】為例,仿照之前的全排列構建回溯過程的選擇樹如下:

初始集合為空
第一輪選擇一個元素113
第二輪選擇第二個元素1 11 31 11 33 13 1
第三輪選擇第三個元素1 1 31 3 11 1 31 3 13 1 13 1 1

??????可以發現重復的結果出現在:第一輪中選擇第二個 1 得到的最終結果和第一個 1 是一模一樣的,均為 【1 1 3】和【1 3 1】;在第一輪選擇 3 的情況下,第二輪的兩次選擇都是 1,最終結果也產生了重復。

??????所以剪枝的條件,也即約束條件為:當數組采用升序排列時,如果出現當前元素和上一個元素相等時,就沒有必要繼續遞歸遍歷了;但這還不夠,這樣在第二輪就永遠不會出現【1 1】序列了。如果第一個 1 被選取了之后,第二個 1 也被選取,這是重復結果的第一次出現,這種情況應該被允許發生;對應的重復結果的第二次出現就不允許發生了,比如在第一輪選擇第二個 1 時,第一個 1 未被選取(狀態為 false),由于之后的遞歸遍歷得到的都是重復結果,所以這種情況跳過(剪枝)。

??????所以給出的剪枝條件(約束條件)為:

if(i>0&&(nums[i]==nums[i-1])&&flag[i-1]==false)continue;

??????它表示的含義為:僅允許重復的排列結果第一次出現,后面會重復出現的遞歸遍歷被跳過(剪去)。

??????參考算法如下:

LinkedList<Integer> tmp=new LinkedList<>();List<List<Integer>> ans=new LinkedList<>();boolean[] flag;public List<List<Integer>> permuteUnique(int[] nums) {int len=nums.length;flag=new boolean[len];//由于每次選擇一個元素之后,該元素就不能再被選取了,所以需要標記狀態Arrays.sort(nums);solve(nums,len-1);return ans;}public void solve(int[] nums,int end) {if(tmp.size()==end+1){//得到一次排列時,保存結果ans.add(new LinkedList<>(tmp));return;}for(int i=0;i<=end;i++){//依次選擇每一個元素,由于有標記數組存在,所以此處循環每次都從下標 0 開始。if(flag[i]==false){if(i>0&&(nums[i]==nums[i-1])&&flag[i-1]==false)continue;tmp.addLast(nums[i]);flag[i]=true;solve(nums,end);flag[i]=false;tmp.removeLast();}}}

??????雖說約束條件改為 if(i>0&&(nums[i]==nums[i-1])&&flag[i-1]) 也是可以的,它保存的是重復排列的最后一次出現結果,它和保存第一次出現結果正好相反,并且也不太好理解,不建議采用這種做法。

二、組合總和

??????題目鏈接:https://leetcode-cn.com/problems/combination-sum/
??????題目描述:給你一個 無重復元素 的整數數組 candidates 和一個目標整數 target ,找出 candidates 中可以使數字和為目標數 target 的 所有 不同組合 ,并以列表形式返回。你可以按 任意順序 返回這些組合。

candidates 中的 同一個 數字可以 無限制重復被選取 。如果至少一個數字的被選數量不同,則兩種組合是不同的。

對于給定的輸入,保證和為 target 的不同組合數少于 150 個。

一、初始解法(n 叉樹):

??????直接干:

List<List<Integer>> ans=new ArrayList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combinationSum(int[] candidates, int target) {Arrays.sort(candidates);solve(candidates,target);//先排序return ans;}void solve(int[] candi,int target){if(target==0){ans.add(new LinkedList<>(tmp));return;}for(int i=0;i<candi.length;i++){if(candi[i]>target)break;//由于已經按照升序排序了,直接break即可。tmp.add(candi[i]);solve(candi,target-candi[i]);tmp.remove(tmp.size()-1);}}

??????然而初始解法的答案存在重復情況,對于示例 candidates = [2,3,6,7], target = 7,此解法的答案為:[2, 2, 3] [2, 3, 2] [3, 2, 2] [7],這是由于在搜索過程產生了重復,具體的可以仿照上一題畫出遞歸樹來比對。關于如何去重有兩種方法:

1,采用 Set 去重

??????參考代碼如下:(缺點是效率太慢,勉強能用)

Set<List<Integer>> ans=new HashSet<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combinationSum(int[] candidates, int target) {Arrays.sort(candidates);solve(candidates,target);List<List<Integer>> re=new ArrayList<>();for(List l:ans)re.add(l);return re;}void solve(int[] candi,int target){if(target==0){List<Integer> t=new LinkedList<>(tmp);Collections.sort(t);//這里需要先排序,從而在添加進集合的時候去重。ans.add(t);return;}for(int i=0;i<candi.length;i++){if(candi[i]>target)break;tmp.add(candi[i]);solve(candi,target-candi[i]);tmp.remove(tmp.size()-1);}}

2,在遞歸搜索的時候去重(推薦解法)

??????參考文章:https://leetcode-cn.com/problems/combination-sum/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-2/,本題的去重方式采用的是設置搜索起點。

??????初始解法答案為[2, 2, 3] [2, 3, 2] [3, 2, 2] [7]。約定:第一輪表示選擇一個元素的時候,第二輪選擇第二個元素,第三輪選擇第三個元素,第四輪選擇第四個元素。

初始解法的遍歷過程:

??????當第一輪選擇 2 時 ,接下來的遍歷為:第二輪選2、3、6、7。… 第三輪選2、3、6、7。… 第四輪選2、3、6、7。 …

??????當第一輪選擇 3 時,要么還是按照之前的遍歷思路(第二輪選2、3、6、7;… 第三輪選2、3、6、7;… 第四輪選2、3、6、7 …),要么就舍棄第一輪已經選過的 2,新的遍歷方式(第二輪選3、6、7;… 第三輪選3、6、7;… 第四輪選3、6、7 …)。

新方式的遍歷過程:

??????okk,感覺這樣好像有點合理哈,因為我們的目的是求一些元素的組合,而新的遍歷方式并不會漏掉任何組合方式。也剛好解決了初始解法中重復問題(能夠輕易的發現[3, 2, 2]被排除掉了)。

??????當第一輪選擇 2 時,第二輪選擇2、3、6、7,第三輪選擇 2、3、6、7,第四輪 2、3、6、7時:目標集合[2, 2, 3]表示第一輪選擇 2 、第二輪選擇 2 、第三輪選擇 3 ;繼續執行直到狀態為:第一輪選擇 2 、第二輪選擇 3 時。

??????如果按照舊的遍歷方式,第三輪仍然選 2,得到了重復集合[2, 3, 2]。但是按照新的遍歷方式,在第二輪選擇 3 時,第三輪也會從 3 開始選擇,所以在此也跳過了重復的集合選取,[2, 3, 2]就會被排除掉了。

??????由此可得到的算法參考如下:(采用 begin 變量標記遍歷的起點下標)

List<List<Integer>> ans=new ArrayList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combinationSum(int[] candidates, int target) {Arrays.sort(candidates);solve(candidates,target,0);return ans;}void solve(int[] candi,int target,int begin){if(target==0){ans.add(new LinkedList<>(tmp));return;}for(int i=begin;i<candi.length;i++){if(candi[i]>target)break;tmp.add(candi[i]);solve(candi,target-candi[i],i);//一旦到了下標i處,打死也不會選擇 下標小于i處 的元素了tmp.remove(tmp.size()-1);}}

引申(組合問題)

組合問題的解法-n 叉樹

?????? 參考上面的新遍歷方式的代碼可以得到的一種求組合(在candidates中求 k 個數的組合)的解法如下:

List<List<Integer>> ans=new ArrayList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combine(int[] candidates, int k) {solve(candidates,k,0);return ans;}void solve(int[] candi,int k,int begin){if(tmp.size()==k){ans.add(new LinkedList<>(tmp));return;}for(int i=begin;i<candi.length;i++){if(tmp.size()+candi.length-i)<k) break; //這里添加了剪枝條件tmp.add(candi[i]);solve(candi,k,i+1);//此處同上,選擇了 i 處元素之后之后就不會選擇下標小于等于i處的元素了tmp.remove(tmp.size()-1);}}

組合問題的解法-二叉樹

??????參考博客:https://blog.csdn.net/Little_ant_/article/details/123676777,代碼如下:

List<List<Integer>> ans=new LinkedList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combine(int n, int k) {solve(1,n,k);return ans;}public void solve(int cur,int end,int k){if(tmp.size()+(end-cur+1)<k)return;if(tmp.size()==k){ //碰到每一種成功的情況時應該保存 ans.add(new LinkedList<>(tmp));return;}tmp.add(cur); //選取當前 cur 的情況 solve(cur+1,end,k);tmp.remove(tmp.size()-1); //不選取的情況solve(cur+1,end,k);}

組合問題總結

??????解法1 如下面的二叉樹來示意 (n=4,k=2):(不想畫圖,湊合一看)
????????????????????????????????????????????????????????????????????????【 】
??????????????????????????????????????????【1】????????????????????????????????????????????????????????????????【】
?????????????????????????????????【1,2】????【1】???????????????????????????????????????????【2】?????????????????????????????????????????【】
?????????????????????????????????????????【1,3】????【1】???????????????????????????【2,3】????【2】??????????????????【3】??????????????????【】
??????????????????????????????????????????????????【1,4】???【1】??????????????????????????????【2,4]????【2】????【3,4】?【3】????【4】???【】
??????解法2 如下面的 n 叉樹所示意:
????????????????????????????????????????????????????????????????????????????????????????????????????【 】
??????????????????????????????????????????【1】??????????????????????????????????????????【2】??????????????????????????【3】????????????【4】
?????????????????????????【1,2】????【1,3】?????【1,4】???????????【2,3】??????【2,4】????????????????【3,4】
??????以上兩種示意圖對應兩種不同的解法思路。務必領會!

二、另一種解法(二叉樹):

??????組合問題解法1的思路參考上圖,本題也可以采用類似的方式,直接上代碼:

List<List<Integer>> ans=new ArrayList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combinationSum(int[] candidates, int target) {Arrays.sort(candidates);solve(candidates,target,0);return ans;}void solve(int[] candi,int target,int cur){//cur 為當前元素的下標if(target==0){ans.add(new LinkedList<>(tmp));return;}if(cur==candi.length)//退出條件1return;if(candi[cur]>target)//退出條件2return;//選擇當前元素,target減小,cur由題意可知,應不變。 tmp.add(candi[cur]);solve(candi,target-candi[cur],cur);//不選擇當前元素,target不變,cur應加一。tmp.remove(tmp.size()-1);solve(candi,target,cur+1);}

??????這種解法不會造成有重復結果。因為,一個 solve 中的下標為 start,另一個 solve 中的下標為 start+1,這也表明了,一旦訪問到下標 start 處,就再也不會遇到下標小于 start 的元素了。這和初始解法中的推薦解法含義是一樣的(用 begin 變量標記下一輪起始元素)

??????對應的代碼為(稍加修改后):

LinkedList<Integer> tmp=new LinkedList<>();List<List<Integer>> ans=new LinkedList<>();boolean[] flag;public List<List<Integer>> permute(int[] nums) {int len=nums.length;flag=new boolean[len];//由于每次選擇一個元素之后,該元素就不能再被選取了,所以需要標記狀態solve(nums,len-1);return ans;}public void solve(int[] nums,int end) {if(tmp.size()==end+1){//得到一次排列時,保存結果ans.add(new LinkedList<>(tmp));return;}for(int i=0;i<=end;i++){//依次選擇每一個元素,由于有標記數組存在,所以此處循環每次都從下標 0 開始。if(flag[i]==false){tmp.addLast(nums[i]);flag[i]=true;solve(nums,end);flag[i]=false;tmp.removeLast();}}}

三、組合總和II

??????題目鏈接:https://leetcode-cn.com/problems/combination-sum-ii/

??????題目描述:給定一個候選人編號的集合 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和為 target 的組合。

candidates 中的每個數字在每個組合中只能使用 一次 。

注意:解集不能包含重復的組合。

解法1(n 叉樹)

??????初始代碼為:

List<List<Integer>> ans=new ArrayList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combinationSum2(int[] candidates, int target) {Arrays.sort(candidates);solve(candidates,target,candidates.length,0);return ans;}public void solve(int[] c,int target,int len,int begin){if(target==0){ans.add(new LinkedList<>(tmp));return;}for(int i=begin;i<len;i++){if(c[i]>target)break; //這里和前面的 sort 對應,如果不排序的話應該為 continuetmp.add(c[i]);solve(c,target-c[i],len,i+1);//i+1表示不對當前元素重復選取。tmp.remove(tmp.size()-1);}}

??????但仍會造成重復,重復的原因在于 candidates 數組中本身存在重復元素,比如:[10,1,2,7,6,1,5] ,這里有兩個 1 存在。那么如何剪枝去重呢?這里可以先畫一畫遞歸搜索樹,對 candidates 排完序之后,兩個 1 接連排列,在初始代碼中:前面那個 1 的搜索空間包含了后面那個 1 的搜索空間,這就是重復發生的原因。解決方法:如果前面那個 1 搜索結束之后,那么后面那個 1 直接跳過,在同一層中的重復數字永遠只取第一個。這一點和 全排列II 剪枝非常類似。得到了最終代碼如下:

List<List<Integer>> ans=new ArrayList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combinationSum2(int[] candidates, int target) {Arrays.sort(candidates);boolean[] visited=new boolean[candidates.length];//新加入 visited 數組solve(candidates,target,candidates.length,0,visited);return ans;}public void solve(int[] c,int target,int len,int begin,boolean[] visited){if(target==0){ans.add(new LinkedList<>(tmp));return;}for(int i=begin;i<len;i++){//如果一個 1 被遍歷完之后,其他 1 也不能被訪問了。if(c[i]>target)break;if(i>0&&c[i]==c[i-1]&&visited[i-1]==false)//false 表示重復元素的第一個已遍歷結束,后面的重復元素都需要跳過continue;visited[i]=true;tmp.add(c[i]);solve(c,target-c[i],len,i+1,visited);tmp.remove(tmp.size()-1);visited[i]=false;}}

??????補充:上述代碼將 if(i>0&&c[i]==c[i-1]&&visited[i-1]==false) continue; 修改成為 if (i > begin && candidates[i] == candidates[i - 1]) continue; ,也會具有相同的作用。

解法2(二叉樹)

??????初始代碼為:

List<List<Integer>> ans=new ArrayList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combinationSum2(int[] candidates, int target) {Arrays.sort(candidates);solve(candidates,target,candidates.length,0);return ans;}public void solve(int[] c,int target,int len,int cur){if(target==0){ans.add(new LinkedList<>(tmp));return;}if(cur==len)return;if(c[cur]>target)return;tmp.add(c[cur]);solve(c,target-c[cur],len,cur+1);tmp.remove(tmp.size()-1);solve(c,target,len,cur+1);}

??????需要去重,保證在同一層中的相同元素永遠只取第一個。最終代碼如下:

List<List<Integer>> ans=new ArrayList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combinationSum2(int[] candidates, int target) {Arrays.sort(candidates);solve(candidates,target,candidates.length,0);return ans;}public void solve(int[] c,int target,int len,int cur){if(target==0){ans.add(new LinkedList<>(tmp));return;}if(cur==len)// 退出條件1return;if(c[cur]>target)// 退出條件2return;tmp.add(c[cur]);solve(c,target-c[cur],len,cur+1);int a=tmp.get(tmp.size()-1),i;//若當前層有重復元素,那 a 為第一個,并且此處 a 已經被使用過了tmp.remove(tmp.size()-1);for(i=cur+1;i<len;i++)//同一層的下一個元素值不能為 a 了,if(c[i]!=a)break;solve(c,target,len,i);}

四、子集

??????題目鏈接:https://leetcode-cn.com/problems/subsets/

??????題目描述:給你一個整數數組 nums ,數組中的元素 互不相同 。返回該數組所有可能的子集(冪集)。

解集 不能 包含重復的子集。你可以按 任意順序 返回解集。

解法1(二叉樹)

??????求集合的子集,本題需要熟悉二叉遞歸搜索樹,因為該樹的所有葉子結點就是本題的答案,將所有葉子結點加入到集合并返回。當 nums = { 1, 2, 3 };時,該樹如下圖所示(畫圖有點累…):

??????代碼如下:

List<List<Integer>> ans=new LinkedList<>();Deque<Integer> tmp=new LinkedList<>();public List<List<Integer>> subsets(int[] nums) {solve(nums,0);return ans;}public void solve(int[] nums,int cur){if(cur==nums.length){ans.add(new LinkedList<>(tmp));return;}tmp.add(nums[cur]);solve(nums,cur+1);tmp.removeLast();solve(nums,cur+1);}

解法2(n 叉樹)

??????同理,n 叉遞歸搜索樹的所有結點是本題的答案,所有結點加入集合直接返回即可。當 nums = { 1, 2, 3 };時,該樹如下圖所示:

??????代碼如下:

List<List<Integer>> ans=new LinkedList<>();Deque<Integer> tmp=new LinkedList<>();public List<List<Integer>> subsets(int[] nums) {solve(nums,0);return ans;}public void solve(int[] nums,int begin){ans.add(new LinkedList<>(tmp));for(int i=begin;i<nums.length;i++){tmp.add(nums[i]);solve(nums,i+1);tmp.removeLast();}}

五、子集II

??????題目鏈接:https://leetcode-cn.com/problems/subsets-ii/

??????題目描述:給你一個整數數組 nums ,其中可能包含重復元素,請你返回該數組所有可能的子集(冪集)。

解集 不能 包含重復的子集。返回的解集中,子集可以按 任意順序 排列。

解法1(二叉樹)

??????在上題的基礎上去重即可,代碼如下:

List<List<Integer>> ans=new LinkedList<>();Deque<Integer> tmp=new LinkedList<>();public List<List<Integer>> subsetsWithDup(int[] nums) {Arrays.sort(nums);//含有重復元素要記得排序哦。solve(nums,0);return ans;}public void solve(int[] nums,int cur){if(cur==nums.length){ans.add(new LinkedList<>(tmp));return;}tmp.add(nums[cur]);solve(nums,cur+1);int a=tmp.peekLast(),i;tmp.removeLast();for(i=cur+1;i<nums.length;i++)if(nums[i]!=a)break;solve(nums,i);}

解法2(n 叉樹)

??????同理,代碼如下:

List<List<Integer>> ans=new LinkedList<>();Deque<Integer> tmp=new LinkedList<>();public List<List<Integer>> subsetsWithDup(int[] nums) {Arrays.sort(nums);solve(nums,0);return ans;}public void solve(int[] nums,int begin){ans.add(new LinkedList<>(tmp));for(int i=begin;i<nums.length;i++){if(i>begin&&nums[i]==nums[i-1])//如果有重復元素,只會在其第一次出現時選取continue;tmp.add(nums[i]);solve(nums,i+1);tmp.removeLast();}}

??????采用標記數組的代碼如下:

List<List<Integer>> ans=new LinkedList<>();Deque<Integer> tmp=new LinkedList<>();public List<List<Integer>> subsetsWithDup(int[] nums) {Arrays.sort(nums);boolean[] visited=new boolean[nums.length];solve(nums,0,visited);return ans;}public void solve(int[] nums,int begin,boolean[] visited){ans.add(new LinkedList<>(tmp));for(int i=begin;i<nums.length;i++){if(i>0&&nums[i]==nums[i-1]&&visited[i-1]==false)continue;visited[i]=true;tmp.add(nums[i]);solve(nums,i+1,visited);tmp.removeLast();visited[i]=false;}}

總結

??????未完

總結

以上是生活随笔為你收集整理的LeetCode算法题14:递归和回溯2的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。