算法小结之DFS篇(回溯题目)

算法小结之DFS篇(回溯题目)

参考力扣大神鲂姐的帖子和自己的理解进行了整合

鲂姐原贴:https://leetcode-cn.com/circle/discuss/VFVW01/

回溯类题目的关键点在于:

1、终止条件及相关处理操作

2、对称结构(即先存入后取出)

3、需要另外开辟一个数组来记录“节点是否被访问”的情况

回溯类题目大致分类有:全排列类,组合类,子集类,单词搜索类。

这些题目中,对称结构一般都不会太大改变,区别大多数在于题目中使用到的存储集合类型要适合题意,且终止条件和相关处理要细心,即边界情况要足够理解。

一、全排列类:

最经典的回溯问题,应该是全排列了

力扣46,全排列:https://leetcode-cn.com/problems/permutations/

题目:给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

代码:

class Solution {

public List permute(int[] nums) {

​ int len = nums.length;

​ if(len==0)

​ return null;

​ List list = new ArrayList<>();

​ boolean[] reached = new boolean[len];//reached数组记录当前是否已经探访过

​ List path = new ArrayList<>();

​ dfs(len,nums,reached,0,path,list);

​ return list;

}

public void dfs(int len,int[] nums,boolean[] reached,int depth,List path,List list){

​ if(depth == len){

​ list.add(new ArrayList(path));

​ return;

​ }

​ for(int i=0;i

​ if(reached[i]==false){

​ path.add(nums[i]);

​ reached[i] = true;

​ dfs(len,nums,reached,depth+1,path,list);

​ reached[i] = false;

​ path.remove(path.size()-1);

​ }

​ }

}

}

可以看到,之前提及的回溯题目中的几个要点,其对应:

1、运用 List集合来存储数据

2、对称结构,即遵循“放入子情况-标记其已遍历-继续DFS直至边界情况-抹除其已遍历标记-从子情况中拿出”

3、对于边界条件的理解:本题的题意中,已经指明了这些数字不会有重复情况出现,所以只需要判断是否已经达到边界条件即可,即depth==len。

回到题目本身,考虑题目的变式,该题目中没有提及“原始数据含重复数字”的情况,也没有提到如果子集没有长度限制也可以满足需求的情况(例如原始数据1,2,3。返回[1,2],[1],[]也算满足子集要求,不一定要子集长度为3)。那么随着题目的改动,相应的存储结构和边界条件判断,也会发生改变。

看下一个例子:

力扣47,全排列II:https://leetcode-cn.com/problems/permutations-ii/

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

示例:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

和上一题不同,这里的题目要求返回不重复全排列,但原始数据可包含重复数字。

先看题解:

class Solution {

public List permuteUnique(int[] nums) {

​ List res = new ArrayList<>();

​ List path = new ArrayList<>();

​ if(nums.length==0)

​ return res;

​ int len = nums.length;

​ Arrays.sort(nums);

​ boolean[] isVisited = new boolean[len];

​ dfs(len,nums,0,res,path,isVisited);

​ return res;

}

public void dfs(int len,int[] nums,int depth,List res,List path,boolean[] isVisited){

​ System.out.println("=============================");

​ System.out.println(“这是第 “+depth+” 层的dfs情况:”);

​ if(depth==len){

​ System.out.println(“到达底层,path加入res”);

​ res.add(new ArrayList(path));

​ return;

​ }

​ for(int i=0;i

​ if(isVisited[i]==true){

​ System.out.println(“isVisited[i]==true” + " 下标:" + i + " 数值:" + nums[i] );

​ continue;

​ }

​ if(i>0&&nums[i]==nums[i-1]&&isVisited[i-1]==true){

​ System.out.println(“nums[i]==nums[i-1]” + " i下标:" + i + " i-1数值:" + nums[i] );

​ continue;

​ }

​ path.add(nums[i]);

​ isVisited[i] = true;

​ dfs(len,nums,depth+1,res,path,isVisited);

​ isVisited[i] = false;

​ path.remove(path.size()-1);

​ }

}

}

这里我加上了sout各层情况的语句,各位有兴趣的话可以自己尝试一下,加深理解。

可以看到,这里有一些细节处理,首先是对于原始数据进行了排序,以便之后判断数据重复情况时更为方便而不需要扫描。而存储结构并没有改变,仍然是 List

回到dfs方法里,我们可以看到,其边界条件也没有改变,仍然是达到长度要求即可。对称结构也没有改变。

