图论算法——最短路系列
最短路经典算法整理
1.弗洛伊德:三重循环
例题:
codevs
1020 孪生蜘蛛
时间限制: 1 s
空间限制: 128000 KB
题目等级 : 黄金 Gold
题目描述 Description
在G城保卫战中,超级孪生蜘蛛Phantom001和Phantom002作为第三层防卫被派往守护内城南端一带极为隐秘的通道。
根据防护中心的消息,敌方已经有一只特种飞蛾避过第二层防卫,直逼内城南端通道入口。但优秀的蜘蛛已经在每个通道内埋下了坚固的大网,无论飞蛾进入哪个通道,他只有死路一条!(因为他是无法挣脱超级蛛网的)
现在,001和002分别驻扎在某两个通道内。各通道通过内线相通,通过每条内线需要一定的时间。当特种飞蛾被困某处,001或002会迅速赶来把它结果掉(当然是耗时最少的那个)。
001跟002都想尽早的完成任务,他们希望选择在最坏情况下能尽早完成任务的方案。
输入描述 Input Description
第一行为一个整数N (N<=100) 表示通道数目。
接下来若干行每行三个正整数a,b,t 表示通道a,b有内线相连,通过的时间为t。(t<=100)
(输入保证每个通道都直接/间接连通)
输出描述 Output Description
两个不同的整数x1,x2,分别为001,002驻扎的地点。(如果有多解,请输出x1最小的方案,x1相同则输出x2最小的方案)
样例输入 Sample Input
3
1 2 5
2 3 10
3 1 3
样例输出 Sample Output
1 2
数据范围及提示 Data Size & Hint
1 #include <iostream> 2 #include <string.h> 3 #include <stdio.h> 4 #include <algorithm> 5 #define inf 0x7fffffff 6 using namespace std; 7 int d[110][110],b[110][110],id; 8 int main (){ 9 int n,a,b1,t; 10 cin>>n; 11 for (int i=0;i<=n;i++) 12 for (int j=0;j<=n;j++) 13 if (i==j) d[i][j]=0; 14 else d[i][j]=inf; 15 while(scanf("%d%d%d",&a,&b1,&t)!=EOF) 16 d[a][b1]=d[b1][a]=t; 17 18 for (int k=1;k<=n;k++) 19 for (int i=1;i<=n;i++) 20 for (int j=1;j<=n;j++) 21 if (d[i][k]<inf&&d[k][j]<inf) 22 d[i][j]=min(d[i][j],d[i][k]+d[k][j]);//fuluoyide弗洛伊德算法 23 24 int Min=inf,p,q; 25 for(int i=1;i<=n;i++)//zuihuaiqingkuang最坏情况处理 26 for(int j=1;j<=n;j++) 27 { 28 if(i==j)continue; 29 else 30 { 31 for(int k=1;k<=n;k++) 32 b[i][j]=max(b[i][j],min(d[i][k],d[j][k])); 33 } 34 if(Min>b[i][j])Min=b[i][j],p=i,q=j; 35 } 36 cout<<p<<" "<<q; 37 return 0; 38 }
上述算法注意用弗洛伊德处理最坏情况;
2 Dijkstra
1 设起点为s,dis[v]表示从s到v的最短路径,pre[v]为v的前驱节点,用来输出路径。 2 a)初始化:dis[v]=∞(v≠s); dis[s]=0; pre[s]=0; 3 b)For (i = 1; i <= n ; i++) 4 1.在没有被访问过的点中找一个顶点u使得dis[u]是最小的。 5 2.u标记为已确定最短路径 6 3.For 与u相连的每个未确定最短路径的顶点v 7 if (dis[u]+w[u][v] < dis[v]) 8 { 9 dis[v] = dis[u] + w[u][v]; 10 pre[v] = u; 11 } 12 c)算法结束:dis[v]为s到v的最短距离;pre[v]为v的前驱节点,用来输出路径。
例题:
最小花费
最小花费
【问题描述】
在n个人中,某些人的银行账号之间可以互相转账。这些人之间转账的手续费各不相同。给定这些人之间转账时需要从转账金额里扣除百分之几的手续费,请问A最少需要多少钱使得转账后B收到100元。
【输入格式】
第一行输入两个正整数n,m,分别表示总人数和可以互相转账的人的对数。
以下m行每行输入三个正整数x,y,z,表示标号为x的人和标号为y的人之间互相转账需要扣除z%的手续费 (z<100)。
最后一行输入两个正整数A,B。数据保证A与B之间可以直接或间接地转账。
【输出格式】
输出A使得B到账100元最少需要的总费用。精确到小数点后8位。
【输入样例】
3 3
1 2 1
2 3 2
1 3 3
1 3
【输出样例】
103.07153164
【数据规模】
1<=n<=2000
1 #include<iostream> 2 using namespace std; 3 double a[2001][2001],dis[2001]= {0},minn; 4 int n,m,i,j,k,x,y,f[2001]= {0}; 5 void init() { 6 cin>>n>>m; 7 for(i=1; i<=m; i++) { 8 scanf("%d%d",&j,&k); 9 scanf("%lf",&a[j][k]); 10 a[j][k]=(100-a[j][k])/100; 11 a[k][j]=a[j][k]; 12 } 13 cin>>x>>y; 14 } 15 void dijkstra(int x) { 16 for(i=1; i<=n; i++)dis[i]=a[x][i]; 17 dis[x]=1; 18 f[x]=1; 19 for(i=1; i<=n-1; i++) { 20 minn=0; 21 for(j=1; j<=n; j++) 22 if(f[j]==0&&dis[j]>minn) { 23 k=j; 24 minn=dis[j]; 25 } 26 f[k]=1; 27 if(k==y)break; 28 for(j=1; j<=n; j++) 29 if(f[j]==0&&dis[k]*a[k][j]>dis[j])dis[j]=dis[k]*a[k][j]
} 31 } 32 int main() { 33 init(); 34 dijkstra(x); 35 printf("%0.8lf",100/dis[y]); 36 return 0; 37 }
3 Bellman
此算法性能较差,不经常用;
1 设s为起点,dis[v]即为s到v的最短距离,pre[v]为v前驱。w[j]是边j的长度,且j连接u、v。 2 初始化:dis[s]=0,dis[v]=∞(v≠s),pre[s]=0 3 For (i = 1; i <= n-1; i++) 4 For (j = 1; j <= E; j++) //注意要枚举所有边,不能枚举点。 5 if (dis[u]+w[j]<dis[v]) //u、v分别是这条边连接的两个点。 6 { 7 dis[v] =dis[u] + w[j]; 8 pre[v] = u; 9 }
不做过多介绍;
4 spfa
1 dis[i]记录从起点s到i的最短路径,w[i][j]记录连接i,j的边的长度。pre[v]记录前趋。 2 team[1..n]为队列,头指针head,尾指针tail。 3 布尔数组exist[1..n]记录一个点是否现在存在在队列中。 4 初始化:dis[s]=0,dis[v]=∞(v≠s),memset(exist,false,sizeof(exist)); 5 起点入队team[1]=s; head=0; tail=1;exist[s]=true; 6 do 7 { 8 1、头指针向下移一位,取出指向的点u。 9 2、exist[u]=false;已被取出了队列 10 3、for与u相连的所有点v //注意不要去枚举所有点,用数组模拟邻接表存储 11 if (dis[v]>dis[u]+w[u][v]) 12 { 13 dis[v]=dis[u]+w[u][v]; 14 pre[v]=u; 15 if (!exist[v]) //队列中不存在v点,v入队。 16 { 17 //尾指针下移一位,v入队; 18 exist[v]=true; 19 } 20 } 21 } 22 while (head < tail);
例题;
香甜的黄油(Sweet Butter)
【问题描述】
农夫John发现做出全威斯康辛州最甜的黄油的方法:糖。把糖放在一片牧场上,他知道N(1<=N<=500)只奶牛会过来舔它,这样就能做出能卖好价钱的超甜黄油。当然,他将付出额外的费用在奶牛上。
农夫John很狡猾。像以前的巴甫洛夫,他知道他可以训练这些奶牛,让它们在听到铃声时去一个特定的牧场。他打算将糖放在那里然后下午发出铃声,以至他可以在晚上挤奶。
农夫John知道每只奶牛都在各自喜欢的牧场(一个牧场不一定只有一头牛)。给出各头牛在的牧场和牧场间的路线,找出使所有牛到达的路程和最短的牧场(他将把糖放在那)。
【输入格式】butter.in
第一行: 三个数:奶牛数N,牧场数P(2<=P<=800),牧场间道路数C(1<=C<=1450)。
第二行到第N+1行: 1到N头奶牛所在的牧场号。
第N+2行到第N+C+1行:每行有三个数:相连的牧场A、B,两牧场间距(1<=D<=255),当然,连接是双向的。
【输出格式】butter.out
一行输出奶牛必须行走的最小的距离和.
【输入样例】
3 4 5
2
3
4
1 2 1
1 3 5
2 3 7
2 4 3
3 4 5
样例图形
P2
P1 @--1--@ C1
\ |\
\ | \
5 7 3
\ | \
\| \ C3
C2 @--5--@
P3 P4
【输出样例】
8 //说明:放在4号牧场最优
代码;
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 int n,p,c,i,j,x,y,t,min1,head,tail,tot,u; 6 int a[801][801],b[501],dis[801],num[801],w[801][801],team[1601]; 7 bool exist[801]; 8 int main() { 9 freopen("butter.in","r",stdin); 10 freopen("butter.out","w",stdout); 11 cin>>n>>p>>c; 12 for(i=1; i<=p; i++) { 13 b[i]=0; 14 num[i]=0; 15 for(j=1; j<=p; j++) 16 w[i][j]=0x7fffffff/3; 17 } 18 for(i=1; i<=n; i++) 19 cin>>b[i]; 20 for(i=1; i<=c; i++) { //邻接矩阵存储 21 cin>>x>>y>>t; 22 w[x][y]=t; 23 a[x][++num[x]]=y; 24 a[y][++num[y]]=x; 25 w[y][x]=w[x][y]; 26 } 27 min1=0x7fffffff/3; 28 for(i=1; i<=p; i++) { 29 for(j=1; j<=p; j++) dis[j]=0x7fffffff/3; 30 memset(team,0,sizeof(team)); //队列数组初始化 31 memset(exist,false,sizeof(exist)); //exist标志初始化 32 dis[i]=0; 33 team[1]=i; 34 head=0; 35 tail=1; 36 exist[i]=true; //起始点入队 37 do { 38 head++; 39 head=((head-1)%1601)+1; //循环队列处理 40 u=team[head]; 41 exist[u]=false; 42 for(j=1; j<=num[u]; j++) 43 if (dis[a[u][j]]>dis[u]+w[u][a[u][j]]) { 44 dis[a[u][j]]=dis[u]+w[u][a[u][j]]; 45 if (!exist[a[u][j]]) { 46 tail++; 47 tail=((tail-1)%1601)+1; 48 team[tail]=a[u][j]; 49 exist[a[u][j]]=true; 50 } 51 } 52 } while(head!=tail); 53 tot=0; 54 for(j=1; j<=n; j++) 55 tot+=dis[b[j]]; 56 if (tot<min1) min1=tot; 57 } 58 cout<<min1; 59 return 0; 60 }
5 并查集
普遍代码:
1 #include<iostream> 2 #include<cstdio> 3 using namespace std; 4 #define maxn 20001 5 int father[maxn]; 6 int m,n,i,x,y,q; 7 /* 8 int find(int x) //用非递归的实现 9 { 10 while (father[x] != x) x= father[x]; 11 return x; 12 } 13 */ 14 int find(int x) //用递归的实现 15 { 16 if (father[x] != x) father[x] = find(father[x]); //路径压缩 17 return father[x]; 18 19 } 20 void unionn(int r1,int r2) 21 { 22 father[r2] = r1; 23 24 } 25 26 int main() 27 { 28 freopen("relation.in","r",stdin); 29 freopen("relation.out","w",stdout); 30 cin >> n >> m; 31 for (i = 1; i <= n; i++) 32 father[i] = i; //建立新的集合,其仅有的成员是i 33 for (i = 1; i <= m; i++) 34 { 35 scanf("%d%d",&x,&y); 36 int r1 = find(x); 37 int r2 = find(y); 38 if (r1 != r2) unionn(r1,r2); 39 40 } 41 cin >> q; 42 for (i = 1; i <= q; i++) 43 { 44 scanf("%d%d",&x,&y);; 45 if (find(x) == find(y)) printf("Yes\n")
else printf("No\n");
} return 0; 50 51 }
经典例题:家谱树 codevs
6 prim 经典代码
算法采用与Dijkstra、Bellman-Ford算法一样的“蓝白点”思想:白点代表已经进入最小生成树的点,蓝点代表未进入最小生成树的点。 算法描述: 以1为起点生成最小生成树,min[v]表示蓝点v与白点相连的最小边权。 MST表示最小生成树的权值之和。 a)初始化:min[v]= ∞(v≠1); min[1]=0;MST=0; b)for (i = 1; i<= n; i++) 1.寻找min[u]最小的蓝点u。 2.将u标记为白点 3.MST+=min[u] 4.for 与白点u相连的所有蓝点v if (w[u][v]<min[v]) min[v]=w[u][v]; c)算法结束: MST即为最小生成树的权值之和 算法分析&思想讲解: Prim算法每次循环都将一个蓝点u变为白点,并且此蓝点u与白点相连的最小边权min[u]还是当前所有蓝点中最小的。这样相当于向生成树中添加了n-1次最小的边,最后得到的一定是最小生成树。 我们通过对下图最小生成树的求解模拟来理解上面的思想。蓝点和虚线代表未进入最小生成树的点、边;白点和实线代表已进入最小生成树的点、边
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 int g[101][101]; //邻接矩阵 6 int minn[101]; //minn[i]存放蓝点i与白点相连的最小边权 7 bool u[101]; //u[i]=True,表示顶点i还未加入到生成树中 8 //u[i]=False,表示顶点i已加入到生成树中 9 int n,i,j; 10 int main() 11 { 12 freopen("wire.in","r",stdin); 13 freopen("wire.out","w",stdout); 14 cin >> n; 15 for (i = 1; i <= n; i++) 16 for (j = 1; j <= n; j++) 17 cin >> g[i][j]; 18 memset(minn,0x7f,sizeof(minn)); //初始化为maxint 19 minn[1] = 0; 20 memset(u,1,sizeof(u)); //初始化为True,表示所有顶点为蓝点 21 for (i = 1; i <= n; i++) 22 { 23 int k = 0; 24 for (j = 1; j <= n; j++) //找一个与白点相连的权值最小的蓝点k 25 if (u[j] && (minn[j] < minn[k])) 26 k = j; 27 u[k] = false; //蓝点k加入生成树,标记为白点 28 for (j = 1; j <= n; j++) //修改与k相连的所有蓝点 29 if (u[j] && (g[k][j] < minn[j])) 30 minn[j] = g[k][j]; 31 } 32 int total = 0; 33 for (i = 1; i <= n; i++) //累加权值 34 total += minn[i]; 35 cout << total << endl; 36 return 0; 37 }
7 克鲁斯卡尔
1 算法描述: 2 初始化并查集。father[x]=x。 3 tot=0 4 将所有边用快排从小到大排序。 5 计数器 k=0; 6 for (i=1; i<=M; i++) //循环所有已从小到大排序的边 7 if 这是一条u,v不属于同一集合的边(u,v)(因为已经排序,所以必为最小), 8 begin 9 ①合并u,v所在的集合,相当于把边(u,v)加入最小生成树。 10 ②tot=tot+W(u,v) 11 ③k++ 12 ④如果k=n-1,说明最小生成树已经生成,则break; 13 end; 14 6. 结束,tot即为最小生成树的总权值之和。*/
经典代码
1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 using namespace std; 5 struct k{ 6 int x,y,v; 7 8 }a[10000]; 9 int fa[1000]; 10 int find(int x) 11 { 12 if(fa[x]!=x) 13 {fa[x]=find(fa[x]); 14 return fa[x]; 15 } 16 else 17 return x; 18 } 19 void un(int x,int y) 20 { 21 fa[x]=y; 22 } 23 int cmp( k a, k b) 24 { 25 if (a.v < b.v) return 1; 26 else return 0; 27 } 28 int main() 29 { 30 int s,m=0; 31 int n; 32 cin>>n; 33 for(int i=1;i<=n;i++) 34 { 35 for(int j=1;j<=n;j++) 36 { 37 cin>>s; 38 if(s!=0) 39 { 40 m++; 41 a[m].x=i; 42 a[m].y=j; 43 a[m].v=s; 44 } 45 } 46 } 47 for(int i=1;i<=n;i++) 48 { 49 fa[i]=i; 50 } 51 sort(a+1,a+m+1,cmp); 52 int k=0,tot=0; 53 for(int i=1;i<=m;i++) 54 { 55 int r=find(a[i].x); 56 int rr=find(a[i].y); 57 if(r!=rr) 58 { 59 un(r,rr); 60 tot+=a[i].v; 61 k++; 62 } 63 if(k==n-1) 64 { 65 break; 66 } 67 } 68 cout<<tot; 69 return 0; 70 }