【洛谷】图论入门:从基本概念到代码实现(邻接矩阵、邻接表、DFS/BFS)

【洛谷】图论入门:从基本概念到代码实现(邻接矩阵、邻接表、DFS/BFS)

文章目录


一、图的基本概念

图的定义

图 G 是由顶点集 V 和边集 E 组成,记为 G=(V,E),其中 V(G) 表示图 G 中顶点的有限非空集;E(G) 表示图 G 中顶点之间的关系(边)集合。若 V={v1​,v2​,…,vn​},则用 ∣V∣ 表示图 G 中顶点的个数,也称图 G 的阶,E={(u,v)∣u∈V,v∈V},用 ∣E∣ 表示图 G 中边的条数。

图是较线性表和树更为复杂的数据结构。
线性表中,除第一个和最后一个元素外,每个元素只有一个唯一的前驱和唯一的后继结点,元素和元素之间是一对一的关系;
在树形结构中,数据元素之间有着明显的层次关系,每个元素有唯一的双亲,但可能有多个孩子,元素和元素之间是一对多的关系;
而图形结构中,元素和元素之间的关系更加复杂,结点和结点之间的关系是任意的,任意两个结点之间都可能相关,图中元素和元素之间是多对多的关系。

在这里插入图片描述

有向图和无向图

在这里插入图片描述


在这里插入图片描述

简单图与多重图

在这里插入图片描述


在这里插入图片描述

稠密图和稀疏图

在这里插入图片描述

顶点的度

顶点 v 的度是指与它相关联的边的条数,记作 deg(v)。由该顶点发出的边称为顶点的出度,到达该顶点的边称为顶点的入度。

无向图中,顶点的度等于该顶点 的入度(indeg)和出度(outdev),即 deg(v)=indeg(v)=outdeg(v)。
有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点 v 的入度 indeg(v) 是以 v 为终点的有向边的条数,顶点 v 的出度 outdeg(v) 是以 v 为起始点的有向边的条数,deg(v)=indeg(v)+outdeg(v)。

在这里插入图片描述

路径

在这里插入图片描述

简单路径与回路

在这里插入图片描述

路径长度和带权路径长度

在这里插入图片描述

子图

在这里插入图片描述


G1_1 和 G1_2 为无向图 G1 的子图,G1_1 为 G1 的生成子图(G1_1包含G1的所有顶点)。
G2_1 和 G2_2 为有向图 G2 的子图,G2_1 为 G2 的生成子图(G2_1包含G2的所有顶点)。

连通图与连通分量

在这里插入图片描述
  1. 连通(连通性)
    定义:在无向图中,如果从顶点 v1​ 到 v2​ 存在路径,就称 v1​ 和 v2​ 是连通的。
    连通图:如果图中任意两个顶点都连通,这个图就是连通图;否则是非连通图。
    判定条件:若一个有 n 个顶点的图,边数小于 n−1,则它一定是非连通图。
  2. 极大连通子图
    定义:在无向图中,一个子图如果满足:
    它本身是连通的;
    再加入图中任何其他顶点或边,它就不再连通。那么这个子图就是极大连通子图。
  3. 连通分量
    定义:无向图中的极大连通子图,就称为该图的连通分量。
    直观理解:一个非连通图可以被拆分成若干个互不相交的连通子图,每一个这样的子图就是一个连通分量。例如,图中的无向图 G 就被拆分成了 3 个连通分量。

概念关系总结
连通是顶点之间的二元关系。
连通图是满足 “任意两顶点都连通” 这一整体性质的图。
极大连通子图是从图中 “抠” 出来的、不能再大的连通子图。
连通分量就是无向图中的所有极大连通子图,是对非连通图结构的分解。

生成树

在这里插入图片描述


简单理解就是把原始图所有顶点拿出来在保证所有顶点都连通的前提下,用最少的边把它们连起来串成一颗1对多的树。

二、图的存储

图的存储有两种:邻接矩阵和邻接表:

  • 其中,邻接表的存储⽅式与树的孩⼦表⽰法完全⼀样。因此,⽤ vector 数组以及链式前向星就能实现。
  • ⽽邻接矩阵就是⽤⼀个⼆维数组,其中 edges[i][j] 存储顶点 i 与顶点 j 之间,边的信息。

图的遍历分两种:DFS 和 BFS,和树的遍历⽅式以及实现⽅式完全⼀样。因此,可以仿照树这个数据结构来学习。

邻接矩阵

邻接矩阵,指用一个矩阵 (即二维数组) 存储图中边的信息 (即各个顶点之间的邻接关系),存储顶点之间邻接关系的矩阵称为邻接矩阵。
对于带权图而言,若顶点 vi​ 和 vj​ 之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶点 vi​ 和 vj​ 不相连,则用 ∞ (若权值都为正数,也可用-1)来代表这两个顶点之间不存在边。
对于不带权的图,可以创建一个二维的 bool 类型的数组,来标记顶点 vi​ 和 vj​ 之间有边相连。

