深搜与广搜

深度优先搜索DFS理论

递归三部曲+回溯三部曲+深搜三部曲 模板都是一样的

1.确认递归函数的返回值类型和参数

void dfs(图,目前搜索的节点)

一般情况下,深搜需要 路径集合 保存所有路径,需要单一路径。路径集合 和 单一路径 可以放在全局变量中。

传入参数有三个,一个是图,用来遍历的;一个是目前我们遍历的节点,定位为x;最后一个为保存的终点n,当遍历的时候,用来判断当 x==n 的时候,找到了终点。

所以综上,初始代码应该是这样的:

List<List<Integer>> graph; //收集符合条件的路径
List<Integer> path; //初始节点到终点的路径
//x:目前遍历的节点
//graph:存当前的图
//n:终点
void dfs(int[][] graph, int x, int n){}

2.确认终止条件

if(终止条件){

存放结果;

return;

}

3.处理目前搜索节点出发的路径

for(选择:本节点所连接的其它节点){

处理节点;

dfs(图,选择的节点);//递归

回溯,撤销处理结果

}

整体代码框架如下:

int[][] dir = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};// 表示四个方向
// grid 是地图,用二维数组存储
// visited 标记 访问过的 节点,,不要重复访问
// x,y表示开始搜索节点的下标
void bfs(in[][] grid, boolean[][] visited, int x, int y){
//遍历当前节点的四个方向
for(int i = 0; i < 4; i++){
     int nextX = x + dir[i][0];
     int nextY = y + dir[i][1];
     // 坐标越界了 直接跳过
     if(nextX < 0 || nextY < 0 || nextX >= grid.length || nextY >= grid[0].length){
        continue;
     }
     // 如果节点没被访问过
     // 具体处理逻辑 判定 
     if(!visited[nextX][nextY]){
         visited[nextX][nextY] = true;// 表明当前节点已经被访问过,标记为true,避免重复访问
         bfs(grid, visited, nextX, nextY); //  开始递归,遍历下一层
     }
  }
}

98.所有可达路径

图的存储方式:邻接表和邻接矩阵

邻接矩阵如何构建?

邻接矩阵使用二维数组来表示图结构。邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。

本题有n个节点,因为节点标号是从1开始的,为了节点标号和下标对齐,我们申请(n+1)*(n+1)的二维数组。

int[][] graph = new int[n+1][n+1];

输入m个边,构造方式如下:

for(int i = 0; i < m; i++){

int s = scanner.nextInt();

int t = scanner.nextInt();

//使用邻接矩阵,1 表示 节点 s 指向 节点 t

graph[s][t] == 1;

}

深度优先搜索

深度优先搜索三部曲

1.确认递归函数,参数

传入参数有三个,一个是图,用来遍历的;一个是目前我们遍历的节点,定位为x;最后一个为保存的终点n,当遍历的时候,用来判断当 x==n 的时候,找到了终点。

所以综上,初始代码应该是这样的:

List<List<Integer>> graph = new ArrayList<>(); //收集符合条件的路径

List<Integer> path = new ArrayList<>(); //初始节点到终点的路径

//x:目前遍历的节点

//graph:存当前的图

//n:终点

void dfs(int[][] graph, int x, int n){

}

2.确定终止条件

当目前遍历的节点 为 最后一个节点n 的时候,就找到了 一条从出发点到终止点的 路径

if(x == n){

result.add(new ArrayList<>(path));

return;

}

3.处理目前搜索节点出发的路径

接下来是走 当前遍历节点 x 的下一个节点

首先要找到 x节点 都 指向了 哪些节点?遍历方式如下:

for(int i = 1; i <= n; i++){//遍历节点x链接的所有节点

if(graph[x][i] == 1){//说明 x 指向的节点 就是i

  后续操作

}

}

接下来就是 将选中x所指向的节点,加入到单一路径来

path.add(i);

进入下一层递归

dfs(graph, i, b);

回溯,撤销本次添加节点的操作

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

综上第3步整合代码如下:

for(int i = 1; i <= n; i++){//遍历节点x链接的所有节点

if(graph[x][i] == 1){//说明 x 指向的节点 就是i

  path.add(i);//将选中x所指向的节点,加入到单一路径来

  dfs(graph, i, b);//进入下一层递归

  path.remove(path.size()-1);//回溯,撤销本次添加节点的操作

}

}

打印结果

以上代码中,结果都存在result集合中,集合中的每个元素 又都是一个集合,将所有路径打印出来

本题要求路径打印的时候要求:每个元素间都有空格,最后一个元素没有空格

if(result.size() == 0){

System.out.print(-1);

}

for(List<Integer> pa : result){

for(int i = 0; i < pa.size() – 1; i++){

  System.out.print(pa.get(i) + ” “);

}

System.out.println(pa.get(pa.size() – 1));

}

完整ACM代码如下:

import java.util.Scanner;
import java.util.List;
import java.util.ArrayList;

public class Main{
    static List <List<Integer>> result = new ArrayList<>();
    static List <Integer> path = new ArrayList<>();
    //1.确定返回值类型和传入参数
    public static void dfs(int[][] graph, int x, int n){
        //2.确定终止条件
        if(x == n){
            result.add(new ArrayList<>(path));
            return;
        }
        for(int i = 1; i <= n; i++){
            if(graph[x][i] == 1){
                path.add(i);
                dfs(graph, i, n);
                path.remove(path.size()-1);
            }
        }
    }

