当前位置: 首页 > news >正文

图论基础算法入门笔记

图论基础与建图

  1. 图的定义
    图是由若干给定的顶点及连接两顶点的边所构成的图形,顶点用于代表事物,连接两顶点的边用于表示两个事物间的特定关系。
  2. 建图的概念
    建图是指找到合适的方法将图表示出来。

图的存储方法

直接存边
  1. 存储方式:直接使用一个数组,将边的起点与终点信息存储。
  2. 代码实现
#include<bits/stdc++.h>
using namespace std; 
struct Edge{ int u,v; // 边的起点和终点
}; 
int n,m; // n为顶点数,m为边数
vector<Edge> e; // 存储边的向量
vector<bool> vis; // 标记顶点是否被访问void dfs(int u) {if(vis[u]) return; // 如果顶点已访问,直接返回vis[u] = true; // 标记顶点为已访问for(int i=1;i<=m;i++) if(e[i].u==u) dfs(e[i].v); // 遍历以u为起点的边,递归访问终点
}

特点:这种存储方法的遍历效率低下,一般用于需要对边权进行排序的 Kruskal 算法。

邻接矩阵
  1. 存储方式:使用一个二维数组保存边,如果是带权图可以存储边权。
  2. 代码实现
#include<bits/stdc++.h>
using namespace std; 
int n,m; // n为顶点数,m为边数
vector<bool> vis; // 标记顶点是否被访问
vector<vector<bool>> G; // 邻接矩阵void dfs(int u) {if(vis[u]) return ; // 如果顶点已访问,直接返回vis[u] = true; // 标记顶点为已访问for(int v=1; v<=n;v++) if(G[u][v]) dfs(v); // 遍历与u相连的顶点,递归访问
}int main() {cin>>n>>m; // 输入顶点数和边数for(int i=1;i<=m;i++) {int u,v; // 输入边的起点和终点//int t; // 若为带权图,此处存储权值cin>>u>>v; //cin>>t;G[u][v] = 1; // 如果是带权图则记录权值}
}

特点:邻接矩阵对于稀疏图的效率较低,一般用于稠密图。

邻接表
  1. 存储方式:通过存储各点的所有出边信息表示图。
  2. 代码实现
#include<bits/stdc++.h>
using namespace std; 
int n,m; // n为顶点数,m为边数
vector<bool> vis; // 标记顶点是否被访问
vector<vector<int>> G; // 邻接表// 查找是否存在从u到v的边
bool find_edge(int u,int v) { for(int i=0;i<=G[u].size();i++) if(G[u][i]==v) return true; return false; 
}void dfs(int u) { if(vis[u]==true) return; // 如果顶点已访问,直接返回vis[u]=true; // 标记顶点为已访问for(int i=0;i<G[u].size();i++) dfs(G[u][i]); // 遍历u的所有出边,递归访问终点
}int main() { cin>>n>>m; // 输入顶点数和边数vis.resize(n+1); // 调整vis的大小G.resize(n+1); // 调整邻接表的大小for(int i=1;i<=m;i++) {int u,v; // 输入边的起点和终点cin>>u>>v; G[u].push_back(v); // 将边添加到邻接表中}return 0; 
}

二分图判定

  1. 二分图定义
    二分图,又称二部图,是一类结构特殊的图,它的顶点集可以划分为两个互不相交的子集,使得图中的每条边都连接这两个集合之间的一对点,而不会连接同一集合内部的点。
  2. 图着色问题
    把相邻顶点染成不同颜色的问题叫做图着色问题。
  3. 二分图判定算法
    算法思路:通过深度优先搜索对图进行着色,若相邻顶点颜色相同则不是二分图。

 简单来说,就是只有2种颜色,依次对层级(深度)进行两种染色,假如是一颗树,他就是二分图,每一层深度颜色交替的染。如果此时第三深度的节点和第一深度的节点有连线,那么很明显颜色一样,此时判断颜色一样,所以不是二分图了(二分图不止有树哦)

