【挑战程序设计】- 2.5 图论(最短路、最小生成树)
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 图论(最短路、最小生成树)相关推荐
- 挑战程序设计竞赛(第二章:2.5 图论)
文章目录 RoadBlocks Conscription Layout RoadBlocks 参考博文:挑战程序设计竞赛: Roadblocks 参考博文:[dijkstra优化/次短路径]POJ32 ...
- 《挑战程序设计竞赛(第2版)》习题册攻略
本项目来源于GitHub 链接: 项目GitHub链接 1 前言 项目为<挑战程序设计竞赛(第2版)>习题册攻略,已完结.可配合书籍或笔记,系统学习算法. 题量:约200道,代码注释内含详 ...
- 挑战程序设计竞赛(第二章习题总结)
文章目录 搜索 Curling 2.0(POJ 3009) Meteor Shower(POJ 3669) Smallest Difference(POJ 2718) Hopscotch(POJ 30 ...
- 《挑战程序设计竞赛》--初级篇习题POJ部分【2.4 - 2.6】
这次是延续上次的<挑战程序设计竞赛>初级篇,总结部分poj上的练习题,主要是2.4 ~ 2.6部分: 导航 2.4 加工并存储的数据结构 优先队列 Sunscreen MooUnivers ...
- 挑战程序设计竞赛(第2版)》
<挑战程序设计竞赛(第2版)> 基本信息 作者: (日)秋叶拓哉 岩田阳一 北川宜稔 译者: 巫泽俊 庄俊元 李津羽 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787 ...
- 《挑战程序设计竞赛》 读后感(转载)
<挑战程序设计竞赛> 读后感 最近要开始准备面试找工作,算法是准备的重中之重,舍友推荐了<挑战程序设计竞赛>这本书.花了一周的时间大体过了一遍,该书真切地让我理解了" ...
- 0x61.图论 - 最短路
目录 单源最短路径 一.Dijkstra算法 1.常用的优先队列优化 2.更优的线段树优化 3.最强的zkw线段树优化 二.SPFA算法 三.分层图最短路 1.(二维分层图)AcWing 340. 通 ...
- ICPC程序设计题解书籍系列之三:秋田拓哉:《挑战程序设计竞赛》(第2版)
白书<挑战程序设计竞赛>(第2版)题目一览 白书:秋田拓哉:<挑战程序设计竞赛>(第2版) 第1章 蓄势待发--准备篇(例题) POJ1852 UVa10714 ZOJ2376 ...
- 【操作指导 | 代码实现】挑战程序设计竞赛2:算法和数据结构
书籍封面 第一章 前言 1. 本人衷心建议 ~~~~~~ 如果你是一位初学者,我指的是你只会基本的 C/C++ 编程,即使编的很烂,这本书对于你算法和数据结构的提升非常有帮助,所涉及的每一 ...
最新文章
- 【每日一题】剑指 Offer 22. 链表中倒数第k个节点
- Spring Security 决策器前缀修改
- +智能”时代,华为如何将AI赋能到各行各业?
- boost::fusion::joint_view用法的测试程序
- python正则表达式中的转义字符_python 正则表达式之转义字符
- c语言中结构体类型只有,C语言中main()函数不要返回结构体类型(求助)
- java 大型互联网架构_分享一些大型互联网架构常用的高端技术
- plsql数据库异常---plsql 登录后,提示数据库字符集(AL32UTF8)和客户端字符集(ZHS16GBK)不一致
- Leetcode 105. 前序和中序遍历序列构造二叉树
- ssh框架超详细总结
- win10设置透明任务栏
- 分布式系统的SLA如何定义
- 前端高效开发必备的js库梳理,日常使用中会持续更新
- 销量反弹,高管离职,苹果真的要改变高定价策略了?
- 腾讯技术分享:微信小程序音视频与WebRTC互通的技术思路和实践
- (Java)图解排序算法之归并排序
- 验证码登录开发----手机验证码登录
- Huffman编码/译码问题
- 华为路由器配置DHCP服务及给指定PC分配固定IP地址
- GMTUTC,UNIX时间戳,时区