文章目录
假设n为图的点,m为连接的边,若m远小于n*n的图称为稀疏图,而m相对较大的图称为稠密图。稀疏图适合用vector数组保存,还有最近比较流行的邻连接表保存。在这种表示法中,每个结点i都有一个链表,里面保存着从i出发的所有边,对于无向图来说,每条边会在连接表中出现两次。
Floyd算法:
求出每两点之间的最短路,关键代码:
在这之前要先初始化,d[i][i] = 0 ,其他d为无穷大INF,注意INF不能开的太大,否则在d[i][k] + d[k][j] 的时候有可能会溢出,但是太小又有可能成为短路的一部分,一般设置大一点点就可以,例如:有m条边,每条边的长度不超过1000,那么INF可以设置成1000001,或者是在相加前加一个判断(d[i][k] < INF && d[k][j] < INF)
for(int k=1; k<=n; k++) //枚举分割点
for(int i=1; i<=n; i++)
if(graph[i][k] != INF)
for(int j=1; j<=n; j++) 枚举从i到j的路线
if(graph[i][j] > graph[i][k] + graph[k][j])
graph[i][j] = graph[i][k] + graph[k][j];
// graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j]);
三层循环时间复杂度为:n * n * n ,所以只能处理 n < 200 的情况。
但是floyd程序简单,可以一次性求出所有结点之间的最短路,能够处理有负圈的边。
负圈:带负权的边,图中可能有环路边上的权值之和为负数,这就是负圈,每一次走到这个负圈内,总权值会变小导致陷在圈里出不来,用floyd判断负圈只要判断是否存在g[i][i] < 0 即可,因为这是i从外面绕了一圈回来的最小路径,如果小于0,则说明存在负圈。
Dijkstra算法:
适用于边权为正的情况。可计算正权图上的单源最短路(即从单个远点出发,到所有结点的最短路),该算法适用于有向图和无向图。
方法实现: 利用优先队列的性质,每次弹出距离起点最近的点来处理(松弛)。
详细解说移步大佬博客:https://blog.youkuaiyun.com/qq_35644234/article/details/60870719
模板题:hdu2544
#include <cstdio>
#include <vector>
#include <queue>
using namespace std ;
const int N = 105 ;
const int INF = 1e6 ;
struct edge{ //边的终点,权值
int to , w ;
edge(int a,int b){to = a ; w = b ;} ;
} ;
struct node{
int id , dis ; //结点的编号和距离起点的距离
node(int a,int b){
id = a ; dis = b ;
}
bool operator < (const node & c)
const{
return dis > c.dis ;
}
};
vector<edge> e[N] ; //用于存图
int n , m ;
void dijkstra(int s){ //起点
int d[N] ; //存每个结点到起点的距离
bool done[N] ; //每个结点到起点的最短距离是否已经找到
for (int i = 1 ; i <= n ; ++ i ){ //初始化
d[i] = INF ;
done[i] = false ;
}
d[s] = 0 ;
priority_queue<node> q ;
q.push (node(s,d[s])) ;
while(!q.empty()) {
node u = q.top() ;
q.pop() ;
if (done[u.id]) continue ;
done[u.id] = true ;
for (int i = 0 ; i < e[u.id].size() ; ++ i){
edge y = e[u.id][i] ;
if (done[y.to]) continue ; //已找到最短路径,跳过
if (d[y.to] > u.dis + y.w){
d[y.to] = u.dis + y.w ;
q.push(node(y.to ,d[y.to])) ; //扩展新邻居入队
}
}
}
printf ("%d\n",d[n]) ;
}
int main(){
while(~scanf ("%d%d",&n,&m)){
if (m == 0 && n == 0) break ;
for (int i = 1 ; i <= n ; ++ i)
e[i].clear() ;
while(m --){ //建图
int a , b , c ;
scanf ("%d%d%d",&a,&b,&c) ;
e[a].push_back(edge(b,c)) ;
e[b].push_back(edge(a,c)) ;
}
dijkstra(1) ;
}
return 0 ;
}
dijsksta的应用
昂贵的聘礼(入口)
交换的规则可以转化为最短路,题目要求找到到达点1的最短路(最小花费)并且等级不超过m,可以枚举最低的等级起点,每次以0为起点(0到各个点都可达即为每个点本身的花费),然后每次返回0(起点)到1的最短距离,用ans保存每次进行dijksta的最小结果。
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std ;
const int N=110 ;
const int INF=0x3f3f3f ;
int n,m ;
struct node{
int id,d ;
node(int a,int b){id=a,d=b;} ;
bool operator < (const node & c)
const{
return d > c.d ;
}
};
int dis[N] ,cost[N][N] , state[N] ;
int dijksta(int level){
memset(dis,INF,sizeof(dis)) ;
dis[0]=0 ; //以0为起点(到任何点都可达)
priority_queue<node> q ;
q.push(node(0,0)) ;
while(!q.empty()){
node x = q.top() ; q.pop() ;
if(x.d > dis[x.id]) continue ;
for(int i=1 ; i<=n ;++i){
if(state[i]>=level&&state[i]<=level+m){
if(dis[i]>dis[x.id]+cost[x.id][i]){
dis[i] = dis[x.id]+cost[x.id][i] ;
q.push(node(i,dis[i])) ;
}
}
}
}
return dis[1] ;
}
int main(){
scanf("%d%d",&m,&n) ;
for(int i=0 ; i<=n ; ++i){
for(int j=0 ; j<=n ; ++j){
if(i==j) cost[i][j]=0 ;
else cost[i][j]=INF ;
}
}
for(int i=1 ; i<=n ; ++i){
int x; scanf("%d%d%d",&cost[0][i],&state[i],&x) ;
for(int j=0 ; j<x ; ++j){
int u,w; scanf("%d%d",&u,&w) ;
cost[u][i] = w ;
}
}
int ans=INF ;
for(int i=state[1]-m ; i<=state[1] ; ++i){ //从能交换的最低等级开始枚举
ans = min(ans,dijksta(i)) ;
}
printf("%d\n",ans) ;
return 0 ;
}
bellman ford 算法
#include <cstdio>
#include <vector>
using namespace std ;
const int N = 10010 ;
const int INF = 1e6 ;
struct edge{
int v , u , w ;
}e[N] ;
int n , m ;
void bellman(){
int dis[N] ;
for (int i = 1 ; i <= n ; ++ i)
dis[i] = INF ;
dis[1] = 0 ;
for (int i = 1 ; i <= n ; ++ i){ //遍历每一个点
for (int j = 1 ; j <= m ; ++ j){ //遍历每一条边
if (dis[e[j].u] > dis[e[j].v] + e[j].w)
dis[e[j].u] = dis[e[j].v] + e[j].w ;
if (dis[e[j].v] > dis[e[j].u] + e[j].w) //无向图两边都要判断
dis[e[j].v] = dis[e[j].u] + e[j].w ;
}
}
printf ("%d\n",dis[n]) ;
}
int main(){
while(~scanf ("%d%d",&n,&m)){
if (n == 0 && m == 0) break ;
for (int i = 1 ; i <= m ; ++ i){
scanf ("%d%d%d",&e[i].v,&e[i].u,&e[i].w) ;
}
bellman() ;
}
return 0 ;
}
spfa(例题:hdu2066)
移步:大佬解说
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
using namespace std ;
const int N = 10005 ;
const int INF = 1e6 ;
struct egde{
int to , w ;
egde(int a , int b){to = a ; w = b ;} ;
};
vector<egde> e[2*N] ; //要开两倍大不然会WA
queue<int> q ;
int point[N] , want[N] ;
int dis[N] ;
bool done[N] ;
int t , m , d ;
void spfa(int s){
memset(dis,INF,sizeof(dis)) ;
memset(done,false,sizeof(done)) ;
q.push(s) ;
dis[s] = 0 ;
done[s] = true ; //已在队列中
while(!q.empty()){
int head = q.front() ;
q.pop() ;
done[head] = false ; //不在队伍中
for (int i = 0 ; i < e[head].size() ; ++ i){
egde v = e[head][i] ;
if (dis[v.to] > dis[head] + v.w){
dis[v.to] = dis[head] + v.w ;
if (!done[v.to]){
q.push(v.to) ;
done[v.to] = true ; //入队
}
}
}
}
}
int main(){
while(~scanf ("%d%d%d",&t,&m,&d)){
for (int i = 1 ; i <= N ; ++ i)
e[i].clear() ;
for (int i = 1 ; i <= t ; ++ i){
int u , v , w ;
scanf ("%d%d%d",&u,&v,&w) ;
e[u].push_back(egde(v,w)) ;
e[v].push_back(egde(u,w)) ;
}
for (int i = 0 ; i < m ; ++ i)
scanf ("%d",&point[i]) ;
for (int i = 0 ; i < d ; ++ i)
scanf ("%d",&want[i]) ;
int mins = INF ;
for (int i = 0 ; i < m ; ++ i){
spfa(point[i]) ; //搜索每一个起点到个点的最短路
for (int j = 0 ; j < d ; ++ j){ //找出想去的城市离起点的最短路
mins = mins < dis[want[j]] ? mins : dis[want[j]] ;
}
}
printf ("%d\n",mins) ;
}
return 0 ;
}
spfa判断负圈:
假设有n 个点,如果有点x入队超过n次则证明有负圈
例题
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std ;
const int N = 510;
const int INF = 0x3f3f3f3f ;
int g[N][N] ,dis[N] ,cnt[N];
bool done[N] ;
int n,m,p ;
bool spfa(int ss){
memset(dis,INF,sizeof(dis)) ;
memset(cnt,0,sizeof(cnt)) ;
memset(done,false,sizeof(done)) ;
queue<int> q ; q.push(ss) ;
done[ss]=true ; //入队(在队中)
dis[ss]=0 ,cnt[ss]++ ;
while(!q.empty()){
int x = q.front() ; q.pop() ;
done[x]=false ; //出队(不在队中)
for(int i=1 ; i<=n ; ++i){
if(dis[i]>dis[x]+g[x][i]){
dis[i]=dis[x]+g[x][i] ;
if(!done[i]){
q.push(i),done[i]=true,++cnt[i] ;
if(cnt[i]>=n) return true ;//存在负圈
}
}
}
}
return false ; //没有负圈
}
int main(){
int t; scanf("%d",&t) ;
while(t--){
scanf("%d%d%d",&n,&m,&p) ;
for(int i=1 ; i<=n ; ++i){
for(int j=1 ; j<=n ; ++j){
if(i==j) g[i][j]=0 ;
else g[i][j]=INF ;
}
}
for(int i=0 ; i<m ; ++i){
int u,v,w ; scanf("%d%d%d",&u,&v,&w) ;
g[u][v]=g[v][u]=min(g[v][u],w) ;
}
for(int i=0 ; i<p ; ++i){
int u,v,w ; scanf("%d%d%d",&u,&v,&w) ;
g[u][v]=min(g[v][u],-w) ;//虫洞(单向)
}
if(spfa(1)) printf("YES\n") ;
else printf("NO\n");
}
return 0 ;
}
spfa和dijkstra的区别:
-
相似:两者很像,都是从起点s出发,逐步扩展邻居结点。
-
区别:
-
(1)dijkstra每次扩展邻居结点,都从中找一个点,它到s的距离是最小的;
(2)SPFA并没有找这个点,而是更新这些邻居点的状态(这些点到s的距离),这需要进行多轮更新,等所有点都不能再更新了,就结束了。 -
因此,dijkstra比SPFA要快。
-
但是,从上述区别也看出,dikstra不能处理负权边,而SPFA能。
(1)dijkstra每次扩展邻居结点,都从中找一个点,它到s的距离是最小的;
(2)SPFA并没有找这个点,而是更新这些邻居点的状态(这些点到s的距离),这需要进行多轮更新,等所有点都不能再更新了,就结束了。
应用例题:
洛谷p1144 最短路的计数
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
using namespace std ;
const int N = 2e6 + 5 ;
const int INF = 1e6 ;
const int MOD = 100003 ;
vector<int> e[N] ;
int dis[N] , ans[N] ;
bool done[N] ;
int n , m ;
void spfa(){
queue<int> q ;
q.push(1) ;
memset(dis,INF,sizeof(dis)) ;
ans[1] = 1 ;
dis[1] = 0 ;
done[1] = true ;
while(!q.empty()){
int head = q.front() ;
q.pop() ;
done[head] = false ;
for (int i = 0 ; i < e[head].size() ; ++ i){
int t = e[head][i] ;
if (dis[t] > dis[head] + 1){
dis[t] = dis[head] + 1 ;
ans[t] = ans[head] ; //t和head在同一条路上 ,最短路相同
if (!done[t]){
done[t] = true ;
q.push(t) ;
}
}
else if (dis[t] == dis[head] + 1){
ans[t] += ans[head] ;
ans[t] %= MOD ;
}
}
}
}
int main(){
scanf ("%d%d",&n,&m) ;
while(m --){
int u , v ;
scanf ("%d%d",&u,&v) ;
e[u].push_back(v) ;
e[v].push_back(u) ;
}
spfa() ;
for (int i = 1 ; i <= n ; ++ i)
printf ("%d\n",ans[i]) ;
return 0 ;
}