先来看这样一道题目
给你N个点,M条双向边,要求求出1号点到其他所有点的距离。其中 2 <= N <= 1e5, 1 <=M <= 1e6.
对于这样的一道题目 我们当然不可能开一个数组edge[N][N]来记录边的信息,根本不可能开的下。
假如开下了也会有很多边为-1,浪费了很多空间。 所以可以对存边的方式进行优化。
优化1: 对边进行优化。
因为edge[N][N]的空间需要N^2大小,当N稍微大一点点的时候,就没办法开这么大的空间。
并且由于当边的分布比较散的时候,我们每次找到一个新的最小点之后,都要遍历他所有的边去更新d[]数组,
然而当边分布分散的时候,我们会花大部分时间在不存在的边上。
在这里我们用链式前向星来存边,这样可以使得存边的空间大大减小,并且每次更新的时候遍历的边都是真正存在的边,不会在访问那些不存在的边。
链式前向星优化
1 #include2 #include 3 #include 4 #include 5 using namespace std; 6 const int N = 105; 7 const int M = 10010 * 2; 8 const int inf = 0x3f3f3f3f; 9 int d[N]; 10 bool vis[N]; 11 int head[N]; 12 int nt[M], to[M], w[M]; 13 int tot; 14 void init(){ 15 memset(head, -1, sizeof(head)); 16 tot = 0; 17 } 18 void add(int u, int v, int val){ 19 to[tot] = v; 20 w[tot] = val; 21 nt[tot] = head[u]; 22 head[u] = tot++; 23 } 24 int main() 25 { 26 int n, m; 27 while(~scanf("%d%d",&n,&m)&& (n || m)){ 28 int a,b,c; 29 init(); 30 memset(d, inf, sizeof(d)); 31 memset(vis, 0, sizeof(vis)); 32 while (m--){ 33 scanf("%d%d%d", &a, &b, &c); 34 add(a,b,c); 35 add(b,a,c); 36 } 37 d[1] = 0; 38 while (1){ 39 int min1 = inf,z = -1; 40 for (int j = 1;j <= n; j++) 41 if(!vis[j] && min1 > d[j]) 42 z = j, min1 = d[j]; 43 if(z == -1) break; 44 vis[z] = 1; 45 for (int j = head[z]; ~j; j = nt[j]){ 46 d[to[j]] = min(d[to[j]], d[z] + w[j]); 47 } 48 } 49 printf("%d\n", d[n]); 50 } 51 return 0; 52 }
当然,也可以用vector来存边的信息
1 #include2 #include 3 #include 4 #include 5 using namespace std; 6 const int N = 105; 7 const int inf = 0x3f3f3f3f; 8 int d[N]; 9 bool vis[N]; 10 struct Node{ 11 int to; 12 int w; 13 }; 14 vector G[N]; 15 int main() 16 { 17 int n, m; 18 while(~scanf("%d%d",&n,&m)&& (n || m)){ 19 int a,b,c; 20 for(int i = 1; i <= n; i++) 21 G[i].clear(); 22 memset(d, inf, sizeof(d)); 23 memset(vis, 0, sizeof(vis)); 24 while (m--){ 25 scanf("%d%d%d", &a, &b, &c); 26 G[a].push_back({b,c}); 27 G[b].push_back({a,c}); 28 } 29 d[1] = 0; 30 while (1){ 31 int min1 = inf,z = -1; 32 for (int j = 1;j <= n; j++) 33 if(!vis[j] && min1 > d[j]) 34 z = j, min1 = d[j]; 35 if(z == -1) break; 36 vis[z] = 1; 37 for(int j = 0; j < G[z].size(); j++){ 38 int v = G[z][j].to, dis = G[z][j].w; 39 d[v] = min(d[v], d[z] + dis); 40 } 41 } 42 printf("%d\n", d[n]); 43 } 44 return 0; 45 }
我个人还是更喜欢用链式前向星,虽然要写add可是链式前向星的的常数小一点。
经过优化之后,他的复杂度就变成了 N*N + 2*E,虽然还是N^2级别,可是他的常数比没优化前的小。
当然对于开头的那个题目来说,我们可以存下边的信息了,但是N^2的复杂度还是没办法接受的。
优化2:对时间进行优化(需要先明白优先队列)
我们发现 在优化1后的代码实现中,我们需要 1 找到d最小的点 2用最小的点去更新d数组。3 重复1->2的过程,直到所有的点都不会发生改变。
对于操作2来说,我们进行了优化1之后,操作2做的已经是最优了,他所干的事情没有一个是没意义的,
对于操作3来说,从dijkstra来说,只有进行了n次之后,才能保证每个点都到了最短的距离。
所以我们只能优化操作1,找到d[]值最小的点。
在这里我们使用优先队列对于时间进行。
#define pll pair
priority_queue
que.push(pll(0,1));//左边为dis 右边为点
优先队列本来是优先把大的元素放在顶上,我们可以使用top()函数来获取优先队列的优先级最高的元素。
优先队友可以自定义优先级,在这里,我们将优先级定义为
pair的第一维越小就在队列的最前面,我们把距离放在第一维,把点放在第二维。
这样每次我们从优先队列中取出一个pair,都是队列中离原点距离最小的点了。
这样我们只需要lgn的复杂度就可以找到最小的那个点了,而不是每次都n的代价扫一遍d[]的数组找到最小的那个点了。
当我们每次找到一个点之后,假设找到点为u,我们都先判断一下u 是不是被标记过了,如果被标记过了,那么我就继续再找下一个点。
如果没有被标记过,那么我们就从u点出发,看一下附近的点能不能通过这个点出发使得他的d更小。
加入现在存在点v, d[v] > d[u] + w。那么我们就更新d[v],然后把 pll(d[v], v)放进队列中,等待选取。
直到优先队列为空,那么就结束更新。
代码:
1 #include2 using namespace std; 3 #include 4 #include 5 #include 6 #include 7 typedef pair<int,int> pll; 8 using namespace std; 9 const int N = 105; 10 const int M = 10010 * 2; 11 const int inf = 0x3f3f3f3f; 12 int d[N]; 13 bool vis[N]; 14 int head[N]; 15 int nt[M], to[M], w[M]; 16 int tot; 17 void init(){ 18 memset(head, -1, sizeof(head)); 19 tot = 0; 20 } 21 void add(int u, int v, int val){ 22 to[tot] = v; 23 w[tot] = val; 24 nt[tot] = head[u]; 25 head[u] = tot++; 26 } 27 void dijkstra(){ 28 priority_queue ,greater >que; 29 que.push(make_pair(0,1));//左边为dis 右边为点 30 while(!que.empty()){ 31 pll temp = que.top(); 32 que.pop(); 33 int dis = temp.first; 34 int u = temp.second; 35 //if(dis != d[u]) continue; 36 if(vis[u]) continue; 37 vis[u] = 1; 38 for(int i = head[u]; ~i; i = nt[i]){ 39 int v = to[i]; 40 if(d[v] > d[u] + w[i]){ 41 d[v] = d[u] + w[i]; 42 que.push(pll(d[v],v)); 43 } 44 } 45 } 46 47 } 48 int main() 49 { 50 int n, m; 51 while(~scanf("%d%d",&n,&m)&& (n || m)){ 52 int a,b,c; 53 init(); 54 memset(d, inf, sizeof(d)); 55 memset(vis, 0, sizeof(vis)); 56 while (m--){ 57 scanf("%d%d%d", &a, &b, &c); 58 add(a,b,c); 59 add(b,a,c); 60 } 61 d[1] = 0; 62 dijkstra(); 63 printf("%d\n", d[n]); 64 } 65 return 0; 66 }
代码中有一个被注释的地方
if(dis != d[u]) continue;
我们可以用这一句话代替vis数组。
假设优先队列中存在 pll(10,5) pll(100,5) 2个pair, 通过上面我们可以知道, 第一个肯定是先把(10,5)的这一个pair取出来,并且d[5] = 10,以为d[n]会被更新成最小的值。
我们取出(10,5)的时候,d[5] = 10, 我们通过 d[5] = 10 去更新别的点, 更新完了之后, 假设我们接下来取出的是 (100,5) d[5] != 100, 说明5号点已经通过最优的距离更新过其他点了, 就不需要再更新一次了,从而达到标记的效果。
现在代码的复杂度就是 n*lg(n) + 2*E了。就可以解决一开始的问题了。
关于多源点最短路的问题。
现在有n个地点,m条双向边,现在有p个商店,小明想知道从任意一个点出发,到附近最近的商店的距离至少是多少。 1 <= n <= 1e5 1 <= m <= 1e6
这个问题咋一看,需要求出每个点到附近的商店的最短路是多少,没有任何头绪,然后我们转化思路,求出所有商店到任意一点的最短路。也还是多个点到多个点的最短路。
难道跑p遍dijkstra 还是跑一遍flody?
都不对,其实我们可以发现,从第1个商店走到x点的和第2个商店走到x点的距离,他的性质是不发生变化的,也就是说,都是某一个商店到x的距离。
我们转化一下思路, 开一个假的节点 s, 然后s和每个商店都存在着一条距离为0的边,我们再以s为起点,跑出s点到任意点的最短路,那么我们就可以通过一遍dijkstra得到每个点到s的最短路是多少,也是任意一点到商店的最短路径是多少,就解出来了。
HDU - 6166
题意:就是有n个城市,m条单向边,现在有p个特殊城市,求这p个特殊城市中 两两之间的最小距离是多少。
题解:这个题目的思路和上面是一样的,就是我们走路的时候带一个从特殊城市出发的标记,每次往前走的时候,遇到相同的标记就不再走,遇到不同的标记,就更新一下答案。最后找到答案然后输出就好了。
代码:
1 #include2 using namespace std; 3 #define Fopen freopen("_in.txt","r",stdin); freopen("_out.txt","w",stdout); 4 #define LL long long 5 #define ULL unsigned LL 6 #define fi first 7 #define se second 8 #define pb push_back 9 #define lson l,m,rt<<1 10 #define rson m+1,r,rt<<1|1 11 #define lch(x) tr[x].son[0] 12 #define rch(x) tr[x].son[1] 13 #define max3(a,b,c) max(a,max(b,c)) 14 #define min3(a,b,c) min(a,min(b,c)) 15 typedef pair<int,int> pll; 16 const int inf = 0x3f3f3f3f; 17 const LL INF = 0x3f3f3f3f3f3f3f3f; 18 const LL mod = (int)1e9+7; 19 const int N = 4e5 + 100; 20 struct Node{ 21 int fa; 22 LL dis; 23 int o; 24 bool operator < (const Node & x) const{ 25 return dis > x.dis; 26 } 27 }; 28 LL dis[N], ans[N], vis[N]; 29 int a[N]; 30 int head[N], nt[N], to[N]; LL d[N]; 31 int tot; 32 void add(int u, int v, LL val){ 33 to[tot] = v; 34 d[tot] = val; 35 nt[tot] = head[u]; 36 head[u] = tot++; 37 } 38 priority_queue q; 39 int n, m, p; 40 void dijk(){ 41 for(int i = 1; i <= p; i++){ 42 q.push({a[i],0,a[i]}); 43 dis[a[i]] = 0; 44 vis[a[i]] = a[i]; 45 } 46 while(!q.empty()){ 47 LL dd = q.top().dis; int rt = q.top().fa, u = q.top().o; 48 q.pop(); 49 if(dis[u] < dd) continue; 50 vis[u] = rt; 51 for(int i = head[u]; ~i; i = nt[i]){ 52 int v = to[i]; 53 LL w = d[i]; 54 if(vis[v] == vis[u]) continue; 55 if(vis[v]) { 56 LL tmp = dis[v] + dd + w; 57 ans[rt] = min(ans[rt], tmp); 58 ans[vis[v]] = min(ans[vis[v]], tmp); 59 } 60 if(w + dis[u] < dis[v]){ 61 dis[v] = dis[u] + w; 62 q.push({rt, dis[v], v}); 63 } 64 } 65 } 66 } 67 void init(){ 68 memset(dis, INF, sizeof(dis)); 69 memset(head, -1, sizeof(head)); 70 memset(ans, INF, sizeof(ans)); 71 memset(vis, 0, sizeof(vis)); 72 tot = 0; 73 } 74 int main(){ 75 int T; 76 scanf("%d", &T); 77 for(int cas = 1; cas <= T; cas++){ 78 scanf("%d%d", &n, &m); 79 int u, v, w; 80 init(); 81 for(int i = 1; i <= m; i++){ 82 scanf("%d%d%d", &u, &v, &w); 83 add(u, v, w); 84 } 85 scanf("%d", &p); 86 for(int i = 1; i <= p; i++) 87 scanf("%d", &a[i]); 88 dijk(); 89 LL Ans = INF; 90 for(int i = 1; i <= p; i++) 91 Ans = min(Ans, ans[a[i]]); 92 printf("Case #%d: %lld\n", cas, Ans); 93 } 94 95 return 0; 96 }