代码随想录算法训练营第50天 | 第十一章:图论part01:

第十一章:图论part01

图论理论基础

文章讲解

注意

  • 图的构造:邻接矩阵和邻接表

深搜理论基础

文章讲解

  • 注意一下回溯的过程,使用递归最方便。
  • 深搜三部曲:
  1. 确认递归函数,参数
    一般情况,深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。
    例如这样:
    List<List<Integer>> result = new ArrayList<>(); // 保存符合条件的所有路径
    List<Integer> path = new ArrayList<>(); // 起点到终点的路径
    void dfs(int[][] graph, int currentNode) {}
  1. 确认终止条件
if (终止条件) {
    存放结果;
    return;
}

另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归。

  1. 处理目前搜索节点出发的路径
    一般这里就是一个for循环的操作,去遍历 目前搜索节点 所能到的所有节点。
for (选择:本节点所连接的其他节点) {
    处理节点;
    dfs(图,选择的节点); // 递归
    回溯,撤销处理结果
}

98. 所有可达路径

文章讲解

图的储存

邻接矩阵

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

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

Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(); // 节点数
int[][] graph = new int[n + 1][n + 1]; // 邻接矩阵,初始化为0

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

int m = scanner.nextInt(); // 边数
while (m-- > 0) {
    int s = scanner.nextInt();
    int t = scanner.nextInt();
    graph[s][t] = 1; // 表示节点s指向节点t
}

邻接表
List<List<Integer>> graph = new ArrayList<>(n + 1); // 初始化邻接表

for (int i = 0; i <= n; i++) {
    graph.add(new LinkedList<>());
}

while (m-- > 0) {
    int s = scanner.nextInt();
    int t = scanner.nextInt();
    graph.get(s).add(t); // 表示s -> t是相连的
}

本题我们使用邻接表 或者 邻接矩阵都可以,因为后台数据并没有对图的大小以及稠密度做很大的区分。

深搜三部曲

  1. 确认递归函数,参数
    首先我们dfs函数一定要存一个图,用来遍历的,需要存一个目前我们遍历的节点,定义为x。
    还需要存一个n,表示终点,我们遍历的时候,用来判断当 x==n 时候 标明找到了终点。
    (其实在递归函数的参数 不容易一开始就确定了,一般是在写函数体的时候发现缺什么,参加就补什么)
    至于 单一路径 和 路径集合 可以放在全局变量
List<List<Integer>> result = new ArrayList<>(); // 收集符合条件的路径
List<Integer> path = new ArrayList<>(); // 1节点到终点的路径
// x:目前遍历的节点
// graph:存当前的图
// n:终点
void dfs(List<List<Integer>> graph, int x, int n) {}
  1. 确认终止条件
    什么时候我们就找到一条路径了?
    当目前遍历的节点 为 最后一个节点 n 的时候 就找到了一条 从出发点到终止点的路径。
if (x == n) { // 找到符合条件的一条路径
   result.add(new ArrayList<>(path));
   return;
}
  1. 处理目前搜索节点出发的路径
    接下来是走 当前遍历节点x的下一个节点。
    首先是要找到 x节点指向了哪些节点呢? 遍历方式是这样的:
for (int i : graph.get(x)) 

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

在使用邻接矩阵表示图的结构时,graph[x][i]表示从节点 x 到节点 i 是否存在一条边。邻接矩阵是一种二维数组,假设有 n 个节点,那么这个矩阵的大小就是 n x n。矩阵中的每个元素 graph[x][i] 的含义如下:

  • 如果 graph[x][i] 为 1,表示节点 x 和节点 i 之间存在一条边。
  • 如果 graph[x][i] 为 0,表示节点 x 和节点 i 之间不存在一条边。
if (graph[x][i] == 1) { // 找到 x链接的节点
     path.add(i); // 遍历到的节点加入到路径中来
}

进入下一层递归

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

最后就是回溯的过程,撤销本次添加节点的操作。

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

该过程整体代码:

for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点
            if (graph[x][i] == 1) { // 找到 x链接的节点
                path.add(i); // 遍历到的节点加入到路径中来
                dfs(graph, i, n); // 进入下一层递归
                path.remove(path.size() - 1); // 回溯,撤销本节点
            }
}
打印结果

