2.5 图论(最短路、最小生成树)

文章目录

  • 2.5 图论(最短路、最小生成树)
    • 2.5.1 定义们
    • 2.5.2 图的表示
    • 2.5.3 图的搜索
    • 2.5.4 最短路问题
      • 单源1:bellman-ford
      • 单源2:dijkstra算法
      • (单源3:spfa)
      • 任意两点:floyd-warshall
      • 路径还原
    • 2.5.5 最小生成树
      • Prim算法
      • Kruskal算法
    • 2.5.6 应用解题

2.5.1 定义们

图:点和边组成。
V为顶点集,E为边集,图记为G(V,E),连接两点u和v的边用e=(u,v)表示。

分为有向图和无向图。边上有权值的是带权图。

连通图:任意两点间均可达。度数是顶点连的边数。

树:没有圈的连通图。边数=点数-1。

有向图:度数分为入度和出度。

没有圈的有向图称为DAG(Direct Acyclic Graph)。
对DAG给顶点标记顺序,就是拓扑序。求拓扑序的算法是拓扑排序。

2.5.2 图的表示

邻接矩阵:|V|*|V|二维数组来表示图。g[i] [j]表示顶点 i 和 j 的关系

在权值情况下,通常没连的边设为INF。

邻接表:空间占的少,取值慢一点。

一种STL写法:

vector<int> G[VMAX];
for(int i = 0; i < E; i++){int s,t;scanf("%d%d",&s,&t);G[s].push_back(t);
}

另一种头接法(更少空间、更快插入、更慢查找):

struct Edge{int to,nex;
}e[EMAX];int etot = 0;
int head[NMAX]; //每个点的第一个边void init(){etot=0;memset(head,-1,sizeof(head));
}void addedge(int u,int v){e[etot].to = v;e[etot].next = head[u];head[u] = etot++;
}//遍历like:
for(int i=head[now];i!=-1;i=head[i]){Edge te = e[i];
}

2.5.3 图的搜索

例题1:二分图判定

给定一个具有n个顶点的图,用两种颜色给图上每个顶点染色,相邻顶点颜色不同。无重复的边和自环。
问能否染色?
1<=n<=1000

思路:dfs一遍搞定。±1分别代表黑白色,若未染过就是0。

int color[VMAX];
int V;
bool judge(int now, int c){for(int i=head[now]; i!=-1; i=head[i]){int v = e[i].to;if(color[v]==0 && !judge(v,-c))return false;if(color[v]==c) return false;}return true;
}
for(int i=0;i<V;i++){if(color[v]==0){if(!dfs(i,1)){printf("NO\n");break;}}
}
printf("YES\n");

2.5.4 最短路问题

给定起点 s 和终点 t,求边权值和最小的路径。

单源1:bellman-ford

d[i] = min{d[j] + e(j,i) }

思路:
【初始化】将所有d[j] 设为 INF,除了d[s]为0
【不断更新】d的值,直到无法再更新

复杂度是O(|E|*|V|)

struct edge{int from,to,cost;};
edge es[EMAX];
int d[VMAX];
int V,E;
void bellman_ford(){for(int i=0; i<V; i++) d[i] = INF;d[s]=0;while(true){bool update = false;for(int i=0; i<E; i++){edge e = es[i];if(d[e.from]!=INF && d[e.from]+e.cost<d[e.to]){d[e.to] = d[e.from]+e.cost;update = true;}}if(!update) break;}
}

每一次迭代,至少能确认一个点。

而我们已经确认了起点位置,因此最多要进行|V|-1次迭代(while true循环)——所有点一直线的情况。

如果第|V|次依然更新,说明存在【负环】,bellman-ford判负环:

【初始化】d都为0。