在这里插入图片描述


矩阵中元素个数为 n×n,即空间复杂度为 O(n2),n 为顶点个数,和实际边的条数无关,适合存储稠密图。

#include<iostream>#include<cstring>usingnamespace std;constint N =1010;// 结点个数、边的个数int n, m;// i到j有一条权值为edges[i][j]的边int edges[N][N];intmain(){memset(edges,-1,sizeof edges); cin >> n >> m;// 读入结点个数以及边的个数for(int i =1; i <= n; i++){int a, b, c; cin >> a >> b >> c;// a - b 有一条边,权值为 c edges[a][b]= c;// 如果是无向边,需要反过来再存一下 edges[b][a]= c;}return0;}

邻接表

用邻接表存储图类似于我们之前存储树的孩子表示法,孩子表示法是把某个结点的所有孩子结点以链表的形式挂在该节点后面,而对于图来说就是把和某个结点相邻的全部结点以链表的形式挂在该节点后面。

对于链表来说有两种存储方式:vector数组和链式向前星,不理解的读者点这里:vector数组和链式向前星详细介绍

vector 数组

和树的存储⼀模⼀样,只不过如果存在边权的话,我们的 vector 数组⾥⾯放⼀个结构体或者是 pair 即可。

#include<iostream>#include<vector>usingnamespace std;// <与哪条边相连,该边的权值>typedef pair<int,int> PII;constint N =1e5+10;// 结点个数、边的个数int n, m;//结点i到结点edges[i].first.有一条权值为edges[i].second的边 vector<PII> edges[N];intmain(){ cin >> n >> m;// 读入结点个数以及边的个数for(int i =1; i <= m; i++){int a, b, c; cin >> a >> b >> c;// a 和 b 之间有一条边,权值为 c edges[a].push_back({b, c});// 如果是无向边,需要反过来再存一下 edges[b].push_back({a, c});}return0;}

链式前向星

和树的存储⼀模⼀样,只不过如果存在边权的话,我们多创建⼀维数组w,⽤来存储边的权值即可。

#include<iostream>usingnamespace std;constint N =1e5+10;// 链式前向星int h[N], e[N *2], ne[N *2], w[N *2], id;int n, m;// 其实就是把 b 头插到 a 所在的链表后面voidadd(int a,int b,int c){ id++; e[id]= b; w[id]= c;// 多存一个权值信息 ne[id]= h[a]; h[a]= id;}intmain(){ cin >> n >> m;// 读入结点个数以及边的个数for(int i =1; i <= m; i++){int a, b, c; cin >> a >> b >> c;// a 和 b 之间有一条边,权值为 cadd(a, b, c);add(b, a, c);}return0;}

数组含义
h[N]:表头数组,h[a] 表示从节点 a 出发的第一条边在 e 数组中的下标。初始值为 -1(表示没有边)。
e[N2]:存储每条边指向的终点节点编号。
ne[N
2]:存储同一起点的下一条边的下标,用于构成链表。
w[N*2]:存储每条边的权值。
id:全局计数器,用于分配边的下标。

add 函数
这个函数的作用是在图中添加一条从 a 指向 b,权值为 c 的有向边。它采用 “头插法” 将新边插入到以 a 为起点的链表头部:
id++:为新边分配一个唯一的下标。
e[id] = b:记录这条边指向的终点是 b。
w[id] = c:记录这条边的权值是 c。
ne[id] = h[a]:让新边的 ne 指针指向原来的表头,建立链表连接。
h[a] = id:更新表头,让新边成为从 a 出发的第一条边。

main 函数
首先读入节点数 n 和边数 m。
然后循环 m 次,每次读入一条边的两个端点 a、b 和权值 c。
因为是无向图,所以需要调用两次 add 函数:
add(a, b, c):添加一条从 a 到 b 的边。
add(b, a, c):添加一条从 b 到 a 的边,以表示双向连通。

三、图的遍历

DFS

在这里插入图片描述


小编以上图为例,用dfs带大家走一遍:
递归:1——>2——>5——>6——>4 此时和4相邻的1、2都遍历过了,所以要递归返回:4——>6,对于6来说6相邻的2、5都遍历过了,故该结点是叶子结点,要继续返回(后续递归返回的结点5、2也是与之相邻的结点都遍历过了,直接返回,小编就不一一说明了):6——>5——>2——>1 此时对于1来说还有3没遍历过,所以继续递归:1——>3,3也是叶子结点,递归返回:3——>1.递归结束。

