回溯問題Python框架總結——排列組合問題

一隻黃燜雞發表於2020-11-26

本文是對 leetcode 回溯題的一些模板進行整理總結,很多關於回溯的 blog 都會引用對回溯演算法的 official definition 和通用的解題步驟,如果是真的想研究這一演算法思想,按照這樣的方式來完全沒有問題。不過個人覺得如果僅僅只是為了應試,那麼掌握一些解題的模板會更直接的幫助理解回溯的演算法思想。本文將舉一些簡單的例子來說明這些模板,不採用樹來描述,使得對於資料結構不太瞭解的讀者也相對友好。

基本思想:

回溯問題是對多叉樹的深度搜尋,遇到不滿足條件的節點則回退,遞迴的搜尋答案。在遞迴呼叫前,嘗試一種可能的方案,那麼在遞迴呼叫的時候,函式的開始,有判斷語句,如果這種方案可行,記錄下這種方案,並且 return,否則,繼續進行嘗試,找到滿足條件的解以後,回退到之前的選擇。

常見模板:

1、無重複元素的全排列問題(或者有重複元素但是不需要去重)

一般在回溯的過程中,不斷縮小原來陣列的範圍並新增至 t r a c k track track 中,直至列舉完所有的元素,滿足條件的新增到 r e s u l t result result 陣列中, 模板如下

def problem(nums):
	res = []
	def backtrack(nums, track):
		if (判斷滿足題目所給的條件):  # 如果不限制每個結果都需要用到所有元素,就不需要 if 判斷,直接加入 res
			res.append(track[:])   # 這裡必須傳入track的拷貝,track[:], 否則答案全是空
			return
		for i in range(len(nums)):
			backtrack(nums[:i] + nums[i+1:], track + nums[i])
		backtrack(nums, [])
	return 題目需要的res相關的引數,輸出本身,長度,或者其他的

以下題目為實戰中套用框架解題

Leetcode 46 全排列

由於是全排列,只要沒得選了,那就是我們所需的答案,加入 r e s u l t result result 並且 r e t u r n return return

class Solution:
	def permute(self, nums: List[int]) -> List[List[int]]:
		res = []
		def backtrack(nums, track):
			if not nums:
				res.append(track[:])
				return
			for i in range(len(nums)):
				backtrack(nums[:i] + nums[i+1:], track + [nums[i]])
		backtrack(nums, [])
		return res
2、有重複元素的全排列問題

遇到有重複元素的問題,最好先進行排序,再採用剪枝的方法來進行去重,具體分析見 4。這裡給出全排列有重複元素去重的框架:

def problem(nums):
	res = []
	nums.sort()
	def backtrack(nums, track):
		if (判斷滿足題目所給的條件):  # 如果不限制每個結果都需要用到所有元素,就不需要 if 判斷,直接加入 res
			res.append(track[:])   # 這裡必須傳入track的拷貝,track[:], 否則答案全是空
			return
		for i in range(len(nums)):
			if i > 0 and nums[i] == nums[i-1]:  #剪枝去重
				continue
			backtrack(nums[:i] + nums[i+1:], track + nums[i])
	backtrack(nums, [])
	return 題目需要的res相關的引數,輸出本身,長度,或者其他的

Leetcode 1079 活字印刷

先將字串放在入列表中進行排序,後進行剪枝去重。

由於不需要求具體有哪些排列,因此只需要用一個變數來記錄過程中的結果。類似的, N N N皇后與 N N N皇后Ⅱ 的差別也僅在於是否需要建立一個列表或者一個變數來儲存結果。

初始 a n s ans ans 設為 -1,因為題目要求最後的結果非空,提前減去一個空字串。

class Solution:
	def numTilePossibilities(self, tiles: str) -> int:
		self.ans = -1
		tiles = list(tiles)
		tiles.sort()
		def backtrack(tiles):
			self.ans += 1
			for i in range(len(tiles)):
				if i > 0 and tiles[i] == tiles[i-1]:
					continue
				backtrack(tiles[:i] + tiles[i+1:])
		backtrack(tiles)
		return self.ans
3、陣列元素不重複且陣列元素不可以重複使用的組合問題

