leetcode 經典回溯算法題目(思路、方法、code)

用於回顧數據結構與算法時刷題的一些經驗記錄

  • 回溯與遞歸都要注意剪枝,避免重複計算

  • 回溯的問題,一定要把整個樹圖畫出來,然後再去考慮如何縮小問題,還要注意恢復狀態

  • 回溯算法的大致模板:

    • 根據這個模板,然後根據自己畫出的樹圖,基本所有回溯問題都可以搞定。
    void backtrack(已經做的選擇, 選擇列表):
        if 當前達到了結束位置:
            result.push(最終的選擇序列)
        for 每個選擇 in 選擇列表:
            做選擇 //可能這裏需要判斷選擇是否可行
            backtrack(新的已選擇, 新的選擇列表) //這裏的已選擇和選擇列表都要更新
            撤銷選擇
    
    //在此基礎上,如果部分節點已經無解,則將它能夠推理出的無解的樹枝均剪掉
    

46. 全排列

給定一個 沒有重複 數字的序列,返回其所有可能的全排列。

示例:
輸入: [1,2,3]
輸出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

分析: 全排列的發現過程:

在這裏插入圖片描述

因此,每一步,我們需要確定一下當前深度的位置的元素,然後就可以繼續向下展開。例如:如果在第一個位置選擇了1,則在第二個位置可以選擇2或者3,選擇後再次選擇第三個位置的元素。

如果在第i個位置選過後,則之後等價於前i個位置的元素排列已經確定,而後面實際上就是還沒有排列的元素的全排列。這裏就可以發現遞歸的思想。

因此回溯的大致思路爲:

  • 函數需要保存的內容有:當前已經確定的序列,當前的位置,序列的總長度
  • 每次判斷,是否已經遍歷完,即當前位置是否等於序列的總長度,如果等於說明當前序列已經是一個結果,存儲
  • 如果沒有遍歷完,則確定一下該位置的元素,然後繼續遞歸,遞歸時需要將當前位置右移
  • 在這裏確定元素時,可以用循環的方式直接確定,但是每次遞歸結束後,需要恢復現場,將狀態均恢復,否則將導致回溯的難以結束或者結果不足
class Solution {
public:
	vector<vector<int> > result;
    vector<vector<int> > permute(vector<int>& nums) 
	{
    	backtrack(nums,0,nums.size());    
    	return result;
    }
    void backtrack(vector<int>& output,int first,int len) 
    //output爲當前已有的排列,first表示現在排列到的位置,len爲長度
	{
		if(first==len)
		{
			result.emplace_back(output);
			return ;
		}
		for(int i=first;i<len;i++) //進行回溯,每次確定好當前位置的元素後繼續遞歸即可
		{
			swap(output[i],output[first]); //將目前的位置依次換成output[i]
			backtrack(output,first+1,len);
			swap(output[first],output[i]);  //回溯的關鍵,在於遞歸後要恢復現場 
		}		 
	 } 
};

78. 子集

給定一組不含重複元素的整數數組 numsnums,返回該數組所有可能的子集(冪集)。

**說明:**解集不能包含重複的子集。

示例:
輸入: nums = [1,2,3]
輸出:
[[3],[1],[2],[1,2,3],[1,3],[2,3],[1,2],[]]

分析:

方法一:枚舉或遞歸的思想

對於 numsnums 中每個元素,都有兩種考慮,即選擇或者不選擇,因此最終結果數目是 2n2^n ,時間複雜度也是O(n2n)O(n2^n),因爲每個結果都需要加到列表中,這是一個 O(n)O(n) 的複雜度

因此,整個問題將化爲類似於決策樹的思路,模擬的方法很多,但思想一致,複雜度爲 O(N2n)O(N*2^n)

方法二:回溯
在這裏插入圖片描述

回溯法的思路更多地向該圖所示,只要定好順序,每次選擇其中一個作爲頭,即可。這個過程也避免了繁重的剪枝

每次選擇該節點或者不選擇該節點,然後去回溯。

子集問題時,我們可以發現,該樹中每一個節點都是結果,因此遍歷到每一個節點都需要將其加入。