#include<bits/stdc++.h>
using namespace std; 
vector<vector<int>> G; // 邻接表
vector<int> color,vis; // color存储顶点颜色,vis标记顶点是否被访问
int n,m; // n为顶点数,m为边数// 深度优先搜索进行二分图判定
bool dfs(int v,int c) { color[v]=c; // 给顶点v着色为cfor(int i=0;i<G[v].size();i++) {// 如果相邻顶点颜色与v相同,不是二分图if(color[G[v][i]]==c) return false; // 如果相邻顶点未着色,递归着色并判断if(color[G[v][i]]==0&&!dfs(G[v][i],-c)) return true; }return true; 
}void solve() { for(int i=0;i<n;i++) {if(color[i]==0) { // 如果顶点未着色if(!dfs(i,1)) { // 从该顶点开始着色判定cout<<"NO"<<endl; // 不是二分图return; }}}cout<<"YES"<<endl; // 是二分图return; 
}

最短路问题

单源最短路定义
单源最短路是固定一个起点,求它到其他点的最短路问题

Floyd 算法

算法特点:Floyd 可以处理无论有向无向还是边是正是负的图,但最短路必须存在,且不能有负环。

算法核心:(这里k的空间可以不开,因为都是顺次更新,不开没有影响)

for (k = 1; k <= n; k++) {for (x = 1; x <= n; x++) {for (y = 1; y <= n; y++) {// 状态转移方程,f[k][x][y]表示经过前k个顶点,x到y的最短距离f[k][x][y] = min(f[k - 1][x][y], f[k - 1][x][k] + f[k - 1][k][y]);//是经过k还是不经过k小f[x][y] = min(f[x][y], f[x][k] + f[k][y]);}}
}

Bellman-Ford 算法

算法原理:通过边的松弛操作更新最短距离,公式为dis[i]=min(dis[i],dis[j]+e[j,i]),其中dis[i]是从起点 s 到顶点 i 的最短距离,e[j,i]是 j 到 i 的边权。

算法特点:这种算法不能用于负环

代码实现

#include<bits/stdc++.h>
using namespace std; 
struct Edge { int u,v,w; // 边的起点、终点和权值
}; 
vector<Edge> e; // 存储边的向量
int dis[MAX_N],u,v,w; // dis存储最短距离,u、v、w为边的信息
const int INF = 0x3f3f3f; // 表示无穷大// Bellman-Ford算法
bool bellmanford(int n, int s) { memset(dis, 0x3f, (n + 1) * sizeof(int)); // 初始化最短距离为无穷大dis[s] = 0; // 起点到自身的距离为0bool flag = false; // 标志是否发生更新操作for (int i = 1; i <= n; i++) {flag = false;for (int j = 0; j < e.size(); j++) { // 对边进行遍历u = e[j].u, v = e[j].v, w = e[j].w;if (dis[u] == INF) continue; // 如果起点不可达,跳过// 如果通过当前边可以得到更短的距离,更新最短距离if (dis[v] > dis[u] + w) {dis[v] = dis[u] + w;flag = true; // 标记发生了更新}}// 没有可以更新的边时就停止算法if (!flag) break;}return flag; 
}

SPFA 算法(判负环)

算法核心思想:只有上一次被更新过的节点,其下一次更新的路径才有可能成为最短路,因此用队列维护有意义发生下一次更新的节点。

负环判定:SPFA 也可以用于判断 s 点是否能抵达一个负环,只需记录最短路经过了多少条边,当经过了至少 n 条边时,说明 s 点可以抵达一个负环。

ps:没有负环建议不要用,容易被卡数据

代码实现

#include<bits/stdc++.h>
using namespace std; 
struct Edge { int v,w; // 边的终点和权值
}; 
vector<Edge> e[MAX_N]; // 邻接表存储边
int dis[MAX_N],cnt[MAX_N],vis[MAX_N]; // dis存储最短距离,cnt记录路径边数,vis标记是否在队列中
queue<int> q; // 队列用于维护待更新的节点bool spfa(int n,int s) { memset(dis,0x3f,(n+1)*sizeof(int)); // 初始化最短距离为无穷大dis[s] = 0,vis[s]=1; // 起点距离为0,标记为在队列中q.push(s); // 起点入队while(!q.empty()) {int u = q.front(); // 取出队首元素q.pop(); vis[u] = 0; // 标记为不在队列中for(auto ed : e[u]) { // 遍历u的所有出边int v = ed.v, w=ed.w; // 边的终点和权值// 如果可以通过u得到更短的距离到vif(dis[v]>dis[u]+w) {dis[v]=dis[u] + w; // 更新最短距离cnt[v] = cnt[u]+1; // 路径边数加1// 如果路径边数达到n,说明存在负环if(cnt[v]>=n) return false; // 如果v不在队列中,加入队列if(!vis[v]) q.push(v),vis[v] = 1; }}}return true; 
}

Dijkstra 算法

 Dijkstra 算法#图论-CSDN博客

最短路算法进阶:

分层图最短路(模板)-CSDN博客

最小生成树(MST)

Prim 算法

算法核心思想:加点法,每次选择一个距离最小的节点,用其更新新的边和到其他节点的距离,可使用优先队列进行优化。

代码实现

#include<bits/stdc++.h>
using namespace std; 
struct E { int v, w, x; // v为终点,w为权值,x为下一条边的索引
} e[MAX_M]; 
int n, m, h[MAX_N], cnte; // n为顶点数,m为边数,h[u]表示以u为起点的第一条边的索引,cnte为边计数器// 添加边
void adde(int u, int v, int w) { e[++cnte] = E{v, w, h[u]}, h[u] = cnte; 
} struct S { int u, d; // 顶点和距离
}; 
bool operator<(const S &x, const S &y) { return x.d > y.d; // 优先队列比较函数,小根堆
} 
priority_queue<S> q; // 小根堆优先队列
int dis[MAX_N]; // 存储到各顶点的距离
bool vis[MAX_N]; // 标记顶点是否已加入生成树
int res = 0, cnt = 0; // res为最小生成树的权值和,cnt为已加入的顶点数void Prim() { memset(dis, 0x3f, sizeof(dis)); // 初始化距离为无穷大dis[1] = 0; // 从顶点1开始q.push({1, 0}); // 顶点1入队while (!q.empty()) {if (cnt >= n) break; // 所有顶点已加入,结束int u = q.top().u, d = q.top().d; // 取出距离最小的顶点q.pop(); if (vis[u]) continue; // 如果已加入生成树,跳过vis[u] = true; // 标记为已加入生成树++cnt; // 已加入顶点数加1res += d; // 累加权值for (int i = h[u]; i!=0 ; i = e[i].x) { // 遍历u的所有出边int v = e[i].v, w = e[i].w; // 边的终点和权值// 如果通过u可以得到到v的更短距离if (w < dis[v]) {dis[v] = w; // 更新距离q.push({v, w}); // 将v入队}}}
}

Kruskal 算法

算法核心思想:与 Prim 不同,Kruskal 的基本思想是从小到大加入边

#include <algorithm>
#include <iostream>
using namespace std; 
int fa[1010]; // 定义并查集父节点数组,用于维护连通性
int n, m, k; // n为顶点数,m为边数,k为目标连通块数量
struct edge { int u, v, w; // 边的起点、终点和权值
}; 
int l; 
edge g[10010]; // 存储所有边的数组// 添加边到数组
void add(int u, int v, int w) { l++; // 边计数器加1g[l].u = u; // 记录起点g[l].v = v; // 记录终点g[l].w = w; // 记录权值
} // 并查集查找根节点(带路径压缩)
int findroot(int x) { return fa[x] == x ? x : fa[x] = findroot(fa[x]); // 若x是根节点则返回自身,否则递归查找并压缩路径
} // 合并两个连通块
void Merge(int x, int y) { x = findroot(x); // 找到x的根节点y = findroot(y); // 找到y的根节点fa[x] = y; // 合并两个连通块
} // 边权比较函数,用于排序
bool cmp(edge A, edge B) { return A.w < B.w; // 按边权从小到大排序
} void kruskal() { int tot = 0; // 已选边数计数器int ans = 0; // 最小生成树总权值// 按边权从小到大排序所有边sort(g + 1, g + m + 1, cmp); for (int i = 1; i <= m; i++) { int xr = findroot(g[i].u); // 查找边起点的根节点int yr = findroot(g[i].v); // 查找边终点的根节点if (xr != yr) { // 若两端点不在同一连通块Merge(xr, yr); // 合并连通块tot++; // 已选边数加1ans += g[i].w; // 累加边权// 当连通块数量达到要求时(n-k条边)if (tot >= (n - k)) { cout << ans << '\n'; // 输出总权值return;}}}cout << "No Answer\n"; // 无法形成满足条件的生成树
} int main() { cin >> n >> m >> k; // 输入顶点数、边数、目标连通块数// 初始化并查集,每个节点的父节点为自身for (int i = 1; i <= n; i++) { fa[i] = i;}for (int i = 1; i <= m; i++) { int u, v, w; // 输入边的起点、终点、权值cin >> u >> v >> w;add(u, v, w); // 添加边到数组}kruskal(); // 执行Kruskal算法return 0;
}

Tarjan 

 Tarjan 算法的两种用法-CSDN博客

http://www.lryc.cn/news/579398.html

相关文章:

  • MySQL 8.0 OCP 1Z0-908 题目解析(18)
  • 深度学习2(逻辑回归+损失函数+梯度下降)
  • 在 VSCode 中高效配置自定义注释模板 (无需插件)
  • Python 中如何使用 Conda 管理版本和创建 Django 项目
  • Flowable多引擎架构搭建方案
  • 车载以太网-IP 掩码 vlan 端口
  • 前端的一些报错
  • Odoo 中国特色高级工作流审批模块研发
  • 编程基础:成员函数
  • 【LUT技术专题】3DLUT压缩-CLUT
  • 朝鲜APT组织使用Nim语言恶意软件对macOS发起隐秘Web3与加密货币攻击
  • .net wpf混淆
  • uniapp 使用ffmpeg播放rtsp
  • QT常用类和模块
  • Qt宝藏库:20+实用开源项目合集
  • Java——初始guava(1)
  • 【python】OOP:Object-Oriented Programming
  • Linux基本命令篇 —— tar命令
  • Redis缓存架构实战
  • 微算法科技(NASDAQ MLGO)基于量子图像处理的边缘检测算法:开拓图像分析新视野
  • 中国户外品牌全球竞争力榜单发布:科技突围与文化赋能重塑行业格局
  • 扫地机产品--电池是否存在类似充电宝自燃问题?
  • 【JS笔记】JS 和 noodjs 的常见操作(十)
  • 依赖属性附加属性
  • 从混沌到澄明,AI如何重构我们的决策地图与未来图景
  • CSS `@scope` 实战指南:开启局部样式隔离新时代
  • NVIDIA Spectrum-3 SN4000 系列SN4000 SN4000 系列速度高达 400Gb/秒的现代横向扩展分布式数据中心应用提供支持。
  • React 学习(3)
  • http、SSL、TLS、https、证书
  • KMP(Kotlin Multiplatform)改造(Android/iOS)老项目