[NOIP 2013 提高组] 货车运输
[NOIP 2013 提高组] 货车运输
题目背景
NOIP2013 提高组 D1T3
题目描述
A 国有 $n$ 座城市,编号从 $1$ 到 $n$,城市之间有 $m$ 条双向道路。每一条道路对车辆都有重量限制,简称限重。
现在有 $q$ 辆货车在运输货物, 司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。
输入格式
第一行有两个用一个空格隔开的整数 $ n,m$,表示 A 国有 $ n$ 座城市和 $m$ 条道路。
接下来 $m$ 行每行三个整数 $x, y, z$,每两个整数之间用一个空格隔开,表示从 $x $ 号城市到 $ y $ 号城市有一条限重为 $z$ 的道路。
注意: $x \neq y$,两座城市之间可能有多条道路 。
接下来一行有一个整数 $q$,表示有 $q$ 辆货车需要运货。
接下来 $q$ 行,每行两个整数 $x,y$,之间用一个空格隔开,表示一辆货车需要从 $x$ 城市运输货物到 $y$ 城市,保证 $x \neq y$
输出格式
共有 $q$ 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。
如果货车不能到达目的地,输出 $-1$。
输入输出样例 #1
输入 #1
4 3
1 2 4
2 3 3
3 1 1
3
1 3
1 4
1 3
输出 #1
3
-1
3
说明/提示
对于 $30\%$ 的数据,$1 \le n < 1000$,$1 \le m < 10,000$,$1\le q< 1000$;
对于 $60\%$ 的数据,$1 \le n < 1000$,$1 \le m < 5\times 10^4$,$1 \le q< 1000$;
对于 $100\%$ 的数据,$1 \le n < 10^4$,$1 \le m < 5\times 10^4$,$1 \le q< 3\times 10^4 $,$0 \le z \le 10^5$。
解题思路
这道题做法还蛮多的,先给出基于结论的做法。
定理:对于无向图 $G$ 中连通的两点 $u$ 和 $v$,在以 $u$ 和 $v$ 构成的所有路径中,具有最大瓶颈边权(即路径上最小边权的最大值)的路径必然存在于 $G$ 的最大生成树的唯一简单路径上。
证明如下:若存在非简单路径满足最大瓶颈边权,则可通过删除该路径中环路的边将其转化为简单路径,同时保持瓶颈边权不变。因此只需考察简单路径的最大瓶颈性质。设 $w$ 为所有 $u$ 到 $v$ 的简单路径中瓶颈边权的最大值。反证法,假设 $G$ 的最大生成树 $T$ 的 $u$ 到 $v$ 的路径上存在边权小于 $w$ 的边,选择其中一条并将其从 $T$ 中删除,会得到两个连通分量,$u$ 和 $v$ 分别属于其中一个。由于 $u$ 到 $v$ 存在一条最大瓶颈边权为 $w$ 的路径,该路径中必然存在一条边权至少为 $w$ 的边来连接两个连通分量。将该边加入 $T$ 后得到新的生成树 $T'$,由于 $T'$ 的边权和严格大于 $T$,与 $T$ 是 $G$ 的最大生成树矛盾。
类似的定理还有,对于无向图 $G$ 中连通的两点 $u$ 和 $v$,在以 $u$ 和 $v$ 构成的所有路径中,具有最小瓶颈边权(即路径上最大边权的最小值)的路径必然存在于 $G$ 的最小生成树的唯一简单路径上。证明同上。
回到本题,在处理每个询问前,我们借鉴 Kruskal 算法求最小生成树的方法,先将边按边权降序排序,然后逐条尝试将边加入生成树,从而构建出最大生成树 $T$,其正确性与最小生成树的构建方法相同。那么对于每个询问 $(u,v)$,我们只需求出 $T$ 中 $u$ 到 $v$ 路径上的最小边权,为此我们还需要求出 $u$ 和 $v$ 的最近公共祖先,以及用倍增去维护 $T$ 中每个点到其祖先路径的最小边权。
AC 代码如下,时间复杂度为 $O(m\log{m} + (n+q)\log{n})$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e4 + 5, M = 5e4 + 5, INF = 0x3f3f3f3f;
array<int, 3> p[M];
int h[N], e[M], wt[M], ne[M], idx;
int fa[N];
int f[N][14], g[N][14], d[N];
void add(int u, int v, int w) {
e[idx] = v, wt[idx] = w, ne[idx] = h[u], h[u] = idx++;
}
int find(int x) {
return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
}
void dfs(int u, int p) {
d[u] = d[p] + 1;
for (int i = h[u]; i != -1; i = ne[i]) {
int v = e[i];
if (v == p) continue;
f[v][0] = u;
g[v][0] = wt[i];
for (int i = 1; i <= 13; i++) {
f[v][i] = f[f[v][i - 1]][i - 1];
g[v][i] = min(g[v][i - 1], g[f[v][i - 1]][i - 1]);
}
dfs(v, u);
}
}
int query(int u, int v) {
if (d[u] < d[v]) swap(u, v);
int m1 = INF, m2 = INF;
for (int i = 13; i >= 0; i--) {
if (d[f[u][i]] >= d[v]) {
m1 = min(m1, g[u][i]);
u = f[u][i];
}
}
if (u == v) return m1;
for (int i = 13; i >= 0; i--) {
if (f[u][i] != f[v][i]) {
m1 = min(m1, g[u][i]);
m2 = min(m2, g[v][i]);
u = f[u][i];
v = f[v][i];
}
}
if (!f[u][0]) return -1;
m1 = min(m1, g[u][0]);
m2 = min(m2, g[v][0]);
return min(m1, m2);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> p[i][1] >> p[i][2] >> p[i][0];
}
sort(p, p + m, greater<array<int, 3>>());
iota(fa + 1, fa + n + 1, 1);
memset(h, -1, sizeof(h));
for (int i = 0; i < m; i++) {
if (find(p[i][1]) != find(p[i][2])) {
add(p[i][1], p[i][2], p[i][0]);
add(p[i][2], p[i][1], p[i][0]);
fa[fa[p[i][1]]] = fa[p[i][2]];
}
}
memset(g, 0x3f, sizeof(g));
for (int i = 1; i <= n; i++) {
if (!d[i]) dfs(i, 0);
}
cin >> k;
while (k--) {
int u, v;
cin >> u >> v;
cout << query(u, v) << '\n';
}
return 0;
}
当然如果不知道上述性质的话还有其他做法,甚至比上述做法更简单。
如果 $u$ 到 $v$ 的最大瓶颈边权为 $w$,意味着我们仅需边权至少为 $w$ 的边就能使得 $u$ 和 $v$ 连通(从而存在路径)。与上述做法类似,我们可以先将边按边权降序排序,然后依次枚举每条边 $(u,v,w)$ 。如果 $u$ 和 $v$ 不在同一个连通块,设 $u$ 所在的连通块为 $C_u$,$v$ 所在的连通块为 $C_v$,那么不失一般性的,对于所有 $(u' \in C_u, v' \in C_v)$ 的询问,其答案就是 $w$。最后记得把边 $(u,v,w)$ 加入以连通 $C_u$ 和 $C_v$。
因此现在的问题是应该如何快速知道有哪些询问满足 $(u' \in C_u, v' \in C_v)$?这里提供一个离线的做法,为每个点 $u$ 开一个 std::set,记为 $\text{st}_{u}$。对于第 $i$ 个询问 $(u_i,v_i)$,分别在 $\text{st}_{u_i}$ 和 $\text{st}_{v_i}$ 插入 $i$。然后按边权从大到小枚举每条边 $(u,v,w)$,如果 $u$ 和 $v$ 不在一个连通块,并设 $C_u$ 和 $C_v$ 就是 $u$ 和 $v$ 所在连通块的代表元素,由于 $C_u$ 和 $C_v$ 会进行合并,我们也需要对 $\text{st}_{C_u}$ 和 $\text{st}_{C_v}$ 维护,方法是进行启发式合并。不失一般性假设 $\text{st}_{C_u}$ 中的元素数量小于 $\text{st}_{C_v}$,启发式合并的操作就是枚举 $\text{st}_{C_u}$ 的每个元素 $x$ 并将其插入 $\text{st}_{C_v}$,其中在插入 $x$ 前如果 $\text{st}_{C_v}$ 也含有 $x$,说明第 $x$ 个询问的两点分别在连通块 $C_u$ 和 $C_v$ 中,即第 $x$ 个询问的答案为 $w$。
AC 代码如下,时间复杂度为 $O(m\log{m} + n + q\log^2{q})$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e4 + 5, M = 5e4 + 5;
array<int, 3> p[M];
int fa[N];
set<int> st[N];
int ans[M];
int find(int x) {
return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> p[i][1] >> p[i][2] >> p[i][0];
}
cin >> k;
for (int i = 0; i < k; i++) {
int u, v;
cin >> u >> v;
st[u].insert(i);
st[v].insert(i);
}
sort(p, p + m, greater<array<int, 3>>());
iota(fa + 1, fa + n + 1, 1);
memset(ans, -1, sizeof(ans));
for (int i = 0; i < m; i++) {
int a = find(p[i][1]), b = find(p[i][2]);
if (a == b) continue;
if (st[a].size() > st[b].size()) swap(a, b);
for (auto &x : st[a]) {
if (st[b].count(x)) ans[x] = p[i][0];
st[b].insert(x);
}
st[a].clear();
fa[a] = b;
}
for (int i = 0; i < k; i++) {
cout << ans[i] << '\n';
}
return 0;
}
还有整体二分的做法,容易知道询问的答案具有二段性,假设询问的答案为 $\text{ans}$,那么对于任意的 $0 \leq w \leq \text{ans}$,在图中保留所有边权至少为 $w$ 的边,删去小于 $w$ 的边,询问的 $u$ 和 $v$ 仍保持连通。而对于任意的 $\text{ans} < w \leq W$,在图中保留所有边权至少为 $w$ 的边,删去小于 $w$ 的边,询问的 $u$ 和 $v$ 不在同一个连通块。
而如果对于每个询问都二分,然后通过并查集检验 $u$ 和 $v$ 的连通性,时间复杂度为 $O(q\, m\log{W})$。此时就很容易想到整体二分,只需一次二分,就可以求出所有询问的答案。
这里只提供大致的做法,细节可以参考 oi-wiki 的整体二分。把询问记作操作 $1$,第 $i$ 个询问对应的四元组为 $(0, u, v, i)$。把往并查集加边记作操作 $0$,加入边 $(u,v,w)$ 对应的四元组为 $(0,u,v,w)$。初始时,先把每条边对应的四元组压入一个队列(不需要考虑顺序),再把每个询问对应的四元组压入队列(同样不需要考虑顺序)。然后二分答案,假设当前答案的区间为 $[l,r]$,记 $m = \left\lceil \frac{l+r}{2} \right\rceil$,依次枚举队列中的每个元素,目标是将队列中的所有操作分成 $q_1$ 和 $q_2$ 两个部分,然后分别递归 $[l,m]$ 处理 $q_1$ 和 $[m+1,r]$ 处理 $q_2$。
具体而言,对于操作 $0$,如果 $w \geq m$ 则合并 $u$ 和 $v$ 所在的连通块(如果 $u$ 和 $v$ 不在同一个连通块),并将该四元组压入队列 $q_2$;如果 $w < m$ 则直接将四元组压入 $q_1$。对于操作 $1$,如果 $u$ 和 $v$ 在同一个连通块,说明该询问的答案至少为 $m$,则将该四元组压入 $q_2$;否则说明该询问的答案小于 $m$,将四元组压入 $q_1$。当然这里有个假设是在处理区间 $[l,r]$ 时,之前已经将边权大于 $r$ 的边加入并查集,所以对于操作 $0$ 其实加入的是边权在 $[m,r]$ 的边。为了保证这个假设成立,我们先递归到 $[m+1,r]$ 处理 $q_2$,然后通过可撤销并查集把在 $q_2$ 中通过操作 $0$ 加到的并查集的边撤销掉,再递归到 $[l,m]$ 处理 $q_1$。
递归的边界是 $l=r$,此时队列中的询问的答案就是 $l$(或 $r$)。
本题整体二分的做法与 atcdoer 的 G - Dense Buildings 非常像。
AC 代码如下,时间复杂度为 $O((m + q)\log{n}\log{W})$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e4 + 5, M = 8e4 + 5;
array<int, 4> q[M], q1[M], q2[M];
int fa[N], s[N], tp;
array<int, 2> stk[N];
int ans[M];
int find(int x) {
return fa[x] == x ? fa[x] : find(fa[x]);
}
bool merge(int x, int y) {
x = find(x), y = find(y);
if (x == y) return false;
if (s[x] > s[y]) swap(x, y);
s[y] += s[x];
fa[x] = y;
stk[++tp] = {x, y};
return true;
}
void dfs(int l, int r, int ql, int qr) {
if (ql > qr) return;
if (l == r) {
for (int i = ql; i <= qr; i++) {
if (q[i][0]) ans[q[i][3]] = l;
}
return;
}
int mid = l + r + 1 >> 1, c1 = 0, c2 = 0, c = 0;
for (int i = ql; i <= qr; i++) {
if (q[i][0]) {
if (find(q[i][1]) == find(q[i][2])) q2[c2++] = q[i];
else q1[c1++] = q[i];
}
else {
if (q[i][3] >= mid) {
if (merge(q[i][1], q[i][2])) c++;
q2[c2++] = q[i];
}
else {
q1[c1++] = q[i];
}
}
}
memcpy(q + ql, q1, c1 * sizeof(q[0]));
memcpy(q + ql + c1, q2, c2 * sizeof(q[0]));
dfs(l, mid - 1, ql, ql + c1 - 1);
while (c--) {
auto p = stk[tp--];
s[p[1]] -= s[p[0]];
fa[p[0]] = p[0];
}
dfs(mid, r, ql + c1, qr);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> q[i][1] >> q[i][2] >> q[i][3];
q[i][0] = 0;
}
cin >> k;
for (int i = m; i < m + k; i++) {
cin >> q[i][1] >> q[i][2];
q[i][0] = 1, q[i][3] = i - m;
}
iota(fa + 1, fa + n + 1, 1);
fill(s + 1, s + n + 1, 1);
dfs(-1, 1e5, 0, m + k - 1);
for (int i = 0; i < k; i++) {
cout << ans[i] << '\n';
}
return 0;
}
参考资料
绯想天 | 最小生成树上路径为最小瓶颈路的证明:https://blog.fei.ac/zh/posts/proof-mbp-mst/
题解 P1967 【货车运输】 - 洛谷专栏:https://www.luogu.com.cn/article/kp5bu2h8
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/18891761

浙公网安备 33010602011771号