這種問題在高中找多少種不同的組合比較常見,比如找 [ 1 , 2 , 3 ] [1,2,3] [1,2,3] 這樣的陣列有多少種非空的子集,那麼我們按照高中的不重複不遺漏的找法,一般是先確定 1 1 1,然後找 2 2 2, 3 3 3 裡面的,第一輪找出來是 [ 1 ] [1] [1] , [ 1 , 2 ] [1,2] [1,2] , [ 1 , 3 ] [1,3] [1,3] , [ 1 , 2 , 3 ] [1,2,3] [1,2,3],這時候對於 1 1 1 來說,沒有更多的元素可以和它組成子集了,那麼現在去掉 1 1 1,再從 [ 2 , 3 ] [2,3] [2,3] 裡面找剩餘的,第二輪出來的是 [ 2 ] [2] [2], [ 2 , 3 ] [2,3] [2,3],最後一輪從 [ 3 ] [3] [3] 中找,也就是 [ 3 ] [3] [3]。這樣我們就得到了不重複不遺漏的所有非空子集。

可以看到,這種問題,越搜尋,資料範圍越小,比上一輪起始資料向後移動了一位,那麼在遞迴呼叫中就可以用一個 i n d e x index index 標誌 + 1 +1 +1 來表示現在的起始位置從上一輪 + 1 +1 +1 的位置開始。框架如下

def problem(nums):
	res = []
	def backtrack(index, track):
		if (滿足題目中的條件):
			res.append(track[:])
				return
		for i in range(index, len(nums)):
			backtrack(i + 1, track + [nums[i]])
	backtrack(0, []) # 這裡不一定是0,根據實際的起始條件來給
	return res

以下三題為實戰中用框架解題

Leetcode 77 組合

實際問題的返回條件是每個組合內有 k k k 個數,那麼就是 t r a c k track track 長度需要是k的時候返回。由於這裡題目並沒有直接給出陣列,是用 1 − n 1-n 1n 來代替,那麼起始條件就是 1 1 1,陣列用 1 − n 1-n 1n 的範圍來代替就好。

class Solution:
	def combine(self, n: int, k: int) -> List[List[int]]:
		res = []
		def backtrack(index, track):
			if len(track) == k:
				res.append(track[:])
				return
			for i in range(index, n+1):
				backtrack(i + 1, track + [i])
		backtrack(1, [])
		return res

Leetcode 78 子集

直接套入框架,這裡每一次搜尋的路徑都要記錄下來,那就記錄一下每次的路徑就行了,不需要再判斷什麼時候的結果才儲存

class Solution:
	def subsets(self, nums: List[int]) -> List[List[int]]:
		res = []
		def backtrack(index, track):
			res.append(track[:])
			for i in range(index, len(nums)):
				backtrack(i + 1, track + [nums[i]])
		backtrack(0, [])
		return res

Leetcode 17 電話號碼中的字母組合

此題看上去陣列中的數可以重複,比如可以撥打“232”,但是由於是字串,順序是一定的,而且撥打第一個 2 2 2 和第二個 2 2 2,對應的字母也可能不同,所以仍然可以看做是陣列中元素不重複且不能重複使用的問題。

用字典記錄下對應關係,之後代入框架即可,注意讀取字典鍵和值的各種括號就行,最終結果是字串的時候, t r a c k track track 初始設為“”替代 [ ] [] []

class Solution:
	def letterCombinations(self, digits: str) -> List[str]:
		if not digits:
			return []
		res = []
		dic = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'}
		def backtrack(index, track):
			if len(track) == len(digits):
				res.append(track)
				return
			for i in range(len(dic[digits[index]])):
				backtrack(index + 1, track + dic[digits[index]][i])
		backtrack(0, "")
		return res
4、陣列元素有重複但不可以重複使用的組合問題

