【IOI 2018】Highway 高速公路收费
这是一道极好的图论题,虽然我一开始只会做$18$分,后来会做$51$分,看着题解想了好久才会做(吐槽官方题解:永远只有一句话),但这的确是一道好题,值得思考,也能启发思维。
如果要讲这道题,还是要从部分分一点一点讲起,毕竟解题时的思路也是慢慢这么推进的。
首先第一次把所有边都变成同一种颜色,询问可以得到$s$到$t$的无权最短路径的长度$dist$。这个询问是必须的,因为这个$dist$在接下来的判定中起来很大的作用。
$Subtask 2$:给定一棵树,知道$s$是根,求$t$:
这个问题将作为子问题多次出现在后续的问题中。由于询问次数的限制,猜想得到答案的方法是二分,事实上也是如此。
思考我们该在什么上二分呢?我们可以在$Bfs$序上二分。
具体来讲,如果二分到区间$[l,r]$时,我们需要判定的就是$t$会不会存在于$[l, mid]$中。假设一开始所有树边都是$B$,我们把一条边染成$A$当且仅当它是某一个在$Bfs$序的前缀$[1,mid]$中出现过的点的祖先链上。这样,可以发现,所有在$[1,mid]$里的点到根的距离都是完全被$A$覆盖的,而$[mid + 1, n]$里的点到根的距离至少存在一条$B$边。所以我们可以通过$ask$返回值是否为$dist * A$来判定二分。
有一个小问题,在这个子问题中我们把$A$和$B$互换是不会产生什么问题的,因为树上的最短路径是唯一的。虽然这个看起来十分显然,但可能就在后面的任务中引发问题,先留作读者思考。
如果需要解决这个子问题,你需要$log_{2}n = 17$次询问。
$Subtask 4$:给定一棵树,找出$s$和$t$:
这个子任务的形式已经和最终的任务很像了,只不过一个是树,一个是图。考虑到我们能不能把这个问题转化成已知的问题。
如果可以找到一条边$e=u -> v$,使得$e$一定在$s$到$t$的路径上。那我们就可以割掉$e$,然后分成两棵树,在$u$为根的树里找$s$,在$v$为根的树里找$t$。并且如果当前在$u$的树里找$s$,则需要把$e$和$v$为根的树里的所有边都赋成$A$,就能和$Subtask 2$一样做了。
如果需要解决这个子问题,你需要$log_{2}m + 2 log_{2}\frac{n}{2} = 17+16+16=49$次询问。
$Subtask 6$:给定一张图,找出$s$和$t$:
这里同样考虑我们如果可以找到一条边$e=u->v$,使得$e$至少在$s$到$t$的一条最短路上(不带权,以下的最短路皆指不带权)。这里考虑到图上的最短路可能有多条,会比树上稍麻烦一点。这里详细介绍一下具体怎么找到这条边,因为在$Subtask 4$中也有类似的问题。我们把$m$条边列到一个序列中,同样用二分来解决。假设一开始所有边都是$A$,我们知道可行的$e$一定会在序列的$[l,r]$中出现至少一条,$s->t$的最短路仍然是$dist*A$,判定$e$会不会在$[l,mid]$之间存在:我们先把$[l,mid]$的边染成$B$,询问$s$到$t$的最短路,如果最短路不再是$dist*A$,说明原先图中没有被$B$覆盖任何一条边的一条从$s->t$的最短路上,现在被$[l,mid]$中至少有一条边覆盖了,说明$[l,mid]$中至少存在一条可行的$e$,于是把$[l,mid]$染回$A$,在$[l,mid]$中二分;如果询问到的最短路仍是$dist * A$,说明至少存在一条最短路还全部由$A$覆盖,答案会存在于$[mid + 1,r]$中。这个想法的出发点,主要是每次缩小二分范围时,始终保证存在一条最短路还全部都由$A$覆盖。
现在我们找到的这条$u->v$的边了,假设这条路是$s->u->v->t$的,那么可得$s$到$u$的距离一定严格小于到$v$的距离,$t$到$v$的距离严格小于到$u$的距离,请读者自行证明。那我们可以分别从$u,v$两点$Bfs$一遍,得到的$s$的候选点集$S$和$t$的候选点集$T$是不相交的,并且我们得到了两棵$Bfs$树。当我们把不在$Bfs$树上的边全都染成$B$,树边染成$A$时,我们发现问题已经转化成了$Subtask4$的样子。事实上,我们接着按照$Subtask4$的方法去做,就能完整地解决这个问题了。
但是这里我还是想要讲一下细节上存在的问题,也就是染$A$染$B$交换什么时候会出问题。在树上做的时候完全不会有问题,我前面说了,因为树上的最短路径是唯一的。但图上我们始终坚持保留一条全$A$的路径,也就是让判定条件是$dist*A$,原因很简单,如果不这么做会导致另一条不一定是最短路的路径成为当前染色情况下的带权最短路。
举一个在找$e=u->v$时的例子,假设$A<<B$,$s$到$t$有两条长为$2,3$的路径,一开始所有边都是$B$:如果我们第一次二分把长度为$3$的路径染为$A$,得到的回答会是$3 * A$,也就是走了长度为$3$的路,我们无法从中知道这些染成$A$中的边中是否一定存在最短路上的边。
以及在$Bfs$树上二分的时候,如果调换$A,B$的角色,只把树边染成$B$,每次二分的时候把树上的二分范围以外的边染成$A$,问题就会和上个例子类似。实际结果就是询问得到的最短路有可能是从横跨两棵树的边上绕过去的,而并没有经过$u->v$,因为一个点可能从它的子树中绕出去会更短,具体例子在这里就不做说明了。
于是来说明一下本题的询问的次数:
最开始需要$1$次询问,得到最短路径长度$dist$。
找出$e=u->v$的复杂度是$log_{2}m$的,最坏需要$17$次。
$e$把所有点都分成两棵树,分别二分,最坏情况下两棵树大小相同,为$\frac{n}{2}$,那需要$2log_{2}\frac{n}{2}=32$次。(!此行有关最坏次数的计算存在一点小问题,请读者仔细思考为什么并不是相等时最劣)
总共需要$50$次询问,正好卡到题目限制的上界。事实上,这个算法的询问次数很难被卡到正好$50$次,至少官方数据没有。
至于时间复杂度,$O(nlogn)$想必是没有什么问题的。
$\bigodot$技巧&套路:
- $Bfs$树的利用
- 交互题中二分判定准则的选定
- 未知问题向已知的子问题的转化
- 严谨的证明与细心的考虑
#include "highway.h" #include <queue> #include <algorithm> using namespace std; typedef long long LL; const int N = 150005; int n, m, a, b, dist, ide; int dis_u[N], dis_v[N], bo[N], fu[N], fv[N]; vector<int> u, v, qr, gu, gv; vector<pair<int, int> > g[N]; int Search_edge() { fill(qr.begin(), qr.end(), 0); dist = ask(qr) / a; int l = 0, r = m - 1; for (int md; l < r; ) { md = (l + r) >> 1; fill(qr.begin() + l, qr.begin() + md + 1, 1); if (ask(qr) == (LL)dist * a) { l = md + 1; } else { fill(qr.begin() + l, qr.begin() + md + 1, 0); r = md; } } return l; } void Bfs(int s, int *dis, int *fa) { static queue<int> Q; fill(dis, dis + n, -1); Q.push(s), dis[s] = 0; for (int x; !Q.empty(); ) { x = Q.front(), Q.pop(); for (auto p : g[x]) { if (dis[p.first] == -1) { dis[p.first] = dis[x] + 1; fa[p.first] = p.second; Q.push(p.first); } } } } int Solve(vector<int> &gr, vector<int> &gf, int book, int *fr, int *ff) { int l = 0, r = gr.size() - 1; for (int md; l < r; ) { md = (l + r) >> 1; fill(qr.begin(), qr.end(), 1); qr[ide] = 0; for (int i = 1; i < gf.size(); ++i) qr[ff[gf[i]]] = 0; for (int i = 1; i <= md; ++i) qr[fr[gr[i]]] = 0; if (ask(qr) != (LL)dist * a) { l = md + 1; } else { r = md; } } return gr[l]; } void find_pair(int _n, vector<int> _u, vector<int> _v, int _a, int _b) { n = _n, a = _a, b = _b; u = _u, v = _v; m = u.size(), qr.resize(m); ide = Search_edge(); for (int i = 0; i < m; ++i) { g[u[i]].emplace_back(v[i], i); g[v[i]].emplace_back(u[i], i); } Bfs(u[ide], dis_u, fu); Bfs(v[ide], dis_v, fv); for (int i = 0; i < n; ++i) { if (dis_u[i] < dis_v[i]) gu.push_back(i), bo[i] = -1; if (dis_u[i] > dis_v[i]) gv.push_back(i), bo[i] = 1; } sort(gu.begin(), gu.end(), [](int x, int y) { return dis_u[x] < dis_u[y]; }); sort(gv.begin(), gv.end(), [](int x, int y) { return dis_v[x] < dis_v[y]; }); int as = Solve(gu, gv, -1, fu, fv); int at = Solve(gv, gu, 1, fv, fu); answer(as, at); }