Hướng dẫn giải của Lễ hội Flatland


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.

Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.

Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.

Bạn phải đăng nhập để mở lời giải này.

Đăng nhập

Tác giả: Hiếu Nguyễn, haidang3004, zuanopro

Hiểu bài toán

Một cây có N đỉnh và N-1 cạnh, mỗi cạnh u - v có trọng số c (số lượt xe tối đa qua cạnh đó). Cần di chuyển xe từ c1 đến c2 (theo đường duy nhất của cây). Để tăng dung lượng, ta được thêm 1 cạnh mới (đường cao tốc) giữa 2 đỉnh bất kỳ không phải c1, c2 và không có sẵn cạnh. Cạnh mới có dung lượng vô hạn (không bao giờ hỏng). Xe có thể đi theo bất kỳ đường nào, nhưng lưu lượng qua mỗi cạnh không được vượt quá trọng số của nó. Tìm lưu lượng xe tối đa từ c1 đến c2 sau khi thêm cạnh mới.

Giải thích chi tiết:

  • Cây ban đầu có dung lượng đường đi từ c1 đến c2 là dung lượng của đường ống hẹp nhất (min-cut) trên đường đó.
  • Thêm cạnh (u, v) tạo ra một đường đi vòng mới: c1 -> ... -> u -> v -> ... -> c2.
  • Do đường cao tốc vô hạn, dung lượng của đường đi vòng này phụ thuộc vào dung lượng các nhánh còn lại.
  • Cụ thể, dung lượng đường đi vòng qua u, v là min(L(u), L(v)), trong đó L(x) là dung lượng từ c1 đến x (trên cây) sau khi loại bỏ đường thẳng đến c2 (tức là dung lượng nhánh rẽ ở x).
  • Đồng thời, xe vẫn có thể đi theo đường gốc.
  • Vì xe có thể chia đôi luồng, dung lượng tối đa tổng cộng là dung lượng đường gốc + dung lượng đường vòng mới.
  • Bài toán quy về tìm dung lượng nhánh rẽ L(x) cho mọi x, rồi tìm max(min(L(u), L(v))) cho tất cả cặp (u, v) không có cạnh trực tiếp.

Các cách tiếp cận

Cách Brute Force
// Dùng cho N nhỏ, tham khảo ý tưởng
// O(N^2) hoặc O(N^3)
// Code đầy đủ quá dài, chỉ minh họa logic
int solve_small() {
    // Tính L(x) cho mọi x
    // Duyệt tất cả cặp (u, v) không kề nhau
    // Tính min(L(u), L(v))
    // Lấy max
    return 0;
}
  • Time Complexity: O(N^2)
  • Space Complexity: O(N)

Cách này chỉ khả thi với N rất nhỏ. Tính L(x) cho mọi x bằng cách loại bỏ từng cạnh trên đường c1-c2. Với mỗi cặp (u, v), kiểm tra xem chúng có kề nhau không (nếu kề thì không xây được). Lấy max(min(L(u), L(v))).

Cách Tối ưu hóa bằng cách phân loại và Thăm dò
int n, c1, c2;
vector<pair<int, int>> adj[100005];
bool on_path[100005];
long long L[100005];
int parent[100005];
int path_idx[100005];
int label[100005];

void dfs_path(int u, int p) {
    parent[u] = p;
    for (auto& edge : adj[u]) {
        int v = edge.first;
        if (v != p) dfs_path(v, u);
    }
}

void dfs_L(int u, int p, long long current_flow) {
    long long sum = 0;
    for (auto& edge : adj[u]) {
        int v = edge.first;
        int w = edge.second;
        if (v == p) continue;
        if (on_path[v]) continue; // Cắt nhánh
        dfs_L(v, u, w);
        sum += L[v];
    }
    L[u] = min(current_flow, sum);
}

int main() {
    // 1. Đọc input
    // 2. Tìm đường đi từ c1 đến c2 (dùng parent)
    // 3. Đánh dấu các node trên đường
    // 4. Tính L(x) cho mọi node (từ trong ra)
    // 5. Phân loại node: Left, Right, Center
    // 6. Duyệt để tìm max(min(L(u), L(v)))
    return 0;
}
  • Time Complexity: O(N log N)
  • Space Complexity: O(N)

