算法-图论(建图,拓扑排序)

news2024/11/15 13:47:33

文章目录

    • 建图的三种方式
      • 邻接矩阵
      • 邻接表
      • 链式前向星
    • 拓扑排序
      • 拓扑排序基础原理介绍
      • 拓扑排序步骤解析
      • 拓扑排序模板leetcode-课程表

建图的三种方式

我们建图的三种方式分别是邻接矩阵, 邻接矩阵, 链式前向星

邻接矩阵

假设我们的点的个数为N个, 我们就把他们的下标依次标为1, 2 ,…, 然后在一个矩阵表上进行边的添加, 比如我要在点2和点4之间添加一个权值为5的边, 那么我就在矩阵matrix[2][4] = 5即可, 唯一注意的就是如果是无向图, 就需要把matrix[4][2]位置也设置为5, 所以这个方法的弊端是十分明显的, 就是消耗的空间过大, 所以我们在大厂的笔试或者是比赛, 不会用这种方式进行建图, 下面是邻接矩阵法的代码实现

public class day23 {
    public static void main(String[] args) {
        int[][] edges = {{1,2,5},{5,3,1},{1,4,4},{2,5,2}};
        //测试一下使用邻接矩阵法建图
        GraphUseMatrix graphUseMatrix = new GraphUseMatrix();
        graphUseMatrix.build(5);
        graphUseMatrix.directGraph(edges);
        //graphUseMatrix.unDirectGraph(edges);
        graphUseMatrix.showGraph();
    }
}

/**
 * 邻接矩阵建图的方法
 * 下面我们介绍的所用的图都是带权值的图, 不带权的更简单就不说了
 */
class GraphUseMatrix{
    //设置一个最大的点数(根据题意)
    private static final int MAXN = 11;
    //设置一个最大的边数(根据题意, 无向图 * 2)
    private static final int MAXM = 21;

    //构建一个当前的点数curn
    private static int curN = 0;
    //设置一个邻接的矩阵(大小就是(点数 + 1) * (点数 + 1), 这里我们保证是够用的)
    private static final int[][] matrix = new int[MAXN][MAXN];

    //初始化矩阵的方法(传入的点的数量)
    public void build(int n){
        curN = n;
        //清空矩阵的脏数据
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= n; j++){
                //把邻接矩阵中的数据刷为最大值(因为权值可能为0)
                matrix[i][j] = Integer.MAX_VALUE;
            }
        }
    }

    //添加边的方法
    private void addEdge(int from, int to, int weight){
        matrix[from][to] = weight;
    }

    //在有向带权图中添加边
    public void directGraph(int[][] edges){
        for(int[] edge : edges){
            addEdge(edge[0], edge[1], edge[2]);
        }
    }

    //在无向带权图中添加边
    public void unDirectGraph(int[][] edges){
        for(int[] edge : edges){
            addEdge(edge[0], edge[1], edge[2]);
            addEdge(edge[1], edge[0], edge[2]);
        }
    }

    //展示图的方式
    public void showGraph(){
        System.out.println("邻接矩阵法建图展示");
        for(int i = 1; i <= curN; i++){
            for(int j = 1; j <= curN; j++){
                if(matrix[i][j] == Integer.MAX_VALUE){
                    System.out.print("∞  ");
                }else{
                    System.out.print(matrix[i][j] + "  ");
                }
            }
            System.out.println();
        }
    }
}

上述代码的测试结果见下
在这里插入图片描述
在这里插入图片描述

测试结果也是很明显的, 证明我们之前的代码是没有问题的, 无向图总体是按照正对角线呈对称分布

邻接表

邻接表是一种动态的建图的方式, 关于大厂的笔试面试题, 邻接表以及完全够用了, 如果涉及到比赛内容的话, 我们会使用链式前向星建图法, 等会我们会介绍到这种算法, 说回来邻接表, 其实就是一个
ArrayList<ArrayList<int(无权)/int>的结构, 也就是顺序表套顺序表的结构, 假如我们要建立一个从点2到点4的边,如果不带权值, 我们就让外层顺序表2下标对应的顺序表添加一个元素4, 如果同时带有一个权值的话, 我们就添加一个数组(假设权值是8)我们就让2下标对应的顺序表添加一个[4,8]数组, 下面是邻接表的代码实现