class Solution {
public:
vector<vector<int> > res;

vector<vector<int> > subsets(vector<int>& nums) {
    vector<int> track; //記錄走過的路徑
    backtrack(nums, 0, track);
    return res;
}

void backtrack(vector<int>& nums, int start, vector<int>& track) //start記錄位置 
{
    res.push_back(track); //每次把當前位置代表的子集加入result中即可 
    for (int i = start; i < nums.size(); i++) //在這裏將選擇不選擇的思想化爲以哪個位置開頭 
	{
        track.push_back(nums[i]); //將該位置的值放入到track 
        backtrack(nums, i + 1, track); //向下遍歷,向自己的子節點進行遍歷 
        track.pop_back(); //回溯回來後,將之前操作恢復,之後就走向自己的兄弟節點去遍歷 
    }
}
};

90. 子集 II

給定一個可能包含重複元素的整數數組 nums,返回該數組所有可能的子集(冪集)。

**說明:**解集不能包含重複的子集。

分析:該問題,和上個問題幾乎一樣,只是可能包含重複元素,因此,如果對重複元素進行剪枝是該題的關鍵。

思路:我們將 numsnums 進行排序,按照上題思路,採用回溯法進行判斷,但是,當做選擇時,需要判斷當前選擇是否與之前做的一致,如果上一輪遍歷時候做過該選擇(也就是在樹中表示的意思是該節點的左兄弟節點與子集一致),則不進行選擇。

回溯的思想和順序仍爲下圖,只是如果左兄弟節點和自身是一致時,則將該枝減去
在這裏插入圖片描述

class Solution {
public:
    vector<vector<int> > res;
    vector<vector<int>> subsetsWithDup(vector<int>& nums) 
    {
        vector<int> track; //記錄走過的路徑
        sort(nums.begin(),nums.end());
        backtrack(nums, 0, track);
        return res;
    }
	void backtrack(vector<int>& nums, int start, vector<int>& track) //start記錄位置 
	{
    	res.push_back(track); //每次把當前位置代表的子集加入result中即可 
    	for (int i = start; i < nums.size(); i++)//在這裏將選擇不選擇的思想化爲以哪個位置開頭 
		{ 
            if(i==start||nums[i]!=nums[i-1]) 
            //如果該節點是最左側子節點或者與左側節點不同的話,則正常向下遍歷,否則取消這一輪的遍歷
			{
                track.push_back(nums[i]); //將該位置的值放入到track 
        	    backtrack(nums, i + 1, track); //向下遍歷,向自己的子節點進行遍歷 
        	    track.pop_back(); //回溯回來後,將之前操作恢復,之後就走向自己的兄弟節點去遍歷 
            }
    	}
	}
};

39. 組合總和

給定一個無重複元素的數組 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和爲 target 的組合。 candidates 中的數字可以無限制重複被選取。

  • 所有數字(包括 target)都是正整數。
  • 解集不能包含重複的組合。
示例:
輸入: candidates = [2,3,6,7], target = 7,
所求解集爲:
[
  [7],
  [2,2,3]
]

分析:

畫出樹形結構,確定出結束方式,進行剪枝優化
在這裏插入圖片描述

首先對節點進行了排序,然後如果一個節點當前的和已經大於target,則說明其右方兄弟節點肯定不成立,故將右方全部剪枝。爲了避免重複,每個節點向下延伸的值,只能是小於等於其的。

類似於之前的代碼,補充模板即可。

class Solution {
public:
	vector<vector<int> > res;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) 
	{
        sort(candidates.begin(),candidates.end()); 
        vector<int> track;
        backtrack(candidates,track,0,target);
        return res;
    }
    bool backtrack(vector<int>& candidates,vector<int> track,int start,int target)
    {
    	if(target<0)
    		return false;
        if(target==0)
    	{
                res.push_back(track);return false;
        }
        bool conti=true;
    	for(int i=start;i<candidates.size();i++)
    	{
            if(conti==false) break;  //conti爲false說明子節點和右兄弟節點一定都不成立
    		track.push_back(candidates[i]);
    		conti=backtrack(candidates,track,i,target-candidates[i]);
    		track.pop_back();
		}
        return true;
	}
};

40. 組合總和 II

給定一個數組 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和爲 target 的組合。

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

輸入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集爲:
[ [1, 7], [1, 2, 5], [2, 6],[1, 1, 6] ]

分析:和上個題比較,candidates 中每個數字只能出現一次,實際上只需要在上題基礎上,在選擇的時候,我們減去了部分選擇即可。當然這裏還需要注意的是candidates 中數字可以重複,因此如果一開始對數組進行了排序,則將重複的部分也直接剪去即可。

