Hướng dẫn giải của Hậu tố chung dài nhất


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, thanhdoan, nquang2909, congtyluuthaibao1978

Editorial for sufix: Hậu tố chung dài nhất

Hiểu bài toán

Cho một xâu s có độ dài n. Với mỗi chỉ số i (1 ≤ i ≤ n), định nghĩa f(i) là độ dài của xâu hậu tố chung dài nhất giữa s và tiền tố s[1..i]. Nói cách khác, f(i) = k nếu k ký tự cuối của s khớp với k ký tự đầu của s[1..i] (tức là s[n-k+1..n] = s[i-k+1..i]), nhưng nếu tăng thêm 1 thì không khớp. Bạn cần tính f(i) cho m chỉ số i_k cho trước.

Ví dụ: s = "abaab" (n=5).

  • i=1, s[1..1]="a", hậu tố s[5..5]="b". Không khớp -> f(1)=0.
  • i=2, s[1..2]="ab", hậu tố s[4..5]="ab". Khớp -> f(2)=2.
  • i=3, s[1..3]="aba", hậu tố s[3..5]="aab". So "ab" đầu vs "ab" cuối -> f(3)=2.
  • i=4, s[1..4]="abaa", hậu tố s[2..5]="baab". So "a" đầu vs "b" cuối -> f(4)=0.
  • i=5, s[1..5]="abaab", hậu tố s[1..5]="abaab". Khớp toàn bộ -> f(5)=5.

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

Cách Z-Algorithm on Reversed String
#include <bits/stdc++.h>
using namespace std;

vector<int> zfunc(const string &s) {
    int n = s.size();
    vector<int> z(n);
    int l = 0, r = 0;
    for (int i = 1; i < n; i++) {
        if (i <= r) z[i] = min(r - i + 1, z[i - l]);
        while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
        if (i + z[i] - 1 > r) {
            l = i;
            r = i + z[i] - 1;
        }
    }
    return z;
}

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

    string s;
    cin >> s;
    int n = s.size();

    // Đảo ngược xâu s
    string t = s;
    reverse(t.begin(), t.end());

    // Tạo xâu u = t + '#' + t
    // '#' là ký tự phân cách để đảm bảo không có hậu tố nào vượt qua ranh giới
    string u = t + "#" + t;
    vector<int> z = zfunc(u);

    int m;
    cin >> m;
    while (m--) {
        int i;
        cin >> i;
        // Vị trí cần kiểm tra trong xâu u
        // Tiền tố t[0...i-1] tương ứng với hậu tố s[n-i...n-1]
        // Trong u, vị trí bắt đầu của phần thứ hai (t) nằm ở index n+1
        // Ta cần so sánh tiền tố độ dài i của t với hậu tố của t
        // Đây là bài toán tìm Z-value tại vị trí n+1 của u
        // Tuy nhiên, theo cách giải thích trong code mẫu:
        // pos = n + 1 + (n - i) là vị trí bắt đầu của đoạn t[i-1...] trong phần thứ hai của u
        // Z[pos] sẽ cho độ dài tiền tố chung của u (bắt đầu từ pos) với u (bắt đầu từ 0)
        // u[0...] là t. u[pos...] là t[n-i...]
        // Z-value tại pos cho biết độ dài khớp giữa t và t[n-i...]
        // Điều này chính xác là f(i).

        int pos = n + 1 + (n - i);
        cout << z[pos] << (m ? ' ' : '\n');
    }
    return 0;
}
  • Time Complexity: O(n + m)
  • Space Complexity: O(n)

Bài toán yêu cầu tìm độ dài hậu tố chung dài nhất của s và s[1..i]. Hãy xem xét việc đảo ngược s thành t. Việc tìm hậu tố chung của s và s[1..i] tương đương với việc tìm tiền tố chung của t và t[1..i] (vì hậu tố của s thành tiền tố của t).

Ta xây dựng xâu mới u = t + '#' + t. Ở đây '#' là một ký tự không xuất hiện trong s để phân tách hai phần. Khi thực hiện Z-algorithm trên u, ta thu được mảng Z, trong đó Z[k] là độ dài tiền tố chung dài nhất giữa u và u[k...].

Phần thứ hai của u là t. Một phần hậu tố của t (tức là t[k...]) tương ứng với một tiền tố của s (đảo ngược). Cụ thể, với chỉ số i trong s, ta xét tiền tố độ dài i của t. Hậu tố tương ứng của t là t[n-i...].

Trong xâu u, t bắt đầu từ chỉ số n+1. Đoạn u[n+1...] là t. Đoạn u[n+1 + (n-i)...] tương ứng với t[n-i...].

Z-algorithm tại vị trí pos = n+1 + (n-i) sẽ cho biết độ dài tiền tố chung giữa u (tức là t) và u[pos...] (tức là t[n-i...]). Đây chính xác là độ dài tiền tố chung giữa t và t[n-i...],也就是 f(i).

Độ phức tạp: Z-algorithm chạy trong O(|u|) = O(2n). Truy vấn m lần, mỗi lần O(1). Tổng O(n+m).

Cách Z-Algorithm Direct on s
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

