Hướng dẫn giải của Nói chuyện riêng


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, sussyboy, tieuc908, congtyluuthaibao1978

Hiểu bài toán

Cho một hàng gồm N người, mỗi người thuộc một trong ba bộ tộc A, B, hoặc C. Mục tiêu là thay đổi ít nhất số người để đảm bảo không có hai người cùng bộ tộc đứng cạnh nhau. Bạn có thể thay thế một người bằng người của bất kỳ bộ tộc nào. Vấn đề yêu cầu tìm số lần thay đổi tối thiểu.

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

Cách Greedy (Xử lý từ trái sang phải)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    string s;
    cin >> s;

    int ans = 0;
    for (int i = 1; i < n; i++) {
        if (s[i] == s[i - 1]) {
            ans++;
            // Chọn ký tự khác với trước đó và (nếu có) khác với tiếp theo
            for (char c : {'A', 'B', 'C'}) {
                if (c != s[i - 1] && (i + 1 == n || c != s[i + 1])) {
                    s[i] = c;
                    break;
                }
            }
        }
    }

    cout << ans << "\n";
    return 0;
}
  • Time Complexity: O(N)
  • Space Complexity: O(1) (Nếu thao tác trên string được coi là mutable)

Đây là cách tiếp cận trực quan nhất. Ta duyệt qua hàng người từ trái sang phải. Nếu gặp một người đứng ngay sau người cùng bộ tộc (s[i] == s[i-1]), ta buộc phải thay đổi người này. Để đảm bảo tối ưu, ta thay đổi người đó thành một bộ tộc khác sao cho: (1) Khác với người đứng trước (để không tạo ra xung đột mới ở cặp (i-1, i)), và (2) Khác với người đứng sau (nếu có) để tránh tạo ra xung đột mới ở cặp (i, i+1). Vì có 3 lựa chọn, ta luôn có thể tìm được ít nhất một ký tự thỏa mãn. Phương pháp này đảm bảo số lần thay đổi là tối thiểu cho cấu trúc hiện tại khi duyệt từ trái sang phải.

Cách Quy hoạch động (Dynamic Programming)
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    string s;
    cin >> s;

    if (n == 0) {
        cout << 0;
        return 0;
    }

    // dp[i][0]: số lần đổi ít nhất cho đến i mà người thứ i giữ nguyên
    // dp[i][1]: số lần đổi ít nhất cho đến i mà người thứ i bị đổi
    // Tuy nhiên, code mẫu chỉ phân biệt 2 trạng thái 'giữ' và 'đổi', không phân biệt loại cụ thể.
    // Để đơn giản cho giải thích, ta tư duy theo hướng: tối ưu chi phí cho đến vị trí i.

    // Code thực tế từ giải pháp 2:
    vector<int> a(n + 1);
    for(int i=1; i<=n; i++) a[i] = s[i-1]-'A';

    // dp[i][0]: người thứ i không đổi (hoặc đổi thành đúng loại cũ)
    // dp[i][1]: người thứ i đổi thành loại khác
    // *Lưu ý: Code mẫu có thể chưa hoàn thiện logic cho mọi trường hợp 3 loại, nhưng cho thấy tư duy DP.*

    // Dưới đây là logic DP chuẩn chỉnh hơn cho bài toán này:
    // dp[i][char_idx] là số lần đổi ít nhất đến vị trí i, với người thứ i là char_idx (0=A, 1=B, 2=C)
    vector<vector<int>> dp(n + 1, vector<int>(3, 1e9));

    // Khởi tạo vị trí đầu tiên
    for (int k = 0; k < 3; k++) {
        dp[1][k] = (k + 'A' == s[0] ? 0 : 1);
    }

    for (int i = 2; i <= n; i++) {
        for (int curr = 0; curr < 3; curr++) { // Trạng thái hiện tại (người thứ i)
            for (int prev = 0; prev < 3; prev++) { // Trạng thái trước đó (người thứ i-1)
                if (curr == prev) continue; // Không được để 2 người giống nhau đứng cạnh
                int cost = (curr + 'A' == s[i-1] ? 0 : 1);
                dp[i][curr] = min(dp[i][curr], dp[i-1][prev] + cost);
            }
        }
    }

    int ans = 1e9;
    for (int k = 0; k < 3; k++) ans = min(ans, dp[n][k]);
    cout << ans;
    return 0;
}
  • Time Complexity: O(N)
  • Space Complexity: O(N)