這一類問題和第二種型別的問題相似,最主要的是要對結果進行去重,也就是對深搜的N叉樹進行剪枝。比如我們要找 [ 2 , 1 , 2 , 4 ] [2,1,2,4] [2,1,2,4] 有多少種不重複的子集組合,按照我們的高中知識,為了不重複不遺漏,我們應該先排序這個陣列,得到 [ 1 , 2 , 2 , 4 ] [1,2,2,4] [1,2,2,4],這時候從1開始找,第一輪是 [ 1 ] [1] [1] , [ 1 , 2 ] [1,2] [1,2],接下來遇到一個相同的 2 2 2,我們為了不重複,會跳過它,不看,因為 l e n = 2 len = 2 len=2 的時候,如果再選 2 2 2,就會得到重複的結果,然後是 [ 1 , 4 ] [1,4] [1,4], [ 1 , 2 , 2 ] [1, 2, 2] [1,2,2], [ 1 , 2 , 4 ] [1, 2, 4] [1,2,4], [ 1 , 2 , 2 , 4 ] [1,2,2,4] [1,2,2,4],我們在找 l e n = 3 len=3 len=3 的時候,同樣,當第二位選了第一個 2 2 2 以後,第二位就不再考慮選第二個 2 2 2 的情況,因為它們的結果相同,至此,第一輪結束。

第二輪去掉 1 1 1,在 [ 2 , 2 , 4 ] [2,2,4] [2,2,4] 裡面找, [ 2 ] [2] [2], [ 2 , 2 ] [2,2] [2,2], [ 2 , 4 ] [2,4] [2,4], [ 2 , 2 , 4 ] [2,2,4] [2,2,4], 第三輪去掉一個 2 2 2,本來應該在 [ 2 , 4 ] [2,4] [2,4] 裡面找,假如我們這樣找結果,會得到 [ 2 ] [2] [2], [ 2 , 4 ] [2,4] [2,4],產生重複,因為 [ 2 , 4 ] [2,4] [2,4] 的情況已經包含在 [ 2 , 2 , 4 ] [2,2,4] [2,2,4] 中了,這就是有重複元素的情況下,我們在同一個位置進行選擇的時候,應該跳過相同的元素,否則會產生重複。第三輪實際在 [ 4 ] [4] [4] 裡面找,得到 [ 4 ] [4] [4]

框架如下

def problem(nums):
	res = []
	nums.sort() # 排序,為了後面去重做準備
	def backtrack(index, track):
		if (滿足題目條件):
			res.append(track[:])
		for i in range(index, len(nums)):
		### 進行剪枝,跳過相同位置重複的數字選擇
			if i > index and nums[i] == nums[i-1]: 
				continue
			backtrack(i + 1, track + [nums[i]])
	backtrack(0, [])
	return res 

以下兩題為實戰中用框架解題

Leetcode 90 子集2

搜尋路徑上所有結果全部保留,直接套入上述框架即可

class Solution:
	def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
		res = []
		nums.sort()
		def backtrack(index, track):
			res.append(track[:])
			for i in range(index, len(nums)):
				if i > index and nums[i] == nums[i-1]:
					continue
				backtrack(i + 1, track + [nums[i]])
		backtrack(0, [])
		return res

Leetcode 40 組合總和2

這裡唯一的差別是在於需要把目標和也一起代入進遞迴呼叫中,每次判斷如果是目標和就加入最終結果,加超過了目標和那就不符合,直接跳出

class Solution:
	def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
		candidates.sort()
		res = []
		def backtrack(index, track, target):
			if target == 0:
				res.append(track[:])
				return
			for i in range(index, len(candidates)):
				if target - candidates[i] < 0: # 超過目標和
					break
				if i > index and candidates[i] == candidates[i-1]:
					continue
				backtrack(i + 1, track + [candidates[i]], target - candidates[i])
		backtrack(0, [], target)
		return res
5、陣列元素不重複但可以重複使用

這一類的問題同樣也是第二種問題演變而來,唯一的區別是遞迴呼叫 b a c k t r a c k backtrack backtrack 的時候,把 i + 1 i + 1 i+1 改成 i i i ,那麼下一個位置又可以用這個元素了,即可實現有重複

Leetcode 39 組合總和

class Solution:
	def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
		res = []
		candidates.sort()
		def backtrack(index, track, target):
			if target == 0:
				res.append(track[:])
				return
			for i in range(index, len(candidates)): 
				if target - candidates[i] < 0:
					break
                ### 把原來遞迴的時候 i+1 改成 i,當前的元素又可以再用一次了
				backtrack(i, track + [candidates[i]], target - candidates[i])
		backtrack(0, [], target)
		return res
文章在cnblogs 同步更新,喜歡的話為我點個贊吧~

https://www.cnblogs.com/HMJIang/p/13575005.html

相關文章