dfs和bfs
dfs
dfs解决的问题
深度优先搜索常用于解决需要给出所有方案的问题,因为它的搜索顺序就是能够得到一个完整的搜索路径(方案)后回退再去搜索其它的方案。
剑指 Offer 38. 字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
限制:
1 <= s 的长度 <= 8
思路:题解
dfs所有排列方案,通过字符交换,先固定第一个字符,再固定第二个字符、…、 最后固定第n个字符。
对于固定位,将后面所有的字符都依次交换到固定位上,就得到了固定位所有可能的情况
即当前固定位为x,遍历i=x到length-1
swap(i,x) //c[i]固定在x
dfs(x+1) // 固定位值为c[i],看下一个固定位的值
swap(i,x) // 回溯
终止条件:
遍历到最后一位了,即得到了一个可行解,加入答案
对于去重:
需在固定某位字符时,保证 “每种字符只在此位固定一次” ,即遇到重复字符时不交换,直接跳过。从 DFS 角度看,此操作称为 “剪枝” 。
class Solution {
List<String> res = new LinkedList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return res.toArray(new String[res.size()]);
}
void dfs(int x) {
if(x == c.length - 1) {
res.add(String.valueOf(c)); // 添加排列方案
return;
}
HashSet<Character> set = new HashSet<>();
for(int i = x; i < c.length; i++) {
if(set.contains(c[i])) continue; // 重复,因此剪枝
set.add(c[i]);
swap(i, x); // 交换,将 c[i] 固定在第 x 位
dfs(x + 1); // 开启固定第 x + 1 位字符
swap(i, x); // 恢复交换
}
}
void swap(int a, int b) {
char tmp = c[a];
c[a] = c[b];
c[b] = tmp;
}
}
思路2:
由于要求所有排列的方案,可以每次从s中拿一个字符,然后记录一下这个数拿过了,再递归地搜索下一个数字,当所有数字都取完之后,就得到了一种方案,将这种方案输出,回退之后去搜下一个方案。
回退之后搜下一个方案,其实就是下一个位置放哪个字母
主要是要用一个vis来记录这个数字是否用过
class Solution {
List<String> res = new LinkedList<>();
StringBuffer path = new StringBuffer();
char[] c;
boolean[] vis;
public String[] permutation(String s) {
c = s.toCharArray();
// 保证在填每一个空位的时候重复字符只会被填入一次。具体地,我们首先对原字符串排序,保证相同的字符都相邻,在递归函数中,我们限制每次填入的字符一定是这个字符所在重复字符集合中「从左往右第一个未被填入的字符」
Arrays.sort(c);
vis = new boolean[s.length()];
dfs();
return res.toArray(new String[res.size()]);
}
void dfs() {
if(path.length()==c.length) {
res.add(path.toString()); // 添加排列方案
return;
}
for(int i = 0; i < c.length; i++) {
// 前一个和当前字符相等 且前一个字符已经被访问过 说明会重复 就直接跳过
if (vis[i] || (i > 0 && !vis[i - 1] && c[i - 1] == c[i])) {
continue;
}
vis[i] = true;
path.append(c[i]);
dfs();
vis[i] = false;
path.deleteCharAt(path.length()-1);
}
}
}
矩阵搜索问题
剑指 Offer 12. 矩阵中的路径/79. 单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
例如,在下面的 3×4 的矩阵中包含单词 “ABCCED”(单词中的字母已标出)。
示例 1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
示例 2:
输入:board = [[“a”,“b”],[“c”,“d”]], word = “abcd”
输出:false
提示:
1 <= board.length <= 200
1 <= board[i].length <= 200
board 和 word 仅由大小写英文字母组成
思路:
遍历矩阵的每一个位置,以这个位置为起点向下dfs搜索只要找到一条路径就返回true
dfs:
-
终止条件
- 返回false:越界、当前字符与矩阵字符不同、当前矩阵以访问过(我们把访问过的字符置修改为空字符串,这样肯定和当前字符不同 所以这里可以不判断)
- 返回true:字符串word已经全部匹配
-
递推工作
- 标记当前矩阵元素: 将 board[i][j] 修改为 空字符 ‘’ ,代表此元素已访问过,防止之后搜索时重复访问。
- 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res 。
- 还原当前矩阵元素: 将 board[i][j] 元素还原至初始值,即 word[k] 。
-
返回值:返回布尔量 res ,代表是否搜索到目标字符串。
class Solution {
public boolean exist(char[][] board, String word) {
for(int i = 0; i < board.length; i++) {
for(int j = 0; j < board[0].length; j++) {
if(dfs(board, word, i, j, 0)) {
return true;
}
}
}
return false;
}
public boolean dfs(char[][] board, String word,int i, int j, int k){
// 递归终止条件
// 返回false
if(i>=board.length || i<0 || j >=board[0].length || j<0 ||board[i][j]!=word.charAt(k)){
return false;
}
// 返回true
if(k==word.length()-1){
return true;
}
// 将board[i][j] 修改为空字符串,表示已经被访问过
board[i][j] = '\0';
// 搜索下一个单元格: 上下左右方向
boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||
dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
// 还原当前矩阵
board[i][j] = word.charAt(k);
return res;
}
}
剑指 Offer 13. 机器人的运动范围
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 2:
输入:m = 3, n = 1, k = 0
输出:1
提示:
1 <= n,m <= 100
0 <= k <= 20
找到递归终止条件:行越界或列越界或已经访问过
这一次递归要做的事:对每一位求和,如果满足条件就ret++,继续向右向下;不满足条件相当于这一格不走,就不做任何操作
int ret=0;
public int movingCount(int m, int n, int k) {
boolean [][]vis = new boolean[m][n];
dfs(0,0,m,n,k,vis);
return ret;
}
public void dfs(int i,int j, int m, int n, int k,boolean [][]vis){
// 递归终止条件
if(i<0 || i>=m || j<0 || j>=n || vis[i][j]==true){
return ;
}
// 表示这一格已经被访问过
vis[i][j] = true;
// 对i和j的每一位求和
int sum = sumBit(i) + sumBit(j);
if(sum<=k){
ret++;
dfs(i+1,j,m,n,k,vis);
dfs(i,j+1,m,n,k,vis);
}
}
public int sumBit(int num){
int sum = 0;
while(num>0){
sum += num%10;
num = num/10;
}
//System.out.println(sum);
return sum;
}
51. N 皇后
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
输入:n = 4
输出:[[".Q…","…Q",“Q…”,"…Q."],["…Q.",“Q…”,"…Q",".Q…"]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[[“Q”]]
提示:
1 <= n <= 9
皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。
可以dfs 每行,然后确定Q在这一行的位置,用col存这一列是否被放置过,dg和udg存对角线和反对角线有没有被放置过
如何知道一个位置是在哪个正反对角线上的?可以发现正反对角线上的元素(x,y)的特征,分别是x+y等于一个固定值,以及x−y等于一个固定值,所以我们可以把dg和udg开成2n的大小来存放
class Solution {
// n皇后
List<List<String>> res1;
char[][] arr;
// 这一列是否被放置过
boolean[] col;
// 正对角线有没有被放置过
boolean[] dg;
// 反对角线有没有被放置过
boolean[] udg;
public List<List<String>> solveNQueens(int n) {
res1 = new ArrayList<>();
arr = new char[n][n];
col = new boolean[n];
dg = new boolean[2*n];
udg = new boolean[2*n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
arr[i][j] = '.';
}
}
// dfs 每一行 确定Q在哪一列
dfs(0,n);
return res1;
}
public void dfs(int u, int n){
// 终止条件:最后一行已经遍历完
if(u==n){
List<String> ans = new ArrayList<>();
for (int i = 0; i < n; i++) {
ans.add(String.valueOf(arr[i]));
}
res1.add(ans);
return ;
}
// 遍历该放到哪一列
for (int j = 0; j < n; j++) {
// 如何知道一个位置是在哪个正反对角线上的?可以发现正反对角线上的元素(x,y)的特征,分别是x+y等于一个固定值,以及x−y等于一个固定值
if(col[j] || dg[u+j] || udg[u-j+n]){
continue;
}
//可以放置
col[j] = true;
dg[u+j] = true;
udg[u-j+n] = true;
arr[u][j] = 'Q';
dfs(u+1,n);
// 回溯
col[j] = false;
dg[u+j] = false;
udg[u-j+n] = false;
arr[u][j] = '.';
}
}
}
bfs
广度优先搜索每次扩展当前结点的所有结点,所以需要一个栈/队列来维护要扩展的结点。
由于广度优先搜索每次都是把所有能到的下一步搜完,所以能够得到最短path的解,所以一些不带权求最短路径的问题也可以直接用BFS解决。
一般要用到的:
队列:要思考队列怎么定义
距离:如果记录每一个状态的距离
AcWing 844. 走迷宫
【题目描述】
给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。
最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。
数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路。
【输入格式】
第一行包含两个整数n和m。
接下来n行,每行包含m个整数(0或1),表示完整的二维数组迷宫。
【输出格式】
输出一个整数,表示从左上角移动至右下角的最少移动次数。
【数据范围】
1≤n,m≤100
【样例】
输入样例:
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出样例:
8
思路:
这个就是一个不带边权的最短路径问题,因此直接用bfs搜。搜的时候注意已经访问过的和是1的不加入。
由于要求的是最短距离,所以我们应该用二维数组dis[i][j]来存左上角到当前(i,j)的最短距离,
注意这里用dist中某个点值为-1表示这个点没有访问过,注意边界:到(0,0)位置自己的距离是0。
对于队列中的每个值,都让他走上下左右四个方向,走的通的就对当前dis加值
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
/**
* @author flora.zxf
* @date 2021/7/5
*/
public class AC844 {
static int map[][] = null;
static int n = 0;
static int m = 0;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
map = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
map[i][j] = scanner.nextInt();
}
}
fbs(n,m);
}
public static void fbs(int n, int m){
// 存左上角到当前(i,j)的最短距离
int[][] dis = new int[n][m];
Queue<Node> queue = new LinkedList<>();
// 上下左右四个方向
int fx[][] = new int[][]{{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
queue.offer(new Node(0,0));
while(!queue.isEmpty()){
Node node = queue.poll();
// 走到终点 返回
if(node.x==n-1 && node.y==m-1){
break;
}
// 判断上下左右四个方向
for (int i = 0; i < 4; i++) {
int x = node.x + fx[i][0];
int y = node.y + fx[i][1];
if(x>=0 && y>=0 && x<n && y<m && map[x][y]==0 && dis[x][y]==0){
queue.offer(new Node(x,y));
dis[x][y] = dis[node.x][node.y]+1;
}
}
}
System.out.println(dis[n - 1][m - 1]);
}
}
class Node {
int x;
int y;
public Node(int x, int y) {
this.x = x;
this.y = y;
}
}
Acwing845 八数码
在一个 3×3 的网格中,1∼8 这 8 个数字和一个 x 恰好不重不漏地分布在这 3×3 的网格中。
例如:
1 2 3
x 4 6
7 5 8
在游戏过程中,可以把 x 与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 x
例如,示例中图形就可以通过让 x 先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
1 2 3 1 2 3 1 2 3 1 2 3
x 4 6 4 x 6 4 5 6 4 5 6
7 5 8 7 5 8 7 x 8 7 8 x
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
输入格式:
输入占一行,将 3×3 的初始网格描绘出来。
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个整数,表示最少交换次数。
如果不存在解决方案,则输出 −1。
输入样例:
2 3 4 1 5 x 7 6 8
输出样例
19
将题目转换: 网格的所有状态看成图的一个节点,如果一个状态可以通过一步变到另一个状态那么这两个节点是联通的
队列:队列中放状态,我们将状态用String表示即
Queue<String> queue
距离:状态和状态之间的距离, 用map来记录
Map<String,Integer> dist
然后就是看每个状态的x的位置,得到其x和y下标,然后上下左右去移。
最后得到是目标状态即12345678x的话就得到了结果
import java.util.*;
/**
* @author flora.zxf
* @date 2021/7/12
*/
public class AC845 {
public static void swap(char c[], int k, int t){
char g = c[k];
c[k] = c[t];
c[t] = g;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String state = "";
// 以字符串的形式存储
for (int i = 0; i < 9; i++) {
state+=scanner.next();
}
System.out.println(bfs(state));
}
public static int bfs(String start){
Queue<String> queue = new LinkedList<>();
Map<String,Integer> dis = new HashMap<>();
// 目标状态
String endState = "12345678x";
// 上下左右四个方向
int fx[][] = new int[][]{{1,0},{0,1},{-1,0},{0,-1}};
queue.offer(start);
dis.put(start,0);
while(!queue.isEmpty()){
String state = queue.poll();
int distance = dis.get(state);
// 走到终点 返回
if(endState.equals(state)){
return distance;
}
// 得到x的位置
int k = state.indexOf('x');
// x和y的坐标
int x = k / 3, y = k % 3;
// 判断上下左右四个方向
for (int i = 0; i < 4; i++) {
int a = x+fx[i][0];
int b = y+fx[i][1];
if(a>=0 && a<3 && b>=0 && b<3){
char[] chars = state.toCharArray();
swap(chars,k,a*3+b);
String s = new String(chars);
// map不包含 则加入
if(!dis.containsKey(s)){
queue.offer(s);
dis.put(s,distance+1);
}
}
}
}
System.out.println(dis.get("123456789x"));
return -1;
}
}