Sử dụng DP để tính toán chi phí tối thiểu. Trạng thái dp[i][k] biểu thị số lần thay đổi ít nhất cho đến người thứ i, với người thứ i thuộc bộ tộc k. Công thức chuyển trạng thái: dp[i][curr] = min(dp[i-1][prev]) + cost, với điều kiện curr != prev. Cost bằng 1 nếu phải thay đổi người thứ i, 0 nếu giữ nguyên. Phương pháp này đảm bảo tìm ra phương án tối ưu toàn cục.

Cách Quy hoạch động tối ưu bộ nhớ
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    string s;
    if (n > 0) cin >> s;

    // Chỉ cần lưu 2 trạng thái trước đó thay vì cả mảng N
    // dp_prev[k]: chi phí tối ưu đến i-1 với người i-1 là k
    // dp_curr[k]: chi phí tối ưu đến i với người i là k
    vector<int> dp_prev(3, 0);

    // Khởi tạo cho i=0
    for (int k = 0; k < 3; k++) {
        dp_prev[k] = (n > 0 && k + 'A' == s[0]) ? 0 : 1;
    }

    if (n == 0) {
        cout << 0;
        return 0;
    }

    for (int i = 1; i < n; i++) {
        vector<int> dp_curr(3, 1e9);
        for (int curr = 0; curr < 3; curr++) {
            for (int prev = 0; prev < 3; prev++) {
                if (curr == prev) continue;
                int cost = (curr + 'A' == s[i] ? 0 : 1);
                dp_curr[curr] = min(dp_curr[curr], dp_prev[prev] + cost);
            }
        }
        dp_prev = dp_curr;
    }

    int ans = 1e9;
    for (int k = 0; k < 3; k++) ans = min(ans, dp_prev[k]);
    cout << ans;
    return 0;
}
  • Time Complexity: O(N)
  • Space Complexity: O(1)

Cải tiến từ phương pháp DP. Thay vì lưu toàn bộ bảng dp[N][3], ta chỉ cần lưu chi phí của bước trước đó (dpprev) và tính toán bước hiện tại (dpcurr). Vì trạng thái của bước hiện tại chỉ phụ thuộc vào bước trước. Điều này giảm bộ nhớ từ O(N) xuống O(1) (chỉ cần lưu 6 giá trị nguyên).

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

Cách tiếp cận Time Space Tên
1 O(N) O(1) (Nếu thao tác trên string được coi là mutable) Greedy (Xử lý từ trái sang phải)
2 O(N) O(N) Quy hoạch động (Dynamic Programming)
3 O(N) O(1) Quy hoạch động tối ưu bộ nhớ

Bài học kinh nghiệm

  • Để tránh xung đột, người tại vị trí i chỉ được phép khác với người tại vị trí i-1.
  • Vì có 3 loại bộ tộc, bài toán luôn có lời giải (luôn tìm được cách thay thế thỏa mãn).
  • Phương pháp Greedy xử lý từ trái sang phải là cách tiếp cận đơn giản và hiệu quả nhất về mặt lập trình cho bài toán này.

Lỗi thường gặp

  • Quên kiểm tra điều kiện người cuối cùng (hoặc người ở biên) khi áp dụng quy tắc thay đổi.
  • Trong phương pháp Greedy, nếu chỉ thay đổi người hiện tại sao cho khác người trước đó, có thể gây ra xung đột với người tiếp theo. Cần ưu tiên chọn người khác cả trước và sau.
  • Xử lý sai trường hợp N=0.

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.