例如示例输出是:
[1 3 5]而不是 [1 3 5 ]
即 5 的后面没有空格!

 if (result.size() == 0) {
            System.out.println(-1);
        } else {
            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));
           }
      }
}
邻接矩阵写法
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Main{
    static List<List<Integer>> result = new ArrayList<>();
    static List<Integer> path = new ArrayList<>();
    public static void dfs(int[][] graph, int x, int n){
        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 sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();
        int[][] graph = new int[n + 1][n + 1];
        for(int i = 0; i < m; i++){
             int s = sc.nextInt();
             int t = sc.nextInt();;
             graph[s][t] = 1;
        }
        path.add(1); // 无论什么路径已经是从1节点出发
        dfs(graph, 1, n); // 开始遍历
        // 输出结果
        if(result.size() == 0) System.out.println(-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));
        }
    }
}
邻接表写法
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;

public class Main{
    static List<List<Integer>> result = new ArrayList<>();
    static List<Integer> path = new ArrayList<>();
    public static void dfs(List<LinkedList<Integer>> graph, int x, int n){
        if(x == n){
            result.add(new ArrayList<>(path));
            return;
        }
        for(int i : graph.get(x)){
            path.add(i); // 遍历到的节点加入到路径中来
            dfs(graph, i, n); // 进入下一层递归
            path.remove(path.size() - 1); // 回溯,撤销本节点
        }
        
    }
    
    public static void main (String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();
        List<LinkedList<Integer>> graph = new ArrayList<>(n + 1);
        for (int i = 0; i <= n; i++) {
            graph.add(new LinkedList<>());
        }
       while(m-- > 0){
           int s = sc.nextInt();
           int t = sc.nextInt();
           graph.get(s).add(t);
       }
        
        path.add(1); // 无论什么路径已经是从1节点出发
        dfs(graph, 1, n); // 开始遍历
        // 输出结果
        if(result.isEmpty()) System.out.println(-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));
        }
    }
}

广搜理论基础

文章讲解

  • 使用场景:适合于解决两个点之间的最短路径问题。因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。

  • 也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行。

代码框架

其实,我们仅仅需要一个容器,能保存我们要遍历过的元素就可以,那么用队列,还是用栈,甚至用数组,都是可以的。

用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针。

因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。

如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历。

因为栈是先进后出,加入元素和弹出元素的顺序改变了。

那么广搜需要注意 转圈搜索的顺序吗? 不需要!

模板代码

以这个图为例


image.png
import java.util.LinkedList;
import java.util.Queue;
public class BFS {
    static int[][] dir = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}}; // 表示四个方向

    public static void bfs(char[][] grid, boolean[][] visited, int x, int y) {
        Queue<int[]> queue = new LinkedList<>(); // 定义队列
        queue.offer(new int[]{x, y}); // 起始节点加入队列
        visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点

        while (!queue.isEmpty()) { // 开始遍历队列里的元素
            int[] cur = queue.poll(); // 从队列取元素
            int curx = cur[0];
            int cury = cur[1]; // 当前节点坐标

            for (int i = 0; i < 4; i++) { // 开始向当前节点的四个方向左右上下去遍历
                int nextx = curx + dir[i][0];
                int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标

                // 坐标越界了,直接跳过
                if (nextx < 0 || nextx >= grid.length || nexty < 0 || nexty >= grid[0].length) continue;

                // 如果节点没被访问过
                if (!visited[nextx][nexty]) {
                    queue.offer(new int[]{nextx, nexty}); // 队列添加该节点为下一轮要遍历的节点
                    visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
                }
            }
        }
    }

    public static void main(String[] args) {
        char[][] grid = {
            {'1', '1', '0', '0', '0'},
            {'1', '1', '0', '0', '0'},
            {'0', '0', '1', '0', '0'},
            {'0', '0', '0', '1', '1'}
        };
        boolean[][] visited = new boolean[grid.length][grid[0].length];
        bfs(grid, visited, 0, 0);
        
        // 输出 visited 数组查看结果
        for (int i = 0; i < visited.length; i++) {
            for (int j = 0; j < visited[0].length; j++) {
                System.out.print(visited[i][j] ? "T " : "F ");
            }
            System.out.println();
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容