class Solution {
public:
	vector<vector<int> > res;
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) 
	{
        sort(candidates.begin(),candidates.end()); 
        vector<int> track;
        backtrack(candidates,track,0,target);
        return res;
    }
    bool backtrack(vector<int>& candidates,vector<int> track,int start,int target)
    {
    	if(target<0)
    		return false;
        if(target==0)
    	{
                res.push_back(track);return false; //右兄弟節點和子節點均不會成立
        }
        bool conti=true;
    	for(int i=start;i<candidates.size();i++)
    	{
            if(i==start||candidates[i]!=candidates[i-1])//剪去重複的節點
			{
			    if(conti==false) break;  //conti爲false說明子節點和右兄弟節點一定都不成立
    			track.push_back(candidates[i]);
    			conti=backtrack(candidates,track,i+1,target-candidates[i]);
    			track.pop_back();
			}
		}
        return true;
	}
};

22. 括號生成

數字 n 代表生成括號的對數,請你設計一個函數,用於能夠生成所有可能的並且 有效的 括號組合。

輸入:n = 3
輸出:[  "((()))", "(()())", "(())()", "()(())","()()()" ]

分析:(有效?因爲只有一種括號且數量相同,因此只要在任意位置右括號前左括號大於右括號數量即合法)

最暴力的方法,生成所有可能的字符串,然後保留有效的。

顯然很多結果在過程中就已經是無效的,因此採用回溯法,儘可能剪枝

因此,形成決策樹:(大致的樹形結構爲)

在這裏插入圖片描述

也就是在個節點,都可以選擇左括號或者右括號,然後繼續生成,直至用完。

限制條件(剪枝條件):

  • 如果在某處右括號數量大於左括號數量,則說明已經非法,剪枝。
  • 左括號和右括號數量至多n個
  • 因爲在字符串生成過程中已經判斷是否非法,因此最終生成的字符串都是合法的
class Solution {
public:
	vector<string> result;
    vector<string> generateParenthesis(int n) 
	{
        string a="";
        backtrack(a,n,n);
        return result;
    }
    void backtrack(string a,int left,int right)
    {
    	if(left==0&&right==0) //字符串已經形成,則push
    	{
    		result.push_back(a);
    		return ;
		}
		if(left>0)   //還可以選左括號
		{
			backtrack(a+'(',left-1,right);	
		}
		if(right>0) //還可以選右括號
		{
			if(left>=right) return; //說明當前放置的右括號已經大於等於左括號,再放置則非法 
			else
				backtrack(a+')',left,right-1);
		} 
	}
};

77. 組合

給定兩個整數 nk,返回 1 … n 中所有可能的 k 個數的組合。

示例:
輸入: n = 4, k = 2
輸出:
[ [2,4],[3,4],[2,3],[1,2],[1,3],[1,4],]

分析:該問題和組合總和問題基本一致,只是在這裏判斷條件是是否已經有k個數,如果數達到,則結束。否則就繼續遞歸。

n=4,k=2n=4,k=2 爲例:

在這裏插入圖片描述

因爲不能重複且是有序的,所以就構成了如圖所示的決策樹

如果深度到達k,則該結果應存儲。

  • 如果一個節點可選擇的數目小於還需要添加的數字的數目,則剪枝
  • 每次選擇的數字,需要比當前數字都要大
class Solution {
public:
	vector<vector<int> >  result;
    vector<vector<int> > combine(int n, int k) 
	{
        vector<int> track;
        if(n<k) return result;
        backtrack(track,0,k,n);
        return result;
    }
    void backtrack(vector<int> track,int length,int k,int n)
    {
    	if(length==k) //當前序列長度到達k,則加入到結果中,返回 
    	{
    		result.push_back(track);
    		return;
		}
        if(length>0&&n-track[length-1]<k-length) //後面的節點數一定小於k-length,故該情況不可能有解,直接剪枝
            return;
         for(int i=(length==0?length:track[length-1]);i<n;i++)
		 {
			    track.push_back(i+1);
			    backtrack(track,length+1,k,n);
			    track.pop_back();	
		 }
          return ;
	}
};

51. N皇后

n 皇后問題研究的是如何將 n 個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。
在這裏插入圖片描述

上圖爲 8 皇后問題的一種解法。

給定一個整數 n,返回所有不同的 n 皇后問題的解決方案。

