Hướng dẫn cho Chung kết LQDOJ CUP 2024 - Vận chuyển hàng hóa


Chỉ sử dụng khi thực sự cần thiết như một cách tôn trọng tác giả và người viết hướng dẫn này.

Chép code từ bài hướng dẫn để nộp bài là hành vi có thể dẫn đến khóa tài khoản.

Tóm tắt đề bài

Cho đồ thị vô hướng \(n\) đỉnh và \(m\) cạnh có trọng số. Đỉnh \(1\) là trung tâm. Thời gian nhận hàng dự kiến của đỉnh \(i\) là \(dist(1, i)\) (khoảng cách ngắn nhất từ \(1\) đến \(i\)). Khi một trạm \(x\) bị gián đoạn, các trạm \(y\) bị "ảnh hưởng" nếu khoảng cách ngắn nhất từ \(1\) đến \(y\) thay đổi (tăng lên hoặc không thể đến được). Với mỗi \(x \in [1, n]\), hãy đếm số lượng trạm \(y\) bị ảnh hưởng.

Phân tích

  • Định nghĩa ảnh hưởng: Trạm \(y\) bị ảnh hưởng bởi \(x\) nếu mọi đường đi ngắn nhất từ \(1\) đến \(y\) đều đi qua \(x\).
  • Cấu trúc đường đi ngắn nhất: Tập hợp tất cả các cạnh \((u, v)\) có trọng số \(w\) thỏa mãn \(dist(1, u) + w = dist(1, v)\) tạo thành một đồ thị có hướng không chu trình (DAG), gọi là Shortest Path DAG.
  • Dominator Tree: Trong một đồ thị có hướng có gốc \(S\), đỉnh \(u\) được gọi là "thống trị" (dominate) đỉnh \(v\) nếu mọi đường đi từ \(S\) đến \(v\) đều phải đi qua \(u\). Cấu trúc này tạo thành một cây gọi là cây thống trị (Dominator Tree). Trong bài toán này, trạm \(y\) bị ảnh hưởng bởi \(x\) khi và chỉ khi \(x\) là một tổ tiên của \(y\) trên cây thống trị của Shortest Path DAG.
  • Yêu cầu: Với mỗi đỉnh \(x\), ta cần đếm số lượng nút thuộc cây con gốc \(x\) trong cây thống trị.

Hướng giải quyết

1. Xây dựng Shortest Path DAG

  • Sử dụng thuật toán Dijkstra để tìm khoảng cách ngắn nhất từ đỉnh \(1\) đến tất cả các đỉnh khác.
  • Duyệt qua tất cả các cạnh \((u, v)\) có trọng số \(w\):
    • Nếu \(dist(1, u) + w = dist(1, v)\), thêm cạnh có hướng \(u \to v\) vào DAG.
    • Nếu \(dist(1, v) + w = dist(1, u)\), thêm cạnh có hướng \(v \to u\) vào DAG.

2. Xây dựng Cây thống trị (Dominator Tree)

Có hai cách tiếp cận chính cho bước này:

Cách 1: Thuật toán Lengauer-Tarjan (Tổng quát)

  • Đây là thuật toán mạnh mẽ nhất để xây dựng Dominator Tree trên một đồ thị có hướng bất kỳ với độ phức tạp \(O(m \alpha(n))\) hoặc \(O(m \log n)\).
  • Thuật toán này khá phức tạp để cài đặt trong phòng thi nhưng đảm bảo độ chính xác cao cho mọi cấu trúc đồ thị.

Cách 2: Xây dựng trên DAG (Đơn giản hơn)

  • Với DAG, ta có thể xây dựng cây thống trị bằng cách duyệt các đỉnh theo thứ tự topo.
  • Với mỗi đỉnh \(v\), nút cha trực tiếp của nó trong cây thống trị (\(idom[v]\)) là LCA (Lowest Common Ancestor) của tất cả các đỉnh \(u\) có cạnh nối trực tiếp đến \(v\) (\(u \to v\)) trên cây thống trị đã xây dựng trước đó.
  • Tuy nhiên, cách này yêu cầu xử lý cẩn thận thứ tự duyệt.

3. Tính toán kết quả

  • Sau khi có Dominator Tree, số lượng trạm bị ảnh hưởng bởi \(x\) chính là kích thước cây con (subtree size) của \(x\) trên cây này.
  • Thực hiện một phép DFS/BFS đơn giản trên Dominator Tree để tính size[x].

Độ phức tạp

  • Dijkstra: \(O(m \log n)\) hoặc \(O(m + n \log n)\).
  • Xây dựng Dominator Tree: \(O(m \log n)\) hoặc \(O(m \alpha(n))\) tùy vào cách cài đặt.
  • DFS tính size: \(O(n)\).
  • Tổng quát: \(O(m \log n)\), hoàn toàn đáp ứng được giới hạn \(n=2 \cdot 10^5, m=3 \cdot 10^5\).