Hàm L(x) có tính chất đơn điệu trên đường đi. Ta có thể chia các node thành 3 loại:

  1. Left: Node nằm trên nhánh rẽ của c1 (nằm ở 'phía' c1 so với đường thẳng).
  2. Right: Node nằm trên nhánh rẽ của c2 (nằm ở 'phía' c2).
  3. Center: Node nằm trên nhánh rẽ của các node ở giữa đường thẳng.

Ta cần tìm max(min(L(u), L(v))) u, v kề nhau.

  • Left + Left: Max là min(L(x), L(y)) với x,y là 2 node Left lớn nhất (không kề).
  • Right + Right: Tương tự.
  • Left + Right: Luôn OK (vì không thể kề nhau do ngăn cách bởi đường thẳng). Max là min(maxLleft, maxLright).
  • Center + Center: Node Center u kề node Center v nếu chúng nằm trên cùng nhánh rẽ của node cha chung. Ta cần xử lý từng nhánh rẽ.
  • Left/Right + Center: Node Center u kề node Left/Right v nếu u là con trực tiếp của node trên đường thẳng mà v thuộc nhánh rẽ đó. Xử lý theo nhánh.

Vì L(x) đã được tính trước, ta chỉ cần duyệt qua các nhánh rẽ và dùng Set/Vector để tìm max min.

Cách Full Solution (Clean Code)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
int n, c1, c2;
struct Edge { int to, w; };
vector<Edge> adj[MAXN];
int parent[MAXN];
long long L[MAXN];
bool on_path[MAXN];

void dfs_parent(int u, int p) {
    parent[u] = p;
    for (auto& e : adj[u]) {
        if (e.to != p) dfs_parent(e.to, u);
    }
}

void dfs_L(int u, int p, int limit) {
    long long sum = 0;
    for (auto& e : adj[u]) {
        if (e.to == p || on_path[e.to]) continue;
        dfs_L(e.to, u, e.w);
        sum += L[e.to];
    }
    L[u] = min((long long)limit, sum);
}

