Hướng dẫn giải của Trò chơi


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, vudinhlong, AnhPham

Hiểu bài toán

Bài toán yêu cầu tìm ra số lượng câu trả lời 'even' (chẵn) trong dãy N bit (0 hoặc 1) sao cho không mâu thuẫn với các câu hỏi và câu trả lời đã cho. Cụ thể, có K câu hỏi, mỗi câu hỏi chỉ một đoạn từ i đến j và trả lời 'odd' (lẻ) hoặc 'even' (chẵn) số lượng bit 1 trong đoạn đó. Tuy nhiên, chỉ có một câu trả lời là đúng, còn lại là sai. Ta cần xác định số lượng câu trả lời 'even'.

Để giải quyết, ta nhận thấy:

  1. Xét tổng số 1 trong toàn dãy là S. Nếu S chẵn, toàn bộ câu trả lời 'even' là đúng. Nếu S lẻ, chỉ có các câu trả lời 'odd' là đúng.
  2. Xét một câu hỏi về đoạn [i, j]. Gọi P[x] là tổng số 1 từ 1 đến x. Số 1 trong đoạn [i, j] là P[j] - P[i-1].
  3. Nếu câu trả lời là 'even', ta có P[j] - P[i-1] ≡ 0 (mod 2) => P[j] ≡ P[i-1] (mod 2).
  4. Nếu câu trả lời là 'odd', ta có P[j] - P[i-1] ≡ 1 (mod 2) => P[j] ≠ P[i-1] (mod 2).

Vấn đề trở thành bài toán tìm xem có thể gán giá trị chẵn/lẻ cho các mốc P[0], P[1], ..., P[N] sao cho vừa khớp với các ràng buộc từ các câu hỏi hay không. Tuy nhiên, ta không cần tìm chính xác cách gán, mà chỉ cần đếm số câu trả lời 'even'.

Giả sử có một cách gán hợp lệ. Khi đó, tổng S = P[N] - P[0] = P[N] (vì P[0]=0).

  • Nếu P[N] chẵn, ta có thể gán P[0]=0, P[N]=0. Các câu hỏi 'even' (P[j]=P[i-1]) sẽ được thỏa mãn.
  • Nếu P[N] lẻ, ta có thể gán P[0]=0, P[N]=1. Các câu hỏi 'odd' (P[j]≠P[i-1]) sẽ được thỏa mãn.

Vậy, ta chỉ cần kiểm tra:

  1. Liệu có thể gán các P[x] sao cho P[N]=0 và tất cả ràng buộc 'even' được thỏa mãn không? Nếu có, số câu trả lời 'even' đúng là toàn bộ.
  2. Liệu có thể gán các P[x] sao cho P[N]=1 và tất cả ràng buộc 'odd' được thỏa mãn không? Nếu có, số câu trả lời 'odd' đúng là toàn bộ.

Nếu trường hợp 1 đúng, đáp án là K (số câu trả lời 'even'). Nếu trường hợp 2 đúng, đáp án là 0. Nếu cả hai đều đúng (có thể xảy ra khi K=0), đáp án có thể là K hoặc 0, nhưng theo logic bài toán, ta cần tìm số câu trả lời 'even' chưa mâu thuẫn, thường là K.

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

Cách DSU (Disjoint Set Union) - Theo dõi quan hệ chẵn lẻ
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 20005; // 2*N do có thể chênh lệch chỉ số
int parent[MAXN];
int diff[MAXN]; // diff[v] = P[v] xor P[parent[v]]
int n, k;

int find(int u) {
    if (parent[u] == u) return u;
    int root = find(parent[u]);
    diff[u] ^= diff[parent[u]]; // Path compression
    return parent[u] = root;
}

bool merge(int u, int v, int w) { // w = 0 if P[u] == P[v], 1 if P[u] != P[v]
    int root_u = find(u);
    int root_v = find(v);
    if (root_u == root_v) {
        // Check consistency
        return (diff[u] ^ diff[v]) == w;
    }
    parent[root_u] = root_v;
    // diff[root_u] ^ diff[v] ^ diff[u] = w (cần P[root_u] xor P[root_v] = w)
    // P[root_u] xor P[u] = diff[u] => P[root_u] = P[u] xor diff[u]
    // P[root_v] xor P[v] = diff[v] => P[root_v] = P[v] xor diff[v]
    // P[root_u] xor P[root_v] = w
    // (P[u] xor diff[u]) xor (P[v] xor diff[v]) = w
    // P[u] xor P[v] = w xor diff[u] xor diff[v]
    // diff[root_u] = P[root_u] xor P[root_v] = w xor diff[u] xor diff[v]
    diff[root_u] = w ^ diff[u] ^ diff[v];
    return true;
}

