最短路径问题
前言
最短路问题(short-path problem)是网络理论解决的典型问题之一,可用来解决管路铺设、线路安装、厂区布局和设备更新等实际问题。基本内容是:若网络中的每条边都有一个数值(长度、成本、时间等),则找出两节点(通常是源节点和阱节点)之间总权和最小的路径就是最短路问题。
- 存储方式 1.邻接矩阵 2.邻接表 3.链式前向星
- 无向图视为正反的有向图叠加
算法
一般常用4种方法解决
1.Dijkstra算法 O(N^2)
2.Floyd算法 O(N^3)
3.Bellman-Ford算法 O(NM)
4.SPFA算法 O(NM)
其中由Bellman-Ford算法衍生出SPFA算法 实质就是队列优化的Bellman-Ford
它们具体的原理和实现如下
1.Dijkstra算法
适用问题 单源最短路径(一个源点出发)
适用范围 没有负边权 没有负环的图
实现步骤 1.初始化dis[起点]=0 其余节点为无穷大
2.找出未被标记且dis[x]最小的x 再标记x
3.扫描x所有出边(x,y,edge(x,y))dis[y]=min(dis[y],dis[x]+edge(x,y))
4.重复2-3到所有节点均被标记
总结:贪心思想 不满足负边(与贪心思想相矛盾)
模板题目
题目描述
输入一个无向网络,输出其中2个顶点之间的最短路径长度
输入
输入文件第一行为n和m,表示有n个顶点和m条带权边,其中顶点编号是从1到n,接下来有m行,每行三个整数,分别表示两个顶点编号和对应边的权值,再接下来有一行,两个整数表示要求的最短路径的两个顶点编号
输出
输出文件就一行,即两个顶点间的最短路径长度(权值和)
输入样例
4 5 1 2 2 1 3 1 2 3 2 2 4 1 3 4 6 1 4
输出样例
3
CODE
#include<bits/stdc++.h> const int MAXN=1e4+1; using namespace std; inline int read(){ int ans=0,f=1; char ch=getchar(); while(!isdigit(ch)) f*=(ch=='-')? -1:1,ch=getchar(); do ans=(ans<<1)+(ans<<3)+(ch^48),ch=getchar(); while(isdigit(ch)); return ans*f; } int u[MAXN],v[MAXN],w[MAXN],dis[MAXN]; bool vis[MAXN]; int main(){ int n,m,s,e; int x,y,z; n=read(); m=read(); for(int i=1;i<=m;i++){ x=read(); y=read(); z=read(); u[i]=v[i+m]=x; v[i]=u[i+m]=y; w[i]=w[i+m]=z; } for(int i=1;i<=n;i++) dis[i]=9999999; s=read(); e=read(); dis[s]=0; for(int k=1;k<n;k++){ for(int i=1;i<=2*m;i++) if(dis[v[i]]>dis[u[i]]+w[i]) dis[v[i]]=dis[u[i]]+w[i]; } cout<<dis[e]; return 0; }
Dijkstra算法的堆优化 O(mlogn)
Dijkstra 中每次找到一个未被标记过的且dis值最小的点,进行更新
这个过程进行n次,时间复杂度为O(n^2)
我们可以运用优先队列将找到一个未被标记的dis值最小的点这个过程的时间复杂度 从O(n)降到O(log n)
struct Node { int ver,poi,val; }edge[2*MAXN]; bool vis[MAXN]; int dis[MAXN]; priority_queue < pair < int,int > > q; //优先队列 void Dijkstra() { memset(dis,0x7f7f7f7f,sizeof(dis)); memset(vis,0,sizeof(vis)); pair < int,int > tem; dis[1]=0; tem.first=0; tem.second=1; while( !q.empty() ) { int x=q.top().second; q.pop(); //取出当前dis值最小的点 if( vis[x] ) continue; vis[x]=1; for(int i=head[x];i;i=edge[i].poi) { int y=edge[i].ver,z=edge[i].val; if( dis[y] > dis[x]+z ) dis[y]=dis[x]+z,tem.first=-1*dis[y],tem.second=y,q.push(tem); //更新 } } return; }
普通超时 堆优化
#include<bits/stdc++.h> const int MaxN = 100010, MaxM = 500010; struct edge { int to, dis, next; }e[MaxM]; int head[MaxN],dis[MaxN],cnt; bool vis[MaxN]; int n, m, s; void add(int u, int v, int d ) { cnt++; e[cnt].dis=d; e[cnt].to=v; e[cnt].next=head[u]; head[u]=cnt; } struct node { int dis; int pos; bool operator <( const node &x )const { return x.dis < dis; } }; std::priority_queue<node> q; void dijkstra() { dis[s] = 0; q.push( ( node ){0, s} ); while( !q.empty() ) { node tmp = q.top(); q.pop(); int x = tmp.pos, d = tmp.dis; if( vis[x] ) continue; vis[x] = 1; for( int i = head[x]; i; i = e[i].next ) { int y = e[i].to; if( dis[y] > dis[x] + e[i].dis ) { dis[y] = dis[x] + e[i].dis; if( !vis[y] ) { q.push( ( node ){dis[y], y} ); } } } } } int main() { scanf( "%d%d%d", &n, &m, &s ); for(int i = 1; i <= n; ++i)dis[i] = 0x7fffffff; for( int i = 0; i < m; ++i ) { int u, v, d; scanf( "%d%d%d", &u, &v, &d ); add( u, v, d ); } dijkstra(); for( int i = 1; i <= n; i++ ) printf( "%d ", dis[i] ); return 0; }
2.Floyd算法
分类:
多源最短路径算法。
作用:
1.求最短路
2.判断一张图中的两点是否相连。
优点:
实现极为简单
缺点:
只有数据规模较小且时空复杂度都允许时才可以使用(NOIP上大概不会放出来的吧)。
思想:
3层循环,第一层枚举中间点k,第二层与第三层枚举两个端点i,j。若有dis[i][j] > dis[i][k] + dis[k][j] 则把dis[i][j]更新成dis[i][k] + dis[k][j](原理还是很好理解的)。
实现:
(初始化:点i,j如果有边相连,则dis[i][j] = w[i][j]。如果不相连,则dis[i][j] = 0x7fffffff(int极限值),表示两点不相连(或认为相隔很远)。
核心代码
for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) a[i][j]=min(a[i][j],a[i][k]+a[k][j]);
例题
奶牛的比赛
FJ的N(1 <= N <= 100)头奶牛们最近参加了场程序设计竞赛:)。在赛场上,奶牛们按1..N依次编号。每头奶牛的编程能力不尽相同,并且没有哪两头奶牛的水平不相上下,也就是说,奶牛们的编程能力有明确的排名。 整个比赛被分成了若干轮,每一轮是两头指定编号的奶牛的对决。如果编号为A的奶牛的编程能力强于编号为B的奶牛(1 <= A <= N; 1 <= B <= N; A != B) ,那么她们的对决中,编号为A的奶牛总是能胜出。 FJ想知道奶牛们编程能力的具体排名,于是他找来了奶牛们所有 M(1 <= M <= 4,500)轮比赛的结果,希望你能根据这些信息,推断出尽可能多的奶牛的编程能力排名。比赛结果保证不会自相矛盾。
输入
第1行: 2个用空格隔开的整数:N 和 M 第2..M+1行: 每行为2个用空格隔开的整数A、B,描述了参加某一轮比赛的奶 牛的编号,以及结果(编号为A,即为每行的第一个数的奶牛为 胜者)
输出
输出1个整数,表示排名可以确定的奶牛的数目
数据范围
30% 2 <= n <= 10 2 <= m <= 15 50% 2 <= n <= 30 2 <= m <= 350 100% 2 <= n <= 100 2 <= m <= 4500
输入样例
输入样例1:
5 5
4 3
4 2
3 2
1 2
2 5
输入样例2:
2 1
1 2
输入样例3:
3 1
1 2
2 3
输出样例
输出样例1: 2 输出样例2: 2 输出样例3: 3
思路
a[i][j]表示i是否能赢j
读入之后floyd跑一遍 注意转移方程为
a[i][j]|=(a[i][k]&a[k][j]) 若i能赢k k能赢j 则i也能赢j
然后找每个点的入度+出度是不是n-1
是的话就是能确定排名的点将其记录下来
#include<bits/stdc++.h> const int MAXN=1e2+10; using namespace std; int N,M; int ans; int a[MAXN][MAXN]; inline int read(){ int ans=0,f=1; char ch=getchar(); while(!isdigit(ch)) f*=(ch=='-')? -1:1,ch=getchar(); do ans=(ans<<1)+(ans<<3)+(ch^48),ch=getchar(); while(isdigit(ch)); return ans*f; } int main(){ N=read(); M=read(); for(int i=1;i<=M;i++){ int x,y; x=read(); y=read(); a[x][y]=1; } for(int k=1;k<=N;k++) for(int i=1;i<=N;i++) for(int j=1;j<=N;j++) a[i][j]|=(a[i][k]&a[k][j]); for(int i=1;i<=N;i++){ int tot=0; for(int j=1;j<=N;j++) if(a[j][i]||a[i][j])tot++; if(tot==N-1)ans++; } cout<<ans; return 0; }
例题 贫富差距
题目描述
一个国家有N个公民,标记为0,1,2,…,N-1,每个公民有一个存款额。已知每个公民有一些朋友,同时国家有一条规定朋友间的存款额之差不能大于d。也就是说,a和b是朋友的话,a有x元的存款,b有y元,那么|x-y|<=d。给定d值与N个人的朋友关系,求这个国家最富有的人和最贫穷的人的存款相差最大的可能值是多少?即求贫富差距的最大值的下界。若这个值为无穷大,输出-1.
输入
多组测试数据,第一行一个整数T,表示测试数据数量,1<=T<=5
每组测试数据有相同的结构构成。
每组数据的第一行两个整数N,d,表示人数与朋友间存款差的最大值,其中2<=N<=50,0<=d<=1000.
接下来有一个N*N的数组A,若A[i][j]=’Y’表示i与j两个人是朋友,否则A[i][j]=’N’表示不是朋友。其中A[i][i]=’N’,且保证
A[i][j]=A[j][i].
输出
每组数据一行输出,即这个国家的贫富差距最大值的下界,如果这个值为无穷大输出-1.
题解
并查集+Floyd
算法
根据题意,无穷大的情况连通块儿一定超过1个,所以先用并查集过一遍,如果是一个连通块儿,那么再用Floyed
算法,求任意两点之间的最短距离,默认每条路径长度为1,最后从所有距离中查找最大的距离,乘以d
即为结果。
#include<stdio.h> #include<iostream> #include<algorithm> using namespace std; const int INF = 0x3f3f3f3f; const int maxn = 105; int n,dp[maxn][maxn]; void floyd() { for(int k=0;k<n;k++) { for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]); } } } } char str[maxn][maxn]; int main() { int d,t; scanf("%d",&t); while(t--) { scanf("%d%d",&n,&d); for(int i=0;i<n;i++) scanf("%s",str[i]); for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { if(str[i][j]=='Y') dp[i][j]=1; else dp[i][j]=INF; } } floyd(); int ans=-1; for(int i=0;i<n;i++) { for(int j=i+1;j<n;j++) { ans=max(ans,dp[i][j]); } } if(ans==INF) printf("-1 "); else printf("%d ",ans*d); } return 0; }
3.Bellman-Ford算法
Bellman-Ford算法 O(NE)
分类:
单源最短路径算法。
适用于:
稀疏图(侧重于对边的处理)。
优点:
可以求出存在负边权情况下的最短路径。
缺点:
无法解决存在负权回路的情况。
时间复杂度:
O(NE),N是顶点数,E是边数。(因为和边有关,所以不适于稠密图)
算法思想:
很简单。一开始认为起点是“标记点”(dis[1] = 0),每一次都枚举所有的边,必然会有一些边,连接着“未标记的点”和“已标记的点”。因此每次都能用所有的“已标记的点”去修改所有的“未标记的点”,每次循环也必然会有至少一个“未标记的点”变为“已标记的点”。
模板题
题目描述
输入一个有向网络图,边的权值可正可负,求顶点1到其他各点的最短路。题中数据保证无负权环
输入
输入文件第一行为n和m(n,m<=20),表示n个顶点和m条边,接下来有m行,每行三个整数i、j、k,代表从顶点i到顶点j有一条边,且权值为k(-100<=k<=100)
输出
输出文件为一行,有n-1个数据,即顶点1到其他各顶点间的最短路径长度,如果到不了,则输出-32767
输入样例
3 3
1 2 2
2 3 -2
1 3 1
输出样例
2 0
#include<iostream> #include<cstring> using namespace std; int vis[10001],dis[10001]; int e[1001][1001]; int ji=1; int main() { ios::sync_with_stdio(false); int n; cin>>n; memset(e,127,sizeof(e)); int m; cin>>m; for(int i=1; i<=n; i++) { e[i][i]=0; } for(int i=1; i<=m; i++) { int a,b,q; cin>>a>>b>>q; e[a][b]=q; } int s=1; for(int i=1; i<=n; i++) { dis[i]=e[s][i]; } vis[s]=1; while(ji<=n) { int minn=99292222; int rec=0; for(int i=1; i<=n; i++) { if(dis[i]<minn&&vis[i]==0) { rec=i; minn=dis[i]; } } for(int i=1; i<=n; i++) { if(e[rec][i]!=0&&e[rec][i]<=1000000) { dis[i]=min(dis[i],dis[rec]+e[rec][i]); } } vis[rec]=1; ji++; } for(int i=2; i<=n; i++) { if(dis[i]>1000000)cout<<-32767<<" "; else cout<<dis[i]<<" "; } return 0; }
4.SPFA算法 O(KE)
SPFA算法O(KE)
适用于:
稀疏图(侧重于对边的处理)。
时间复杂度:
O(KE),K是常数,平均值为二,E是边数。(因为和边有关,所以不适于稠密图)
来源:
SPFA是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算。
这个算法简单地说就是队列优化的Bellman-Ford,利用了每个点不会更新次数太多的特点发明的此算法。
SPFA在形式上和广度优先搜索非常类似,不同的是广度优先搜索中的一个点出了队列就不可能重新进入队列,但是SPFA中的一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其他的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其他的点,这样反复进行下去。
if(!vis[temp]) { if(dis[q[head + 1]] < dis[temp]) //注意小于号不要写反,否则时间会爆 { tail = (++tail - 1) % qxun + 1; q[tail] = temp; } else { q[head] = temp; if(--head == 0) head = qxun; } vis[temp] = 1; }
例题水群
总所周知,水群是一件很浪费时间的事,但是其实在水群这件事中,也可以找到一些有意思的东西。
比如现在,bx2k就在研究怎样水表情的问题。
首先,bx2k在对话框中输入了一个表情,接下来,他可以进行三种操作。
第一种,是全选复制,把所有表情全选然后复制到剪贴板中。
第二种,是粘贴,把剪贴板中的表情粘贴到对话框中。
第三种,是退格,把对话框中的最后一个表情删去。
假设当前对话框中的表情数是num0,剪贴板中的表情数是num1,
那么第一种操作就是num1=num0
第二种操作就是num0+=num1
第三种操作就是num0--
现在bx2k想知道,如果要得到n(1<=n<=10^6)个表情,最少需要几次操作。
请你设计一个程序帮助bx2k水群吧。
#include<iostream> #include<stdio.h> #include<queue> using namespace std; const int inf=0x3f3f3f3f; const int maxn=1e6+20; int n,ans[maxn],a[]={2,3,5,7,11,13}; bool vis[maxn]; queue<int> q; void spfa(){ q.push(1);vis[1]=1; while(!q.empty()){ int cur=q.front();q.pop();vis[cur]=0; for(int i=0;i<6;i++){ if(cur*a[i]<n+20&&ans[cur*a[i]]>ans[cur]+a[i]){ ans[cur*a[i]]=ans[cur]+a[i]; if(!vis[cur*a[i]]) q.push(cur*a[i]),vis[cur*a[i]]=1; } } if(ans[cur-1]>ans[cur]+1){ ans[cur-1]=ans[cur]+1; if(!vis[cur-1]) q.push(cur-1),vis[cur-1]; } } } int main(){ scanf("%d",&n); for(int i=2;i<=n+20;i++) ans[i]=inf,vis[i]=0; spfa(); printf("%d ",ans[n]); }