//邻接矩阵#include<iostream>#include<cstring>#include<queue>usingnamespace std;constint N =1010;int n, m;int edges[N][N];bool st[N];// 标记哪些点已经访问过voiddfs(int u){ cout << u << endl; st[u]=true;// 遍历所有孩⼦for(int v =1; v <= n; v++){// 如果存在 u->v 的边,并且没有遍历过if(edges[u][v]!=-1&&!st[v]){dfs(v);}}}intmain(){memset(edges,-1,sizeof edges); cin >> n >> m;// 读⼊结点个数以及边的个数for(int i =1; i <= m; i++){int a, b, c; cin >> a >> b >> c;// a - b 有⼀条边,权值为 c edges[a][b]= c;// 如果是⽆向边,需要反过来再存⼀下 edges[b][a]= c;}return0;}
//邻接表——vector数组#include<iostream>#include<vector>#include<queue>usingnamespace std;typedef pair<int,int> PII;constint N =1e5+10;int n, m; vector<PII> edges[N];bool st[N];// 标记哪些点已经访问过voiddfs(int u){ cout << u << endl; st[u]=true;// 遍历所有孩⼦for(auto& t : edges[u]){// u->v 的⼀条边,权值为 wint v = t.first, w = t.second;if(!st[v]){dfs(v);}}}intmain(){ cin >> n >> m;// 读⼊结点个数以及边的个数for(int i =1; i <= m; i++){int a, b, c; cin >> a >> b >> c;// a 和 b 之间有⼀条边,权值为 c edges[a].push_back({ b, c });// 如果是⽆向边,需要反过来再存⼀下 edges[b].push_back({ a, c });}return0;}
//邻接表——链式向前星#include<iostream>#include<queue>usingnamespace std;constint N =1e5+10;// 链式前向星int h[N], e[N *2], ne[N *2], w[N *2], id;int n, m;// 其实就是把 b 头插到 a 所在的链表后⾯voidadd(int a,int b,int c){ id++; e[id]= b; w[id]= c;// 多存⼀个权值信息 ne[id]= h[a]; h[a]= id;}bool st[N];voiddfs(int u){ cout << u << endl; st[u]=true;// 遍历所有的孩⼦for(int i = h[u]; i; i = ne[i]){// u->v 的⼀条边int v = e[i];if(!st[v]){dfs(v);}}}intmain(){ cin >> n >> m;// 读⼊结点个数以及边的个数for(int i =1; i <= m; i++){int a, b, c; cin >> a >> b >> c;// a 和 b 之间有⼀条边,权值为 cadd(a, b, c);add(b, a, c);}return0;}

BFS

//临接矩阵#include<iostream>#include<cstring>#include<queue>usingnamespace std;constint N =1010;int n, m;int edges[N][N];bool st[N];// 标记哪些点已经访问过voidbfs(int u){ queue<int> q; q.push(u); st[u]=true;while(q.size()){auto a = q.front(); q.pop(); cout << a << endl;for(int b =1; b <= n; b++){if(edges[a][b]!=-1&&!st[b]){ q.push(b); st[b]=true;}}}}intmain(){memset(edges,-1,sizeof edges); cin >> n >> m;// 读⼊结点个数以及边的个数for(int i =1; i <= m; i++){int a, b, c; cin >> a >> b >> c;// a - b 有⼀条边,权值为 c edges[a][b]= c;// 如果是⽆向边,需要反过来再存⼀下 edges[b][a]= c;}return0;}
//邻接表——vector数组#include<iostream>#include<vector>#include<queue>usingnamespace std;typedef pair<int,int> PII;constint N =1e5+10;int n, m; vector<PII> edges[N];bool st[N];// 标记哪些点已经访问过voidbfs(int u){ queue<int> q; q.push(u); st[u]=true;while(q.size()){auto a = q.front(); q.pop(); cout << a << endl;for(auto& t : edges[a]){int b = t.first, c = t.second;if(!st[b]){ q.push(b); st[b]=true;}}}}intmain(){ cin >> n >> m;// 读⼊结点个数以及边的个数for(int i =1; i <= m; i++){int a, b, c; cin >> a >> b >> c;// a 和 b 之间有⼀条边,权值为 c edges[a].push_back({ b, c });// 如果是⽆向边,需要反过来再存⼀下 edges[b].push_back({ a, c });}return0;}
//邻接表——链式向前星#include<iostream>#include<queue>usingnamespace std;constint N =1e5+10;// 链式前向星int h[N], e[N *2], ne[N *2], w[N *2], id;int n, m;// 其实就是把 b 头插到 a 所在的链表后⾯voidadd(int a,int b,int c){ id++; e[id]= b; w[id]= c;// 多存⼀个权值信息 ne[id]= h[a]; h[a]= id;}bool st[N];voidbfs(int u){ queue<int> q; q.push(u); st[u]=true;while(q.size()){auto a = q.front(); q.pop(); cout << a << endl;for(int i = h[a]; i; i = ne[i]){int b = e[i], c = w[i];if(!st[b]){ q.push(b); st[b]=true;}}}}intmain(){ cin >> n >> m;// 读⼊结点个数以及边的个数for(int i =1; i <= m; i++){int a, b, c; cin >> a >> b >> c;// a 和 b 之间有⼀条边,权值为 cadd(a, b, c);add(b, a, c);}return0;}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