public class day23 {
    public static void main(String[] args) {
        int[][] edges = {{1, 2, 5}, {5, 3, 1}, {1, 4, 4}, {2, 5, 2}};
//        //测试一下使用邻接矩阵法建图
//        GraphUseMatrix graphUseMatrix = new GraphUseMatrix();
//        graphUseMatrix.build(5);
//        graphUseMatrix.unDirectGraph(edges);
//        graphUseMatrix.showGraph();

        //测试一下邻接表法建图
        GraphUseList graphUseList = new GraphUseList();
        graphUseList.build(5);
        graphUseList.directGraph(edges);
        //graphUseList.unDirectGraph(edges);
        graphUseList.showGraph();
    }
}

/**
 * 邻接表建图的方法(是一种动态的结构)
 * 和邻接矩阵一样, 我们在这里介绍的都是带权的图
 */
class GraphUseList {

    //构建一个当前的点数
    private static int curN = 0;

    //邻接表的主体
    private static ArrayList<ArrayList<int[]>> list = new ArrayList<>();

    //初始化邻接表(传入一个点的数量)
    public void build(int n) {
        //设置当前的点数
        curN = n;
        //上来直接清空邻接表
        list.clear();
        //开始构建顺序表(新添加n+1个列表)
        for (int i = 0; i <= n; i++) {
            list.add(new ArrayList<>());
        }
    }

    //添加边的方法
    private void addEdge(int from, int to, int weight) {
        list.get(from).add(new int[]{to, weight});
    }

    //构建一个有向的图
    public void directGraph(int[][] edges) {
        for (int[] edge : edges) {
            addEdge(edge[0], edge[1], edge[2]);
        }
    }

    //构建一个无向的图
    public void unDirectGraph(int[][] edges) {
        for (int[] edge : edges) {
            addEdge(edge[0], edge[1], edge[2]);
            addEdge(edge[1], edge[0], edge[2]);
        }
    }

    //展示邻接表图的方式
    public void showGraph() {
        System.out.println("邻接表法建图展示");
        for (int i = 1; i <= curN; i++) {
            System.out.print("点" + i + "(邻点/权值) : ");
            for (int[] elem : list.get(i)) {
                System.out.print("[" + elem[0] + "," + elem[1] + "]");
            }
            System.out.println();
        }
    }
}

执行结果如下图所示
在这里插入图片描述
在这里插入图片描述

链式前向星

前两种建图的方式都有着明显的缺点, 第一个虽然是静态空间但是空间的大小过大, 第二个虽然使用的空间不是很大但是是一种动态的结果, 就会在时间上大打折扣, 所以我们就想要一种既可以是静态空间又可以做到省空间省时间的结构, 我们就需要下面的链式前向星建图法, 这个方法有点类似与前缀树的静态空间建树法(之前有)和链表头插法的结合, 下面我们分析一下建图的过程

准备过程 :
我们准备三个数组, 把每一个加入的边都设置一个编号(从1开始逐渐增加)

数组数组解释
head数组下标表示点的编号, head[i]表示这个点的’头边’的编号
next数组下标表示边的编号, next[i]表示这个边的下一条边的编号
to数组下标表示边的编号, to[i]表示这条边到达的点的编号
weight数组下标表示边的编号, weight[i]表示这条边的权值

具体的例子
假设此时我们准备了五个点, 然后执行4次加边的操作
head数组长度准备为 6 (5 + 1) , next, to, weight 数组的长度都准备 5 (4 + 1)
下面执行加边的过程

[3,1,2], 这条边编号为1, 出发点是3, 终点是1, 权值是2, 在head中插入头边head[3] = 1(这里就是用的头插法, 这里是首条边), next[1] = 0 (头边所以是0) , to[1] = 1, weight[1] = 2

代码实现如下

public class day23 {
    public static void main(String[] args) {
        int[][] edges = {{1, 2, 5}, {5, 3, 1}, {1, 4, 4}, {2, 5, 2}};
//        //测试一下使用邻接矩阵法建图
//        GraphUseMatrix graphUseMatrix = new GraphUseMatrix();
//        graphUseMatrix.build(5);
//        graphUseMatrix.unDirectGraph(edges);
//        graphUseMatrix.showGraph();

//        //测试一下邻接表法建图
//        GraphUseList graphUseList = new GraphUseList();
//        graphUseList.build(5);
//        graphUseList.unDirectGraph(edges);
//        graphUseList.showGraph();

        //测试一下链式前向星建图
        GraphUseLinkedStar graphUseLinkedStar = new GraphUseLinkedStar();
        graphUseLinkedStar.build(5);
        graphUseLinkedStar.unDirectGraph(edges);
        graphUseLinkedStar.showGraph();
    }
}