    public static void main (String[] args) {
        Scanner scanner = new Scanner(System.in);
        int N = scanner.nextInt();//N个节点
        int M = scanner.nextInt();//M条边
        //创建 邻接矩阵
        int[][] graph = new int[N+1][N+1];
        //初始化 邻接矩阵
        while(M > 0){
            M--;
            int s = scanner.nextInt();
            int t = scanner.nextInt();
            graph[s][t] = 1;
        }
        path.add(1);
        dfs(graph, 1, N);
        //输出结果
        if(result.isEmpty()){
            System.out.print(-1);        }
        for(List<Integer> pa : result){
            for(int i = 0; i < pa.size() - 1; i++){
                System.out.print(pa.get(i) + " ");
            }
            System.out.println(pa.get(pa.size()-1));
        }
    }
}

图的存储方式:邻接表如何构建?

邻接表 使用 数组 + 链表 的方式来表示。邻接表是从边的数量来表示图,有多少边才会申请对应大小的链表。

//节点编号从1到n,所以申请 n+1 的数组

List<LinkedList<Integer>> graph = new ArrayList<>(n+1);

//相比较 邻接矩阵初始化(数组默认值为0), 邻接表 需要遍历每个 元素 添加新的 LinkedList 集合

for(int i = 0; i < = n; i++){

graph.add(new LinkedList<>());

}

//初始化邻接表

for(int i = 0; i < m; i++){

int s = scanner.nextInt();

int t = scanner.nextInt();

graph.get(s).add(t);

}

深搜三部曲 与 邻接矩阵 的思路一样

这里唯一不同就是 处理逻辑上 我们需要找到当前遍历节点x 的 所有链接的节点

在邻接矩阵中,我们通过从1到n遍历 然后判断 graph[x][i] == 1 来判定 是否有链接

而在邻接表中,我们需要遍历 当前节点x 后面 的 所有链表元素, 这些链表元素 就是 它 有链接的 点

因此代码如下:

for(int i : graph.get(x)){// 找到 x 指向的节点

path.add(i);// 遍历到节点加入到路径中来

dfs(graph, i, n);// 递归

path.remove(path.seiz() – 1);// 回溯

}

广度优先搜索BFS理论

一圈一圈搜索的模拟是如何实现的?

使用队列或者栈都可以,但是队列是常用的。

BFS一圈一圈的遍历方式,一旦遇到终止点,那么就一定是一条最短路径。

遍历每个节点的所有链接的点,链接的点再继续遍历其所有链接的点,层层遍历,直到终止点,完成广度优先搜索。

模板设置

int[][] dir = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};// 表示四个方向

// grid 是地图,用二维数组存储

// visited 标记 访问过的 节点,,不要重复访问

// x,y表示开始搜索节点的下标

void bfs(in[][] grid, boolean[][] visited, int x, int y){

//遍历当前节点的四个方向

for(int i = 0; i < 4; i++){ int nextX = x + dir[i][0];

  Int nextY = y + dir[i][1];

  // 坐标越界了 直接跳过

  if(nextX < 0 || nextY < 0 || nextX >= grid.length || nextY >= grid[0].length){

  continue;

  }

  // 如果节点没被访问过

  // 具体处理逻辑 判定

  if(!visited[nextX][nextY]){

    visited[nextX][nextY] = true;// 表明当前节点已经被访问过,标记为true,避免重复访问

    bfs(grid, visited, nextX, nextY); // 开始递归,遍历下一层

  }

}

}

99.岛屿数量

深搜版代码:

1.递归函数的 传入参数

//grid 作为 二维数组 存储的是 地图

// visited 作为 布尔类型的 二维数组,与 grid 数组相对应,grid中的位置对应到visited的位置为true表示已经被访问过,为false 表示未被访问。初始化时 默认值 为 false,表示所有点都没有被访问过,符合我们的预期。

// x,y 传入的 坐标值

Public static void dfs(int[][] grid, boolean[][] visited, int x, int y){

}

2.确定终止条件

分析:当前遍历的节点x,y为负数或者越界,则返回

遍历的当前节点为海水(grid[x][y] == 0) 或者 已经被 访问过(visited[x][y] == true)

注意:要先判断x 和 y 是否越界,然后再判断 当前节点是否为海水还是 已经被访问过

if(x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] == 0 || visited[x][y] == true){

Return;

}

3.处理业务逻辑

进入逻辑意味着该节点已经被访问过,立刻将其进行标记

开始遍历该节点所有链接的上下左右四个方向的节点

visited[x][y] = true;

for(int i = 0; i < 4; i++){

Int nextX = x + dir[i][0];

Int nextY = y + dir[i][1];

// 直接进入递归

dfs(grid, visited, nextX, nextY);

}

广搜版代码:

import java.util.Scanner;

import java.util.Deque;

import java.util.ArrayDeque;

public class Main{

public static int[][] dir = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};

//grid 是 地图, 二维数组

// visited 标记访问过的节点,不能重复访问

// x,y 表示 开始 搜索节点的 下标

public static void bfs(int[][] grid, boolean[][] vsisted, int x, int y){

// 定义 队列,队列 中 的每个元素 是 大小为2的一维数组

Deque<int[]> queue = new ArrayDeque<>();

// 起始节点 加入 队列

queue.offer(new int[]{x, y});

// 只要加入队列,立刻标记为访问过的节点

vsisted[x][y] = true;

// 开始遍历队列的元素

while(!queue.isEmpty()){

  // 从 队列 中取出元素

int[] current = queue.poll();

  // 当前节点 坐标

int x1 = current[0];

int y1 = current[1];

  // 开始 向 当前节点 的 四个方向 左右上下 去遍历

for(int i = 0; i < 4; i++){

int x2 = x1 + dir[i][0];

int y2 = y1 + dir[i][1];

if(x2 < 0 || x2 >= grid.length || y2 < 0 || y2 >= grid[0].length){

continue;

}

  // 如果节点没被访问过 且 该节点为 陆地

if(vsisted[x2][y2] == false && grid[x2][y2] == 1){

   // 队列 添加该节点为 下一轮 要遍历的 节点

queue.offer(new int[]{x2, y2});

   // 只要 加入队列 立刻 标记,避免重复访问

vsisted[x2][y2] = true;

}

}

}

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

//初始化

int[][] grid = new int[n][m];

boolean[][] visited = new boolean[n][m];

int ans = 0;

for (int i = 0; i < n; i++) {

for (int j = 0; j < m; j++) {

grid[i][j] = scanner.nextInt();

}

}

for(int i = 0; i <n; i++){

for(int j = 0; j < m; j++){

if(visited[i][j] == false && grid[i][j] == 1){

ans++;

bfs(grid, visited, i, j);

}

}

}

System.out.print(ans);

}

}