Code tham khảo

Dưới đây là mã nguồn sử dụng thuật toán Lengauer-Tarjan để xây dựng Dominator Tree.

C++
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const ll INF = 1e18;
const int MAXN = 200005;

struct Edge {
    int u, v, w;
};

int n, m;
vector<pair<int, int>> adj[MAXN];
ll dist[MAXN];
vector<int> dag[MAXN];
Edge edges[300005];

// Dominator Tree (Lengauer-Tarjan)
namespace DominatorTree {
    vector<int> g[MAXN], rg[MAXN], bucket[MAXN], tree[MAXN];
    int sdom[MAXN], idom[MAXN], ancestor[MAXN], best[MAXN], semi[MAXN];
    int pos[MAXN], vertex[MAXN], parent[MAXN], sz[MAXN], t;

    void dfs(int u) {
        pos[u] = ++t; vertex[t] = u;
        for (int v : g[u]) {
            if (!pos[v]) {
                parent[pos[v] = ++t] = pos[u]; // Simplified for logic
                // Thực tế cần gọi đệ quy đúng chuẩn
            }
        }
    }

    // Để đơn giản và hiệu quả, ta sử dụng cấu trúc dựng sẵn
    int dfn[MAXN], rev[MAXN], fa[MAXN], semi_node[MAXN], idom_node[MAXN], path[MAXN], min_semi[MAXN], timer;

    void dfs_dt(int u) {
        dfn[u] = ++timer; rev[timer] = u;
        for (int v : g[u]) {
            if (!dfn[v]) {
                fa[v] = u;
                dfs_dt(v);
            }
        }
    }

    int find(int v) {
        if (path[v] == v) return v;
        int root = find(path[v]);
        if (dfn[semi_node[min_semi[path[v]]]] < dfn[semi_node[min_semi[v]]])
            min_semi[v] = min_semi[path[v]];
        return path[v] = root;
    }

    int eval(int v) {
        find(v);
        return min_semi[v];
    }

    void build(int s) {
        dfs_dt(s);
        for (int i = timer; i >= 2; i--) {
            int v = rev[i];
            for (int u : rg[v]) {
                if (!dfn[u]) continue;
                int w = eval(u);
                if (dfn[semi_node[w]] < dfn[semi_node[v]]) semi_node[v] = semi_node[w];
            }
            bucket[semi_node[v]].push_back(v);
            int p = fa[v];
            path[v] = p;
            for (int u : bucket[p]) {
                int w = eval(u);
                idom_node[u] = (semi_node[w] == semi_node[u] ? p : w);
            }
            bucket[p].clear();
        }
        for (int i = 2; i <= timer; i++) {
            int v = rev[i];
            if (idom_node[v] != semi_node[v]) idom_node[v] = idom_node[idom_node[v]];
            tree[idom_node[v]].push_back(v);
        }
    }

    int ans[MAXN];
    void dfs_size(int u) {
        ans[u] = 1;
        for (int v : tree[u]) {
            dfs_size(v);
            ans[u] += ans[v];
        }
    }
}

void dijkstra(int s) {
    fill(dist, dist + n + 1, INF);
    priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<pair<ll, int>>> pq;
    dist[s] = 0;
    pq.push({0, s});
    while (!pq.empty()) {
        ll d = pq.top().first;
        int u = pq.top().second;
        pq.pop();
        if (d > dist[u]) continue;
        for (auto& edge : adj[u]) {
            if (dist[edge.first] > dist[u] + edge.second) {
                dist[edge.first] = dist[u] + edge.second;
                pq.push({dist[edge.first], edge.first});
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int u, v, w; cin >> u >> v >> w;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
        edges[i] = {u, v, w};
    }

    dijkstra(1);

    for (int i = 1; i <= n; i++) {
        DominatorTree::semi_node[i] = DominatorTree::path[i] = DominatorTree::min_semi[i] = i;
    }

    for (int i = 0; i < m; i++) {
        int u = edges[i].u, v = edges[i].v, w = edges[i].w;
        if (dist[u] + w == dist[v]) {
            DominatorTree::g[u].push_back(v);
            DominatorTree::rg[v].push_back(u);
        } else if (dist[v] + w == dist[u]) {
            DominatorTree::g[v].push_back(u);
            DominatorTree::rg[u].push_back(v);
        }
    }

    DominatorTree::build(1);
    DominatorTree::dfs_size(1);

    for (int i = 1; i <= n; i++) {
        cout << DominatorTree::ans[i] << "\n";
    }

    return 0;
}

Bình luận

Mới nhất
Tải bình luận...

Không có bình luận nào.