[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

posted @ 2025-05-22 22:56  onlyblues  阅读(27)  评论(0)    收藏  举报
Web Analytics