Read more

用 python 开发一个可调用工具的 AI Agent,实现电脑配置专业评价

用 python 开发一个可调用工具的 AI Agent,实现电脑配置专业评价

在人工智能时代,AI Agent凭借其强大的任务处理能力,逐渐成为开发人员手中的得力工具。今天,我们就来一起动手,用Python打造一个能够调用工具的AI Agent,实现根据电脑信息对电脑配置进行专业评价的功能。 一、项目创建与目录结构 1.1 项目创建 首先,我们需要创建一个新的项目环境。这里使用UV进行项目创建。 uv init demo 项目创建完成后,进入项目文件夹并安装必要的包, 比如安装psutil uv add psutil 安装的包都会记录在pypoject.toml, 看看我都安装了哪些包 [project] name = "demo" version = "0.1.0" description = "Add your description here" readme = "README.

By Ne0inhk
IPIDEA网页抓取API实战:全自动化实现eBay商品数据采集与Python接入

IPIDEA网页抓取API实战:全自动化实现eBay商品数据采集与Python接入

前言:跨境电商数据采集痛点与需求 随着跨境电商、数据驱动决策以及AI模型训练的需求不断增长,开发者与企业需要稳定、合规、可规模化 的网页数据抓取方案。但实际落地往往困难重重:高强度抓取、IP无法访问、JS渲染、数据格式不统一,这些让数据采集的技术门槛与成本居高不下。本篇将带你实操IPIDEA网页抓取API,并构建一个 可直接投入使用的eBay商品信息采集工具,一步步完成抓取、解析到下载的全过程,帮助你快速掌握全球电商数据采集的核心方法。 为什么需要网页抓取API 在跨境电商运营、市场竞品调研、AI模型训练等核心业务场景中,企业与开发者往往需要获取公开的电商商品信息、竞品动态等关键数据,但直接开展数据采集工作会面临三大核心痛点: 抓取门槛居高不下:Amazon、eBay等主流平台普遍部署了验证码校验、IP访问管理、JS动态渲染等多重抓取机制,若自研抓取系统,不仅需要持续投入人力进行技术突破与迭代,还会面临采集稳定性差、数据获取中断等问题,综合成本居高不下 合规风险难以规避:未经合规授权的公开数据采集行为,容易触碰GDPR、CCPA等国际数据合规法规;同时普通代理IP无法满足 “

By Ne0inhk

【Python】6 种方法轻松将 Python 脚本打包成 EXE 应用

以下是 2025–2026 年最实用的 6 种 Python 脚本打包成 Windows EXE 可执行文件 的主流方法,按易用性 × 普及度 × 实际场景排序。 排名方法/工具易用性生成文件大小启动速度运行速度反编译难度典型场景推荐指数 (★5)1PyInstaller★★★★★大(onefile 常 50–300MB)慢(几秒~几十秒)普通低绝大多数 GUI、小工具、初次尝试★★★★★2auto-py-to-exe★★★★★同 PyInstaller同上普通低零基础用户、GUI 操作打包★★★★☆3Nuitka★★★★☆中~小快明显更快(1.5–4×)中~高性能敏感、数值计算、想保护代码★★★★☆4cx_Freeze★★★★中较快普通低~中追求启动快、

By Ne0inhk

Python从0到100完整学习指南(必看导航)

Python 从 0 到 100 完整学习路线(2025–2026 实用版) 这是一条目前在中文社区被验证最多次、性价比最高、就业/副业/考研/转行都适用的 Python 学习路径。 分为 8 个大阶段,每个阶段给出: * 核心目标 * 推荐学习时长(每天 2–4 小时估算) * 最值得学的资源(2025–2026 仍活跃且评价最高的) * 必须掌握的技能清单 * 阶段性小目标 / 实战项目建议 阶段划分总览表 阶段名称目标人群建议时长累计总时长核心关键词0准备期完全零基础3–7 天1 周环境、IDE、学习心态1Python 基础语法零基础 → 能写小工具3–6 周1–2 个月变量、循环、函数、类2Pythonic

By Ne0inhk