100.岛屿的最大面积

在主函数中遍历每个节点,通过深度优先搜索 来 计算 每个 岛屿 的面积 是多少,然后取最大值

注意点有两个:

1.递归函数dfs的终止条件中,必须先判断当前节点x和y是否越界 然后 再判断 该节点 是否为海水 还是 已经被访问过。

2.主函数每次遍历节点进行dfs深搜之前,都要将陆地的计算面积赋值为0,不然会出现累加的现象。

代码:

import java.util.Scanner;

//深搜

public class Main{

public static int[][] dir = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};

public static int count = 0;

public static int result = 0;

public static void dfs(int[][] grid, boolean[][] visited, int x, int y){

//确定终止条件

if (x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || visited[x][y] == true || grid[x][y] == 0){

return; }

visited[x][y] = true;

count++;

//遍历当前节点的四个方向

for(int i = 0; i < 4; i++){

int nextX = x + dir[i][0];

int nextY = y + dir[i][1];

dfs(grid, visited, nextX, nextY);

}

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

int[][] grid = new int[n][m];

boolean[][] visited = new boolean[n][m];

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

grid[i][j] = scanner.nextInt();

}

}

//开始遍历 grid 和 visited

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

if(grid[i][j] == 1 && visited[i][j] == false){

count = 0;//每次计算陆地面积之前都要归0

dfs(grid, visited, i, j);

result = Math.max(result, count);

}

}

}

System.out.print(result);

}

}

import java.util.Scanner;

import java.util.Queue;

import java.util.ArrayDeque;

// 广搜

public class Main{

public static int[][] dir = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};

public static int result = 0;

public static int count = 0;

public static void bfs(int[][] grid, boolean[][] visited, int x, int y){

Queue<Node> q = new ArrayDeque<>();

q.add(new Node(x, y));

visited[x][y] = true;

count++;

while(!q.isEmpty()){

Node n = q.remove();

for(int i = 0; i < 4; i++){

int nextX = n.x + dir[i][0];

int nextY = n.y + dir[i][1];

if(nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length || grid[nextX][nextY] == 0 || visited[nextX][nextY] == true){

continue;

}

q.add(new Node(nextX, nextY));

visited[nextX][nextY] = true;

count++;

}

}

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

int[][] grid = new int[n][m];

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

grid[i][j] = scanner.nextInt();

}

}

boolean[][] visited = new boolean[n][m];

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

if(grid[i][j] == 1 && visited[i][j] == false){

count = 0;

bfs(grid, visited, i, j);

result = Math.max(result, count);

}

}

}

System.out.print(result);

}

static class Node{

int x;

int y;

public Node(int x, int y){

this.x = x;

this.y = y;

}

}

}

101.孤岛的总面积

孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。

解题思路:本题要求找到不靠边的陆地面积,那么我们只要从周边找到陆地,然后 通过 dfs 或者 bfs 将周边靠陆地且相邻的陆地都变成海洋,然后再去重新遍历地图 统计此时还剩下的 陆地 就可以了。

从 左侧边 和 右侧边 开始 向中间遍历

for(int i = 0; i < n; i++){

if(grid[i][0] == 1){

dfs(grid, visited, i, 0);

}

if(grid[i][m-1] == 1){

dfs(grid, visited, i, m-1);

}

}

从 上边 和 下边 向 中间遍历

for(int i = 0; i < m; i++){

if(grid[0][i] == 1){

dfs(grid, visited, 0, i);

}

if(grid[n-1][i] == 1){

dfs(grid, visited, n-1, i);

}

}

完整代码如下:

import java.util.Scanner;

import java.util.List;

import java.util.ArrayList;

//深搜

public class Main{

public static int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

public static int count = 0;

public static int result = 0;

public static void dfs(int[][] grid, boolean[][] visited, int x, int y){

if(x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] == 0 || visited[x][y] == true){

return;

}

grid[x][y] = 0;

visited[x][y] = true;

count++;

for(int i = 0; i < 4; i++){

int nextX = x + dir[i][0];

int nextY = y + dir[i][1];

dfs(grid, visited, nextX, nextY);

}

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

int[][] grid = new int[n][m];

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

grid[i][j] = scanner.nextInt();

}

}

boolean[][] visited = new boolean[n][m];

//只要从周边找到陆地然后 通过 dfs 或者 bfs 将周边靠陆地且相邻的陆地

//都变成海洋,然后再去重新遍历地图 统计此时还剩下的 陆地就可以了

//从左侧边 和 右侧边 向 中间遍历

for(int i = 0; i < n; i++){

if(grid[i][0] == 1){

dfs(grid, visited, i, 0);

}

if(grid[i][m-1] == 1){

dfs(grid, visited, i, m-1);

}

}

//从上边 和 下边 向 中间遍历

for(int i = 0; i < m; i++){

if(grid[0][i] == 1){

dfs(grid, visited, 0, i);

}

if(grid[n-1][i] == 1){

dfs(grid, visited, n-1, i);

}

}

