【最短路问题】
解决最短路问题有以下算法:Dijkstra算法,Bellman-Ford算法,Floyd算法,和SPFA算法和启发式搜索算法A*;
每个算法都有它的特点可以解决某些特定的问题,例如:Floyd算法可以求解任意两点之间的最短路径长度,SPFA可以判定是否存在负环问题
一. Dijkstra 算法:
解决的问题:<非负权图单源最短路>1.从某一点出发到所有点的最短路径,就是最后更新的dis数组2.从某一个点到出发到具体某一点的最短路,只要第一次吧这个点加入最短路就可以终止程序了。3.注意Dijkstar 算法不能处理负权边
思想:贪心
做法: 每次将未加入最短路径的点种找距离出发点最近的点加入,然后用这个点更新所有没有加入的点到起始点的最小距离,每次加入一个点更新一次,直到所有的点都加入数组后结束,就可以统计出这个点到所有点的最短路径了
算法复杂度:斐波那契堆的复杂度O(E+VlgV)
核心代码:
1 void dijk(int s , int n) 2 { 3 int i , j , k ; 4 for( i = 1 ; i <= n ;i++) 5 { 6 p[i] = false; 7 dist[i] = mp[s][i]; 8 } 9 p[s] = true; 10 dist[s] = 0; 11 for(i = 1 ; i < n ; i++) 12 { 13 int Min = INF; 14 int k = 0 ; 15 for( j = 1 ; j <= n ;j++) 16 { 17 if(!p[j]&&dist[j]<Min) 18 { 19 Min = dist[j]; 20 k = j; 21 } 22 } 23 if(Min==INF) return ; 24 p[k] = true; 25 for(j = 1 ; j <= n ;j++) 26 { 27 if(!p[j]&&mp[k][j]!=INF&&dist[j]>dist[k]+mp[k][j]) 28 dist[j] = dist[k]+mp[k][j]; 29 } 30 } 31 }
二.BellmanFord 算法和SPFA 算法(Shortest Path Faster Algorithm)
解决的问题:1.权值有负值得图的单源最短路,并且可以检测到负环,注意,由于B_F算法的复杂度过高而且都可以用SPFA解决,所以我们只学习SPFA算法(一个特例:bellman可以检测并输出负环,单SPFA不能输出负环)2.最长路
思想:松弛操作
做法:设置一个队列,开始将初始点加入这个队列中,如果队列不空的话,取出队列顶端的元素i,用队列顶端的元素对所有的点j进行松弛操作 if(mp[i][j]!=INF&&dis[j]>dis[i]+mp[i][j]) dis[j] = dis[i]+mp[i][j]; 如果j点没有在队列中,说明它还有更新其他点的潜力,所以将被更新的j点也加入到队列中去,设置一个计数器,设总点数为n,如果一个点进队的次数大于n则说明存在负环。
算法复杂度:O(kE) k是每个节点进队的次数,一般来说k<=2;但是这里的复杂度证明是有问题的,所以SPFA的最坏的情况应该是O(VE)
核心代码:
1、初始化所有点,每一个点保存一个值,表示从源点到达这个点的距离,将源点的值设为0,其他点的值设为无穷大(表示不可达) 2、进行循环,循环n-1次。在循环内部,遍历所有的边,进行松弛计算 3、遍历图中所有的边,判断是否存在这样情况d[v]>d[u]+w(u,v); ,则表示图中存在从源点可达的负权回路
1 int SPFA() 2 { 3 memset(inq,0,sizeof(inq)); 4 for(int i =0 ;i <= n ;i++) 5 dis[i] = INF; 6 top = 0; 7 dis[1] = 0; 8 que[top++] = 1; 9 inq[1]=true; 10 for(int i = 0;i != top ;i = i+1%N)//队列不为空,注意i和top不是同时循环到下一次的 11 { 12 int u = que[i]; 13 inq[u]=false; 14 for(int j = head[u] ; j!=-1 ;j = edge[j].next) 15 { 16 Edge e = edge[j]; 17 if(dis[e.to]>dis[u]+e.w) 18 { 19 dis[e.to] = dis[u]+e.w; 20 if(inq[e.to]==false) 21 { 22 que[top++] = e.to; 23 top %= N;//循环队列 24 inq[e.to] = true; 25 } 26 } 27 } 28 } 29 return dis[n]; 30 }
三.Floyd 算法:
解决的问题:1.全局最短路。2.最长路
思想:动态规划
做法:对于每个节点k,都对定点对[i,j]做一次松弛操作
算法复杂度:O(n^3)
核心算法:
1 for (int k=0; k<n; ++k) { 2 for (int i=0; i<n; ++i) { 3 for (int j=0; j<n; ++j) { 4 /* 5 实际中为防止溢出,往往需要选判断 dist[i][k]和dist[k][j 6 都不是Inf ,只要一个是Inf,那么就肯定不必更新。 7 */ 8 if (dist[i][k] + dist[k][j] < dist[i][j] ) { 9 dist[i][j] = dist[i][k] + dist[k][j]; 10 } 11 } 12 } 13 }
说明:最短路的最优子结构(不只是对于Floyd,对于任何的最短路算法都有次性质)可以通过反证法证明,这里应用这个最优子结构的性质,来用Floyd算法来记录最短路的路径
1 void floyd() { 2 for(int i=1; i<=n ; i++){ 3 for(int j=1; j<= n; j++){ 4 if(map[i][j]==Inf){ 5 path[i][j] = -1;//表示 i -> j 不通 6 }else{ 7 path[i][j] = i;// 表示 i -> j 前驱为 i 8 } 9 } 10 } 11 for(int k=1; k<=n; k++) { 12 for(int i=1; i<=n; i++) { 13 for(int j=1; j<=n; j++) { 14 if(!(dist[i][k]==Inf||dist[k][j]==Inf)&&dist[i][j] > dist[i][k] + dist[k][j]) { 15 dist[i][j] = dist[i][k] + dist[k][j]; 16 path[i][k] = i; 17 path[i][j] = path[k][j]; 18 } 19 } 20 } 21 } 22 } 23 void printPath(int from, int to) { 24 /* 25 * 这是倒序输出,若想正序可放入栈中,然后输出。 26 * 27 * 这样的输出为什么正确呢?个人认为用到了最优子结构性质, 28 * 即最短路径的子路径仍然是最短路径 29 */ 30 while(path[from][to]!=from) { 31 System.out.print(path[from][to] +""); 32 to = path[from][to]; 33 } 34 }
当然,这里只是提供一个思路,其实对于所有的最短路保存路径都可以用类似是dfs保存路径的方法,通过保存前驱的方法,每次松弛操作的时候对前驱也进行修改就可以了
【最小生成树问题】
解决最小生成树问题的算法有:prim算法和kruskal算法
一.prim 算法(普利姆算法)
思想:贪心
做法:类似于dijkstra算法,只不过是每次选取的点是距离生成树最近的点,更新的时候不再是更新这个点到起始点的最短距离,而是到这个生成输的最短距离。
核心代码:
1 void prim(int s , int n) 2 { 3 int i , j , k ; 4 for( i = 1 ; i <= n ;i++) 5 { 6 p[i] = false; 7 dist[i] = mp[s][i]; 8 } 9 p[s] = true; 10 dist[s] = 0; 11 for(i = 1 ; i < n ; i++) 12 { 13 int Min = INF; 14 int k = 0 ; 15 for( j = 1 ; j <= n ;j++) 16 { 17 if(!p[j]&&dist[j]<Min) 18 { 19 Min = dist[j]; 20 k = j; 21 } 22 } 23 if(Min==INF) return ; 24 p[k] = true; 25 for(j = 1 ; j <= n ;j++) 26 { 27 if(!p[j]&&mp[k][j]!=INF&&dist[j]>mp[k][j]) 28 dist[j] = mp[k][j]; 29 } 30 } 31 }
时间复杂度:邻接矩阵:O(v^2) 邻接表:O(elog2v)
二.kruskal 算法
思想:并查集
做法:将原图G中所有E条边按权值从小到大排序。循环:从权值最小的边开始遍历每条边,直至图G_new中所有的节点都在同一个连通分量中。每次从图中未加入树种的边种找到边权最小的看,这两个点的祖先是否是一个,如果是一个说明两个点已经是一个树上的了,不做操作,如果两个点来自不同的树即有不同的祖先,那么就把这两个点所代表的两个树合并起来,最后当所有的点都在一棵树上的时候停止操作,一般用合并次数来控制,即n个点需要合并n-1次,所以最好合并操作写在kruskal函数内部,这样方便统计步数
核心代码:
1 struct Edge{ 2 int from; 3 int to; 4 int w; 5 bool operator < (const Edge &a) const 6 { 7 return w<a.w; 8 } 9 }edge[N*N]; 10 int fa[N]; 11 int Getfa(int x){return (fa[x]==x)?x:fa[x] = Getfa(fa[x]); } 12 int fl; 13 int n,m; 14 bool solve(int x){ 15 int cnt = 0;//共合n-1次结束 16 for(int i = 1; i <= n; i++) fa[i] = i;//注意点是从1开始编号的 17 for(int i = x; i < m; i++){ 18 int X = Getfa(edge[i].from); 19 int Y = Getfa(edge[i].to); 20 if(X != Y){ 21 fa[X] = Y; 22 cnt++; 23 if(cnt==n-1){ fl = edge[i].w;return true;} 24 } 25 } 26 return false; 27 }
注意:对于图的题,一定要根据题意来选取是用链表存储还是用数组存储,选择好的存储方法便于解决问题,也要注意点的编号是从几开始的。