Hướng dẫn giải của Trạm điện thành phố


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, Hailun, hohoanghai5042011, lephuochauhungvuong

Editorial for electr: Trạm điện thành phố

Hiểu bài toán

Bài toán yêu cầu tìm tổng chi phí tối thiểu để xây dựng các đường dây dẫn sao cho tất cả các thành phố đều được cung cấp điện, trong điều kiện đã có hai trạm phát điện được đặt tại hai thành phố cho trước (A và B). Đây là bài toán tìm cây bao trùm chi phí tối thiểu (MST) trên đồ thị với ràng buộc hai đỉnh A và B phải thuộc thành phần liên thông của cây bao trùm đó (tức là chúng được coi như đã được 'tự do' kết nối với nhau hoặc đã có sẵn đường truyền).

Cụ thể, với mỗi truy vấn (A, B), ta cần tìm một cây bao trùm (Spanning Tree) của đồ thị sao cho tổng trọng số các cạnh là nhỏ nhất, trong đó các đỉnh A và B được coi là đã được 'kết nối' với nhau (có thể hiểu là chi phí nối A và B về 0).

Dựa vào các giải pháp đã cho, thuật toán chính được sử dụng là biến thể của thuật toán Kruskal để tìm MST, kết hợp với chuẩn hóa các truy vấn để xử lý hiệu quả.

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

Cách Kruskal với xử lý truy vấn trực tiếp (Baseline)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct DSU {
    vector<int> parent, rank;
    DSU(int n) {
        parent.resize(n + 1);
        rank.resize(n + 1, 0);
        for (int i = 1; i <= n; i++) parent[i] = i;
    }
    int find(int i) {
        if (parent[i] != i) parent[i] = find(parent[i]);
        return parent[i];
    }
    bool unite(int i, int j) {
        int root_i = find(i);
        int root_j = find(j);
        if (root_i != root_j) {
            if (rank[root_i] < rank[root_j]) swap(root_i, root_j);
            parent[root_j] = root_i;
            if (rank[root_i] == rank[root_j]) rank[root_i]++;
            return true;
        }
        return false;
    }
};

struct Edge {
    int u, v;
    ll w;
    bool operator<(const Edge& other) const { return w < other.w; }
};

int N, M, Q;
vector<Edge> edges;

ll solve_query(int A, int B) {
    DSU dsu(N);
    ll total_cost = 0;
    int edges_count = 0;

    // Giả sử edges đã được sort sẵn
    for (const auto& e : edges) {
        // Nếu cả hai đều là A và B, ta có thể coi như đã kết nối (bỏ qua)
        // Hoặc xử lý logic sao cho A và B được ưu tiên kết nối
        if ((e.u == A && e.v == B) || (e.u == B && e.v == A)) continue;

        if (dsu.unite(e.u, e.v)) {
            total_cost += e.w;
            edges_count++;
            // Logic ở đây cần xử lý để khi A và B chưa nối thì ưu tiên
            // Tuy nhiên, cách này không đảm bảo tìm được MST đúng nếu chỉ lướt qua 1 lần
        }
    }
    // Cần logic phức tạp hơn để đảm bảo MST khi A, B đã được coi là connected
    return total_cost; 
}
  • Time Complexity: O(Q * M * α(N))
  • Space Complexity: O(N + M)

Đây là cách tiếp cận naive nhất. Với mỗi truy vấn, ta chạy lại thuật toán Kruskal từ đầu. Tuy nhiên, ta cần chỉnh sửa Kruskal một chút để xử lý việc A và B đã được kết nối sẵn (tức là ta phải đảm bảo A và B thuộc cùng một thành phần liên thông ngay từ đầu hoặc dùng cạnh 0 trọng số giữa chúng).

Cách làm:

  1. Sắp xếp các cạnh theo trọng số tăng dần.
  2. Khởi tạo DSU cho mỗi truy vấn. Đặc biệt, ta phải 'gộp' A và B lại ngay từ đầu (thêm cạnh (A, B) với trọng số 0 vào danh sách cạnh).
  3. Duyệt qua các cạnh, nếu nối được thì thêm vào MST.

Phương pháp này quá chậm do Q và M đều lớn.

Cách Kruskal biến thể (Tìm MST Max Edge)
// Chỉ là minh họa cấu trúc, code đầy đủ bị truncate trong submission
#include <bits/stdc++.h>
using namespace std;

struct Edge { int u, v; long long w; };
vector<Edge> edges;
int parent[4005];

int find_set(int v) {
    if (v == parent[v]) return v;
    return parent[v] = find_set(parent[v]);
}

bool union_sets(int a, int b) {
    a = find_set(a);
    b = find_set(b);
    if (a != b) {
        parent[b] = a;
        return true;
    }
    return false;
}

struct Query { int a, b, id; };