/**
 * 链式前向星建图法(静态的建图法)
 */
class GraphUseLinkedStar {
    //定义点最大值
    private static final int MAXN = 11;
    //定义边最大值
    private static final int MAXM = 22;
    //构建head数组(以点为准)
    private static final int[] head = new int[MAXN];
    //构建next数组(以边为准)
    private static final int[] next = new int[MAXM];
    //准备to数组(以边为主)
    private static final int[] to = new int[MAXM];
    //准备weight数组(以边为主)
    private static final int[] weights = new int[MAXM];
    //定义一下当前点的个数
    private static int curN = 0;
    //定义一个计数器用于给边编号
    private static int cnt = 0;

    //传入一个点数n用来初始化图结构
    public void build(int n){
        //更新计数器
        cnt = 1;
        //初始化当前节点个数
        curN = n;
        //清除head即可(这里不用重置to, weights, next)
        Arrays.fill(head, 1, n + 1, 0);
    }

    //添加边的方法(其实就是链表的头插法)
    private void addEdge(int from, int ton, int weight){
        next[cnt] = head[from];
        to[cnt] = ton;
        weights[cnt] = weight;
        head[from] = cnt++;
    }

    //构建一个有向带权图
    public void directGraph(int[][] edges){
        for(int[] edge : edges){
            addEdge(edge[0], edge[1], edge[2]);
        }
    }

    //构建一个无向带权图
    public void unDirectGraph(int[][] edges){
        for(int[] edge : edges){
            addEdge(edge[0],edge[1],edge[2]);
            addEdge(edge[1],edge[0],edge[2]);
        }
    }

    //展示图的方法(类似于链表的遍历)
    public void showGraph(){
        System.out.println("链式前向星建图展示");
        for(int i = 1; i <= curN; i++){
            System.out.print("点" + i + "(邻点/权值) : ");
            for(int ei = head[i]; ei != 0; ei = next[ei]){
                System.out.print("[" + to[ei] + "," + weights[ei] + "]");
            }
            System.out.println();
        }
    }
}

执行结果如下
在这里插入图片描述

在这里插入图片描述

拓扑排序

拓扑排序基础原理介绍

拓扑排序的存在条件是在一个有向且无环图中的排序, 拓扑排序在某种程度上反应的是一件事的执行的先后顺序, 请看下图

在这里插入图片描述
这张图中, 黑色的字符表示节点的名称, 蓝色的数字指的是该位置的入度是多少, 上面我们提到过, 拓扑排序的过程可以视为完成某一件事的先后顺序, 假设我们最终想要完成的任务是f, 那我们下面的字符的序列也就是完成最终事件的顺序
拓扑排序不是唯一的, 比如下面的图
在这里插入图片描述
在这张图中, 先完成a还是先完成b都是可以的, 所以产生的拓扑排序的情况就有两种, 这就说明拓扑排序的结果可能有多种, 只要满足每一个节点的前面所需要的节点都完成了就可以
拓扑排序的应用举例 :
比如在计算机编译程序的过程中, 编译一个程序需要另外的程序结果才能编译完成, 所以很自然的就会出现程序编译的先后顺序问题, 见下图, 左侧的表格是编译一个程序所需要的已经完成的编译结果, 右面是完成a的编译过程的图, 右下角的两串字符串都是完成的顺序, 这同样说明拓扑排序不是唯一的
在这里插入图片描述
拓扑排序要求一个图有向, 这个很好理解, 做事情的步骤肯定是有先后的顺序的, 而且要无环, 这个我们就拿上面的编译过程理解, 假如a的编译过程依赖b, b的编译过程依赖a, 那这不就是矛盾了吗, 两者互相依赖谁也完成不了

拓扑排序步骤解析

实现拓扑排序用的是零入度删除法, 需要用到队列(特殊状况下用小根堆), 核心就是删除0入度的节点和因为该节点所造成的入度影响
这就解释了上面的图为什么我们要进行入度的标记, 首先图解一下0入度删除法的步骤
在这里插入图片描述
最终组合出来的删除节点的步骤就是最终的拓扑排序的答案

拓扑排序模板leetcode-课程表

