算法基础之回溯与深度优先遍历



“回溯法”也称“试探法”。它是从问题的某一状态出发,不断“试探”着往前走一步,当一条路走到“尽头”,不能再前进(拓展出新状态)的时候,再倒回一步或者若干步,从另一种可能的状态出发,继续搜索,直到所有的“路径(状态)”都一一试探过。
回溯算法可以看做是深度优先遍历算法的一种。深度优先遍历的目的是“遍历”,本质是无序的。也就是说访问次序不重要,重要的是都被访问过了(需要记录结点是否被visited)。而回溯的目的是“求解过程”,本质是有序的,不同的次序会导致不一样的结果,一般不需要记录结点是否visited)
深度优先搜索可以采用递归(系统栈)和非递归(手工栈)两种方法实现。往往需要记录整颗搜索树。回溯算法一般不用记录整颗搜索树。

1. 回溯算法的三要素

回溯过程其实就是一个决策树的遍历过程。包含三个要素:

  1. 路径:已经做的选择
  2. 选择列表:当前可以做的选择
  3. 结束条件:即决策树的叶子节点,无法再继续往下选择的条件

回溯算法的基本框架可以概括为:

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. 组合

给定两个整数 nk,返回 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
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值