搜索算法(汇总)
搜索算法就是去搜索每一个解的可能。任何算法的核心都是穷举,回溯算法就是一个暴力穷举算法。
搜索算法的分类:
- 枚举法:暴力搜索
- 深度优先搜索
- 广度优先搜索
- 回溯
目录
Ⅰ 深度优先搜索
Dfs(当前这一步的处理逻辑)
{
1.判断边界,是否已经到头了:向上回退
2.尝试当下的每一种可能
3.确定一种可能之后,继续下一步Dfs
}
(1)放牌
假如有编号为1~3
的3张扑克牌和编号为1~3
的3个盒子,现在需要将3张牌分别放到3个盒子中去,且每个盒子只能放一张牌,一共有多少种不同的放法。
box作为箱子,book==1
表示牌已经被使用。
public static void Dfs(int index, int n, int[] boxs, int[] book){
if(index==n+1){
for(int i=1;i<=n;i++){
System.out.print(boxs[i]+” ”);
System.out.println();
}
return;
}
for (int i=1;i<=n;i++){
if(book[i]==0){ //第i号牌还没有用到
boxs[index] = i;
book[i] = 1;//第i号牌已经被用了
//处理下一个盒子
Dfs(index+1,n,boxs,book);
//从下一个盒子回退到当前盒子,取出当前盒子的牌
//尝试放入其它牌
book[i]=0;
}
}
}
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] boxs = new int[n+1];
int[] books = new int[n+1];
Dfs(1,n,boxs,books);
}
深度优先搜索的关键是解决“当下该如何做”,下一步的做法和当前的做法是相同的。“当前如何做”一般是尝试每一种可能,用for循环遍历,对于每一种可能的情况确定之后,继续走下一步,当前for循环的剩余要等到从下一步回退之后再处理。
(2)员工的重要性
给定一个保存员工信息的数据结构,它包含了员工唯一的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:
输入: [[1, 5, [2, 3]], [2, 3, []], [3, 3, []]], 1
输出: 11
解释:
员工1自身的重要度是5,他有两个直系下属2和3,而且2和3的重要度均为3。因此员工1的总重要度是 5 + 3 + 3 = 11。
注意:
一个员工最多有一个直系领导,但是可以有多个直系下属
员工数量不超过2000。
来源:力扣(LeetCode)
所有下属包括直接下属和间接下属。
深度优先搜索遍历顺序如下:
/*
// Definition for Employee.
class Employee {
public int id;
public int importance;
public List<Integer> subordinates;
};
*/
class Solution {
public int getImportance(List<Employee> employees, int id) {
int sum =0;
for(Employee em:employees){
if(em.id == id){
sum+=em.importance;
for(int subId:em.subordinates){
sum+=getImportance(employees,subId);
}
}
}
return sum;
}
}
(3)图像渲染
有一幅以二维整数数组表示的图画,每一个整数表示该图画的像素值大小,数值在 0 到 65535 之间。
给你一个坐标 (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,因为它不是在上下左右四个方向上与初始点相连的像素点。
来源:力扣(LeetCode)
class Solution {
//四个方向的位置更新
int[][] nextPosition = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public void dfs(int[][] image, int row, int col, int[][] book, int sr, int sc, int oldColor, int newColor) {
//修改颜色并且做标记
image[sr][sc] = newColor;
book[sr][sc] = 1;
//遍历四个方向,即每一种可能
for(int i = 0; i < 4; i++) {
int newSr = sr + nextPosition[i][0];
int newSc = sc + nextPosition[i][1];
//判断新位置是否越界
if(newSr < 0 || newSr >= row || newSc < 0 || newSc >= col)
continue;
//如果颜色和之前一样,并且没有渲染过,继续渲染
if(image[newSr][newSc] == oldColor && book[newSr][newSc] == 0) {
dfs(image, row, col, book, newSr, newSc,oldColor, newColor);
}
}
}
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
int oldColor = image[sr][sc];
int row = image.length;
int col = image[0].length;
int[][] book = new int[row][col];
dfs(image, row, col, book, sr, sc, oldColor, newColor);
return image;
}
}
(4)被围绕的区域
给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
示例:
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’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
来源:力扣(LeetCode)
解析:
本题的意思被包围的区间不会存在于边界上,所以边界上的o以及与o联通的都不算包围,只要把边界上的o以及与之联通的o进行特殊处理,剩下的o替换成x即可。故问题转化为,如何寻找和边界联通的o,我们需要考虑如下情况。
xxxx
xoox
xxox
xoox
从每一个边缘的o开始,只要和边缘的o联通,则它就没有被包围。
- 首先寻找边上的每一个o,如果没有,表示所有的o都被包围
- 对于边上的每一个o进行dfs进行扩散,先把边上的每一个o用特殊符号标记,比如*,#等
- 把和它相邻的o都替换为特殊符号,每一个新的位置都做相同的dfs操作
- 所有扩散结束之后,把特殊符号的位置还原为o,原来为o的位置替换为x即可。
class Solution{
//四个方向的位置更新:顺时针更新
int[][] nextPosition = {{0,1},{1,0},{0,-1},{-1,0}};
public void dfs(char[][] board, int row, int col, int i, int j){
//当前为位置设为’*’
board[i][j] = '*';
for(int k=0; k<4; ++k){
//向四个方向扩散
int ni = i + nextPosition[k][0];
int nj = j+nextPosition[k][1];
//判断边界
if(ni<0 || ni >=row || nj<0 || nj>=col){
continue;
}
//是’0’说明和边界联通,继续搜索是否还有联通
if(board[ni][nj]!= '*' && board[ni][nj] != 'X'){
dfs(board,row,col,ni,nj);
}
}
}
public void solve(char[][] board){
if(board.length==0){
return;
}
//寻找边上的每一个O,如果没有表示所有的O都被包围
int row = board.length;
int col = board[0].length;
//寻找第一行和最后一行
for(int j=0;j<col;++j){
if(board[0][j]=='O'){
dfs(board,row,col,0,j);
}
if(board[row-1][j]=='O'){
dfs(board,row,col,row-1,j);
}
}
//寻找第一列和最后一列
for(int i=0;i<row;++i){
if(board[i][0] == 'O'){
dfs(board,row,col,i,0);
}
if(board[i][col-1] == 'O'){
dfs(board,row,col,i,col-1);
}
}
for(int i=0;i<row;++i){
for(int j=0;j<col;++j){
if(board[i][j] == '*'){
board[i][j] = 'O';
}else if(board[i][j]== 'O'){
board[i][j] = 'X';
}
}
}
}
}
注意:
这一块就是用两个if-else
,别整成if-else-else if
.
//同时寻找更新第一行和最后一行
for(int j=0;j<col;++j){
if(board[0][j]=='O'){
dfs(board,row,col,0,j);
}
if(board[row-1][j]=='O'){
dfs(board,row,col,row-1,j);
}
}
(5)岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。
来源:力扣(LeetCode)
本题的意思是连在一起的陆地都算是一个岛屿,本题可以采用类似渲染的做法,尝试以每个点作为渲染的起点,可以渲染的陆地都算作一个岛屿,最后看渲染了多少次,即深度优先算法执行了多少次。
class Solution{
int[][] nextPosition = {{0,1},{1,0},{0,-1},{-1,0}};
public void dfs(char[][] grid, int row, int col, int[][] book, int x, int y){
//处理当前逻辑
book[x][y] =1;
//遍历每一种可能,四个方向
for(int k=0;k<4;++k){
int nX = x+ nextPosition[k][0];
int nY = y + nextPosition[k][1];
//判断新位置是否越界
if(nX >= row || nX<0 || nY>=col || nY<0){
continue;
}
//如果符合要求,并且之前也没有渲染过,则继续渲染
if(grid[nX][nY]=='1'&& book[nX][nY] ==0){
dfs(grid,row,col,book,nX,nY);
}
}
}
public int numIslands(char[][] grid){
if(grid.length == 0){
return 0;
}
int ret =0;
int row = grid.length;
int col = grid[0].length;
int[][] book = new int[row][col];
//以每一个网格点为渲染起点开始
for(int i=0;i<row;++i){
for(int j=0;j<col;++j){
if(grid[i][j] == '1' && book[i][j] ==0){
++ret;
dfs(grid,row,col,book,i,j);
}
}
}
return ret;
}
}
Ⅱ 广度优先搜索
深度优先递归回退, 广度优先逐层遍历.
Bfs(){
1.建立起始步骤,队列初始化
2.遍历队列中的每一种可能,while(队列不为空)
{
通过队头元素带出下一步的所有可能,并且依次入队
{
判断当前情况是否达成目标:按照目标要求处理逻辑
}
继续遍历队列中的剩余情况
}
}
(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:
输入: [[1, 5, [2, 3]], [2, 3, []], [3, 3, []]], 1
输出: 11
解释:
员工1自身的重要度是5,他有两个直系下属2和3,而且2和3的重要度均为3。因此员工1的总重要度是 5 + 3 + 3 = 11。
注意:
一个员工最多有一个直系领导,但是可以有多个直系下属
员工数量不超过2000。
来源:力扣(LeetCode)
解析:
每次先加第一个下属的重要性,再加第二个下属…
按照相同的操作再去加第一个下属的下属重要性…依次类推,类似层序遍历。
广度优先搜索遍历顺序如下:
/*
// Employee info
class Employee {
// It's the unique id of each node;
// unique id of this employee
public int id;
// the importance value of this employee
public int importance;
// the id of direct subordinates
public List<Integer> subordinates;
};
*/
class Solution{
public int getImportance(List<Employee> employees, int id){
int res=0;
Queue<Integer> q = new LinkedList<>();
//初始化队列
q.offer(id);
//把员工信息保存在map中,方便查询
Map<Integer,Employee> m = new HashMap<>();
for(Employee e:employees){
m.put(e.id,e);
}
//遍历队列
while(!q.isEmpty()){
int t = q.poll();
res+=m.get(t).importance;
for(int num:m.get(t).subordinates){
q.offer(num);
}
}
return res;
}
}
(2)N叉树的层序遍历
给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。
例如,给定一个 3叉树 :
返回其层序遍历:
[
[1],
[3,2,4],
[5,6]
]
来源:力扣(LeetCode)
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public List<List<Integer>> levelOrder(Node root) {
//记录每一层
List<List<Integer>> ret = new ArrayList<>();
Queue<Node> queue = new LinkedList<>();
if(root==null){
return ret;
}
queue.offer(root);
while(!queue.isEmpty()){
//临时保存当前层元素
List<Integer> list = new ArrayList<>();
int size = queue.size();
while(size>0){
Node node = queue.poll();
list.add(node.val);
//将孩子节点加入队列
for(Node x:node.children){
queue.offer(x);
}
size--;
}
ret.add(list);
}
return ret;
}
}
(3)腐烂的橘子
在给定的网格中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。
返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。
输入:[[2,1,1],[1,1,0],[0,1,1]]
输出:4
示例 2:
输入:[[2,1,1],[0,1,1],[1,0,1]]
输出:-1
解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个正向上。
示例 3:
输入:[[0,2]]
输出:0
解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。
来源:力扣(LeetCode)
本题可以先找到所有的腐烂橘子入队,用第一批带出新一批腐烂的橘子,每一批橘子都会在一分钟之内腐烂,所以此题可以转化为求BFS执行的大循环的次数,这里的分钟数ret的更新需要有一个标记flg,只有新的橘子腐烂时才可以让ret++。最后BFS执行完之后,说明所有可以腐烂的都完成了,再去遍历grid,如果还有值为1的,说明没有办法完全腐烂,返回-1,如果没有,则返回ret.
class Pair{
int x;
int y;
public Pair(int x, int y){
this.x = x;
this.y = y;
}
}
class Solution {
public int orangesRotting(int[][] grid) {
int ret =0;
int row = grid.length;
int col = grid[0].length;
Queue<Pair> queue = new LinkedList<>();
//记录第一批腐烂的橘子
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(grid[i][j]==2){
queue.offer(new Pair(i,j));
}
}
}
//蔓延方向
int[][] nums = {{1,0},{-1,0},{0,-1},{0,1},};
while(!queue.isEmpty()){
int size = queue.size();
boolean flg =false;
while((size--)>0){
Pair pair = queue.poll();
for(int i=0;i<4;i++){
int newX = pair.x+nums[i][0];
int newY = pair.y+nums[i][1];
if(newX>=row || newX<0 || newY>=col || newY<0){
continue;
}
if(grid[newX][newY]==1){
flg=true;
grid[newX][newY]=2;
queue.offer(new Pair(newX,newY));
}
}
}
if(flg){
ret++;
}
}
//检查是否还有新鲜橘子
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(grid[i][j]==1){
return -1;
}
}
}
return ret;
}
}
(4)单词接龙
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:
如果不存在这样的转换序列,返回 0。所有单词具有相同的长度。
所有单词只由小写字母组成。字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = “hit”,
endWord = “cog”,
wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
输出: 5
解释: 一个最短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”,
返回它的长度 5。
示例 2:
输入:
beginWord = “hit”
endWord = “cog”
wordList = [“hot”,“dot”,“dog”,“lot”,“log”]
输出: 0
解释: endWord “cog” 不在字典中,所以无法进行转换。
来源:力扣(LeetCode)
- 通过BFS,首先用beginWord找出转换一个字母之后所有可能的结果
- 每一步都要把队列中上一步添加的所有单词转换一遍,最短的转换肯定给在这些单词当中,所有这些词的转换只能算一次转换,因为都是在上一步的基础上转换出来的,这里对于每个单词的每个位置都可以用26个字母进行转换,所以一个单词一次转换的可能有:单词的长度*26
- 把转换成功的新词入队,进行下一步的转换
- 最后整个转换的最短长度就和BFS执行的次数相同
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
int ret = 2;//算上起始单词,中间每转换一个单词就+1
//hash表的查询效率最高,将字典单词放入Set中
Set<String> set = new HashSet<>();
for(String x:wordList){
set.add(x);
}
//标记单词是否已经访问过(访问过的不再访问)
Set<String> book = new HashSet<>();
book.add(beginWord);
//初始化队列
Queue<String> queue = new LinkedList<>();
queue.offer(beginWord);
while(!queue.isEmpty()){
int size = queue.size();
//每一步都要把队列中上一步添加到的所有单词转换一遍
//最短的转换肯定在这些单词当中,所有的这些词的转换只能算一次转换
//因为都是上一步转换出来的
while(size-->0){
String str = queue.poll();
//尝试转换当前单词的每一个位置
for(int j=0;j<str.length();j++){
//每一个位置用26个字母分别替换
for(char i='a';i<='z';i++){
StringBuilder strs = new StringBuilder(str);
strs.setCharAt(j,i);
String strChange = strs.toString();
//如果字典中有此单词,并且没有被访问过,则添加到转换队列中
if(set.contains(strChange) && !book.contains(strChange)){
//转换成功,则在上一步转换的基础上+1,直接返回
if(strChange.equals(endWord)){
return ret++;
}
queue.offer(strChange);
book.add(strChange);
}
}
}
}
ret++;
}
//转换不成功,返回0
return 0;
}
}
(5)打开转盘锁
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。
示例 1:
输入:deadends = [“0201”,“0101”,“0102”,“1212”,“2002”], target = “0202”
输出:6
解释:
可能的移动序列为 “0000” -> “1000” -> “1100” -> “1200” -> “1201” -> “1202” -> “0202”。
注意 “0000” -> “0001” -> “0002” -> “0102” -> “0202” 这样的序列是不能解锁的,因为当拨动到 “0102” 时这个锁就会被锁定。
示例 2:
输入: deadends = [“8888”], target = “0009”
输出:1
解释:
把最后一位反向旋转一次即可 “0000” -> “0009”。
示例 3:
输入: deadends = [“8887”,“8889”,“8878”,“8898”,“8788”,“8988”,“7888”,“9888”], target = “8888”
输出:-1
解释:
无法旋转到目标数字且不被锁定。
示例 4:
输入: deadends = [“0000”], target = “8888”
输出:-1
提示:
死亡列表 deadends 的长度范围为 [1, 500]。
目标数字 target 不会在 deadends 之中。
每个 deadends 和 target 中的字符串的数字会在 10,000 个可能的情况 ‘0000’ 到 ‘9999’ 中产生。
来源:力扣(LeetCode)
向上拨一下,向下拨一下。0000每一位都可以向上拨或者向下拨,一共有1000,9000,0100,0900,0010,0090,0001,0009这8种可能,也就是说每次拨动有这8种可能。
深度优先不适合解此题,递归深度太大,会导致栈溢出。
本题的密码为4位密码,每位密码可以通过波动一次进行改变,注意这里的回环以及拨动的方向。
波动方向:向前,向后
回环:当前数字为9,0时,波动时不是简单的自加自减。
class Solution {
public int openLock(String[] deadends, String target) {
//哈希表查找更快,将死亡数字记录到Set中
Set<String> dead = new HashSet<>();
for(String x:deadends){
dead.add(x);
}
//如果“0000”在死亡字符串中,则永远到达不了,因为起始是0000
if(dead.contains("0000")){
return -1;
}
//加标记,已经搜索过的字符串不需要再次搜索
Set<String> book = new HashSet<>();
book.add("0000");
//初始化队列
Queue<String> queue = new LinkedList<>();
queue.offer("0000");
int ret =0;
while(!queue.isEmpty()){
//从上一步转换之后的字符串都需要进行验证和转换
//并且只算做一次转换,类似于层序遍历,转换的步数和层相同
//同一层的元素都是经过一步转换得到的
int size = queue.size();
while(size-->0){
String str = queue.poll();
//如果匹配上则返回步数(每遍历一层步数就加1)
if(str.equals(target)){
return ret;
}
for(int i=0;i<4;i++){
//四位密码锁,每个位置每次都要转一次,可以向下转down或者向上转up
StringBuilder up = new StringBuilder(str);
StringBuilder down = new StringBuilder(str);
char ch = str.charAt(i);
if(ch=='9'){
down.setCharAt(i,'0');
up.setCharAt(i,(char)(ch-1));
}else if(ch=='0'){
down.setCharAt(i,'9');
up.setCharAt(i,(char)(ch+1));
}else{
down.setCharAt(i,(char)(ch+1));
up.setCharAt(i,(char)(ch-1));
}
if(!dead.contains(up.toString()) && !book.contains(up.toString())){
queue.offer(up.toString());
book.add(up.toString());
}
if(!dead.contains(down.toString()) && !book.contains(down.toString())){
queue.offer(down.toString());
book.add(down.toString());
}
}
}
ret++;
}
return -1;
}
}