“回溯法”也称“试探法”。它是从问题的某一状态出发,不断“试探”着往前走一步,当一条路走到“尽头”,不能再前进(拓展出新状态)的时候,再倒回一步或者若干步,从另一种可能的状态出发,继续搜索,直到所有的“路径(状态)”都一一试探过。
回溯算法可以看做是深度优先遍历算法的一种。深度优先遍历的目的是“遍历”,本质是无序的。也就是说访问次序不重要,重要的是都被访问过了(需要记录结点是否被visited)。而回溯的目的是“求解过程”,本质是有序的,不同的次序会导致不一样的结果,一般不需要记录结点是否visited)
深度优先搜索可以采用递归(系统栈)和非递归(手工栈)两种方法实现。往往需要记录整颗搜索树。回溯算法一般不用记录整颗搜索树。
1. 回溯算法的三要素
回溯过程其实就是一个决策树的遍历过程。包含三个要素:
- 路径:已经做的选择
- 选择列表:当前可以做的选择
- 结束条件:即决策树的叶子节点,无法再继续往下选择的条件
回溯算法的基本框架可以概括为:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
2. 回溯算法的例子
2.1. 求子集问题
2.1.1. leetcode-78. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明: 解集不能包含重复的子集。
python代码:
class Solution:
def backtrack(self,index,decisions,nums,temp_result,results):
if index==len(nums):
results.append(temp_result.copy())
return
for decision in decisions:
if decision:
temp_result.append(nums[index])
self.backtrack(index+1,decisions,nums,temp_result,results)
temp_result.pop()
else:
self.backtrack(index+1,decisions,nums,temp_result,results)
def subsets(self, nums: List[int]) -> List[List[int]]:
decisions=[True, False]
results=[]
temp_result=[]
index=0
self.backtrack(index,decisions,nums,temp_result,results)
return results
上述代码中,所谓的“选择”就是当前子集中是否要保留某个元素decisions=[True,False]
;路径就是当前子集中包含了哪些元素temp_result
;结束条件就是当前路径已经遍历到了数组的最后一个元素:index==len(nums)
;撤销的时候要注意,需要判断当前的选择是否是添加了当前的元素,如果是的话,就撤销该元素,也即最后一个元素(等价于temp_result.pop()
。
假如输入是nums[1,2,3]
,则输出是[[1,2,3],[1,2],[1,3],[1],[2,3],[2],[3],[]]
。读者可以结合这个输入输出例子来理解代码的运行过程
2.2. 排列组合问题
2.2.1. leetcode-77. 组合
给定两个整数 n 和 k,返回 1 … _n _中所有可能的 k 个数的组合。
思路与leetcode-78是一样的,不同的地方在于return的时候,只有当temp_result的长度为k才append到results中。
当然,可以在append前进行剪枝:
class Solution:
def backtrack(self,decisions,index,n,k,temp_result,results):
if n-index+1+len(temp_result)<k:
return
if len(temp_result)==k:
results.append(temp_result.copy())
return
for decision in decisions:
if decision:
temp_result.append(index)
self.backtrack(decisions,index+1,n,k,temp_result,results)
temp_result.pop()
else:
self.backtrack(decisions,index+1,n,k,temp_result,results)
def combine(self, n: int, k: int) -> List[List[int]]:
decisions=[True,False]
results=[]
temp_result=[]
index=1
self.backtrack(decisions,index,n,k,temp_result,results)
return results
2.2.2. leetcode-46. 全排列
给定一个没有重复 数字的序列,返回其所有可能的全排列。
python
class Solution:
def backtrack(self,nums,visited,results,temp_result):
if len(temp_result)==len(nums):
results.append(temp_result.copy())
return
for index in range(0,len(visited)):
if not visited[index]:
temp_result.append(nums[index])
visited[index]=True
self.backtrack(nums,visited,results,temp_result)
temp_result.pop()
visited[index]=False
def permute(self, nums: List[int]) -> List[List[int]]:
visited=[False]*len(nums)
temp_result=[]
results=[]
self.backtrack(nums,visited,results,temp_result)
return results
思路:定义一个长度和nums一样的数组,里面保存着nums里面每个元素是否被使用的标志,作为选择列表;如果选择列表里面每个元素都使用过了,就需要append结果并return。
2.2.3. leetcode-401. 二进制手表
二进制手表顶部有 4 个 LED 代表小时(0-11),底部的 6 个 LED 代表分钟(0-59)。
每个 LED 代表一个 0 或 1,最低位在右侧。
给定一个非负整数 _n _代表当前 LED 亮着的数量,返回所有可能的时间。
提示:
- 输出的顺序没有要求。
- 小时不会以零开头,比如 “01:00” 是不允许的,应为 “1:00”。
- 分钟必须由两位数组成,可能会以零开头,比如 “10:2” 是无效的,应为 “10:02”。
- 超过表示范围(小时 0-11,分钟 0-59)的数据将会被舍弃,也就是说不会出现 “13:00”, “0:61” 等时间。
class Solution {
Set<String> results=new HashSet<String>();
Boolean[] hours={false,false,false,false};
Boolean[] minutes={false,false,false,false,false,false};
public List<String> readBinaryWatch(int num) {
dfs(num);
return new ArrayList<>(results);
}
private String getTime(){
int hour=0;
int minute=0;
for(int i=hours.length-1;i>=0;i--){
if(hours[i]){
hour+=(int)Math.pow(2,i);
}
}
if(hour>11) return"";
for(int i=minutes.length-1;i>=0;i--){
if(minutes[i]){
minute+=(int)Math.pow(2,i);
}
}
if(minute>59) return"";
if(minute<10){
return String.valueOf(hour)+":0"+String.valueOf(minute);
} else {
return String.valueOf(hour)+":"+String.valueOf(minute);
}
}
private void dfs(int num){
if(num==0){
String result=getTime();
if(!"".equals(result)) results.add(result);
return;
}
for(int i=0;i<hours.length;i++){
if(!hours[i]){
hours[i]=true;
dfs(num-1);
hours[i]=false;
}
}
for(int i=0;i<minutes.length;i++){
if(!minutes[i]){
minutes[i]=true;
dfs(num-1);
minutes[i]=false;
}
}
}
}
注意这里的选择列表是两个选择列表(小时和分钟)的并集,但是先选择小时后选择分钟,和先选择分钟后选择小时,是等价的,因此需要对最后的结果去重。这里用Set来存储结果,达到了去重的目的。
3. 深度优先遍历的要素
4. 深度优先遍历的例子
4.1. 矩阵的连通性判断
4.1.1. leetode-200. 岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
class Solution:
island_num=0
def dfs(self,grid,visited,directions,i,j):
# 边界条件
if i>=len(grid) or j>=len(grid[0]) or i<0 or j<0:
return
# 访问过的结点,跳过
if visited[i][j]==1:
return
# 为0 的结点跳过,这里记不记录被访问其实无所谓
if grid[i][j]=="0":
#visited[i][j]=1
return
else:
# 是否将该节点作为新发现的岛屿线索
is_island=1
for k, direction in enumerate(directions):#看看它是否是已经属于某个岛屿
#边界条件
if i+direction[0]>=len(grid) or j+direction[1]>=len(grid[0]) or i+direction[0]<0 or j+direction[1]<0:
continue
#如果它的上下左右是岛屿的一部分,且已经被访问过,则它不能算新发现的岛屿
if grid[i+direction[0]][j+direction[1]]=="1":
if visited[i+direction[0]][j+direction[1]]==1:
is_island=0
break
else:
# 剪枝,如果当前结点是新发现的岛屿,则将其上下左右的非水区域标记为访问过
visited[i+direction[0]][j+direction[1]]==1
self.island_num+=is_island
visited[i][j]=1
for k, direction in enumerate(directions):
self.dfs(grid,visited,directions,i+directions[k][0],j+directions[k][1])
def numIslands(self, grid: List[List[str]]) -> int:
visited=[[0 for e in grid[0]] for e in grid]
directions=[(-1,0),(1,0),(0,-1),(0,1)]# 上,下,左,右
for i in range(0,len(grid)):
for j in range(0,len(grid[i])):
self.dfs(grid,visited,directions,i,j)
return self.island_num
4.1.1.1. 优化点1:省略visited矩阵
visited矩阵需要额外占据空间,可以将原始grid矩阵中被访问的点设置为"0",来等价为被访问过,从而节省空间消耗
class Solution:
directions=[(-1,0),(1,0),(0,-1),(0,1)]# 上,下,左,右
def dfs(self, grid, i, j):
if not (0<=i<len(grid) and 0<=j<len(grid[i]) and grid[i][j]=="1"):
return
grid[i][j]="0"
for k,direction in enumerate(self.directions):
self.dfs(grid,i+direction[0],j+direction[1])
def numIslands(self, grid: List[List[str]]) -> int:
num_islands=0
for i in range(0,len(grid)):
for j in range(0,len(grid[i])):
if grid[i][j]=="1":
num_islands+=1
self.dfs(grid, i, j)
return num_islands
4.1.2. leetcode面试题 16.19. 水域大小
你有一个用于表示一片土地的整数矩阵land,该矩阵中每个点的值代表对应地点的海拔高度。若值为0则表示水域。由垂直、水平或对角连接的水域为池塘。池塘的大小是指相连接的水域的个数。编写一个方法来计算矩阵中所有池塘的大小,返回值需要从小到大排序。
class Solution:
directions=[(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
temp_size=0
results=[]
def dfs(self, land, i, j):
if not(0<=i<len(land) and 0<=j<len(land[i]) and land[i][j]==0):
return
self.temp_size+=1
land[i][j]=-1
for k,direction in enumerate(self.directions):
self.dfs(land,i+direction[0],j+direction[1])
def pondSizes(self, land: List[List[int]]) -> List[int]:
for i in range(0,len(land)):
for j in range(0,len(land[i])):
if land[i][j]==0:
self.dfs(land,i,j)
self.results.append(self.temp_size)
self.temp_size=0
self.results.sort()
return self.results