数据结构-图编程知识详细总结C++(图创建、图遍历、最短路径、拓扑排序、关键路径)
一、图的存储
- 邻接矩阵,适用于节点数不超过1000个的情况:
const int MAXV = 1000;
int n;
const int INF = 1000000000;
int G[MAXV][MAXV];
bool vis[MAXV] = {false};
- 邻接表
const int MAXV;
int n;
struct node{
int id, v;
}
vector<int> Adj[MAXV];
vector<node> Adj[MAXV];
bool vis[MAXV] = {false};
二、基本遍历(DFS、BFS)
1、DFS
- 邻接矩阵版:主要是G邻接矩阵的初始化,使用fill函数, fill(G[0], G[0] + MAXV * MAXV, INF);
- 以及在判断的时候加上如果两点间距离为INF,表示不连通
fill(G[0], G[0] + MAXV*MAXV, INF);
void DFS(int u){
vis[u] = true;
for(int i = 0; i < n; i++){
if(vis[i] == false && G[u][i] != INF){
DFS(i);
}
}
}
void DFSTrave(){
for(int u = 0; u < n; u++){
if(vis[u] == false){
DFS(u);
}
}
}
- 邻接表版:如果是使用DFS的邻接表,一般是尽量不含有其他参数的,不然尽量使用BFS。
vector<int> Adj[MAXV];
void DFS(int u){
vis[u] = true;
for(int v = 0; v < Adj[u].size(); v++){
if(vis[Adj[u][v]] == false){
DFS(Adj[u][v]);
}
}
}
void DFSTrave(){
for(int v = 0; v < n; v++){
if(vis[v] == false){
DFS(v);
}
}
}
2、BFS
- 邻接表版
struct node{
int id, v;
}
bool vis[MAXV] = {false};
vector<node> Adj[MAXV];
void BFS(int u){
queue<int> q;
q.push(u);
vis[u] = true;
while(!q.empty()){
int now = q.front();
//进行操作
for(int v = 0; v < Adj[now].size(); v++){
int id = Adj[u][v].id;
if(vis[id] == false){
q.push(id);
vis[id] = true;
}
}
}
}
一些需要注意的点
- 如果是那种深度有限的遍历,统计结果最好使用BFS,因为DFS可能出现结果重复,以及缺点的情况。
- 还有就是统计连通块的数量,也就是需要添加的路径使得整个区域连接起来,就是连通块的数量减一;
三、最短路径
1. Dijkstra算法(迪杰斯特拉算法)
- 用于解决单源最短路径问题(无负权图)。
- 邻接矩阵版本
const int MAXN;
const int INF = 1000000000;
int G[MAXN][MAXN];
int d[MAXN];
bool vis[MAXN] = {false};
int n;
void Dijkstra(int s){
fill(d, d + MAXN, INF);
d[s] = 0;
for(int i = 0; i < n; i++){
int u = -1, MIN = INF;
for(int j = 0; j < n; j++){
if(d[j] < MIN && vis[j] == false){
u = j;
MIN = d[j];
}
}
if(u == -1) return;
vis[u] = true;
for(int j = 0; j < n; j++){
if(vis[j] == false && G[u][j] != INF && d[u] + G[u][j] < d[j]){
d[j] = d[u] + G[u][j];
}
}
}
}
- 邻接表版本
struct node{
int v, dis;
};
vector<node> G[MAXV];
int n;
int d[MAXV];
bool vis[MAXV] = {false};
void Dijkstra(int s){
fill(d, d + MAXV, INF);
d[s] = 0;
for(int i = 0; i < n; i++){
int u = -1, MIN = INF;
for(int j = 0; j < n; j++){
if(d[j] < MIN && vis[j] == false){
u = j;
MIN = d[j];
}
}
if(u == -1) return;
vis[u] = true;
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v;
if(vis[v] == false && d[u] + G[u][i].dis < d[v]){
d[v] = d[u] + G[u][i].dis;
}
}
}
}
2) 对于一些情况的改进,如加上点权和边权
- 加新的边权的权值
- 城市点的花费,新增判断,首先是以第一标准为判断条件,如果出现第一判断条件相等,那么再使用第二条件进行判断;记得初始化起点的权值为当前的权值
- 最短路径的条数:使用一个path数组进行存储,如果出现路径小于,就用上面的路径覆盖掉当前路径,如果出现最短路径相等的情况,用那就加上当前的路径。要记得初始化起点的路径为1
- 初始化起点路径为0
- 逻辑没问题,一般是判断的时候 == 写成了 =
3) Dijkstra+DFS
- 对于Dijkstra部分的任务是统计出路径最短的条数即可;
- 注意判断的时候双等号;
- 以及添加的pre数组,存储前驱结点;
- G在初始化的时候是fill(G[0], G[0]+maxn*maxn, INF);
void Dijkstra(int s){
fill(d, d+maxn, INF);
d[s] = 0;
for(int i = 0; i < n; i++){
int u = -1, MIN = INF;
for(int j = 0; j < n; j++){
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
if(u == -1) return;
vis[u] = true;
for(int v = 0; v < n; v++){
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];
pre[v].clear();
pre[v].push_back(u);
}else if(d[u] + G[u][v] == d[v]){
pre[v].push_back(u);
}
}
}
}
}
- 对于DFS部分,是用Dijkstra统计出的路径pre统计第二判定条件下选择最优路径;
- 递归边界是,遍历到结点等于起点st;
- 递归条件是对于当前结点接下来可以到的结点;
- 还有就是回溯,一个是起点要自己临时添加进入tempPath,在返回前还要弹出;遍历完当前节点可以到的结点后也要弹出结点;
void DFS(int v){
if(v == s){
tempPath.push_back(v);
int value = 0;
for(int i = tempPath.size() - 1; i > 0; i--){
int id = tempPath[i], next = tempPath[i - 1];
value += Cost[id][next];
}
if(value < optValue){
optValue = value;
path = tempPath;
}
tempPath.pop_back();
return;
}
tempPath.push_back(v);
for(int i = 0; i < pre[v].size(); i++){
DFS(pre[v][i]);
}
tempPath.pop_back();
}
2. Bellman-Ford算法和SPFA算法
- 用于解决单源最短路径的问题,用于可能出现负环的情况,也就是边中存在负的权值;
- 主要思想也就是判断n-1循环,每次判断其中每一条边,最后一次判断如果,还是会有出现距离变短,说明会出现负环的情况;
- 使用邻接表更加方便
- 跟Dijkstra区别是统计最短路径的条数;如果出现更短的路径还是一样直接覆盖;如果出现一样的路径,那么直接需要创建set进行存储,记住前驱是使用set进行存储的
set<int> pre[maxn];
if(d[u] + dis < d[v]){
d[v] = d[u] + dis;
w[v] = w[u] + weight[u];
num[v] = num[u];
pre[v].clear();
pre[v].insert(u);
}else if(d[u] + dis == d[v]){
if(w[u] + weight[v] > w[v]){
w[v] = w[u] + weight[v];
}
pre[v].insert(u);
num[v] = 0;
set<int>::iterator it;
for(auto it = pre[v].begin(); it != pre[v].end(); it++){
num[v] += num[*it];
}
}
struct node{
int v, dis;
};
vector<node> G[maxn];
int n;
int d[maxn];
bool Bellman(int s){
fill(d, d+maxn, INF);
d[s] = 0;
for(int i = 0; i < n - 1; i++){
for(int u = 0; u < n; u++){
for(int j = 0; j < G[u].size(); j++){
int v = G[u][j].v;
int dis = G[u][j].dis;
if(d[u] + dis < d[v]){
d[v] = d[u] + dis;
}
}
}
}
for(int u = 0; u < n; u++){
for(int j = 0; j < G[u].size(); j++){
int v = G[u][j].v;
int dis = G[u][j].dis;
if(d[u] + dis < d[v]){
return false;
}
}
}
return true;
}
3. Floyd算法(弗洛伊德算法)
- 用于解决全源最短路径问题
- 使用邻接矩阵解决问题, 结点限制在200个以内;
- 枚举顶点k数,然后以该结点作为中介点,能否使得i, j顶点之间的距离变短;
int dis[maxn][maxn];
int n;
void Floyd(){
for(int k = 0; k < n; k++){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
if(dis[i][k] != INF && dis[k][j] != INF && dis[i][k] + dis[k][j] < dis[i][j]){
dis[i][j] = dis[i][k] + dis[k][j];
}
}
}
}
}
四、最小生成树
- 给定一个无向图G,然后求其中一棵生成树T,使得这棵树拥有图的所有结点;
- 且所有边来自图中的边,使得整棵树的边权之和最小;
1. prim算法(普里姆算法)
- 用于稠密图,边多
- 跟Dijkstra只有一点不同,即距离数组d的含义,在这里含义是到集合S的距离,即已经生成的树,而在Dijkstra是两结点的距离;
- 同时多一个变量ans,用于存储最小生成树的所有边权之和
const int INF = 100000000;
const int maxn;
int n, G[maxn][maxn];
int d[maxn];
bool vis[maxn] = {false};
//以s为根结点生成的最小生成树
int prim(int s){
fill(d, d+maxn, INF);
d[s] = 0;
int ans = 0;
for(int i = 0; i < n; i++){
int u = -1, MIN = INF;
for(int j = 0; j < n; j++){
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = dis[j];
}
}
}
if(u == -1) return -1;
ans += d[u];
for(int v = 0; v < n; v++){
if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v]){
d[v] = G[u][v];
}
}
return ans;
}
2. kruskal(克鲁斯卡尔算法)
- 用于稀疏图,边少
- 算法思想是需要判断两个端点是否在不同的连通块中,因此两个端点的编号一定是需要的,边权也是需要的;
- 所有边按从小到大进行排序;
- 一个是判断两个顶点是否在不同的连通块中;
- 如何将测试边加入连通块;
struct edge{
int u, v;
int cost;
}E[maxn];
bool cmp(edge a, edge b){
return a.cost < b.cost;
}
int father[N];
int findFather(int x){
int a = x;
while(x != father[x]){
x = father[x];
}
while(a != father[a]){
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
//n是顶点个数,m为图的边数
int kruskal(int n, int m){
int ans = 0, num_edge = 0;
for(int i = 1; i <= n; i++){//假设顶点范围位1-n
father[i] = i;
}
sort(E, E+m, cmp);
for(int i = 0; i < m; i++){
int faU = findFather(E[i].u);
int faV = findFather(E[i].v);
if(faU != faV){
father[faU] = faV;
ans += E[i].cost;
num_edge++;
if(num_edge == n-1) break;
}
}
if(num_edge != n - 1) return -1;
else return ans;
}
五、拓扑排序
- 明确两个东西,一个是有向无环图和拓扑排序之间的关系;对于一个有向无环图,可以生成拓扑序列;同时,也可以根据所给序列判定其是否为该有向无环图的拓扑序列;核心是每个结点的入度
- 在一个就是怎么样判定有向无环图和拓扑排序;
- 首先是必不可少存储图的邻接表和存储每个结点的入度的数组
- 明确一点当访问当该点时,一定这时入度已经是0,否则该序列不是拓扑序列;
- 在判定图是否为有向无环图时,首先将入度为0的结点加入队列中,然后遍历访问到该结点时,将该结点能够到达的左右结点的入度减一,如果出现入度为0,则加入队列中,然后统计结点变为0的数量num,如果等于图的结点数量,说明为有向无环图;
- 如果有向无环图,想要从编号从小开始生成拓扑序列,使用priority_queue, 优先队列是默认从大到小排序,访问队首元素使用top()函数
- 其优先级设置刚好跟sort相反;
//表示从小到大
priority_queue<int, vector<int>, greater<int>> q;
//从大到小
priority_queue<int, vector<int>, less<int>> q;
结构体
struct cmp{
bool operator() (fruit f1, fruit f2){
return f1.price > f2.price;
}
}
//表示从小到大
priority_queue<fruit, vector<fruit>, cmp> q;
vector<int> G[maxn];
int indegree[maxn];
vector<int> tin(indegree, indegree+n+1);//将入度直接赋值到tin向量中,用于判定序列是否为拓扑序列
bool toplogicalSort(){
queue<int> q;
for(int i = 0; i < n; i++){
if(indegree[i] == 0){
q.push(i);
}
}
int num = 0;
while(!q.empty()){
int top = q.front();
//printf("%d", top);
q.pop();
for(int j = 0; j < G[top].size(); j++){
indegree[G[top][j]]--;
if(indegree[G[top][j]] == 0){
q.push(G[top][j]);
}
}
G[top].clear();
num++;
}
if(num == n) return true;
else return false;
}
六、关键路径
- 明确两个点,结点代表事件,他有最早开始时间ve和最迟开始时间vl;而边代表活动,它也有最早开始时间e和最迟开始时间l;
- 怎么求事件的最早和最迟开始时间:最早开始时间直接等于前驱事件最早开始时间ve加上其到达的活动持续时间的最大值;最晚开始时间,根据栈,进行逆序拓扑序列,然后等于后驱事件最迟开始时间vl和其活动持续时间的最小值;
const int maxn = 105;
struct node{
int v, w;
};
vector<node> G[maxn];
int indegree[maxn] = {0};
stack<int> topOrder;
int ve[maxn], vl[maxn];
int n , m;
bool topSort(){
queue<int> q;
for(int i = 1; i <= n; i++){
if(indegree[i] == 0){
q.push(i);
}
}
while(!q.empty()){
int now = q.front();
topOrder.push(now);
q.pop();
for(int i = 0; i < G[now].size(); i++){
int v = G[now][i].v, w = G[now][i].w;
indegree[v]--;
if(indegree[v] == 0){
q.push(v);
}
if(ve[now] + w > ve[v]){
ve[v] = ve[now] + w;
}
}
}
if(topOrder.size() == n) return true;
else return false;
}
- 他们之间的关系,活动的最早开始时间和前驱事件的最早开始时间ve是一样的,而最迟开始时间为后驱事件的最迟开始时间vl减去活动的持续时间;
void cPath(){
fill(ve, ve+maxn, 0);
if(topSort() == false){
printf("0\n");
return;
}
int ans = -1;
for(int i = 1; i <= n; i++){
if(ve[i] > ans) ans = ve[i];
}
printf("%d\n", ans);
fill(vl, vl+maxn, ans);
while(!topOrder.empty()){
int u = topOrder.top();
topOrder.pop();
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v, w = G[u][i].w;
if(vl[v] - w < vl[u]){
vl[u] = vl[v] - w;
}
}
}
for(int u = 1; u <= n; u++){
for(int i = G[u].size()-1; i >= 0 ; i--){
int v = G[u][i].v, w = G[u][i].w;
int e = ve[u], l = vl[v] - w;
if(e == l){
printf("%d->%d\n", u, v);
}
}
}
return;
}
int main(){
cin >> n >> m;
int a, b, w;
for(int i = 0; i < m; i++){
scanf("%d%d%d", &a, &b, &w);
indegree[b]++;
G[a].push_back(node{b, w});
}
cPath();
return 0;
}
作者:睿晞
身处这个阶段的时候,一定要好好珍惜,这是我们唯一能做的,求学,钻研,为人,处事,交友……无一不是如此。
劝君莫惜金缕衣,劝君惜取少年时。花开堪折直须折,莫待无花空折枝。
曾有一个业界大牛说过这样一段话,送给大家:
“华人在计算机视觉领域的研究水平越来越高,这是非常振奋人心的事。我们中国错过了工业革命,错过了电气革命,信息革命也只是跟随状态。但人工智能的革命,我们跟世界上的领先国家是并肩往前跑的。能身处这个时代浪潮之中,做一番伟大的事业,经常激动的夜不能寐。”
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.