int main() {
    ios_base::sync_with_stdio(0); cin.tie(0);
    if (fopen("highway.in", "r")) freopen("highway.in", "r", stdin);

    cin >> n >> c1 >> c2;
    for (int i=1; i<n; ++i) {
        int u, v, c; cin >> u >> v >> c;
        adj[u].push_back({v, c});
        adj[v].push_back({u, c});
    }

    // 1. Find path c1 -> c2
    dfs_parent(c1, 0);
    vector<int> path;
    for (int u = c2; u != 0; u = parent[u]) path.push_back(u);
    reverse(path.begin(), path.end());
    for (int u : path) on_path[u] = true;

    // 2. Calculate L[u]
    for (int u : path) {
        long long sum = 0;
        for (auto& e : adj[u]) {
            if (on_path[e.to]) continue;
            dfs_L(e.to, u, e.w);
            sum += L[e.to];
        }
        L[u] = sum; // L[c1] and L[c2] are sums of their side branches
    }

    // 3. Solve
    long long ans = 0;
    // Calculate base flow (path c1-c2 capacity)
    long long base_flow = 1e18;
    for (size_t i = 0; i < path.size() - 1; ++i) {
        int u = path[i];
        int v = path[i+1];
        for (auto& e : adj[u]) {
            if (e.to == v) {
                base_flow = min(base_flow, (long long)e.w);
                break;
            }
        }
    }
    ans = base_flow;

    // Collect branches
    vector<long long> left_vals, right_vals;
    vector<vector<long long>> center_branches;

    // Left: branches of c1
    for (auto& e : adj[c1]) {
        if (!on_path[e.to]) left_vals.push_back(L[e.to]);
    }
    // Right: branches of c2
    for (auto& e : adj[c2]) {
        if (!on_path[e.to]) right_vals.push_back(L[e.to]);
    }
    // Center: branches of nodes on path (excluding c1, c2)
    for (size_t i = 1; i < path.size() - 1; ++i) {
        int u = path[i];
        vector<long long> branches;
        for (auto& e : adj[u]) {
            if (!on_path[e.to]) branches.push_back(L[e.to]);
        }
        if (!branches.empty()) center_branches.push_back(branches);
    }

    // Maximize
    // Left + Right
    if (!left_vals.empty() && !right_vals.empty()) {
        ans = max(ans, *max_element(left_vals.begin(), left_vals.end()) + *max_element(right_vals.begin(), right_vals.end()));
    }
    // Left + Left
    if (left_vals.size() >= 2) {
        sort(left_vals.begin(), left_vals.end(), greater<long long>());
        ans = max(ans, left_vals[0] + left_vals[1]);
    }
    // Right + Right
    if (right_vals.size() >= 2) {
        sort(right_vals.begin(), right_vals.end(), greater<long long>());
        ans = max(ans, right_vals[0] + right_vals[1]);
    }
    // Center + Center (same branch)
    for (auto& br : center_branches) {
        if (br.size() >= 2) {
            sort(br.begin(), br.end(), greater<long long>());
            ans = max(ans, br[0] + br[1]);
        }
    }
    // Left + Center (tangent)
    for (auto& br : center_branches) {
        for (long long val : br) {
            if (!left_vals.empty()) ans = max(ans, val + *max_element(left_vals.begin(), left_vals.end()));
            if (!right_vals.empty()) ans = max(ans, val + *max_element(right_vals.begin(), right_vals.end()));
        }
    }

    cout << ans << endl;
    return 0;
}
  • Time Complexity: O(N log N)
  • Space Complexity: O(N)
  1. Tạo cây và tìm đường đi từ c1 đến c2.
  2. Tính L[u] cho mọi node u bằng DFS. L[u] là dung lượng nhánh rẽ tại u.
    • L[u] = min(limit_edge, sum(L[children]))
  3. Phân loại các node:
    • Left: Các node thuộc nhánh rẽ của c1.
    • Right: Các node thuộc nhánh rẽ của c2.
    • Center: Các node thuộc nhánh rẽ của các node nằm giữa c1 và c2 trên đường thẳng.
  4. Tính toán các trường hợp:
    • Left + Right: Luôn hợp lệ (không kề nhau). Max = max(Lleft) + max(Lright).
    • Left + Left: Chọn 2 node lớn nhất từ Left (không kề nhau).
    • Right + Right: Tương tự Right.
    • Center + Center: Chỉ có thể kề nhau nếu nằm trong cùng 1 nhánh rẽ (cùng con của 1 node trên đường thẳng). Trong mỗi nhánh, chọn 2 node lớn nhất.
    • Left/Right + Center: Node Center kề node Left/Right nếu nó là con trực tiếp của node trên đường thẳng. Xử lý max min.
  5. Kết quả là baseflow + maxflow_thêm.

Phân tích độ phức tạp

Cách tiếp cận Time Space Tên
1 O(N^2) O(N) Brute Force
2 O(N log N) O(N) Tối ưu hóa bằng cách phân loại và Thăm dò
3 O(N log N) O(N) Full Solution (Clean Code)

Bài học kinh nghiệm

  • Bài toán là biến thể của Max Flow trên cây. Thêm cạnh mới tạo ra đường đi vòng, tăng dung lượng.
  • Dung lượng tăng thêm phụ thuộc vào min(L(u), L(v)) của hai đầu mút cạnh mới.
  • Hàm L(x) có tính chất đơn điệu, cho phép phân loại node để tìm max min hiệu quả thay vì duyệt toàn bộ.
  • Xử lý các trường hợp kề nhau (tangent) bằng cách xét theo nhánh rẽ (branch) của các node trên đường thẳng.

Lỗi thường gặp

  • Quên kiểm tra điều kiện hai node không được kề nhau (adjacent).
  • Tính sai dung lượng nhánh rẽ L(x) khi node đó có nhiều con (cần sum L[con] rồi min với cạnh vào).
  • Bỏ qua các trường hợp kết hợp Left-Left, Right-Right, Center-Center (cùng nhánh).
  • Sử dụng DFS sai cách导致 TLE (cần đánh dấu on_path để không truy cập sai nhánh).

Bình luận

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.