int main() {
    // Input & Sort edges by weight
    // Group queries by pairs (a, b)
    // For each query, run modified Kruskal:
    //   Initialize DSU
    //   Union A and B immediately (conceptually)
    //   Iterate edges:
    //     If edge connects two different components, take it.
    //     If edge connects A and B directly, skip or take? 
    //     Actually, standard Kruskal works if we pre-union A and B.
    //     But we need to handle the case where we take edges until A and B are connected.
    //     Wait, the solution from hohoanghai5042011 seems to do something specific.
    //     Let's look at the logic: 
    //     It calculates something like 'min cost to connect A and B' plus 'MST edges'.
    //     Actually, the problem is: 2 sources MST.
    //     The solution likely does:
    //     1. Build MST of whole graph.
    //     2. For query (u, v), answer is MST_weight + shortest_path(u, v) - max_edge_on_path(u, v).
    //     Or simply: if we add edge (u, v) with weight 0, MST cost = original_MST_weight + (0 - max_edge_on_path(u, v)).
    //     If max_edge_on_path(u, v) > 0, we can remove it and add the 0 edge, reducing cost.
    //     If max_edge_on_path(u, v) < 0 (impossible), we just add 0 edge.
    //     So answer = MST_weight - max_edge_on_path(u, v).
    //     BUT, this assumes we MUST use the MST structure. 
    //     Actually, the correct logic for 2-source MST with 0-cost edge is:
    //     Let MST be the global MST. 
    //     Cost = MST_weight - max_edge(u, v) if max_edge(u, v) > 0.
    //     However, sample: MST weight is sum of edges in global MST.
    //     For query (4, 5): 
    //       Edges chosen in sample output: 1-5(2), 1-3(3), 3-4(4)? No wait.
    //       Let's trace sample:
    //       6 cities.
    //       MST of graph: 
    //       1-5 (2)
    //       1-3 (3)
    //       1-2 (4) or 1-4(4)? 
    //       Let's check edges: 1-2(4), 1-3(3), 1-4(4), 1-5(2), 2-4(6), 3-5(3), 3-4(4), 4-6(5).
    //       Sort: 2, 3, 3, 4, 4, 4, 5, 6.
    //       Kruskal:
    //       1-5 (2) -> connected
    //       1-3 (3) -> connected
    //       3-5 (3) -> cycle, skip
    //       1-2 (4) -> connected
    //       1-4 (4) -> connected
    //       3-4 (4) -> cycle, skip
    //       4-6 (5) -> connected
    //       Total MST weight = 2+3+4+4+5 = 18.
    //       Query (4, 5): 
    //         Path 4-5 in MST: 4-1-5. Edges: 4-1(4), 1-5(2). Max edge = 4.
    //         Answer = 18 - 4 = 14. Matches sample.
    //       Query (6, 4):
    //         Path 6-4 in MST: 6-4(5). Max edge = 5.
    //         Answer = 18 - 5 = 13. Matches sample.
    //     So the logic is indeed: Global MST Weight - Max Edge on path between A and B in MST.
  • Time Complexity: O(M log M + (Q + N) log N)
  • Space Complexity: O(N + M)

Đây là phương pháp tối ưu hóa:

  1. Xây dựng cây bao trùm chi phí tối thiểu (MST) của toàn bộ đồ thị.
  2. Tính tổng trọng số của MST này.
  3. Với mỗi truy vấn (A, B), ta cần tìm cạnh có trọng số lớn nhất trên đường đi duy nhất giữa A và B trong MST.
  4. Đáp án cho truy vấn đó là: Tổng trọng số MST - Trọng số cạnh lớn nhất trên đường đi A-B.

Giải thích: Khi ta thêm một cạnh 0 trọng số giữa A và B vào đồ thị, ta có thể tạo một chu trình mới bao gồm cạnh 0 và đường đi giữa A và B trong MST cũ. Để có MST mới, ta cần loại bỏ một cạnh khỏi chu trình này. Để tổng chi phí nhỏ nhất, ta nên loại bỏ cạnh có trọng số lớn nhất trong chu trình đó (chính là cạnh lớn nhất trên đường đi A-B cũ).

Phương pháp này yêu cầu:

  • Xây dựng MST (Kruskal).
  • Truy vấn Max Edge trên cây (LCA hoặc Tree Traversal).
Cách Tối ưu hóa Truy vấn bằng LCA (Lowest Common Ancestor)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 4005;
const int LOGN = 13;

struct Edge { int u, v, w; bool operator<(const Edge& o) const { return w < o.w; } };
struct Query { int u, v; };

int n, m, q;
vector<Edge> edges;
vector<pair<int, int>> mst_adj[MAXN];
int parent[MAXN][LOGN];
int maxEdge[MAXN][LOGN];
int depth[MAXN];
long long mst_weight = 0;

// DSU for Kruskal
int dsu[MAXN];
int find_set(int v) { return (v == dsu[v]) ? v : dsu[v] = find_set(dsu[v]); }
bool union_sets(int a, int b) {
    a = find_set(a); b = find_set(b);
    if (a != b) { dsu[a] = b; return true; }
    return false;
}

// DFS to build LCA table
void dfs(int u, int p, int w, int d) {
    depth[u] = d;
    parent[u][0] = p;
    maxEdge[u][0] = w;
    for (int i = 1; i < LOGN; i++) {
        parent[u][i] = parent[parent[u][i-1]][i-1];
        maxEdge[u][i] = max(maxEdge[u][i-1], maxEdge[parent[u][i-1]][i-1]);
    }
    for (auto& edge : mst_adj[u]) {
        int v = edge.first;
        int weight = edge.second;
        if (v != p) dfs(v, u, weight, d + 1);
    }
}

int get_max_edge(int u, int v) {
    if (depth[u] < depth[v]) swap(u, v);
    int res = 0;
    // Nâng u lên độ sâu của v
    for (int i = LOGN - 1; i >= 0; i--) {
        if (depth[u] - (1 << i) >= depth[v]) {
            res = max(res, maxEdge[u][i]);
            u = parent[u][i];
        }
    }
    if (u == v) return res;
    // Nâng cả u và v
    for (int i = LOGN - 1; i >= 0; i--) {
        if (parent[u][i] != parent[v][i]) {
            res = max(res, maxEdge[u][i]);
            res = max(res, maxEdge[v][i]);
            u = parent[u][i];
            v = parent[v][i];
        }
    }
    // Nốt cuối cùng
    res = max(res, maxEdge[u][0]);
    res = max(res, maxEdge[v][0]);
    return res;
}

int main() {
    // 1. Read input, sort edges
    // 2. Kruskal to build MST
    // 3. DFS from node 1 (or any node) to fill LCA table
    // 4. For each query (u, v):
    //    cout << mst_weight - get_max_edge(u, v) << '\n';
    return 0;
}
  • Time Complexity: O(M log M + N log N + Q log N)
  • Space Complexity: O(N log N + M)

Đây là cách triển khai chi tiết củaApproach 2.

Bước 1: Xây dựng MST

  • Dùng Kruskal: Sắp xếp tất cả các cạnh theo trọng số tăng dần.
  • Dùng DSU (Disjoint Set Union) để thêm cạnh vào MST nếu hai đỉnh chưa thuộc cùng một tập hợp.
  • Vừa xây dựng MST, vừa lưu adjacency list của cây (để dùng cho việc tìm đường đi sau này).
  • Tính tổng trọng số mst_weight.

Bước 2: Tiền xử lý cây để trả lời truy vấn Max Edge

  • Chọn một đỉnh làm gốc (ví dụ đỉnh 1).
  • Dùng DFS (hoặc BFS) để duyệt cây, tính:
    • depth[u]: độ sâu của đỉnh u.
    • parent[u][k]: tổ tiên thứ 2^k của u.
    • maxEdge[u][k]: trọng số lớn nhất trên đường đi từ u lên parent[u][k].
  • Bảng này có kích thước N * log(N).

Bước 3: Trả lời truy vấn

  • Với mỗi truy vấn (u, v):
    • Dùng thuật toán LCA để tìm đường đi từ u đến v.
    • Trong quá trình tìm LCA, ta đồng thời gom max trọng số của các đoạn đường đi.
    • Đáp án: mst_weight - max_edge_on_path(u, v).

Phương pháp này đảm bảo thời gian xử lý nhanh cho số lượng truy vấn lớn.

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

Cách tiếp cận Time Space Tên
1 O(Q * M * α(N)) O(N + M) Kruskal với xử lý truy vấn trực tiếp (Baseline)
2 O(M log M + (Q + N) log N) O(N + M) Kruskal biến thể (Tìm MST Max Edge)
3 O(M log M + N log N + Q log N) O(N log N + M) Tối ưu hóa Truy vấn bằng LCA (Lowest Common Ancestor)

Bài học kinh nghiệm

  • Bài toán có thể biến đổi về bài toán tìm Max Edge trên đường đi giữa hai node trong cây bao trùm chi phí tối thiểu (MST) của đồ thị.
  • Nếu thêm một cạnh trọng số 0 giữa hai node A và B vào MST, ta có thể tạo một chu trình. Để có MST mới với chi phí thấp nhất, ta cần loại bỏ cạnh có trọng số lớn nhất trong chu trình đó (chính là cạnh lớn nhất trên đường đi A-B trong MST cũ).

Lỗi thường gặp

  • Không xử lý đúng trường hợp hai node A và B đã được nối trực tiếp bởi một cạnh trong MST (cần kiểm tra kỹ thuật LCA).
  • Quên tính toán tổng trọng số MST ban đầu, dẫn đến sai kết quả.
  • Lỗi tràn số khi tính tổng chi phí (sử dụng kiểu dữ liệu long long).

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.