bool check(int target_pn, vector<tuple<int,int,int>>& queries, int type) {
    // type 0: check if we can satisfy even constraints with P[N] = target_pn
    // type 1: check if we can satisfy odd constraints with P[N] = target_pn
    // Actually, we just need to check disjoint sets for consistency.
    // But we must consider P[0] and P[N].
    // P[0] = 0.
    // P[N] = target_pn.
    // So we add constraint P[0] ^ P[N] = target_pn.
    // Also, we only add constraints of the specific type.

    // Reset DSU
    for(int i=0; i<=2*n; ++i) {
        parent[i] = i;
        diff[i] = 0;
    }

    // Add base constraint P[0] vs P[N]
    if (!merge(0, N, target_pn)) return false;

    for (auto& q : queries) {
        int u = get<0>(q);
        int v = get<1>(q);
        int w = get<2>(q);
        if (w == type) {
            if (!merge(u, v, (type == 0 ? 0 : 1))) return false;
        }
    }
    return true;
}

int main() {
    cin >> n >> k;
    vector<tuple<int, int, int>> queries;
    int even_count = 0;
    for (int i = 0; i < k; ++i) {
        int u, v;
        string s;
        cin >> u >> v >> s;
        // Map: P[u-1] vs P[v]
        // DSU indices: 0 to N
        // Let's use u-1 and v for P indices
        queries.push_back({u-1, v, (s == "odd" ? 1 : 0)});
        if (s == "even") even_count++;
    }

    // Case 1: P[N] = 0 (Total sum is even)
    // Then all 'even' queries must be true (P[v] == P[u-1] => w=0)
    // All 'odd' queries must be false (P[v] != P[u-1] => w=1)
    // Wait, if P[N]=0, we don't need to satisfy 'odd' queries? 
    // The problem says: "find the last answer that doesn't contradict"
    // If P[N]=0, then all 'even' answers are correct. 
    // So we only need to check if 'even' constraints are consistent.
    // If they are, then answer is total 'even' count.

    // Case 2: P[N] = 1 (Total sum is odd)
    // Then all 'odd' answers are correct.
    // So we only need to check if 'odd' constraints are consistent.
    // If they are, answer is 0.

    // Actually, the logic is:
    // We need to find a valid assignment of P values.
    // If P[N] = 0, then all 'even' queries must be satisfied. 'Odd' queries can be unsatisfied.
    // If P[N] = 1, then all 'odd' queries must be satisfied.

    // Check Case 1: P[N] = 0
    bool possible_even = true;
    // Reset and build graph with 'even' constraints + P[0]=0, P[N]=0
    for(int i=0; i<=2*n; ++i) { parent[i]=i; diff[i]=0; }
    if (!merge(0, n, 0)) possible_even = false;
    for (auto& q : queries) {
        int u = get<0>(q);
        int v = get<1>(q);
        int w = get<2>(q);
        if (w == 0) { // even constraint
            if (!merge(u, v, 0)) { possible_even = false; break; }
        }
    }

    if (possible_even) {
        cout << even_count << endl;
        return 0;
    }

    // Check Case 2: P[N] = 1
    bool possible_odd = true;
    for(int i=0; i<=2*n; ++i) { parent[i]=i; diff[i]=0; }
    if (!merge(0, n, 1)) possible_odd = false;
    for (auto& q : queries) {
        int u = get<0>(q);
        int v = get<1>(q);
        int w = get<2>(q);
        if (w == 1) { // odd constraint
            if (!merge(u, v, 1)) { possible_odd = false; break; }
        }
    }

    if (possible_odd) {
        cout << 0 << endl;
    } else {
        // Problem implies there is a solution. 
        // If neither works, something is wrong or K=0.
        cout << even_count << endl; // Default
    }

    return 0;
}
  • Time Complexity: O(K * α(N))
  • Space Complexity: O(N)

Phương pháp sử dụng DSU (Disjoint Set Union) để quản lý các quan hệ chẵn/lẻ giữa các tổng tiền tố P[i].

  1. Mô hình hóa:
  • Mảng P: P[i] là tổng số 1 từ 1 đến i. P[0] = 0.
  • Câu hỏi [i, j] 'even': P[j] - P[i-1] chẵn => P[j] ≡ P[i-1] (mod 2) => P[j] XOR P[i-1] = 0.
  • Câu hỏi [i, j] 'odd': P[j] - P[i-1] lẻ => P[j] ≠ P[i-1] (mod 2) => P[j] XOR P[i-1] = 1.
  1. DSU với trọng số:
  • Ta cần một DSU có thể lưu trữ quan hệ XOR giữa các node.
  • parent[u]: node cha của u.
  • diff[u]: giá trị P[u] XOR P[parent[u]].
  • Khi find(u), ta cần update diff[u] để nó lưu P[u] XOR P[root].
  • Khi merge(u, v, w) (với w là P[u] XOR P[v]), ta nối root của u vào root của v và tính diff[root_u].
  1. Hai trường hợp:
  • Trường hợp 1 (Tổng số 1 chẵn): P[N] = 0. Khi đó, tất cả câu trả lời 'even' đều đúng. Ta chỉ cần kiểm tra xem các ràng buộc 'even' có mâu thuẫn không. Ta thêm ràng buộc P[0]=0, P[N]=0 (tức P[N] XOR P[0] = 0) và xử lý các ràng buộc 'even'. Nếu hợp lệ, đáp án là số lượng câu trả lời 'even'.
  • Trường hợp 2 (Tổng số 1 lẻ): P[N] = 1. Khi đó, tất cả câu trả lời 'odd' đều đúng. Ta chỉ cần kiểm tra các ràng buộc 'odd'. Ta thêm ràng buộc P[N] XOR P[0] = 1 và xử lý các ràng buộc 'odd'. Nếu hợp lệ, đáp án là 0.
  1. Kết luận: Nếu trường hợp 1 hợp lệ, in ra K (số 'even'). Ngược lại, in ra 0. (Theo đề bài, luôn có nghiệm).
