回溯算法——DFS
- DFS介绍(Depth First Search)
- DFS经典题目
- 1. 员工的重要性
- 2. 图像渲染
- 3.被围绕的区域
- 4.岛屿数量
- 5. 电话号码的字母组合
- 6.数字组合
- 7. 活字印刷
- 8. N皇后
DFS介绍(Depth First Search)
- 回溯法(back tracking)(探索与回溯法)回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
- 回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。也可以称为剪枝点,所谓的剪枝,指的是把不会找到目标,或者不必要的路径裁剪掉。
- DFS算法就是深度优先算法,在数据结构的学习中,二叉树的前序遍历就是属于深度优先算法。
深度优先搜索其实就是一条路走到黑。我们来看一道很经典的DFS题目让我们来了解深度优先搜索。
✨问题描述:
假如有编号为1~ 3的3张扑克牌和编号为1~3的3个盒子,现在需要将3张牌分别放到3个盒子中去,且每个盒子只能放
一张牌,一共有多少种不同的放法
✨ 思路分析:
上面这张图画出了回溯过程的前半部分,接下来按着深度优先搜索的方式接着进行回溯,我们可以得到剩下的三种情况:
我们进行上述的深度优先搜索的时候,我们在一个盒子中放扑克牌是从1 – 3号扑克牌依次进行放入的,这样我们可以用for循环搞定。但是我们如何确定该位置要放的扑克牌,是否在前面已经被放过了呢?我们可以使用一个标记数组 int[] book 来记录当前扑克牌在前面是否被放入了。
✨ 代码实现:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();//盒子和牌的个数
int[] book = new int[n + 1];
int[] box = new int[n + 1];
Dfs(1,book,box,n);
}
private static void Dfs(int idx, int[] book, int[] box, int n) {
if (idx == n + 1){//此时已经完成了一次深度优先搜索
for (int i = 1; i <= n; i++) {
System.out.print(box[i] + " ");
}
System.out.println();
return;
}
for (int i = 1; i <= n; i++) {//深度优先搜索,去放置牌
if(book[i] == 0){
box[idx] = i;//idx个盒子放第i个牌
book[i] = 1;//代表第i个牌已经使用了
Dfs(idx + 1, book, box, n);//处理下一个盒子
book[i] = 0;//第i张牌重新拿到手里
}
}
}
DFS经典题目
1. 员工的重要性
原题链接
✨问题描述:
给定一个保存员工信息的数据结构,它包含了员工 唯一的 id ,重要度 和 直系下属的 id 。
比如,员工 1 是员工 2 的领导,员工 2 是员工 3 的领导。他们相应的重要度为 15 , 10 , 5 。那么员工 1 的数据结构是 [1, 15, [2]] ,员工 2的 数据结构是 [2, 10, [3]] ,员工 3 的数据结构是 [3, 5, []] 。注意虽然员工 3 也是员工 1 的一个下属,但是由于 并不是直系 下属,因此没有体现在员工 1 的数据结构中。
现在输入一个公司的所有员工信息,以及单个员工 id ,返回这个员工和他所有下属的重要度之和。
✨输入案例:
输入:[[1, 5, [2, 3]], [2, 3, []], [3, 3, []]], 1
输出:11
解释:
员工 1 自身的重要度是 5 ,他有两个直系下属 2 和 3 ,而且 2 和 3 的重要度均为 3 。因此员工 1 的总重要度是 5 + 3 + 3 = 11 。
✨题目分析
该问题就类似于二叉树的前序遍历。从单个员工的重要度开始计算,依次遍历员工的下属,下属有员工再次搜索下属。直到没有下属员工再依次回退查看有没有其他的子员工。
✨ 解题代码:
public int getImportance(List<Employee> employees, int id) {
Map<Integer,Employee> empMap = new HashMap<>();//用哈希表存储数据查找会更方便更快速。
for (Employee employee: employees) {
empMap.put(employee.id,employee);
}
return DFS(empMap,id);
}
private int DFS(Map<Integer, Employee> empMap, int id) {
int sum = empMap.get(id).importance;//加上该员工的重要度
for (int curId:empMap.get(id).subordinates) {//该员工有下属就进行for-each循环DFS,没有的话就不进去循环
sum += DFS(empMap,curId);
}
return sum;
}
2. 图像渲染
原题链接
✨问题描述:
有一幅以 m x n 的二维整数数组表示的图画 image ,其中 image[i][j] 表示该图画的像素值大小。
你也被给予三个整数 sr , sc 和 newColor 。你应该从像素 image[sr][sc] 开始对图像进行 上色填充 。
为了完成 上色工作 ,从初始像素开始,记录初始坐标的 上下左右四个方向上 像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应 四个方向上 像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为 newColor 。
最后返回 经过上色渲染后的图像 。
✨题目样例:
案例1:
输入: image = [[1,1,1],[1,1,0],[1,0,1]],sr = 1, sc = 1, newColor = 2
输出: [[2,2,2],[2,2,0],[2,0,1]]
解析: 在图像的正中间,(坐标(sr,sc)=(1,1)),在路径上所有符合条件的像素点的颜色都被更改成2。
注意,右下角的像素没有更改为2,因为它不是在上下左右四个方向上与初始点相连的像素点。
案例2:
输入: image = [[0,0,0],[0,0,0]], sr = 0, sc = 0, newColor = 2
输出: [[2,2,2],[2,2,2]]
✨ 题目分析:
这个题目用DFS算法来解决。把初始四围与其颜色相同的位置进行渲染,渲染了一个位置,就找其周围与其颜色相同的位置进行渲染。同时还需要判断四周的位置是否越界。如果渲染颜色和初始位置的颜色不同的话,我们渲染时候,仅仅需要判断是否为原始位置的颜色相同,渲染后的位置和未被渲染的位置能够清楚的区分,但是如果初始位置的颜色和渲染染色相同就会区分不了该位置是渲染过的,还是未被渲染的和初始位置原色相同的。为了解决这个问题,我们就引入一个标记数组 int[][] book 来标记该位置是否被渲染过了。
✨ 解题代码
int[][] nextP= {{1,0},{-1,0},{0,-1},{0,1}};//偏移数组,通过原位置找到相邻的四个位置
public int[][] floodFill(int[][] image, int sr, int sc, int color) {
int oldColor = image[sr][sc];//记录要修改坐标的旧颜色
int row = image.length;
int col = image[0].length;
int[][] book = new int[row][col];//创建一个标记数组
DFS(image,book,sr,sc,oldColor,color,row,col);//深度优先搜索寻找是否有相同颜色的位置
return image;
}
private void DFS(int[][] image, int[][] book, int sr, int sc, int oldColor, int color,int row,int col) {
image[sr][sc] = color;//修改当前位置的颜色
book[sr][sc] = 1;//标记当前位置已经被修改过了
for (int i = 0; i < 4; i++) {//搜索上下左右四个位置的颜色是否符合原颜色
int newX = sr + nextP[i][0];
int newY = sc + nextP[i][1];
if (newX < 0 || newX >= row
|| newY < 0 || newY >= col) {//判断新坐标是否越界
continue;
}
//判断当前位置是否需要进行图像渲染
if (image[newX][newY] == oldColor && book[newX][newY] == 0){
//新的位置的处理
DFS(image,book,newX,newY,oldColor,color,row,col);
}
}
}
3.被围绕的区域
原题链接
✨问题描述:
给你一个 m x n 的矩阵 board ,由若干字符 ‘X’ 和 ‘O’ ,找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
✨题目案例:
案例1: 输入:board =
[[“X”,“X”,“X”,“X”],[“X”,“O”,“O”,“X”],[“X”,“X”,“O”,“X”],[“X”,“O”,“X”,“X”]]
输出:[[“X”,“X”,“X”,“X”],[“X”,“X”,“X”,“X”],[“X”,“X”,“X”,“X”],[“X”,“O”,“X”,“X”]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的
‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
案例2:
输入:board = [[“X”]]
输出:[[“X”]]
✨ 题目分析:
把整个区域扩大一下,变成上图这种。上图中红圈里的就是被包围的区域,被包围的区域比较不容易找到,但是未被包围的区域,就比较容易的找到了。我们可以用深度优先搜索的方式进行找到没有被包围的区域,间接找到被包围的区域。
✨ 解题代码
int[][] nextP = {{1,0},{-1,0},{0,1},{0,-1}};//方向数组,下一步从哪个方向搜索 下 上 右 左
public void solve(char[][] board) {
int row = board.length;
int col = board[0].length;
//用逆向思维的方式求未被包围的区域,间接求出被包围的区域
for (int i = 0; i < row; i++) {//寻找左列边界的区域中‘O’的位置
if (board[i][0] == 'O'){
DFS(board,i,0,row,col);
}
if (board[i][col - 1] == 'O'){//寻找右列边界的区域中‘O’的位置
DFS(board,i,col - 1,row,col);
}
}
for (int j = 0; j < col; j++) {
if (board[0][j] == 'O'){//判断上边界是否有‘O’
DFS(board,0,j,row,col);
}
if (board[row - 1][j] == 'O'){//判断下边界是否有‘O’
DFS(board,row - 1,j,row,col);
}
}
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (board[i][j] == 'O'){
board[i][j] = 'X';
}
if (board[i][j] == '*'){
board[i][j] = 'O';
}
}
}
}
private void DFS(char[][] board, int x, int y, int row, int col) {
board[x][y] = '*';//代表该‘O’已经被搜索过了
for (int i = 0; i < 4; i++) {
int newX = x + nextP[i][0];
int newY = y + nextP[i][1];
if (newX < 0 || newX >= row
||newY < 0 || newY >= col){//判断探索位置是否越界
continue;
}
if (board[newX][newY] == 'O'){//判断该位置是否需要进行探索
DFS(board,newX,newY,row,col);
}
}
}
4.岛屿数量
原题链接
✨ 题目描述:
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
✨ 题目案例:
案例1:
输入:grid = [
[“1”,“1”,“1”,“1”,“0”],
[“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“0”,“0”,“0”]
]
输出:1
案例2:
输入:grid = [
[“1”,“1”,“0”,“0”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“1”,“0”,“0”],
[“0”,“0”,“0”,“1”,“1”]
]
输出:3
✨ 题目分析:
这个问题和第二题的思路有很多类似的地方。都是进行一个深度优先搜索,但是第二题给出了搜索的起始位置。这个题目搜索的起始位置我们可以对整个数组进行遍历。我们可以对搜索过的陆地位置进行标记,因为此问题我们仅仅只有‘0’和‘1’两种情况,我们可以对grid数组本身进行进行字符的改变来表示他是一个已经被搜索过的陆地了,这个字符我们可以换成一个不同于‘0’和‘1’的,比如‘*’;
✨ 解题代码:
int[][] nextP = {{1,0},{-1,0},{0,1},{0,-1}};//方向数组,下一步从哪个方向搜索 下 上 右 左
public int numIslands(char[][] grid) {
int row = grid.length;
int col = grid[0].length;
int count = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (grid[i][j] == '1'){
DFS(grid,i,j,row,col);
count++;
}
}
}
return count;
}
private void DFS(char[][] grid,int posX, int poxY ,int row, int col) {
grid[posX][poxY] = '*';//对已经搜索过的陆地进行标记,以防重复搜索造成死递归。
for (int i = 0; i < 4; i++) {
int newX = posX + nextP[i][0];
int newY = poxY + nextP[i][1];
if (newX < 0 || newX >= row
|| newY < 0 || newY >= col) {//判断是否是越界了
continue;
}
if (grid[newX][newY] == '1'){//是未被搜索的陆地,就进行下一步搜索
DFS(grid, newX, newY, row, col);
}
}
}
5. 电话号码的字母组合
原题链接
✨ 题目描述:
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
✨ 题目案例:
案例1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
案例2:
输入:digits = “”
输出:[]
案例3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
✨ 题目分析:
结合案例可以看出,这个问题得到的字母组合和digits字符串的长度相同。当digits长度为0的时候,得到的结果是没有任何元素的一个集合。我们可以建立一个联系,用电话按键数字对应字符串在深度优先搜索的时候用于查找我们数字对应的字符串,我们一听到建立关系用于查找我们第一反应是建立hashMap,但是在java中的hashMap一下定义那么多关系,书写时比较麻烦的,我们可以用字符串数组来代替hashMap来实现建立这个关系,用数组的下标(数字)来对应数组内容(字符串)。
我们这个问题在运用深度优先搜索对于前几个题不同的是,不是每个位置放置的内容可以相互交换,而是每个位置的数字对应的字符串依次放在对应位置。
✨ 题目代码:
public List<String> letterCombinations(String digits) {
List<String> list = new ArrayList<>();
String curStr = "";
int len = digits.length();//所需获得字符串的长度
DFS(digits,list,curStr,0,len);
return list;
}
String[] strings = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
private void DFS(String digits, List<String> list, String curStr, int digitIdx,int len) {
if (len == digitIdx){
if (len != 0){
list.add(curStr);
}
return;
}
//进行深度优先搜索,
int num = Integer.parseInt(digits.charAt(digitIdx) + "");//获取当钱位置的数字字符
for (char ch: strings[num].toCharArray()) {
DFS(digits,list,curStr + ch,digitIdx + 1,len);
}
}
6.数字组合
原题链接
✨ 题目描述:
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
✨ 题目案例:
案例1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
案例2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
案例3:
输入: candidates = [2], target = 1
输出: []
✨ 题目解析:
我们这个题目也是用DFS进行解决。我们对一个数字进行递归,直到这个组合的数字和大于等于目标数字的时候我们进行终止继续向下搜索,我们需要进行回溯到上一个步骤,试试其他的数字怎么样。
下面就是案例1的DFS过程:
这个问题为了避免出现一个组合重复出现的情况,我们可以在DSF的时候,循环遍历从该数字对应字符串的位置往后进行搜索。
✨ 解题代码:
DFS求解,需要注意的是:allRet.add(new ArrayList<>(curRet)); 其中两个对象的类型都是List,所以我们得转化为 ArrayList<>()这种具体类
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> allRet = new ArrayList<>(new ArrayList<>());
List<Integer> curRet = new ArrayList<>();
int curSum = 0;//搜索当前情况下的数字和
int prev = 0;//当前你所需要的DFS时加和的下标
DFS(candidates,target,allRet,curRet,curSum,prev);
return allRet;
}
private void DFS(int[] candidates, int target, List<List<Integer>> allRet, List<Integer> curRet, int curSum, int prev) {
if (curSum >= target){
if (curSum == target){
//保存当前的解集
allRet.add(new ArrayList<>(curRet));
}
return;
}
//累加的起始位置为上一项的位置
for (int i = prev; i < candidates.length; i++) {
//累加当前项
curRet.add(candidates[i]);
DFS(candidates,target,allRet,curRet,curSum + candidates[i],i);
//回溯
curRet.remove(curRet.size() - 1);
}
}
7. 活字印刷
原题链接
✨ 题目描述:
你有一套活字字模 tiles,其中每个字模上都刻有一个字母 tiles[i]。返回你可以印出的非空字母序列的数目。
注意:本题中,每个活字字模只能使用一次。
✨ 题目案例:
案例1:
输入:“AAB”
输出:8
解释:可能的序列为 “A”, “B”, “AA”, “AB”, “BA”, “AAB”, “ABA”, “BAA”。
案例2:
输入:“AAABBC”
输出:188
案例3:
输入:“V”
输出:1
提示:
- 1 <= tiles.length <= 7
- tiles 由大写英文字母组成
✨ 题目分析:
该题目一样是用DFS来进行解题。但是改题目不一样的地方是每个活字字模只能使用一次,也就是每个位置的字符只能用一次,我们为了避免出现一个位置的字符进行多次出现,我们可以创建一个标记数组来标记是否该位置的字符已经在搜索过程中被使用到了。
✨ 解题代码:
public int numTilePossibilities(String tiles) {
Set<String> set = new HashSet<>();
String curStr = "";
int[] book = new int[tiles.length()];//标记字符串中的字符是否已经搜索过了。
DFS(tiles,set,curStr,book);
return set.size();
}
private void DFS(String tiles, Set<String> set, String curStr, int[] book) {
if (curStr.length() != 0){
set.add(curStr);
}
if (curStr.length() == tiles.length()){
return;
}
for (int i = 0; i < tiles.length(); i++) {
if (book[i] == 0){
book[i] = 1;
//进行深度优先搜索
DFS(tiles,set,curStr + tiles.charAt(i),book);
//进行回溯
book[i] = 0;
}
}
}
8. N皇后
✨ 题目描述:
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
✨ 题目案例:
案例1:
输入:n = 4输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
案例2:
输入:n = 1
输出:[[“Q”]]
✨ 题目分析:
我们就以案例1,来进行DFS分析如下图:
改图中展示 了找到其中一种n皇后的摆法,其他摆法也是按照这种思路进行DFS和回溯。
我们现在最主要的是如何进行判断该位置是否是违规的位置。我们从横纵方向+斜主对角线的坐标,进行了以下分析:
横:x坐标相同
纵:y坐标相同
正对角线:newX - newY = x - y(坐标差相同)
斜对角线:newX + newY == x + y(坐标和相同)
我们就可以针对已经摆放好的皇后来进行判断该位置是否违法,不违法就可以摆放新的皇后,摆放皇后的数量等于n就是一种摆放方式。
✨ 解题代码:
class pair{
public int x;
public int y;
public pair(int x, int y) {
this.x = x;
this.y = y;
}
}
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<pair>> allRet = new ArrayList<>();
List<pair> curRet = new ArrayList<>();
DFS(allRet,curRet,0,n);
//System.out.println(allRet.toString());
return transResult(allRet,n);
}
void DFS(List<List<pair>> allRet,List<pair> curRet,int curRow,int n){
//如果每一行都没有冲突,则是一种可行方案
if (curRow == n){
allRet.add(new ArrayList<>(curRet));
return;
}
//确定当前行的每一个位置是否和已经确定的位置有冲突
for (int i = 0; i < n; i++) {
if(isValidPos(curRet,curRow,i)){
curRet.add(new pair(curRow,i));//把当前位置存放到当前情况中
//处理下一行
DFS(allRet,curRet,curRow + 1,n);
//回溯
curRet.remove(curRet.size() - 1);//尾删
}
}
}
private boolean isValidPos(List<pair> curRet, int row, int col) {
for (pair pos: curRet) {
if (pos.y == col || pos.x + pos.y == row + col
|| pos.x - pos.y == row - col){
return false;
}
}
return true;
}
private List<List<String>> transResult(List<List<pair>> allRet, int n) {
List<List<String>> allMet = new ArrayList<>();
for (List<pair> curRet : allRet) {
List<String> curMat = new ArrayList<>();
for (pair pos: curRet) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < n; i++) {
str.append('.');
}
str.setCharAt(pos.y,'Q');
curMat.add(str.toString());
}
allMet.add(curMat);
}
return allMet;
}
}