在竞赛中,单源最短路的话,就算是使用SPFA算法,也可能被神奇的数据卡成复杂度接近O(n * m)的,那么就GG了……所以我们主要学习Dijkstra算法(复杂度在:O(n^2))及其堆优化(优化后复杂度在:O(n * logn))。
它的算法思想是贪心,咱们可以这样去理解:假设起点是 1号节点,我们要求出从 1号节点出发,到其他节点的最短路径,现在我们在每条邻接边上摆放多米诺骨牌,假设权重越大的边上摆放的骨牌数目越多,并且规定骨牌的倒下速度是一样的,那么也就是说骨牌数目越多倒下的时间越长,我没以起点 1号节点开始推到骨牌,很明显,骨牌少的那条边相邻的节点 u 是最先到达的,最先到达的节点与起点之间的路径必然是最短路径,如果不是最短路径,则可以找到更短的路径更先到达,然后 u 就可以算作已经确定最短路径的节点了。然后,此时倒塌的多米诺骨牌就分成了两路:一路还是从起点 1 开始继续倒塌,另一路就是从新的已经确定的节点 u 开始倒塌,然后更新已经确定最短路的节点集合A与未确定最短路径的节点集合B之间的直连路径,然后不断反复这个过程。
以上图为例子:先设出两个集合A和B,分别为已经确定最短路径的点的集合,另一个为A集合中的节点的相邻节点,先假设以 1号节点为起点
第零步:A{1} B{2 3 4}
第一步:A{1 4} B{2 3 6}
第二步:A{1 2 4} B{5 3 6}
……
直到所有节点的都在A集合中
int Dijkstra(int s, int t) { for (int i = 0; i <= n; ++i) dis[i] = inf; dis[s] = 0; for (int i = 0; i < n; ++i) { int u = 0; for (int j = 1; j <= n; ++j) if (!vis[j] && (!u || dis[u] > dis[j])) u = j; vis[u] = true; for (int k = 0; k < G[u].size(); k++) { int v = G[u][k].to, w = G[u][k].cost; if (dis[v] > dis[u] + w) dis[v] = dis[u] + w; } } return dis[t]; }
这个题乍一看不太像求最短路的问题,但是看数据规模,用DFS肯定会超时,不过神奇的是,在dengdengoj上提交是可以用DFS解决掉的(肯定是数据水,但是洛谷数据不水)
这个题说每次经过一个点,都要交手续费,那么怎么才能用最小的花费让对方收到100的金额?我们假设经过第 i 条边需要交付 zi%的手续费,也就是说,真正对方收到的是我付出的金额:MyMoney * (100 - zi)%的金额
假设要经过k条边,那么我们这个题要满足的公式就是:
MyMoney * PI((100 - zi)/100) = 100
我们要想MyMoney尽量少,那么这个 PI(100 - zi)/100 就要尽量大!
于是就变成了最大路径问题,这里注意初始化:dis[s] = 1(重要!!)
因为我们自己转给自己是不要手续费的,于是就是100/100=1,如果不这样初始化的话,由于我们是乘法,其他乘起来都是0了!而其他的点,暂时还没有连接,全都初始化为0,因为最开始,无法互通,也就是多少钱都汇不过去!
#include <cstdio> #include <vector> using namespace std; const int maxp = 2005; struct Edge{ int to; double cost; Edge(int t, double c) : to(t), cost(c) {} }; int n, m, vis[maxp], a, b; double dis[maxp], c; vector<Edge> G[maxp]; void Solve(int s){ dis[s] = 1.0; for(int i = 0;i < n;++i){ int u = 0; for(int j = 1;j <= n;++j) if(!vis[j] && (!u || dis[j] > dis[u])) u = j; vis[u] = 1; for(int k = 0;k < (int)G[u].size();++k) if(dis[G[u][k].to] < dis[u] * G[u][k].cost) dis[G[u][k].to] = dis[u] * G[u][k].cost; } } int main(){ scanf("%d %d", &n, &m); while(m--){ scanf("%d %d %lf", &a, &b, &c); G[a].push_back(Edge(b, (100 - c) / 100.0)); G[b].push_back(Edge(a, (100 - c) / 100.0)); } int s, t; scanf("%d %d", &s, &t); Solve(s); printf("%.8lf", 100.0 / dis[t]); return 0; }
#define _CRT_SECURE_NO_WARNINGS #include <cstdio> #include <vector> using namespace std; const int maxp = 2005; struct Edge { int to, cost; Edge(int t, int c) : to(t), cost(c) {} }; int n, m, a, b, s, t, vis[maxp]; vector<Edge> G[maxp]; double ans = 9.9999999999, c; void dfs(int p, double cnt) { if (t == p) { if (ans * 10000000000.0 > cnt * 10000000000.0) ans = cnt; return; } for (int i = 0; i < G[p].size(); ++i) { int v = G[p][i].to; if (!vis[v]) { vis[v] = 1; dfs(v, cnt * 100.0 / (100 - G[p][i].cost)); vis[v] = 0; } } } int main() { scanf("%d %d", &n, &m); while (m--) { scanf("%d %d %lf", &a, &b, &c); G[a].push_back(Edge(b, c)); G[b].push_back(Edge(a, c)); } scanf("%d %d", &s, &t); vis[s] = 1; dfs(s, 1.0); printf("%.8lf", ans * 100); return 0; }
为什么要堆优化呢???
首先我们来分析一下Dijkstra算法的复杂度:
我们要处理N个点,每次呢要在集合B中找到最优的点作为下一个点去更新集合A,如果集合B是乱序的,那么每次更新集合A就要去遍历集合B,集合B的最坏情况是包含了N个点,所以复杂度是O(NM)
如果设想B集合是有序的,那么每次找到最优的点就只需要找第一个元素就好了,有没有高效的算法或者是数据结构呢?其实有很多,平衡树、线段树都可以,但是代码量最短的肯定是运用STL包含的模板类:优先队列就可以了!
可以达到复杂度:O(MlogN)
#define _CRT_SECURE_NO_WARNINGS #include <cstdio> #include <vector> #include <queue> using namespace std; const int maxn = 1e5 + 5; struct Edge { int to, cost; Edge(int t, int c) : to(t), cost(c) {} }; vector<Edge> G[maxn]; struct Node { int N_id, N_dis; Node(int id, int di) : N_id(id), N_dis(di) {} bool operator< (const Node& n) const { return N_dis > n.N_dis; } }; int n, m, a, b, c, s, vis[maxn], dis[maxn]; void Dijkstra() { for (int i = 0; i <= n; ++i) dis[i] = 0x3f3f3f3f; dis[s] = 0; priority_queue<Node> q; q.push(Node(s, dis[s])); while (!q.empty()) { Node Now = q.top(); q.pop(); if (vis[Now.N_id]) continue; vis[Now.N_id] = 1; for (int j = 0; j < (int)G[Now.N_id].size(); ++j) { Edge e = G[Now.N_id][j]; if (vis[e.to]) continue; if (dis[e.to] > e.cost + dis[Now.N_id]) { dis[e.to] = e.cost + dis[Now.N_id]; q.push(Node(e.to, dis[e.to])); } } } } int main() { scanf("%d %d %d", &n, &m, &s); while (m--) { scanf("%d %d %d", &a, &b, &c); G[a].push_back(Edge(b, c)); } Dijkstra(); for (int i = 1; i <= n; ++i) if (dis[i] < 0x3f3f3f3f) printf("%d ", dis[i]); else printf("2147483647 "); return 0; }
这里我们不再明确的维护一个“确定最短路径的点”的一个集合A了,而是只是去研究集合B,因为图的边有M条,每次就需要向集合B拓展M次,因为要更新B集合中的连接状态,所以我们维护集合B其实也就隐性地维护了集合A!然而我们每次都要从集合B中找到最优的状态作为下一个确定最短路的节点。这个是可以用优先队列O(1)获取,O(logn)维护的。但是这就牵扯到入队和出队的问题,我们分析一下:
最开始,连起点都在集合B中,所以最初优先队列只有一个点:起点。然后逐步扩展,在扩展的时候,就自动维护了,让最短(最优)的节点放在队首,然后依次往后。每次队首出队(意味着被确定为新的最短路径的点),那么这个队首的邻接的点的距离就需要被更新,这个更新不光是距离的更新,还要把他们入队!因为我们可以知道,节点 u 的所有邻接的节点比不邻接的节点近(没有负权边的时候一定是这样的!)所以他们必须入队作为下一波的候选节点!
优化之后,复杂度:O(M*logN),但是用其他更高级的数据结构优化能够有更高的效率,在有些数据中,这种效率优势相当明显,等小良水平到了那个层次再和大家分享!