Cách Optimized DSU (Minified)
#include <bits/stdc++.h>
using namespace std;

int parent[20005], diff[20005];

int find(int u) {
    if (parent[u] == u) return u;
    int r = find(parent[u]);
    diff[u] ^= diff[parent[u]];
    return parent[u] = r;
}

bool unite(int u, int v, int w) {
    int ru = find(u), rv = find(v);
    if (ru == rv) return (diff[u] ^ diff[v]) == w;
    parent[ru] = rv;
    diff[ru] = w ^ diff[u] ^ diff[v];
    return true;
}

int main() {
    ios_base::sync_with_stdio(0); cin.tie(0);
    int n, k; cin >> n >> k;
    vector<tuple<int, int, int>> qs;
    int cnt_even = 0;
    for (int i = 0; i < k; ++i) {
        int u, v; string s; cin >> u >> v >> s;
        int w = (s == "odd");
        qs.push_back({u - 1, v, w});
        if (s == "even") cnt_even++;
    }

    // Check if P[N] = 0 is possible (All 'even' answers correct)
    bool ok0 = true;
    for (int i = 0; i <= 2 * n; ++i) parent[i] = i, diff[i] = 0;
    if (!unite(0, n, 0)) ok0 = false;
    for (auto& q : qs) {
        if (get<2>(q) == 0 && !unite(get<0>(q), get<1>(q), 0)) { ok0 = false; break; }
    }
    if (ok0) { cout << cnt_even; return 0; }

    // Check if P[N] = 1 is possible (All 'odd' answers correct)
    for (int i = 0; i <= 2 * n; ++i) parent[i] = i, diff[i] = 0;
    if (!unite(0, n, 1)) return 0;
    for (auto& q : qs) {
        if (get<2>(q) == 1 && !unite(get<0>(q), get<1>(q), 1)) { cout << 0; return 0; }
    }
    cout << 0;
    return 0;
}
  • Time Complexity: O(K * α(N))
  • Space Complexity: O(N)

Đây là phiên bản tối ưu hóa của phương pháp DSU. Thay vì tách biệt hoàn toàn hai trường hợp trong code dài dòng, ta có thể viết gọn logic.

  • Mảng parentdiff được khai báo toàn cục.
  • Hàm findunite được viết ngắn gọn, thực hiện chuẩn xác việc path compression và cập nhật trọng số XOR.
  • Logic kiểm tra hai trường hợp được giữ nguyên:
    1. Thử gán P[N] = 0, kiểm tra các ràng buộc 'even'. Nếu ổn, in ra số 'even'.
    2. Nếu không, thử gán P[N] = 1, kiểm tra các ràng buộc 'odd'. Nếu ổn, in ra 0.

Phương pháp này về bản chất là giống Solution 1 và 2 nhưng code sạch sẽ và dễ hiểu hơn.

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

Cách tiếp cận Time Space Tên
1 O(K * α(N)) O(N) DSU (Disjoint Set Union) - Theo dõi quan hệ chẵn lẻ
2 O(K * α(N)) O(N) Optimized DSU (Minified)

Bài học kinh nghiệm

  • Phân tích tính chẵn lẻ của tổng số 1 trong đoạn [i, j] dựa trên tổng tiền tố P[j] - P[i-1].
  • Bài toán có thể chia thành 2 trường hợp độc lập: tổng số 1 toàn dãy chẵn hoặc lẻ.
  • DSU With Offsets (Weighted DSU) là thuật toán chuẩn để giải quyết các bài toán quan hệ XOR/Modulo 2.

Lỗi thường gặp

  • Sử dụng sai chỉ số mảng (1-based vs 0-based) khi chuyển đổi từ câu hỏi [i, j] sang quan hệ giữa các tổng tiền tố.
  • Không xử lý path compression đúng cách trong DSU có trọng số.
  • Lầm tưởng rằng phải tìm ra chính xác cách gán giá trị, trong khi chỉ cần kiểm tra tính khả thi của từng trường hợp chẵn/lẻ của tổng.

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.