每一種解法包含一個明確的 n 皇后問題的棋子放置方案,該方案中 ‘Q’ 和 ‘.’ 分別代表了皇后和空位。

示例:
輸入: 4
輸出: [
 [".Q..",  // 解法 1
  "...Q",
  "Q...",
  "..Q."],

 ["..Q.",  // 解法 2
  "Q...",
  "...Q",
  ".Q.."]
]
解釋: 4 皇后問題存在兩個不同的解法。

分析:N皇后問題是非常經典的問題,也就是如何放置N個皇后在N*N的棋盤中,使其不攻擊,皇后可以攻擊同一行、同一列、左上左下右上右下四個方向的任意單位。

因此這也是一個排列問題,在這裏可以認爲決策樹的每一層就是棋盤的一行,在每個節點所做的選擇就是,在該行的某一列放置一個皇后。

因此基本思路

  • 首先對棋盤進行初始化
  • 進行回溯,每一行做出一個選擇,即將該位置的’.‘換位’Q’,做出選擇後需要判斷該選擇是否非法,如果非法則直接返回即剪枝
  • 如果可以到達第n行且仍合法,則說明該結果合理,存儲
  • 判斷非法時,只需要判斷列以及右上方和左上方就可以了,首先判斷是否越界,沒有越界的話判斷是否已經存在皇后。可以用一個方向數組表示,這裏由於只判斷上方,故方向數組只處理列的變化即可
class Solution {
public:
	vector<vector<string> > result;  //存儲結果
    vector<vector<string> > solveNQueens(int n) 
	{
        vector<string> map(n, string(n, '.')); //直接初始化爲n行n列的點
		backtrack(map,0,n); 
		return result;
    }
	bool isVaild(vector<string>& map, int row, int col) {
    	int n = map.size();
    	static const int dx[]={-1,0,1}; //標記col的三種變化
    	for(int i=1;i<=row;i++) //檢測上方,左上方,右上方是否存在Q
    	{
        	int new_row=row-i;
       	 	for(int j=0;j<3;j++)
        	{
            	int new_col=col+i*dx[j];
            	if(new_col>=0&&new_col<n&&map[new_row][new_col]=='Q')
                	return false;
        	}
    	}
      return true;
	}
    void backtrack(vector<string>&map,int pos,int n) //pos表示當前高度
	{
		if(pos==n)  //已經到達決策樹的最底端 
		{
			result.push_back(map);
			return;	
		}
		for(int i=0;i<n;i++)
		{
			if(!isVaild(map,pos,i))  //如果在(pos,i)的位置放置Q不合理的話,剪枝 
				continue;			//否則在該處放置Q進行回溯 
			map[pos][i]='Q';
			backtrack(map,pos+1,n);
			map[pos][i]='.';  //回溯回去時候需要恢復狀態 
		}	
	} 
};
52. N皇后 II

對於N皇后Ⅱ,只需要將結果進行修改即可,每次成立時候計數+1,不需要存儲具體的值。

class Solution {
public:
	int result;  //存儲結果
    int totalNQueens(int n) 
	{
        vector<string> map(n, string(n, '.')); //直接初始化爲n行n列的點
		backtrack(map,0,n); 
		return result;
    }
	bool isVaild(vector<string>& map, int row, int col) {
    	int n = map.size();
    	static const int dx[]={-1,0,1}; //標記col的三種變化
    	for(int i=1;i<=row;i++) //檢測上方,左上方,右上方是否存在Q
    	{
        	int new_row=row-i;
       	 	for(int j=0;j<3;j++)
        	{
            	int new_col=col+i*dx[j];
            	if(new_col>=0&&new_col<n&&map[new_row][new_col]=='Q')
                	return false;
        	}
    	}
      return true;
	}
    void backtrack(vector<string>&map,int pos,int n) //pos表示當前高度
	{
		if(pos==n)  //已經到達決策樹的最底端 
		{
			result++;
			return;	
		}
		for(int i=0;i<n;i++)
		{
			if(!isVaild(map,pos,i))  //如果在(pos,i)的位置放置Q不合理的話,剪枝 
				continue;			//否則在該處放置Q進行回溯 
			map[pos][i]='Q';
			backtrack(map,pos+1,n);
			map[pos][i]='.';  //回溯回去時候需要恢復狀態 
		}	
	} 
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章