1 条题解
-
0
题目就是让我们求一个有各种优惠政策的单源点 ,单汇点 的最短路。优惠政策是:
- 到达能力为 的点,可以让之前走过的距离除以 。
- 到达能力为 的点,可以让之前走过的距离直接变成 。
有限制:不能经过 多次。那其实也就是说, 只能作为答案路径的终点,不能作为路径的途经点。
优惠政策最短路,再加上 97 分中 的限制为 ,优惠政策能使用的次数很低。
上面加粗的两句正是分层图最短路的两条重要特征。于是想到建立 层的分层图,来解决“除以 ”这个优惠政策。
这里默认读者已经学会分层图,若不会,请先学习并完成分层图模板:JLOI2011 飞行路线。这里还有分层图题单,供读者练习。
原图为第 层图,总共建立 层图(编号 ),第 层的点 代表之前使用优惠恰好 次,到达点 的状态。我们记其为 。设它的最短路为 。
最短路算法必须使用 dijkstra,用 spfa 虽然可以通过原题的数据,但复杂度是错的,讨论区中有 hack 数据。
定义 为原图上 和 两点之间的边权。如果 , 两点之间没有相连接的边则 未定义。
思路一
首先想一下,到达能力为 的点可以清空最短路,那其实就相当于从这个点开始走。
因此,我们将所有能从 出发,不途径 到达的,能力同时为 的点看做源点,跑多源最短路即可。
那么能力为 就解决了,现在只需要解决能力 。
考虑在 dijkstra 枚举出边时进行如下松弛:
- 尝试用 更新 。代表不使用优惠政策。
- 尝试用 更新 。代表使用优惠政策。
同时注意 不能是途经点,因此 强制不松弛其它点即可。
跑一个源点为 的单源最短路,然后取 ,,……中的最小值,作为答案即可。
然而这种思路的问题在于,上面的第二种松弛并不是平凡的最短路松弛,违背了 dijkstra 的正确性。
但是只要进行一步修改,上面的思路就是正确的了:
考虑朴素的 dijkstra 的优先队列,我们会优先取出 最小的 。而这里我们修改成:
- 对于 不同层 的两个点 和 ,不妨令 ,则 更优先,不管最短路;
- 对于 同层 的两个点 和 ,则取 更小的更优先。
每次取最优先的开始松弛就一定没有问题。解释一下原因:
分层图是单向导通的,高层点的最短路一定不会影响低层点。
观察全局上优先队列取出的过程,大体上是优先低层,然后再优先低距离。
也就是说,在第 层图上,只有从点 出发能到达(中途不经过 )的所有点,都当过一次队头,并松弛一遍其它点之后,才会开始处理第 层图的点。
我们称 dijkstra 的过程中,层号为 的点充当队头的阶段为第 阶段。那么整个 dijkstra 过程就是 个阶段的综合:第 阶段,第 阶段,……,第 阶段。
那么在第 阶段之后,第 层的所有点的最短路都已经被计算好,并且已经为真实值了(原因就是高层最短路不会影响低层)。此时,目前第 层的点 最短路的值是:只途径 层的所有点,到达 这个点的距离的一半。
先不进行第 阶段,来看第 阶段之前,我们只看第 层的点,会发现只有这个变化:
一般的单源最短路,在初始时,都是源点距离为 ,而其它点距离为 。
而这里的单源最短路,若干点的距离已知,而其它点的距离为 。
会发现这种变化是不影响 dijkstra 的。可以联想一下,将这些距离已知的点 ,新建一个虚点,对虚点向 连一条权值为 的边。然后对虚点为源点,跑单源最短路,那么第一步松弛的结果就是目前第 层点的状态。
显然 dijkstra 经过一次松弛之后仍然是正确的,所以该算法是没问题的。
上面我们阐述了从第 阶段到第 阶段的正确性,事实上,在第 个阶段结束后,前 层的最短路都已经确定,而第 层的最短路可以看作若干个点的距离已经已知的最短路,所以第 个阶段仍然是正确的。
因此所有阶段都是正确的,算法正确。
这里设边数为小写 ,复杂度为 。
100 分做法
上面的做法是 97 分的,。考虑 100 分的 。
我们发现,事实上用一些次优惠政策之后,最短路会变的很低,远远低于精度。
具体来说,本题中最短路最大不超过边数乘边权最大值,即 。只需让这个数除以 次 就可以掉到 以下()。
也就意味着使用 次优惠政策之后,再优惠只可能会让最短路再少个 以下。
而题目要求精度 就够了,所以求出的最短路比真实最短路多个 以下是可以接受的。
因此,令 ,然后再按照 97 分做法做即可。复杂度 。
当然,如果你当心上面的 误差能多次累计,以至于影响答案(虽然笔者感觉应该是不会的),把阈值设的高一点也没问题。
#include <bits/stdc++.h> const int N = (int)1e5 + 5; const int K = 75; typedef std :: pair <int, int> pii; struct node { int p; int u; double dis; bool operator < (const node b) const { if (this -> p != b.p) return this -> p > b.p; return this -> dis > b.dis; } }; std :: vector <pii> G[N]; double dis[K][N]; std :: bitset <N> vis[K]; std :: bitset <N> con; inline void init(int n, int k) { con.reset(); for (int u = 0; u < n; ++u) G[u].clear(); for (int p = 0; p <= k; ++p) vis[p].reset(); for (int p = 0; p <= k; ++p) for (int u = 0; u < n; ++u) dis[p][u] = 1e17; } void dfs(int u, int t) { con.set(u); for (pii e : G[u]) { int v = e.first; if (!con[v] && v != t) dfs(v, t); } } double solve(int n, int m, int k, int t, std :: vector <int> us, std :: vector <int> vs, std :: vector <int> wei, std :: vector <int> a) { if (k > 70) k = 70; init(n, k); for (int i = 0; i < m; ++i) { int u = us[i], v = vs[i], w = wei[i]; G[u].push_back({v, w}); G[v].push_back({u, w}); } dfs(0, t); std :: priority_queue <node> q; for (int u = 0; u < n; ++u) { if (con[u] && (a[u] == 0 || u == 0)) { dis[0][u] = 0; q.push({0, u, 0}); } } while (!q.empty()) { node now = q.top(); q.pop(); int u = now.u, p = now.p; if (vis[p][u] || u == t) continue; vis[p].set(u); for (pii e : G[u]) { int v = e.first, w = e.second; if (dis[p][v] > dis[p][u] + w) { dis[p][v] = dis[p][u] + w; if (!vis[p][v]) q.push({p, v, dis[p][v]}); } if (a[v] == 2 && p < k) { if (dis[p + 1][v] > (dis[p][u] + w) / 2) { dis[p + 1][v] = (dis[p][u] + w) / 2; if (!vis[p + 1][v]) q.push({p + 1, v, dis[p + 1][v]}); } } } } double ans = DBL_MAX; for (int p = 0; p <= k; ++p) ans = std :: min(ans, dis[p][t]); if (ans > 1e15) return -1; return ans; }思路二
如果你想不到修改堆排序方式,还有一种思路,我们只需要对原题进行转化并进行合理地分层之后,套一个裸 dijkstra 就可以解决。
考虑建立反图(虽然无向图的反图和原图是一样的),观察反的最短路径上,优惠政策的含义:
- 原先遇到能力为 的点,会把当前距离设置为 ;
- 那么现在遇到能力为 的点,相当于让之后走过的所有边权都变成 。
- 原先遇到能力为 的点,会把当前距离减半;
- 那么现在遇到能力为 的点,相当于让之后走过的所有边权都变成原来的一半。
我们发现,直接让第 层上 和 之间的边权为原图上 和 之间边权的 ,就能满足能力为 的点;至于能力为 的点,我们可以新建一层 层,让这一层内 和 之间的边权都改成 。然后让所有 的 直接单向导通到 层即可。
这样以来,裸的 dijkstra 就可以解决问题。此时汇点是 ,源点是 。
同时注意一下不能途径 的问题。思路一中,我们是允许某个点松弛 的,只是不允许 松弛别人而已;思路二中我们是从 出发,这里我们不能允许任意一个点松弛 。
#include <bits/stdc++.h> const int N = (int)1e5 + 5; const int K = 75; typedef std :: pair <int, int> pii; struct node { int p; int u; double dis; bool operator < (const node b) const { return this -> dis > b.dis; } }; std :: vector <pii> G[N]; double dis[K][N]; std :: bitset <N> vis[K]; double val[K]; inline void init(int n, int k) { val[0] = 1; for (int i = 1; i <= k; ++i) val[i] = val[i - 1] / 2; val[k + 1] = 0; for (int u = 0; u < n; ++u) G[u].clear(); for (int p = 0; p <= k + 1; ++p) vis[p].reset(); for (int p = 0; p <= k + 1; ++p) for (int u = 0; u < n; ++u) dis[p][u] = 1e17; } double solve(int n, int m, int k, int t, std :: vector <int> us, std :: vector <int> vs, std :: vector <int> wei, std :: vector <int> a) { if (k > 70) k = 70; init(n, k); for (int i = 0; i < m; ++i) { int u = us[i], v = vs[i], w = wei[i]; G[u].push_back({v, w}); G[v].push_back({u, w}); } std :: priority_queue <node> q; dis[0][t] = 0; q.push({0, t, 0}); while (!q.empty()) { node now = q.top(); q.pop(); int u = now.u, p = now.p; if (vis[p][u]) continue; vis[p].set(u); for (pii e : G[u]) { int v = e.first, w = e.second; if (v == t) continue;// 这里阻止松弛的方式和思路一不同,原理已经解释 if (a[v] == 0) { if (dis[k + 1][v] > dis[p][u] + w * val[p]) { dis[k + 1][v] = dis[p][u] + w * val[p]; if (!vis[k + 1][v]) q.push({k + 1, v, dis[k + 1][v]}); } continue; } if (dis[p][v] > dis[p][u] + w * val[p]) { dis[p][v] = dis[p][u] + w * val[p]; if (!vis[p][v]) q.push({p, v, dis[p][v]}); } if (a[v] == 2 && p < k) { if (dis[p + 1][v] > dis[p][u] + w * val[p]) { dis[p + 1][v] = dis[p][u] + w * val[p]; if (!vis[p + 1][v]) q.push({p + 1, v, dis[p + 1][v]}); } } } } double ans = DBL_MAX; for (int p = 0; p <= k + 1; ++p) ans = std :: min(ans, dis[p][0]); if (ans > 1e15) return -1; return ans; }
- 1
信息
- ID
- 7476
- 时间
- 2500ms
- 内存
- 2048MiB
- 难度
- 10
- 标签
- 递交数
- 5
- 已通过
- 1
- 上传者