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】為例,仿照之前的全排列構建回溯過程的選擇樹如下:
| 第一輪選擇一個元素 | 1 | 1 | 3 | |||
| 第二輪選擇第二個元素 | 1 1 | 1 3 | 1 1 | 1 3 | 3 1 | 3 1 |
| 第三輪選擇第三個元素 | 1 1 3 | 1 3 1 | 1 1 3 | 1 3 1 | 3 1 1 | 3 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 變量標記下一輪起始元素)
??????對應的代碼為(稍加修改后):
三、組合總和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的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LeetCode算法题13:DFS/BF
- 下一篇: 分治法实现最近点对问题——C语言可视化