leetcode210-课程表题目链接
在这里插入图片描述

class Solution {
    //我们采用邻接表建图就已经够用了
    private static ArrayList<ArrayList<Integer>> list = new ArrayList<>();
    //课的最大数目
    private static final int MAXN = 2001;
    //同时定义一个入读表
    private static final int[] indegree = new int[MAXN];
    //当前的数据个数
    private static int curN = 0;

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        build(numCourses);
        directGraph(prerequisites);
        //定义一个计数器看一看队列弹出了多少次(建议使用数组形式队列)
        int cntNum = 0;
        Queue<Integer> queue = new ArrayDeque<>();
        //首先遍历入度数组加入入度为0的节点
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) {
                queue.offer(i);
            }
        }

        //从队列中逐渐弹出元素进行判断
        int[] res = new int[numCourses];
        while (!queue.isEmpty()) {
            int index = queue.poll();
            //在返回数组中添加上该元素
            res[cntNum++] = index;
            //遍历其所属的列表删除这个元素造成的入度
            for(int elem : list.get(index)){
                if(--indegree[elem] == 0){
                    queue.offer(elem);
                }
            }
        }
        return cntNum == res.length ? res : new int[0];
    }

    //初始化图结构
    private static void build(int n) {
        //更新当前的数据个数
        curN = n;
        //更新顺序表
        list.clear();
        for (int i = 0; i < n; i++) {
            list.add(new ArrayList<>());
        }
        //更新入度表
        Arrays.fill(indegree, 0, n, 0);
    }

    //建图的主函数(本质是有向无权图), 建图的过程中同时完成入度的统计
    private static void directGraph(int[][] edges) {
        for (int[] edge : edges) {
            addEdge(edge[1], edge[0]);
            indegree[edge[0]]++;
        }
    }

    //插入边的函数
    private static void addEdge(int from, int to) {
        list.get(from).add(to);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2130541.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java邮件:如何配置以实现自动化邮件通知?

Java邮件发送性能优化策略&#xff1f;怎么实现Java 发邮件功能&#xff1f; Java邮件API提供了一个强大且灵活的框架&#xff0c;使得开发者能够轻松地集成邮件发送功能到他们的应用程序中。AokSend将详细介绍如何配置Java邮件&#xff0c;以实现自动化邮件通知。 Java邮件&…

大数据新视界 --大数据大厂之数据治理之道:构建高效大数据治理体系的关键步骤

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Leetcode 盛水最多的容器

算法思路&#xff1a; Explanation: Two-pointer technique: Start with two pointers, one at the beginning (left) and one at the end (right) of the height array.Calculate the area formed between the two vertical lines. The area is calculated as: Area ( righ…

Leetcode Hot 100刷题记录 -Day14(矩阵置0)

矩阵置0 问题描述&#xff1a; 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,1,1],[1,0,1],[1,1,1]] 输出&#xff1a;[[1,0,1],[0,0,0],[1,0,1]]示例 2&#xff1a;…

如何看待2020年的最新国际视频编解码标准H.266?

技术背景与发布 H.266&#xff0c;也被称为多功能视频编码&#xff08;Versatile Video Coding&#xff0c;简称VVC&#xff09;&#xff0c;是2020年定稿并发布的最新一代国际视频编码标准。 发布时间与机构&#xff1a;H.266由ISO/IEC MPEG和ITU-T VCEG联合制定&#xff0c;…

初学者指南:MyBatis 入门教程

主要介绍了Mybatis的基本使用、JDBC、数据库连接池、lombok注解&#xff01; 文章目录 前言 什么是Mybatis? 快速入门 使用Mybatis查询所有的用户信息 配置SQL提示 JDBC介绍 Mybatis 数据库连接池 lombok 总结 前言 主要介绍了Mybatis的基本使用、JDBC、数据库连接…

Prometheus 监控平台(Prometheus Monitoring Platform)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

使用TableGeneration生成表格数据集

使用TableGeneration生成表格数据集 1.TableGeneration介绍2.TableGeneration使用2.1配置环境2.2下载Chrome驱动2.3将驱动放入指定目录2.4 验证环境 3参数说明 1.TableGeneration介绍 TableGeneration是一个可以生成多种格式表格图片的工具&#xff0c;主要用于批量生成给AI训…

Element UI入门笔记(个人向)

Element UI入门笔记 将页面分割为一级菜单、二级菜单、导航栏三个部分&#xff1b;使用npm下载安装&#xff0c;使用语句npm i element-ui -s; 布局组件 el-form 用于创建和管理表单&#xff1b;从属性上看&#xff1a; :model&#xff1a;用于双向数据绑定&#xff0c;将表单…

轮询解决方案

概述 轮询的使用场景&#xff1a; 股票 K 线图聊天重要通知&#xff0c;实时预警 这些场景都是都要实时性的。 http 是请求响应模式&#xff0c;一定需要先请求&#xff0c;后响应。 解决方案&#xff1a; 短轮询&#xff1a;interval 定时发送请求。问题&#xff1a;大量…

18 C语言实现深度优先搜索

#include "stdio.h" #include "stdlib.h" #include "stdbool.h"#define MaxVertex 10typedef char ElemType;typedef struct Node { //链表中的值int nextVertex;//指向的位置struct Node *next; } Node;struct HeadNode {//链表头ElemType data…

第309题|证明函数单调有界的核心思路 |武忠祥老师每日一题

解题思路&#xff1a;两个极限存在准则&#xff1a;1.夹闭。 2.单调有界。 这里题目告诉了我们f(x)的导数,如果我们判断出了 导数的正负&#xff0c;就能得出f&#xff08;x&#xff09;的单调性。 显然是大于0的&#xff0c;看后半部分:是否大于0&#xff0c;这里直接比较和…

借助大模型将文档转换为视频

利用传统手段将文档内容转换为视频&#xff0c;比如根据文档内容录制一个视频&#xff0c;不仅需要投入大量的时间和精力&#xff0c;而且往往需要具备专业的视频编辑技能。使用大模型技术可以更加有效且智能化地解决上述问题。本实践方案旨在依托大语言模型&#xff08;Large …

[001-03-007].第26节:分布式锁迭代1->基于setnx命令实现分布式锁:

我的博客大纲 我的后端学习大纲 1、setnx命令&#xff1a; 2、逻辑梳理&#xff1a; 1.借助于redis中的命令setnx(key, value)&#xff0c;key不存在就新增&#xff0c;存在就什么都不做。同时有多个客户端发送setnx命令&#xff0c;只有一个客户端可以成功&#xff0c;返回1&…

velero v1.14.1迁移kubernetes集群

1 概述 velero是vmware开源的一个备份和恢复工具&#xff0c;可作用于kubernetes集群下的任意对象和应用数据&#xff08;PV上的数据&#xff09;。github地址是https://github.com/vmware-tanzu/velero。 对于应用数据&#xff0c;可分文件级别的复制和块级别的复制。文件级…

大模型国产化算力方案

方案1 - 摩尔线程 MTT S4000 | 摩尔线程 大模型训练平台架构 摩尔线程大模型训练平台&#xff0c;完全兼容 CUDA 和 Pytorch 训练系统&#xff0c;支持 Megatron-LM、DeepSpeed、FSDP 和 Colossal-AI 等大模型分布式训练框架。具有全兼容、高性能、高灵活性和简单易用等特点…

【算法系列】双指针:283.移动零

目录 双指针介绍 283.移动零 1.题目介绍 2. 解决思路&#xff1a; 动图演示 代码&#xff1a; 双指针介绍 常⻅的双指针有两种形式&#xff0c;⼀种是对撞指针&#xff0c;⼀种是快慢指针。 对撞指针&#xff1a;一般用于顺序结构中&#xff0c;也称左右指针。 1、对撞指…

dp+观察,CF 1864 D. Matrix Cascade

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 https://codeforces.com/problemset/problem/1864/D 二、解题报告 1、思路…

在虚拟机安装mysql数据库

一、安装步骤&#xff08;下载包-传输软件包-安装包-启用仓库-使用yum安装服务器&#xff09; 1、要在mysql官网下载yum仓库包 2、下载好rpm包后&#xff0c;将其通过xftp传输到root目录下 3、使用sudo yum install yum的仓库名&#xff08;sudo yum install mysql-community-…

当人工智能聊天机器人出现问题时

在快速发展的人工智能领域&#xff0c;出现了一项新的挑战。“人工智能私语者”正在通过说服行为良好的聊天机器人打破自己制定的规则来探索人工智能伦理的界限。 这些漏洞被称为即时注入或“越狱”&#xff0c;它们暴露了人工智能系统的漏洞&#xff0c;引发了人们对其安全性…