但是增加了一个if-continue语句,从而达到了“存在前后相同数值,则剪枝”的效果。

if中的语句:(i>0&&nums[i]==nums[i-1]&&isVisited[i-1]==true)

首先,i>0是为了后面的i-1不越界,nums[i]==nums[i-1]则是判断是否前后数值一致。

而isVisited[i-1]==true这条语句则是关键,这判断了当前节点的前一个节点是否已经被扫描过并纳入子集。这样就完成了剪枝。

PS:其实这里isVisited[i-1]==false也可以运行成功,并且复杂度效果更好,可以思考一下为什么。

另外的题目有:

无重复字符串的排列组合

有重复字符串的排列组合

这两道题的思路与上面的两道题一致,只不过其数据类型改变了,可以考虑用更灵活的集合来优化。

二、组合类题目

电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

img

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

先放源码:

class Solution {

public List letterCombinations(String digits) {

​ List combinations = new ArrayList();

​ if(digits.length()==0)

​ return combinations;

​ Map phoneMap = new HashMap<>();

​ phoneMap.put(‘2’,“abc”);

​ phoneMap.put(‘3’,“def”);

​ phoneMap.put(‘4’,“ghi”);

​ phoneMap.put(‘5’,“jkl”);

​ phoneMap.put(‘6’,“mno”);

​ phoneMap.put(‘7’,“pqrs”);

​ phoneMap.put(‘8’,“tuv”);

​ phoneMap.put(‘9’,“wxyz”);

​ backtrack(combinations,phoneMap,digits,0,new StringBuffer());

​ return combinations;

}

public void backtrack(List combinations,Map phoneMap,String digits,int index,StringBuffer combination){

​ if(index==digits.length()){

​ combinations.add(combination.toString());

​ }else{

​ char digit = digits.charAt(index);

​ String letters = phoneMap.get(digit);

​ int lettersCount = letters.length();

​ for(int i=0;i

​ combination.append(letters.charAt(i));

​ backtrack(combinations,phoneMap,digits,index+1,combination);

​ combination.deleteCharAt(index);

​ }

​ }

}

}

可以看到,其实与之前的题目思路类似,但是需要对集合理解更为熟练。

三、子集类

还记得在全排列题目时,提到“子集没有长度限制也可以满足需求的情况(例如原始数据1,2,3。返回[1,2],[1],[]也算满足子集要求,不一定要子集长度为3)”的这种情况么?

子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

这里改变的就是边界条件了,我们习惯性将边界条件设置为子情况满足长度,那么这时只需要取消这个边界条件即可。即每次dfs中都先进行add操作即可。

子集 II

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]

可以拿全排列II+子集I的思路相结合,很轻松能完成这道题的思路构建。

四、单词搜索类

单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

board =
[
[‘A’,‘B’,‘C’,‘E’],
[‘S’,‘F’,‘C’,‘S’],
[‘A’,‘D’,‘E’,‘E’]
]

给定 word = “ABCCED”, 返回 true
给定 word = “SEE”, 返回 true
给定 word = “ABCB”, 返回 false

单词搜索,可谓是相当经典的回溯题目了

放上解题里很漂亮的一种解法:

public class Solution {

private static final int[][] DIRECTIONS = {
    {-1, 0}, {0, -1}, {0, 1}, {1, 0}};
private int rows;
private int cols;
private int len;
private boolean[][] visited;
private char[] charArray;
private char[][] board;

public boolean exist(char[][] board, String word) {
    rows = board.length;
    if (rows == 0) {
        return false;
    }
    cols = board[0].length;
    visited = new boolean[rows][cols];

    this.len = word.length();
    this.charArray = word.toCharArray();
    this.board = board;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (dfs(i, j, 0)) {
                return true;
            }
        }
    }
    return false;
}

private boolean dfs(int x, int y, int begin) {
    if (begin == len - 1) {
        return board[x][y] == charArray[begin];
    }
    if (board[x][y] == charArray[begin]) {
        visited[x][y] = true;
        for (int[] direction : DIRECTIONS) {
            int newX = x + direction[0];
            int newY = y + direction[1];
            if (inArea(newX, newY) && !visited[newX][newY]) {
                if (dfs(newX, newY, begin + 1)) {
                    return true;
                }
            }
        }
        visited[x][y] = false;
    }
    return false;
}

private boolean inArea(int x, int y) {
    return x >= 0 && x < rows && y >= 0 && y < cols;
}

}

除此外类似的还有路径搜索,和单词搜索II。有兴趣的朋友可以尝试下。

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