List<Integer> resultList = new ArrayList<>();

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

if(grid[i][j] == 1 && visited[i][j] == false){

count = 0;

dfs(grid, visited, i, j);

resultList.add(count);

}

}

}

for(Integer r : resultList){

result += r;

}

System.out.print(result);

}

}

广搜代码

import java.util.Scanner;

import java.util.List;

import java.util.ArrayList;

import java.util.Deque;

import java.util.ArrayDeque;

//广搜

public class Main{

public static int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

public static int count = 0;

public static int result = 0;

public static void bfs(int[][] grid, boolean[][] visited, int x, int y){

Deque<Node> q = new ArrayDeque<>();

q.push(new Node(x, y));

grid[x][y] = 0;

visited[x][y] = true;

count++;

while(!q.isEmpty()){

Node n = q.pop();

for(int i = 0; i < 4; i++){

int nextX = n.x + dir[i][0];

int nextY = n.y + dir[i][1];

if(nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length || grid[nextX][nextY] == 0 || visited[nextX][nextY] == true){

continue;

}

if(grid[nextX][nextY] == 1 && visited[nextX][nextY] == false){

q.push(new Node(nextX, nextY));

grid[nextX][nextY] = 0;

visited[nextX][nextY] = true;

count++;

}

}

}

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

int[][] grid = new int[n][m];

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

grid[i][j] = scanner.nextInt();

}

}

boolean[][] visited = new boolean[n][m];

//只要从周边找到陆地然后 通过 dfs 或者 bfs 将周边靠陆地且相邻的陆地

//都变成海洋,然后再去重新遍历地图 统计此时还剩下的 陆地就可以了

//从左侧边 和 右侧边 向 中间遍历

for(int i = 0; i < n; i++){

if(grid[i][0] == 1){

bfs(grid, visited, i, 0);

}

if(grid[i][m-1] == 1){

bfs(grid, visited, i, m-1);

}

}

//从上边 和 下边 向 中间遍历

for(int i = 0; i < m; i++){

if(grid[0][i] == 1){

bfs(grid, visited, 0, i);

}

if(grid[n-1][i] == 1){

bfs(grid, visited, n-1, i);

}

}

List<Integer> resultList = new ArrayList<>();

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

if(grid[i][j] == 1 && visited[i][j] == false){

count = 0;

bfs(grid, visited, i, j);

resultList.add(count);

}

}

}

for(Integer r : resultList){

result += r;

}

System.out.print(result);

}

static class Node{

int x;

int y;

public Node(int x, int y){

this.x = x;

this.y = y;

}

}

}

102.沉没孤岛

解题思路:本题要将所有孤岛由1变成0

先将周边陆地及其相邻单元陆地 都 从 1 变成 2

然后 再遍历grid将 grid的 1 变成 0 , 2 变成 1

最后打印出grid

import java.util.Scanner;

//深搜

public class Main{

public static int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

public static void dfs(int[][] grid, int x, int y){

if(x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] == 0 || grid[x][y] == 2){

return;

}

//将周边陆地及其相邻单元陆地 都改为 2

grid[x][y] = 2;

for(int i = 0; i < 4; i++){

int nextX = x + dir[i][0];

int nextY = y + dir[i][1];

dfs(grid, nextX, nextY);

}

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

int[][] grid = new int[n][m];

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

grid[i][j] = scanner.nextInt();

}

}

//从 左侧边 和 右侧边 向中间遍历

for(int i = 0; i < n; i++){

if(grid[i][0] == 1){

dfs(grid, i, 0);

}

if(grid[i][m-1] == 1){

dfs(grid, i, m-1);

}

}

//从上边 和 下边 向中间遍历

for(int i = 0; i < m; i++){

if(grid[0][i] == 1){

dfs(grid, 0, i);

}

if(grid[n-1][i] == 1){

dfs(grid, n-1, i);

}

}

//开始遍历grid 找到 为1的陆地 变成 0 即可

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

if(grid[i][j] == 1){

grid[i][j] = 0;

}

if(grid[i][j] == 2){

grid[i][j] = 1;

}

System.out.print(grid[i][j] + ” “);

}

System.out.println();

}

}

}

103.水流问题

解题思路:

每遍历一个点,都是重新创建一个属于他的visited,调用dfs,得到该点的visited,然后判断这张visited中是否在第一组边界和第二组边界中都有true,如果都有,就说明该点符合要求。

使用深搜dfs

1.在dfs方法中,将点(x, y) 可流经 的 地方 全部标记为true

2.在isResult方法中 调用 dfs方法,判断点 (x, y) 对应的 visited表是否 流经 第一组边界和第二组边界。通过标记位isFirst和isSecond来判断。

3.在主函数中 遍历 grid 的每一个点,调用isResult函数 看是否符合要求

代码如下:

import java.util.Scanner;

//深搜

public class Main{

public static int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

//利用深搜 将 点 x ,y 可流经的 地方全部标记为 true

public static void dfs(int[][] grid, boolean[][] visited, int x, int y){

if(x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || visited[x][y] == true){

return;

}

visited[x][y] = true;

for(int i = 0; i < 4; i++){

int nextX = x + dir[i][0];

int nextY = y + dir[i][1];

if(nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length || visited[nextX][nextY] == true){

continue;

}

if(grid[nextX][nextY] > grid[x][y]){

continue;

}

dfs(grid, visited, nextX, nextY);

}

}

// 判断 点 x,y 通过 dfs 深搜后 得到的 visited 中是否达到在第一组边界和第二组边界

public static boolean isResult(int[][] grid, int x, int y, int n, int m){

//每个点 都 对应 一张visited

boolean[][] visited = new boolean[n][m];

//调用dfs

dfs(grid, visited, x, y);

//标记为isFirst 和 isSecond 用来判断是否流到第一组边界和第二组边界

boolean isFirst = false;

boolean isSecond = false;

//遍历 第一组边界的左边界

for(int i = 0; i < n; i++){

if(visited[i][0] == true){

isFirst = true;

}

}

//遍历 第一组边界的上边界

for(int j = 0; j < m; j++){

if(visited[0][j] == true){

isFirst = true;

}

}

//遍历 第二组边界 的右边界

for(int i = 0; i < n; i++){

if(visited[i][m-1] == true){

isSecond = true;

}

}

//遍历 第二组边界 的下边界

for(int j = 0; j < m; j++){

if(visited[n-1][j] == true){

isSecond = true;

}

}

if(isFirst == true && isSecond == true){

return true;

}

return false;

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

int[][] grid = new int[n][m];

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

grid[i][j] = scanner.nextInt();

}

}