bool bellman_ford_negative_loop(){memset(d,0,sizeof(d));for(int j=1; j<=V; j++){for(int i=0; i<E; i++){edge e = es[i];if(d[e.from]!=INF && d[e.from]+e.cost<d[e.to]){d[e.to] = d[e.from]+e.cost;//第V次仍然更新,说明存在负环。if(j==V) return false;}}} return true;
}

单源2:dijkstra算法

**【没有负环】**的情况下考虑:d[ j ] = d[ i ] + e( i, j )

每次循环检查所有边太浪费时间。修改:

找到最短距离已经确定的点,从它出发进行更新。

【最短距离已定的点】距离d[i]最小的点。

复杂度O(∣V∣2)O(|V|^2)O(∣V∣2)

int cost[VMAX][VMAX];
int d[VMAX];
bool used[VMAX];
int V;
void dijkstra(){fill(d, d+V, INF);fill(used, used+V, false);d[s] = 0;while(true){//在未使用的顶点中选择距离最小的int v = -1;for(int u = 0; u < V; u++){if(!used[u] && (v==-1 || d[u]<d[v]) )v = u;}//均已更新过if(v==-1) break;used[v] = true;for(int u = 0; u<V; u++){d[u] = min(d[u], d[v]+cost[v][u]);}}
}

每次循环的首先操作:实质是每次找到最优的点并删除,这个部分可以用优先队列优化。

更新数据的操作要|E|次,复杂度O(∣E∣log∣V∣)O(|E|log|V|)O(∣E∣log∣V∣)

struct edge{int to,cost};
typedef pair<int, int> P;
int V;
vector<edge> G[VMAX];
int d[VMAX];void dijkstra(int s){//小顶堆,小的在上面priority_queue<P, vector<P>, greater<P> > pq;fill(d, d+V, INF);d[s] = 0;pq.push(P(0,s));while(!pq.empty()){P p = pq.top();pq.pop();int v = p.second;if(d[v] < p.first) continue;for(int i=0; i<G[v].size(); i++){edge e = G[v][i];if(d[e.to]>d[v]+e.cost){d[e.to] = d[v]+e.cost;pq.push(P(d[e.to], e.to));}}}
}

再次记住dijkstra的条件是【没有负环】

(单源3:spfa)

最差:O(|V|*|E|)

struct edge{int to,cost};int V;
vector<edge> G[VMAX];
int d[VMAX], inq[VMAX];void spfa(int s){queue<int> q;fill(d, d+V, INF);fill(inq, inq+V, 0);d[s] = 0;q.push(s);while(!q.empty()){int u = q.front();q.pop();inq[u] = 0;for(int i=0; i<G[v].size(); i++){edge e = G[v][i];if(d[e.to]>e.cost+d[u]){d[e.to]=e.cost+d[u];if(!inq[e.to]){inq[e.to]=1;q.push(e.to);}}}}
}

任意两点:floyd-warshall

d[i] [j] = min(d[i] [j] , d[i] [k] + d[k] [j])

【可以处理负权边】判断d[i] [i] 为负数的点

复杂度:O(∣V∣3)O(|V|^3)O(∣V∣3)

int d[VMAX][VMAX]; //d[i][i]=0, 有边时边权,无边时INF
int V;
void floyd(){for(int i=0; i<V; i++){for(int j=0; j<V; j++){for(int k=0; k<V; k++){d[i][j] = min(d[i][j], d[i][k]+d[k][j]);}}}
}

路径还原

以dijkstra为例,记录前向点prev,存入数组将其reverse就是答案。

int cost[VMAX][VMAX];
int d[VMAX];
bool used[VMAX];
int V;//记录前向点
int prev[VMAX];void dijkstra(int s){fill(d, d+V, INF);fill(used, used+V, false);//初始化-1fill(prev, prev+V, -1);d[s] = 0;while(true){//在未使用的顶点中选择距离最小的int v = -1;for(int u = 0; u < V; u++){if(!used[u] && (v==-1 || d[u]<d[v]) )v = u;}//均已更新过if(v==-1) break;used[v] = true;for(int u = 0; u<V; u++){if(d[u] > d[v]+cost[v][u]){d[u] = d[v]+cost[v][u];prev[u]=v;}}}
}vector<int> getpath(int t){vector<int> path;while(t!=-1){path.push_back(t);t=prev[t];}reverse(path.begin(), path.end());return path;
}

2.5.5 最小生成树

生成树:无向图中某个子图中,任意两个顶点都互相连通且是一棵树。

最小生成树:使得边权和最小的生成树。

Prim算法

【思想】:对于当前树T,不断贪心地选取T和其他顶点之间最小权值的边,并将其加入T。

(充满dijkstra内味)

复杂度:O(∣V∣2)O(|V|^2)O(∣V∣2) 优先队列优化:O(∣E∣log∣V∣)O(|E|log|V|)O(∣E∣log∣V∣)

int cost[VMAX][VMAX];
int mincost[VMAX];
bool used[VMAX];
int V;int prim(){fill(mincost, mincost+V, INF);fill(used, used+V, false);mincost[0] = 0;int res = 0;while(true){int v = -1;for(int u = 0; u<V; u++){if(!used[u] && (v==-1||mincost[u]<mincost[v]) )v = u;}if( v==-1 ) break;used[v] = true;res += mincost[v];for(int u=0; u<V; u++){if(mincost[u]>mincost[v]+cost[v][u]){mincost[u] = mincost[v]+cost[v][u];}}}return res;
}

Kruskal算法

按照边权从小到大check一遍,不产生圈就加入。

【是否产生圈】:用并查集高效查看是否在同一个连通分量中

复杂度O(∣E∣log∣V∣)O(|E|log|V|)O(∣E∣log∣V∣)

struct edge{int u, v, cost;};
bool cmp(const edge& e1, const edge& e2){return e1.cost<e2.cost;
}
edge es[EMAX];
int V,E;int kruskal(){sort(es,es+E,cmp);init(V); // 并查集初始化int res = 0;for(int i=0; i< E; i++){edge e = es[i];if(!same(e.u,e.v)){unite(e.u,e.v); //并查集合并res+=e.cost;}}return res;
}//并查集部分添加:
int fa[VMAX], rk[VMAX];
void init(int V){for(int i=0;i<V;i++)fa[i]=i;memset(rk,0,sizeof(rk));
}
int findf(int x){if(fa[x]==x)return x;return fa[x] = findf(fa[x]);
}
void unite(int x, int y){x = findf(x);y = findf(y);if(x==y) return;if(rk[x]<rk[y])fa[x]=y;else{fa[y]=x;if(rk[x]==rk[y]) rk[x]++;}
}
bool same(int x, int y){return findf(x)==findf(y);
}

2.5.6 应用解题

例1:次短路 POJ 3255

有R个道路,N个路口,双向通行、问从1号路口到N号路口的次短路长度。

1<=N<=5000, 1<=R<=100000

思路:【次短距离】d2[v] = d[u]+e(u,v) OR d2[u]+e(u,v),

因此queue中最短路和次短路都要加入

【这题四年前没AC,就是没理解最短路和次短路都要加队列这一点!】

#include <cstdio>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
const int VMAX=5050;
const int INF=0x3f3f3f3f;typedef pair<int,int> P;
struct edge{int to,cost;edge(int a,int b):to(a),cost(b){}
};
vector<edge> G[VMAX];
int d[VMAX], d2[VMAX];//次短距离d2[v]=d[u]+e(u,v) 或者 d2[u]+e(u,v)
bool used[VMAX];
int R,V;void dijkstra(int s){fill(d, d+V, INF);fill(d2, d2+V, INF);priority_queue<P,vector<P>,greater<P> >pq;pq.push(P(0,s));d[s] = 0;while(!pq.empty()){P p = pq.top();pq.pop();int u = p.second, dist = p.first;if(d2[u]<dist) continue;for(int i = 0; i<G[u].size(); i++){edge e = G[u][i];// d[v] = d2[u]|d[u] + e(u,v), dist = d2[u]|d[u];int dv = dist + e.cost;// dv是最短距离,更新最短距离if( dv < d[e.to]){//最短距离d[e.to]更新为dv,dv值变成次短距离(即原来的最短距离)swap( dv, d[e.to] );//将最短距离放入队列pq.push(P(d[e.to], e.to));}// dv是次短距离,更新次短距离if( dv > d[e.to] && dv<d2[e.to]){d2[e.to] = dv;pq.push(P(d2[e.to], e.to));}}}printf("%d\n",d2[V-1]);
}int main(){int u,v,w;scanf("%d%d",&V,&R);for(int i=0;i<R;i++){scanf("%d%d%d",&u,&v,&w);G[u-1].push_back(edge(v-1,w));G[v-1].push_back(edge(u-1,w));}dijkstra(0);return 0;
}

例2:征兵顺序 POJ3723

需要召集N个女兵,M个男兵。
给出若干男女之间的亲密度,征募某个人的费用=10000-已招募人员亲密度最大值。
通过适当的招募顺序使得总费用最低。

1<=N,M<=10000, 0 ≤ R ≤ 50,000,
0 ≤ xi < N, 0 ≤ yi < M, 0 < di < 10000

思路:最小生成树裸题。

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;const int EMAX = 50050, VMAX=20020;
struct edge{int u, v, cost;};
bool cmp(const edge& e1, const edge& e2){return e1.cost<e2.cost;
}
edge es[EMAX];
int V,E;//并查集部分添加:
int fa[VMAX], rk[VMAX];
void init(int V){for(int i=0;i<V;i++)fa[i]=i;memset(rk,0,sizeof(rk));
}
int findf(int x){if(fa[x]==x)return x;return fa[x] = findf(fa[x]);
}
void unite(int x, int y){x = findf(x);y = findf(y);if(x==y) return;if(rk[x]<rk[y])fa[x]=y;else{fa[y]=x;if(rk[x]==rk[y]) rk[x]++;}
}
bool same(int x, int y){return findf(x)==findf(y);
}int kruskal(){sort(es,es+E,cmp);init(V); // 并查集初始化int res = 0;int totv = V;for(int i=0; i< E; i++){edge e = es[i];if(!same(e.u,e.v)){unite(e.u,e.v); //并查集合并res+=e.cost;totv--;}}res += totv*10000;return res;
}int main(){int t,N,M,R;scanf("%d",&t);while(t--){scanf("%d%d%d",&N,&M,&R);V = N+M;E = 0;while(R--){scanf("%d%d%d",&es[E].u, &es[E].v, &es[E].cost);es[E].cost = 10000-es[E].cost;es[E].v += N;E++;}printf("%d\n",kruskal());}
}

例3 排列牛 POJ3169

有N头牛,编号1-N。按顺序拍成一排。
有些牛关系好(AL,BL,DL)距离不超过DL,有些牛关系不好(AD,BD,DD)距离不小于DD。

求1和N的牛的最大距离。不存在输出-1。无限大输出-2。
2 <= N <= 1,000, 1 <= A < B <= N,1 <= D <= 1,000,000

思路:

BL<=AL+DL, AL向BL连一条DL
BD>=AD+DD,即 AD<=BD-DD, BD向AD连一条-DD
另外有B>=A,即A<=B+0,B向A连0

求最短路,若有负环-1,若为INF则-2,其他情况正常输出。

#include <cstdio>
#include <algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
const int EMAX = 30020, VMAX=1010;
struct edge{int from,to,cost;};
edge es[EMAX];
int d[VMAX];
int V,E;int bellman_ford_negative_loop(){fill(d,d+V+1,INF);d[1]=0;for(int j=1; j<=V; j++){bool update = false;for(int i=0; i<E; i++){edge e = es[i];if(d[e.from]!=INF && d[e.from]+e.cost<d[e.to]){d[e.to] = d[e.from]+e.cost;//第V次仍然更新,说明存在负环。if(j==V) return -1;update = true;}}if(!update) break;}if(d[V]==INF) return -2;return d[V];
}int main(){int ML,MD;scanf("%d%d%d",&V,&ML,&MD);E = 0;for(int i=2; i<=V; i++){es[E].from = i;es[E].to = i-1;es[E].cost = 0;E++;}while(ML--){scanf("%d%d%d",&es[E].from,&es[E].to,&es[E].cost);E++;}while(MD--){scanf("%d%d%d",&es[E].to,&es[E].from,&es[E].cost);es[E].cost = -es[E].cost;E++;}printf("%d",bellman_ford_negative_loop());return 0;
}

【挑战程序设计】- 2.5 图论(最短路、最小生成树)相关推荐

  1. 挑战程序设计竞赛(第二章:2.5 图论)

    文章目录 RoadBlocks Conscription Layout RoadBlocks 参考博文:挑战程序设计竞赛: Roadblocks 参考博文:[dijkstra优化/次短路径]POJ32 ...

  2. 《挑战程序设计竞赛(第2版)》习题册攻略

    本项目来源于GitHub 链接: 项目GitHub链接 1 前言 项目为<挑战程序设计竞赛(第2版)>习题册攻略,已完结.可配合书籍或笔记,系统学习算法. 题量:约200道,代码注释内含详 ...

  3. 挑战程序设计竞赛(第二章习题总结)

    文章目录 搜索 Curling 2.0(POJ 3009) Meteor Shower(POJ 3669) Smallest Difference(POJ 2718) Hopscotch(POJ 30 ...

  4. 《挑战程序设计竞赛》--初级篇习题POJ部分【2.4 - 2.6】

    这次是延续上次的<挑战程序设计竞赛>初级篇,总结部分poj上的练习题,主要是2.4 ~ 2.6部分: 导航 2.4 加工并存储的数据结构 优先队列 Sunscreen MooUnivers ...

  5. 挑战程序设计竞赛(第2版)》

    <挑战程序设计竞赛(第2版)> 基本信息 作者: (日)秋叶拓哉 岩田阳一 北川宜稔 译者: 巫泽俊 庄俊元 李津羽 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787 ...

  6. 《挑战程序设计竞赛》 读后感(转载)

    <挑战程序设计竞赛> 读后感 最近要开始准备面试找工作,算法是准备的重中之重,舍友推荐了<挑战程序设计竞赛>这本书.花了一周的时间大体过了一遍,该书真切地让我理解了" ...

  7. 0x61.图论 - 最短路

    目录 单源最短路径 一.Dijkstra算法 1.常用的优先队列优化 2.更优的线段树优化 3.最强的zkw线段树优化 二.SPFA算法 三.分层图最短路 1.(二维分层图)AcWing 340. 通 ...

  8. ICPC程序设计题解书籍系列之三:秋田拓哉:《挑战程序设计竞赛》(第2版)

    白书<挑战程序设计竞赛>(第2版)题目一览 白书:秋田拓哉:<挑战程序设计竞赛>(第2版) 第1章 蓄势待发--准备篇(例题) POJ1852 UVa10714 ZOJ2376 ...

  9. 【操作指导 | 代码实现】挑战程序设计竞赛2:算法和数据结构

    书籍封面 第一章 前言 1. 本人衷心建议 ~~~~~~       如果你是一位初学者,我指的是你只会基本的 C/C++ 编程,即使编的很烂,这本书对于你算法和数据结构的提升非常有帮助,所涉及的每一 ...

最新文章

  1. 【每日一题】剑指 Offer 22. 链表中倒数第k个节点
  2. Spring Security 决策器前缀修改
  3. +智能”时代,华为如何将AI赋能到各行各业?
  4. boost::fusion::joint_view用法的测试程序
  5. python正则表达式中的转义字符_python 正则表达式之转义字符
  6. c语言中结构体类型只有,C语言中main()函数不要返回结构体类型(求助)
  7. java 大型互联网架构_分享一些大型互联网架构常用的高端技术
  8. plsql数据库异常---plsql 登录后,提示数据库字符集(AL32UTF8)和客户端字符集(ZHS16GBK)不一致
  9. Leetcode 105. 前序和中序遍历序列构造二叉树
  10. ssh框架超详细总结
  11. win10设置透明任务栏
  12. 分布式系统的SLA如何定义
  13. 前端高效开发必备的js库梳理,日常使用中会持续更新
  14. 销量反弹,高管离职,苹果真的要改变高定价策略了?
  15. 腾讯技术分享:微信小程序音视频与WebRTC互通的技术思路和实践
  16. (Java)图解排序算法之归并排序
  17. 验证码登录开发----手机验证码登录
  18. Huffman编码/译码问题
  19. 华为路由器配置DHCP服务及给指定PC分配固定IP地址
  20. GMTUTC,UNIX时间戳,时区

热门文章

  1. PDF转Word非常好的网站
  2. Win11远程桌面怎么用?Win11家庭版开启远程桌面
  3. 运算符重载为成员函数,友元函数
  4. RememberMe简介
  5. 【毕业季·进击的技术er】自己的选择,跪着也要走
  6. 安全狗技术分享|Web应用防火墙之攻击防护
  7. Unity-URP学习笔记(四)赛璐珞高光
  8. shell判断文件目录或文件是否存在
  9. 眼部结构+糖尿病视网膜病变+黄斑病变学习
  10. java 中showinfo方法,jmockito模拟方法中参数如何指定