五大算法----回溯算法

回溯算法

思想:在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

回溯算法就是在一个树形问题上做一次深度优先遍历,以达到搜索所有可能的解的效果。

执行深度优先遍历,从较深层的结点返回到较浅层结点的时候,需要做**「状态重置」,即「回到过去」「恢复现场」**

一定不要忘记恢复现场

“回溯”算法总结(深度优先遍历 + 状态重置 + 剪枝)

常出现情况:

当题目中出现 “所有组合” 等类似字眼时,我们第一感觉就要想到用回溯。

五大算法之一回溯算法

五大算法----回溯算法_第1张图片

回溯问题三要素

1.有效结果(可能也有无效结果

2.回溯范围及答案更新

3.剪枝条件

回溯模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

`

回溯问题lc相关练习:

题型一:排列、组合、子集相关问题

提示:这部分练习可以帮助我们熟悉「回溯算法」的一些概念和通用的解题思路。解题的步骤是:先画图,再编码。去思考可以剪枝的条件, 为什么有的时候用 used 数组,有的时候设置搜索起点 begin 变量,理解状态变量设计的想法。

  1. 全排列(中等)

  2. 全排列 II(中等):思考为什么造成了重复,如何在搜索之前就判断这一支会产生重复;

  3. 组合总和(中等)

  4. 组合总和 II(中等)

  5. 组合(中等)

  6. 子集(中等)

  7. 子集 II(中等):剪枝技巧同 47 题、39 题、40 题;

  8. 第 k 个排列(中等):利用了剪枝的思想,减去了大量枝叶,直接来到需要的叶子结点;

  9. 复原 IP 地址(中等)

题型二:Flood Fill

提示:Flood 是「洪水」的意思,Flood Fill 直译是「泛洪填充」的意思,体现了洪水能够从一点开始,迅速填满当前位置附近的地势低的区域。类似的应用还有:PS 软件中的「点一下把这一片区域的颜色都替换掉」,扫雷游戏「点一下打开一大片没有雷的区域」。

下面这几个问题,思想不难,但是初学的时候代码很不容易写对,并且也很难调试。我们的建议是多写几遍,忘记了就再写一次,参考规范的编写实现(设置 visited 数组,设置方向数组,抽取私有方法),把代码写对。

  1. 图像渲染(Flood Fill,中等)
  2. 岛屿数量(中等)
  3. 被围绕的区域(中等)
  4. 单词搜索(中等)
    说明:以上问题都不建议修改输入数据,设置 visited 数组是标准的做法。可能会遇到参数很多,是不是都可以写成成员变量的问题,面试中拿不准的记得问一下面试官

题型三:字符串中的回溯问题

提示:字符串的问题的特殊之处在于,字符串的拼接生成新对象,因此在这一类问题上没有显示「回溯」的过程,但是如果使用 StringBuilder 拼接字符串就另当别论。
在这里把它们单独作为一个题型,是希望朋友们能够注意到这个非常细节的地方。

  1. 电话号码的字母组合(中等),题解;
  2. 字母大小写全排列(中等);

括号生成(中等) :这道题广度优先遍历也很好写,可以通过这个问题理解一下为什么回溯算法都是深度优先遍历,并且都用递归来写。

题型四:游戏问题

回溯算法是早期简单的人工智能,有些教程把回溯叫做暴力搜索,但回溯没有那么暴力,回溯是有方向地搜索。「力扣」上有一些简单的游戏类问题,解决它们有一定的难度,大家可以尝试一下。

  1. N 皇后(困难):其实就是全排列问题,注意设计清楚状态变量,在遍历的时候需要记住一些信息,空间换时间;
  2. 解数独(困难):思路同「N 皇后问题」;
  3. 祖玛游戏(困难)
  4. 扫雷游戏(困难)

算法思想

dfs(深度优先遍历算法)

回溯算法思想如果在递归中使用其实就是深度优先遍历。

二叉树的 DFS 有两个要素:「访问相邻结点」和「判断 base case

一:非递归写法
public void dfs(){
    Stack<TreeNode> stack = new Stack<>();
    stack.add(root);
    while(!stack.isEmpty){
        TreeNode node = stack.poll();
        system.out.println(node.val);
        if(node.right != null){
            stack.add(node.right);
        }
        if(node.left != null){
            stack.add(node.left);
        }
    }
}
二:递归写法
public void dfs(){
	if(root==null)
		return;
	system.out.println(root.val);
	dfs(root.left);
	dfs(root.right);
}

力扣题解:

题型一:排列、组合、子集相关问题

47. 全排列 II

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

1.有效结果

​ 要添加的数组长度等于原数组长度

  if(path.size() == nums.length) {
       list.add(new ArrayList(path));
       return;
   }

2.回溯范围及答案更新

//执行遍历
view[i] = true;
path.add(nums[i]);
dfs(nums);
//恢复现场
path.removeLast();
view[i] = false;

3.剪枝条件

如果该数字已经使用过或者当前数字等于前面一个(排除重复排列)

用一个数组保存用过的数字位置

i>0 && nums[i]==nums[i-1] && view[i-1]==false

全排列代码
class Solution {
    List<List<Integer>> list = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    boolean[] view;

    public List<List<Integer>> permuteUnique(int[] nums) {
        if(nums.length==0){
            return list;
        }
        Arrays.sort(nums);
        view = new boolean[nums.length];
        dfs(nums);
        return list;
    }

    public void dfs(int[] nums){
        if(path.size() == nums.length) {
            list.add(new ArrayList(path));
            return;
        }
        
        for(int i = 0 ;i < nums.length ; i++){
                if(i>0 && nums[i]==nums[i-1] && view[i-1]==false){
                    continue;
                }

            if(view[i] == false){
                view[i] = true;
                path.add(nums[i]);
                dfs(nums);
                path.removeLast();
                view[i] = false;
            }
                
        }
    }
}

题型二:Flood Fill

这类问题是在一种「网格」结构中进行的。岛屿问题是这类网格 DFS 问题的典型代表。

网格类问题 DFS 通用思路

访问相邻结点」和「判断 base case

访问相邻结点」:该方块的上下左右四个区域

判断 base case」:数组下标越界

通用代码:

void dfs(int[][] grid, int r, int c) {
    // 判断 base case
    // 如果坐标 (r, c) 超出了网格范围,直接返回
    if (!inArea(grid, r, c)) {
        return;
    }
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}
200. 岛屿数量

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。


限制条件:陆地不能重复读即1不能重复

解决方法:读取1的时候将它改为2,添加判断避免重复读取

岛屿数量代码
class Solution {
    
    public int numIslands(char[][] grid) {
        int sum = 0;
        int m = grid.length;
        int n = grid[0].length;


        for(int i = 0;i<m;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j]=='1'){
                    dfs(grid,i,j,m,n);
                    sum++;
                }
            }
        }
        return sum;
    }

    public void dfs(char[][] grid,int i,int j,int m,int n){

        if(i<0 || j<0 || i>=m || j>=n || grid[i][j]!='1'){
            return;
        }

        grid[i][j]='2';
        dfs(grid,i+1,j,m,n);
        dfs(grid,i-1,j,m,n);
        dfs(grid,i,j+1,m,n);
        dfs(grid,i,j-1,m,n);
    }
}

题型三:字符串中的回溯问题

20. 有效的括号

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

​ 1.左括号必须用相同类型的右括号闭合。
​ 2.左括号必须以正确的顺序闭合

1.有效结果(可能也有无效结果

​ 左右括号都不剩余了,递归终止

res.add(curStr);

2.回溯范围及答案更新

如果**左括号还剩余**的话,可以拼接左括号

dfs(left - 1, right, curStr + "(");

​ 如果右括号剩余多于左括号剩余的话,可以拼接右括号

`dfs(left, right - 1, curStr + ")");`

3.剪枝条件

左括号的数目一旦小于右括号的数目,以及,左括号的数目和右括号数目均小于n

有效的括号代码
class Solution {
    List<String> res = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        dfs(n, n, "");
        return res;
    }

    private void dfs(int left, int right, String curStr) {
        if (left == 0 && right == 0) { // 左右括号都不剩余了,递归终止
            res.add(curStr);
            return;
        }

        if (left > 0) { // 如果左括号还剩余的话,可以拼接左括号
            dfs(left - 1, right, curStr + "(");
        }
        if (right > left) { // 如果右括号剩余多于左括号剩余的话,可以拼接右括号
            dfs(left, right - 1, curStr + ")");
        }
    }

}

题型四:游戏问题

51. N 皇后

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

使其中任意两个皇后都不同列同行和在一条斜线

1.有效结果(可能也有无效结果

​ 递归到棋盘最底层

if(n==row){
   path = new ArrayList<>();
   for(char[] c : view){
         path.add(String.copyValueOf(c));
   }
   list.add(path);
   return;
}

2.回溯范围及答案更新

递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。

每次都是要从新的一行的起始位置开始搜,所以都是从0开始。

for(int j = 0 ;j<n;j++){
   if(isValid(view,row,j)){
       //执行遍历
      view[row][j]='Q';
      dfs(view,n,row+1);
       //恢复现场
      view[row][j]='.';
   }
}

3.剪枝条件

  1. 不能同行

  2. 不能同列

  3. 不能同斜线

     public boolean isValid(char[][] view,int row,int col)
    

五大算法----回溯算法_第2张图片

N皇后代码
class Solution {
    List<List<String>> list = new ArrayList<>();
    List<String> path;

    public List<List<String>> solveNQueens(int n) {
       char[][] view = new char[n][n];
       for(char[] c : view){
           Arrays.fill(c,'.');
       }
       dfs(view,n,0);
       return list;
    }

    public void dfs(char[][] view,int n , int row){
        if(n==row){
            path = new ArrayList<>();
            for(char[] c : view){
                path.add(String.copyValueOf(c));
            }
            list.add(path);
            return;
        }

        for(int j = 0 ;j<n;j++){
            if(isValid(view,row,j)){
                view[row][j]='Q';
                dfs(view,n,row+1);
                view[row][j]='.';
            }
        }
    }

    public boolean isValid(char[][] view,int row,int col){
        //判断同列
        for(int i=0;i<row;i++){
            if(view[i][col] == 'Q'){
                return false;
            }
        }
        
        //判断左上角
        for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--){
            if(view[i][j] == 'Q'){
                return false;
            }
        }
        //判断右上角
        for(int i=row-1,j=col+1;i>=0&&j<view.length;i--,j++){
            if(view[i][j] == 'Q'){
                return false;
            }
        }
       
        return true;
    }
}


你可能感兴趣的:(算法,数据结构)