//开始遍历

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

if(isResult(grid, i, j, n, m) == true){

System.out.println(i + ” ” + j);

}

}

}

}

}

104.建造最大岛屿

解题思路:

本题暴力思想是,遍历地图 尝试 将每一个 0 改成 1,然后去搜索地图中的 最大 岛屿面积。

计算地图的 最大面积: 遍历 地图 + 深搜岛屿,时间复杂度为n*n

每改变一个0 的 方格, 都需要重新计算一个地图的 最大面积,所以 整体时间复杂度为n^4

优化思路:

只用一次深搜 把 每个岛屿 的面积记录下来即可

第一步:第一次遍历地图,得出每个岛屿 的 面积,并做好编号记录。即将对应的陆地1改为对应的mark编号grid[x][y] = mark。可以使用map记录,key 为 岛屿编号,value是岛屿的面积。

第二步:第二次遍历地图,遍历 0 的 方格,将0变成1(海水变成陆地),并统计该1(由0变成的1)周边岛屿面积,将其相邻面积相加在一起。在相加面积时,将遍历的四个方向点对应的mark存入Set集合中,进行去重,加入判断if(Set.contains(grid[x][y]) || !HashMap.containsKey(grid[x][y])),如果满足该条件说明该节点的陆地面积不应该加入。遍历所有0之后,就可以得到 选一个 0 变成 1 之后的 最大面积。

完整代码:

import java.util.Scanner;

import java.util.Map;

import java.util.HashMap;

import java.util.Set;

import java.util.HashSet;

//深搜

public class Main{

public static int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

public static int count;

public static int mark;

public static int result;

public static void dfs(int[][] grid, boolean[][] visited, int x, int y){

if(x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || visited[x][y] == true || grid[x][y] == 0){

return; }

count++;

visited[x][y] = true;

grid[x][y] = mark;

for(int i = 0; i < 4; i++){

int nextX = x + dir[i][0];

int nextY = y + dir[i][1];

dfs(grid, visited, nextX, nextY);

}

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

int[][] grid = new int[n][m];

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

grid[i][j] = scanner.nextInt();

}

}

boolean[][] visited = new boolean[n][m];

//第一次遍历grid 将 每个陆地 进行编号 统计其面积,存入map

Map<Integer, Integer> numS = new HashMap<>();

mark = 2;

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

if(grid[i][j] == 1){

count = 0;

dfs(grid, visited, i, j);

numS.put(mark, count);

//更新最大陆地面积,不将海水改成陆地的情况

result = Math.max(result, count);

mark++;

}

}

}

Set<Integer> isIland = new HashSet<>();

//第二次遍历grid ,遍历到海水就将海水变成陆地,遍历该点的四个方向,

//查看是否存在陆地

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

if(grid[i][j] == 0){

isIland.clear();

int curCount = 1;

for(int k = 0; k < 4; k++){

int nextX2 = i + dir[k][0];

int nextY2 = j + dir[k][1];

if(nextX2 < 0 || nextX2 >= grid.length || nextY2 < 0 || nextY2 >= grid[0].length) continue;

if(isIland.contains(grid[nextX2][nextY2]) || !numS.containsKey(grid[nextX2][nextY2])){

continue;

}

isIland.add(grid[nextX2][nextY2]);

curCount += numS.get(grid[nextX2][nextY2]);

}

   //更新最大陆地面积,将海水改成陆地的情况

result = Math.max(result, curCount);

}

}

}

System.out.print(result);

}

}

110.字符串接龙

思路:无向图+BFS求单源最短路径

本题只需求出最短路径的长度即可,不用找出具体路径

本题要解决两个问题:

图中的线是如何连在一起的

起点和终点的最短路径长度

题目中没有给出点与点之间的连线,而是需要自己去连,条件是字符只差一个。

所以判断点与点之间的关系,需要判断是不是差一个字符,如果差一个字符,那就是有链接。

然后就是求起点和终点的最短路径长度,这里无向图求最短路径,广搜最为合适,广搜只要搜到了终点,那么就一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。

注意:

本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环

使用set来检查字符串是否出现在字符串集合里更快一些

代码:

import java.util.*;