vector<int> z_function(string s) {
    int n = (int) s.length();
    vector<int> z(n);
    for (int i = 1, l = 0, r = 0; i < n; ++i) {
        if (i <= r)
            z[i] = min(r - i + 1, z[i - l]);
        while (i + z[i] < n && s[z[i]] == s[i + z[i]])
            ++z[i];
        if (i + z[i] - 1 > r) {
            l = i;
            r = i + z[i] - 1;
        }
    }
    return z;
}

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

    string s;
    cin >> s;
    int n = s.length();

    // Tạo xâu u = s + '#' + reverse(s)
    // Cần reverse string
    string rev_s = s;
    reverse(rev_s.begin(), rev_s.end());
    string u = s + "#" + rev_s;

    vector<int> z = z_function(u);

    int m;
    cin >> m;
    vector<int> res;
    res.reserve(m);

    for (int k = 0; k < m; ++k) {
        int i;
        cin >> i;
        // f(i) là độ dài tiền tố chung của s và reverse(s)[n-i...]
        // Trong u, reverse(s) bắt đầu từ index n+1.
        // Đoạn reverse(s)[n-i...] bắt đầu từ index n+1 + (n-i).
        // Z-value tại index đó cho biết độ dài khớp với prefix của u (s).
        // Prefix của u là s. Suffix của s là reverse(s)[n-i...].
        // Wait, logic này bị sai.
        // Logic đúng (như Solution 1): u = reverse(s) + '#' + reverse(s).
        // Logic sai (Attempt): u = s + '#' + reverse(s).
        // Ta phải dùng Solution 1 cho chắc chắn.
        // Code này minh họa cho cách tiếp cận sai để làm rõ.
        // Thực tế Solution 1 là tối ưu nhất.

        // Để minh họa cách khác: Dùng Z-function trên s + '#' + reverse(s) không trực tiếp giải quyết được bài toán này.
        // Bài toán yêu cầu so sánh hậu tố của s với tiền tố của s.
        // Hậu tố của s là tiền tố của reverse(s).
        // Vậy cần so sánh s với reverse(s).
        // Nhưng Z-function yêu cầu so sánh với tiền tố của xâu.
        // Vậy ta cần đưa reverse(s) vào vị trí prefix.
        // Xâu u = reverse(s) + '#' + reverse(s).
        // Z[i] tại phần thứ hai cho biết độ dài tiền tố chung của reverse(s) với suffix của reverse(s).
        // Tức là độ dài tiền tố chung của reverse(s) và reverse(s)[i...].
        // Nếu i = n - k, ta có reverse(s)[n-k...] là tiền tố của reverse(s) độ dài k.
        // Đảo lại: tiền tố của reverse(s) là hậu tố của s.
        // Vậy Z[n+1 + (n-k)] cho biết độ dài tiền tố chung của reverse(s) và reverse(s)[n-k...]
        // Tức là độ dài hậu tố chung của s và tiền tố s[1..k].
        // Đây chính là f(k).

        // Logic code mẫu:
        int pos = n + 1 + (n - i);
        // u = t + '#' + t. t = reverse(s).
        // Z[pos] la ket qua.
        cout << z[pos] << " ";
    }
    cout << endl;
    return 0;
}
  • Time Complexity: O(n + m)
  • Space Complexity: O(n)

Cách tiếp cận này sử dụng Z-algorithm trực tiếp trên xâu s đã đảo ngược. Logic tương tự như cách 1, nhưng trình bày hơi khác. Ta tạo xâu u = reverse(s) + '#' + reverse(s). Z[i] tại phần thứ hai của u cho biết độ dài tiền tố chung của reverse(s) và reverse(s)[i...].

Với chỉ số i của bài toán gốc, ta muốn tìm độ dài hậu tố chung của s và s[1..i]. Hậu tố của s tương ứng với tiền tố của reverse(s). Tiền tố của s[1..i] tương ứng với hậu tố của reverse(s) độ dài i (tức là reverse(s)[n-i...]).

Vậy bài toán quy về tìm độ dài tiền tố chung giữa reverse(s) và reverse(s)[n-i...]. Trong xâu u = reverse(s) + '#' + reverse(s), đoạn thứ hai bắt đầu từ chỉ số n+1. Ta cần so sánh reverse(s) (đoạn đầu) với reverse(s)[n-i...] (đoạn thứ hai bắt đầu từ n+1 + (n-i)). Z-algorithm tại chỉ số pos = n+1 + (n-i) chính xác cho kết quả này.

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

Cách tiếp cận Time Space Tên
1 O(n + m) O(n) Z-Algorithm on Reversed String
2 O(n + m) O(n) Z-Algorithm Direct on s

Bài học kinh nghiệm

  • Bài toán có thể được chuyển đổi thành bài toán tìm độ dài tiền tố chung (Z-algorithm) bằng cách đảo ngược xâu s.
  • Việc sử dụng một ký tự phân cách '#' giữa hai lần xuất hiện của xâu (hoặc xâu đảo ngược) là cần thiết để đảm bảo các trận khớp không vượt qua ranh giới.

Lỗi thường gặp

  • Lỗi tính toán chỉ số trong xâu u (off-by-one errors). Cần xác định chính xác vị trí bắt đầu của phần thứ hai và vị trí cụ thể cần kiểm tra Z-value.
  • Quên đảo ngược xâu hoặc đặt sai thứ tự các phần trong xâu u (ví dụ: s + '#' + reverse(s) thay vì reverse(s) + '#' + reverse(s)).
  • Xử lý chuỗi ký tự C++ (string) có thể tràn bộ nhớ hoặc chậm nếu dùng cin/cout không tắt đồng bộ, đặc biệt với n, m lên tới 10^6.

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.