【图论】最短路
最短路最常用的算法有:
单源最短路: Bellman-Ford 算法,Dijkstra 算法,SPFA 算法。
任意两点间最短路:Floyd算法。
Bellman-Ford 可以处理有负边的情况,也可以处理负圈。最多进行V - 1次迭代操作,如果第V次还进行更新操作,说明存在负圈。
Dijkstra 不仅不能处理负圈,连有负边都不能。
Floyd 也可以处理负边的情况。判断负圈只需检查最dp[i][i]是否存在负数的顶点i即可。
SPFA 可以处理负边的情况,是Bellman-Ford的一种队列实现。判断负圈只需判断顶点入队列次数,超过V次就存在负圈。
Bellman-Ford 算法:时间复杂度:O(VE)
模板:
const int INF = 0x3f3f3f3f; int d[MAX_V];//最大顶点数,设置为题目中顶点数的最大值 int V, E;//顶点数;边数 struct Edge{ int from; int to; int cost;
Edge(int _from = 0, int _to = 0, int _cost = 0) : from(_from), to(_to), cost(_cost){} } Edge es[MAX_E]; bool bellman_ford(int s){//true:有负圈 false:没有负圈 for(int i = 1; i <= V; ++i){//定点编号从1开始 d[i] = INF; } d[s] = 0;//d[i]为从s到i的最短路 for(int i = 1; i < V; ++i){//最多做V-1次 bool flag = false; for(int j = 0; j < E; ++j){ Edge e = es[i]; if(d[e.to] > d[e.from] + e.cost){ d[e.to] = d[e.from] + e.cost; flag = true; } } if(!flag) return false; } for(int i = 0; i < E; ++i){ Edge e = es[i]; if(d[e.to] > d[e.from] + e.cost) return true; } return false; }
Dijkstra 算法:O(V2)或O(ElogV)
未优化:O(V2)
const int INF = 0x3f3f3f3f; int cost[MAX_V][MAX_V]; bool used[MAX_V];//使用过的顶点 int d[MAX_V];//最大顶点数,设置为题目中顶点数的最大值 int prev[MAX_V];//最短路上的前驱顶点 int V;//顶点数 void dijkstra(int s){ for(int i = 0; i < V; ++i){ d[i] = INF; used[i] = false; pre[i] = -1; } d[s] = 0; for(int i = 1; i < V; ++i){//最多走 int k = -1; for(int j = 0; j < V; ++j){ if(!used[j] && d[j] < d[k]){ k = j;
} } if(k == -1) break; used[k] = true; for(int j = 0; j < V; ++j){ if(d[j] > d[k] + cost[k][j]){ d[j] = d[k] + cost[k][j]; prev[j] = k; } } } }
堆优化:O(ElogV)
const int INF = 0x3f3f3f3f; int d[MAX_V];//最大顶点数,设置为题目中顶点数的最大值 int V;//顶点数 struct Edge{ int to; int cost; Edge(int _v = 0, int _cost = 0) : v(_v), cost(_cost){} } typedef pair<int, int> P; vector<Edge> G[MAX_V]; void dijkstra(int s){ priority_queue<P, vector<P>, greater<P>> que; for(int i = 0; i < V; ++i){ d[i] = INF; } d[s] = 0; que.push(P(0, s)); while(!que.empty()){ P p = que.top(); que.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; que.push(P(d[e.to], e.to)); } } } } void addEdge(int s, int t, int cost){ G[s].push_back(Edge(t, cost)); }
Floyd 算法:O(V3)
Floyd实质上为dp问题。假设dp[i][j]为i与j之间的最短路,然后依次取集合中剩余点k,比较dp[i][j]和dp[i][k] + dp[k][j],即最短路要么不经过点k,要么经过点k。得到递推式:dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j])。这是一个O(n3)时间复杂度的算法。
模板:
const int INF = 0x3f3f3f3f;//巧妙设置无穷大值
int dp[MAX_V][MAX_V];//邻接矩阵表 int V;//顶点数 void floyd(){ for(int k = 0; k < V; ++k){ for(int i = 0; i < V; ++i){ for(int j = 0; j < V; ++j){ dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]); } } } }
SPFA 算法:O(KE) K为所有顶点进入队列的平均次数,一般小于等于2
const int INF = 0x3f3f3f3f; struct Edge{ int to; int cost; Edge(int _v = 0, int _cost = 0) : v(_v), cost(_cost){} } vector<Edge> G[MAX_V]; int d[MAX_V];//最大顶点数,设置为题目中顶点数的最大值 bool used[MAX_V]; int cnt[MAX_V];
int pre[MAX_V]; int V;//顶点数 bool SPFA(int s){ for(int i = 0; i < V; ++i){ d[i] = INF; cnt[i] = 0; used[i] = false;
pre[i] = -1; } d[s] = 0; cnt[i] = 1;
pre[i] = s; used[s] = true; queue<int> que; que.push(s); while(!que.empty()){ int v = que.front(); que.pop(); used[v] = false; 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;
pre[e.to] = v; if(!used[v]){ used[v] = true; que.push(v); if(++cnt[v] >= V)//入队列次数超过顶点数,则存在负圈 return true; } } } } return false; } void addEdge(int s, int t, int cost){ G[s].push_back(Edge(t, cost)); }