public class Main{

//广搜

public static int bfs(String beginStr, String endStr, List<String> strList){

int len = 1;//路径最开始从1开始计算,题目计算路径是计算节点的个数

Queue<String> q = new LinkedList<>();

Set<String> strSet = new HashSet<>(strList);//通过Set的构造函数将List集合转成

//Set集合,题目要求转换过程中的中间字符串必须是字典strList中的字符串

//用于存储遍历过的字符串,因为题目要求strList里的每个字符串只能用一次

Set<String> visited = new HashSet<>();

q.add(beginStr);

q.add(null);//用于判断是否进入下一层进行广搜

visited.add(beginStr);

while(!q.isEmpty()){

String node = q.remove();

//判断是否进入下一层广搜

if(node == null){

if(!q.isEmpty()){

len++;

q.add(null);

}

continue;

}

//开始遍历该节点node所有链接的节点

char[] strCharArray = node.toCharArray();

for(int i = 0; i < strCharArray.length; i++){

   //暂存 此 字符串,用于后续 没有 路径可走可以回滚

char old = strCharArray[i];

for(char j = ‘a’; j <= ‘z’; j++){

strCharArray[i] = j;

//将字符数组转成字符串

String newWord = new String(strCharArray);

if(strSet.contains(newWord) && !visited.contains(newWord)){

visited.add(newWord);

q.add(newWord);

if(newWord.equals(endStr)){

return len + 1;

}

}

}

   //回滚,把字符串复原

strCharArray[i] = old;

}

}

return 0;

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

scanner.nextLine();//按下回车在下一行输入

String beginStr = scanner.next();//输入内容,以空格为分割

String endStr = scanner.next();//输入内容,以空格为分割

scanner.nextLine();//按下回车在下一行输入

List<String> strList = new ArrayList<>();

strList.add(beginStr);

strList.add(endStr);

for(int i = 0; i < n; i++){

strList.add(scanner.nextLine());//每输入一个字符串,按下回车在下一行继续输入

}

int result = bfs(beginStr, endStr, strList);

System.out.print(result);

}

}

105.有向图的完全可达性

本题只需要判断从1号节点为起点,是否可以到达所有其他节点,若能,输出1,否则输出-1

有向图搜索全路径的问题,只能使用dfs或者bfs来解决。

深搜三部曲

1.确定递归函数,传入参数

需要传入地图graph(本题地图graph采用的是链接表实现)

当前遍历的节点key

一个布尔类型的一维数组visited,用来标记节点key开始都走过哪些位置

public static void dfs(List<LinkedList<Integer>> graph, int key, boolean[] visited){

}

2.确定终止条件

如果当前遍历的节点key是被访问过的,那么就结束递归,否则继续递归

if(visited[key] == true){

return; }

3.处理当前遍历的节点所有链接的节点

//遍历该节点所有链接节点之前,先要把该节点标记为已经访问过

visited[key] = true;

//开始遍历链接节点

for(int k : graph.get(key)){

dfs(graph, visited, k);

}

综上,深搜方法dfs代码如下:

public static void dfs(List<LinkedList<Integer>> graph, int key, boolean[] visited){

  if(visited[key] == true){

   return; }

  visited[key] = true;

  for(int k : graph.get(key)){

   dfs(graph, visited, k);

  }

}

在主函数中,创建graph,初始化、赋值;创建visited,然后调用dfs方法,最后判断visited从下标1开始,是否有false就说明遍历不到所有节点,输出-1;如果没有false,说明可以遍历所有节点,输出1。

完整代码如下:

import java.util.*;

//深搜

public class Main{

public static void dfs(List<LinkedList<Integer>> graph, boolean[] visited, int key){

if(visited[key] == true){

return;

}

visited[key] = true;

//遍历当前节点key的所有链接的节点

for(int k : graph.get(key)){

dfs(graph, visited, k);

}

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int k = scanner.nextInt();

List<LinkedList<Integer>> graph = new ArrayList<>(n + 1);

//初始化邻接表

for(int i = 0; i <= n; i++){

graph.add(new LinkedList<>());

}

//赋值

for(int i = 0; i < k; i++){

int s = scanner.nextInt();

int t = scanner.nextInt();

graph.get(s).add(t);

}

//初始化默认都是false

boolean[] visited = new boolean[n + 1];

dfs(graph, visited, 1);

int result = 1;

//遍历visited结果

for(int i = 1; i < visited.length; i++){

if(visited[i] == false){

result = -1;

break;

}

}

System.out.print(result);

}

}

106.岛屿的周长

该题用不上BFS或者DFS

该题就是遍历每一个空格,遇到岛屿就计算它的上下左右节点的情况

有边可以算入周长的情况一共有两种

情况一:该陆地节点的上下左右节点是海水,则说明是边

情况二:该陆地节点的上下左右节点越界了,则说明是边

代码如下:

import java.util.*;

public class Main{

public static int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

int[][] grid = new int[n][m];

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

grid[i][j] = scanner.nextInt();

}

}

int cir = 0;

//直接遍历grid

//两种情况有边

//情况一:该节点的上下左右节点是海水,则是边

//情况二:该节点的上下左右节点出界了,则是边

for(int i = 0; i < n; i++){

for(int j = 0; j < m; j++){

if(grid[i][j] == 1){

for(int k = 0; k < 4; k++){

int nextX = i + dir[k][0];

int nextY = j + dir[k][1];

if(nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length || grid[nextX][nextY] == 0){

cir++;

}

}

}

}

}

System.out.print(cir);

}

}

并查集

并查集理论基础

并查集用来解决连通性问题

连通图 + 强连通图

在无向图中,任何两个节点都是可以到达的,我们称之为连通图;如果有节点不能到达其他节点,则为非连通图。

在有向图中,任何两个节点都是可以到达的,我们称之为强连通图。与无向图中的连通图有些不同,因为强连通图是有向图作为基础的,必须任何两个节点都是可以相互到达的,所以要么两节点是双向连接的,要么整个图成环。

何为解决连通性问题?通俗理解就是判断两个元素是否在同一个集合里。

并查集主要有两个功能:

功能一:将两个(或多个)元素添加到一个集合中;

功能二:判断两个(或多个)元素在不在同一个集合。

先来看功能一如何实现:将两个(或多个)元素添加到一个集合中。

我们将三个元素A,B,C(分别是数字)放在同一个集合,其实就是将三个元素连通在一起,如何连通呢?

只需要用一个一维数组来表示,即:father[A] = B,(表示A指向B),father[B] = C,(表示B指向C),这样一来就表示A与B与C连通了(有向连通图)。

代码如下:

//将节点 v 指向 节点 u 的这条边 加入 并查集

void join(int u, int v){

u = find(u);//找节点u的根节点,并赋值为u

v = find(v);//找节点v的根节点,并赋值为v

if(u == v) return;//如果节点u和节点v的根节点是同一个,则说明在同一个集合中,不用两个节点相连直接返回即可

father[v] = u;//说明两个节点根节点不是一个,那么就把两个节点相连,节点v指向节点u

}

至此,通过join方法实现了功能一。我们可以知道A连通B,因为A是索引下标,根据father[A]的数值就可以知道A连通B。那么怎么知道B连通A呢?

因为我们的目的是实现功能二:判断多个元素是否在同一个集合中,所以知道A连通B就已经足够了。

寻根思路就是A,B,C在同一个根下就是同一个集合。

具体来说:给出A元素,就可以通过father[A] = B,father[B] = C,找到根为C

给出B元素,通过father[B] = C,找到根也为C。说明A和B是在同一个集合中。

代码如下:

//并查集寻根的过程

int find(int u){

if(u == father[u]) return u;//如果根就是自己,直接返回

else return find(father[u]); //如果根不是自己,就根据其父节点不断递归找到根节点

}

如何表示C也在同一个元素里呢?我们需要father[C] = C,即C的根也为C,这样就方便表示A,B,C都在同一个集合中了。

所以father数组初始化的时候要father[i] = i,默认自己指向自己

代码如下:

// 并查集初始化

void init(){

for(int i = 0; i < n; i++){

  father[i] = i;

}

}

最后再来看看如何实现功能二:判断两个(或多个)元素在不在同一个集合。

如果通过 find 函数 找到 两个元素属于同一个根的话,那么这两个元素就是同一个集合

代码如下:

// 判断 u 和 v 是否找到同一个根

boolean isSame(int u, int v){

u = find(u);

v = find(v);

return u == v;

}

路径压缩:

在实现find函数的过程中,通过递归的方式,不断获取father数组下标对应的数值,最终找到这个集合的根。如果递归次数非常多会导致效率低。我们的目的只需要知道这些节点在同一个根下就可以,所以除了根节点其他所有节点都挂在根节点下,这样在寻根的时候就很快,只需要一步,由原来的时间复杂度O(logn)降为O(1)。

想要实现这样的效果,需要路径压缩,将非根节点的所有节点直接指向根节点。

只需要在递归的过程中,让 father[u] 接住 递归函数 find(father[u]) 的返回结果。

因为find 函数 向上寻找根节点,father[u]表述 u 的父节点,那么让 father[u] 直接获取 find 函数 返回的根节点,这样就让节点 u 的 父节点 变成 根节点。

代码如下:

//并查集寻根的过程

int find(int u){

return u == father[u] ? u : father[u] = find(father[u]);

}

综上:并查集主要有三个功能:

1.寻找根节点,函数:find(int u),也就是判断节点u的根节点是哪个;

2.将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点下;

3.判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点。

模板代码总结如下:

// 并查集初始化

void init(){

for(int i = 0; i < n; i++){

  father[i] = i;

}

}

//将节点 v 指向 节点 u 的这条边 加入 并查集

void join(int u, int v){

u = find(u);//找节点u的根节点,并赋值为u

v = find(v);//找节点v的根节点,并赋值为v

if(u == v) return;//如果节点u和节点v的根节点是同一个,则说明在同一个集合中,不用两个节点相连直接返回即可

father[v] = u;//说明两个节点根节点不是一个,那么就把两个节点相连,节点v指向节点u

}

//并查集寻根的过程

int find(int u){

return u == father[u] ? u : father[u] = find(father[u]);

}

// 判断 u 和 v 是否找到同一个根

boolean isSame(int u, int v){

u = find(u);

v = find(v);

return u == v;

}

107.寻找存在的路径

通过并查集将图转成树的形式。判断两个节点在不在一个集合(就是判断两个节点是不是拥有同一个根节点)+将两个节点添加到一个集合中(就是将这两个节点都指向同一个根节点)。

代码如下:

import java.util.*;

//并查集

public class Main{

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

int m = scanner.nextInt();

DisJoint disJoint = new DisJoint(n);

for(int i = 0; i < m; i++){

int s = scanner.nextInt();

int t = scanner.nextInt();

disJoint.join(s, t);

}

int source = scanner.nextInt();

int destination = scanner.nextInt();

if(disJoint.isSame(source, destination) == true){

System.out.print(1);

}else{

System.out.print(0);

}

}

}

//并查集

class DisJoint{

private int[] father;

public DisJoint(int n){

father = new int[n + 1];

for(int i = 0; i <= n; i++){

father[i] = i;

}

}

public void join(int u, int v){

u = find(u);

v = find(v);

if(u == v){

return;

}

father[v] = u;

}

public int find(int u){

return u == father[u] ? u : (father[u] = find(father[u]));

}

public boolean isSame(int u, int v){

u = find(u);

v = find(v);

return u == v;

}

}

108.冗余连接

解题思路:

从前向后遍历每一条边(优先让前面的边连上),边的两个节点如果不在同一个集合(isSame(u, v) == false),那就加入集合(join(u, v))(即同一个根节点)。如果边的两个节点已经出现同一个集合中(isSame(u, v) == true),说明这条边的两个节点已经连在一起了,再加入这条边就一定会成环。

代码如下:

import java.util.*;

//思路 :如果两个节点 是 都指向同一个根节点(即是在同一个集合中),那么再将这两个节点

//连起来,就一定构成环

public class Main{

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

DisJoint disJoint = new DisJoint(n);

for(int i = 0; i < n; i++){

int s = scanner.nextInt();

int t = scanner.nextInt();

if(disJoint.isSame(s, t) == false){//说明两节点不在同一个集合

//加入同一个集合

disJoint.join(s, t);

}else{//说明两节点在同一个集合

System.out.print(s + ” ” + t);

}

}

}

}

class DisJoint{

private int[] father;

public DisJoint(int n){

father = new int[n + 1];

for(int i = 0; i <= n; i++){

father[i] = i;

}

}

public void join(int u, int v){

u = find(u);

v = find(v);

if(u == v){

return;

}

father[v] = u;

}

public int find(int u){

return u == father[u] ? u : (father[u] = find(father[u]));

}

public boolean isSame(int u, int v){

u = find(u);

v = find(v);

return u == v;

}

}

109.冗余连接II

有向树:该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都 有且只有 一个父节点,而根节点没有父节点。有向树拥有n个节点和n-1条边。

现在有一个有向图,有向图是在 有向树 中的两个没有直接连接 的 节点 中间添加一条有向边。所以有向图是有 n 个节点 和 n条边组成的。

现在需要返回一条可以删除的边,使得删除这条边之后 该有向图 变成了 一颗 有向树

解题思路:

有向树的性质是:只有根节点入度为0,其他节点入度都为1。

情况一:找到入度为2的点,那么删除一条指向该节点的边即可。

情况二:找到入度为2的点,但是只能删除特定的一条边。

情况三:如果没有入度为2的点,说明 图中 有环了,就和 冗余连接 那道题一样了,删除构成环的边即可。

具体写法步骤:

1、把每条边记录下来,并统计节点入度,这道题只会存在一个入度为2的点

2、前两种入度为2的情况,一定是删除指向入度为2的节点的两条边其中的一条。如果删了一条,就判断这个图是不是一颗有向树,如果是,那么这条边就是答案;如果不是,就删除另外那条边。同时注意要从后向前遍历,因为如果两条边删哪一条都可以成为有向树,就删最后那一条。

3、第三种情况3,明确没有入度为2的情况下,那么一定有 有向环, 找到构成环 的 边就是要删除的边。

综上 我们 需要构建两个方法:

方法一:isTreeAfterRemoveEdge() 判断删一个边之后是不是有向树:将所有边的两端节点分别加入并查集,遇到要删除的边则跳过;如果遇到即将加入并查集的边的两端节点 本来就在并查集了,则说明构成了环。如果顺利将所有边的两端节点(除了要删除的边)加入了并查集,则说明 删除 该条边 还是一棵有向树。

方法二:getRemoveEdge() 确定图中 一定 有了 有向环,那么要找到需要删除的那条边:将所有边的两端节点分别加入并查集,如果遇到即将加入并查集的边的两端节点本来就在并查集了,说明构成了环。

代码如下:

import java.util.*;

public class Main{

static class Disjoint {

private final int[] father;

public Disjoint(int n) {

father = new int[n + 1];

for (int i = 0; i <= n; i++) {

father[i] = i;

}

}

public void join(int n, int m) {

n = find(n);

m = find(m);

if (n == m) return;

father[n] = m;

}

public int find(int n) {

return father[n] == n ? n : (father[n] = find(father[n]));

}

public boolean isSame(int n, int m) {

return find(n) == find(m);

}

}

static class Edge{

int s;

int t;

public Edge(int s, int t){

this.s = s;

this.t = t;

}

}

static class Node{

int id;

int in;

int out;

}

public static void main (String[] args) {

Scanner scanner = new Scanner(System.in);

int n = scanner.nextInt();

//记录入度为2的节点是哪一个(本题只能有一个入度为2的点)

//装填所有输入的节点以及对应的边信息

List<Edge> edges = new ArrayList<>();

Node[] nodeMap = new Node[n + 1];

for (int i = 1; i <= n; i++) {

nodeMap[i] = new Node();

}

int doubleIn = 0;

for(int i = 0; i < n; i++){

int s = scanner.nextInt();

int t = scanner.nextInt();

nodeMap[t].in++;

if(nodeMap[t].in == 2){

doubleIn = t;

}

edges.add(new Edge(s, t));

}

//开始走三个情况的逻辑

Edge result = null;

if(doubleIn != 0){//说明有入度为2的节点

List<Edge> doubleInEdges = new ArrayList<>();

for(Edge edge : edges){

if(edge.t == doubleIn){

doubleInEdges.add(edge);

}

if(doubleInEdges.size() == 2){

break;

}

}

Edge edge = doubleInEdges.get(1);

if (isTreeAfterRemoveEdge(edges, edge, nodeMap)) {

result = edge;

} else {

result = doubleInEdges.get(0);

}

}else{//说明没有入度为2的节点

result = getRemoveEdge(edges, nodeMap);

}

System.out.println(result.s + ” ” + result.t);

}

public static boolean isTreeAfterRemoveEdge(List<Edge> edges, Edge exculdEdge, Node[] nodeMap) {

Disjoint disjoint = new Disjoint(nodeMap.length – 1);

for (Edge edge : edges) {

if (edge == exculdEdge) continue;

//成环则不是树

if (disjoint.isSame(edge.s, edge.t)) {

return false;

}

disjoint.join(edge.s, edge.t);

}

return true;

}

public static Edge getRemoveEdge(List<Edge> edges, Node[] nodeMap) {

int length = nodeMap.length – 1;

Disjoint disjoint = new Disjoint(length);

for (Edge edge : edges) {

if (disjoint.isSame(edge.s, edge.t)) return edge;

disjoint.join(edge.s, edge.t);

}

return null;

}

}

Categories:

Tags:

No responses yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注