Wiki › Thuật toán › Thuật toán xâu › Z-function

Z-function

huunguyen huunguyen Updated Tháng 9. 29, 2026 Đọc 22 phút 12 lượt xem

Z-function (hàm Z) của xâu s độ dài n là mảng z[0..n−1], trong đó z[i] là độ dài tiền tố chung dài nhất của s và hậu tố s[i..]. Chỉ một mảng này đủ để tìm mọi vị trí xuất hiện của một mẫu, tìm chu kỳ của xâu, đếm biên (border), so sánh một đoạn với tiền tố... Tính ngây thơ (so từng ký tự tại mỗi i) mất O(n2) trên xâu như aaaa...a; thuật toán Z dùng lại thông tin cũ qua một "hộp Z" (Z-box) và chạy trong O(n).

Ý tưởng / Trực giác

Định nghĩa

z[i]=max{k≥0:s[0..k−1]=s[i..i+k−1]},1≤i<n.

z[0] không có ý nghĩa tự nhiên (cả xâu khớp với chính nó); trang này quy ước z[0]=n, nhiều đề quy ước z[0]=0. Ví dụ:

 i :  0  1  2  3  4  5  6  7
 s :  a  b  a  c  a  b  a  b
 z :  8  0  1  0  3  0  2  0

z[4]=3 vì aba (bắt đầu tại 4) khớp tiền tố aba, còn ký tự tiếp theo b khác c.

Hộp Z: dùng lại đoạn đã khớp

Khi duyệt i từ trái sang, ta giữ một đoạn [l,r) gọi là hộp Z: đoạn có r lớn nhất đã tìm được mà s[l..r−1]=s[0..r−l−1] (tức l+z[l]=r). Hộp Z là "bản sao" của một tiền tố.

Xét i mới:

  • Trường hợp i≥r: i nằm ngoài mọi đoạn đã biết, không có gì để dùng lại. So từng ký tự s[0],s[1],… với s[i],s[i+1],… như cách ngây thơ.
  • Trường hợp i<r: i nằm trong hộp. Vì s[l..r−1] giống hệt s[0..r−l−1], đoạn bắt đầu tại i trong hộp giống đoạn bắt đầu tại k=i−l trong tiền tố, ít nhất tới biên r. Mà z[k] đã tính rồi, nên:

z[i]≥min(z[k], r−i).

        0     k                         l     i           r
  s :  [=====|=====……]                 [=====|=====……]    |
        \___________/                   \___________/
         tiền tố s[0..r-l-1]   ==        hộp Z s[l..r-1]

Có hai khả năng:

  • z[k]<r−i: đoạn khớp tại k kết thúc trước khi chạm biên. Ký tự làm nó dừng, s[k+z[k]]≠s[z[k]], cũng có bản sao nằm trong hộp: s[i+z[k]]=s[k+z[k]]. Vậy z[i]=z[k] chính xác; về mặt lý thuyết không cần so sánh ký tự nào (code bên dưới vẫn thử một phép so tại vị trí i+z[k], thất bại ngay rồi dừng, và đó chính là phép thất bại duy nhất của i trong lập luận đếm ở mục sau).
  • z[k]≥r−i: ta chỉ biết chắc z[i]≥r−i (khớp tới biên). Từ r trở đi là vùng chưa ai so, nên phải so tiếp từng ký tự bắt đầu tại vị trí r.

Cuối mỗi bước, nếu i+z[i]>r thì hộp mới là [i,i+z[i]).

Vì sao là O(n): r chỉ tăng

Đếm các phép so sánh trong vòng while:

  • Mỗi phép so sánh thành công xảy ra ở vị trí i+z[i]≥r (trường hợp i≥r thì bắt đầu từ i≥r; trường hợp chạm biên thì bắt đầu đúng từ r). Sau bước đó, r được cập nhật thành i+z[i], nên mỗi lần khớp đẩy r lên ít nhất 1. Vì r≤n và không bao giờ giảm, tổng số lần khớp ≤n.
  • Mỗi i có nhiều nhất một phép so sánh thất bại (vòng while dừng ngay khi gặp nó).

Tổng cộng không quá 2n phép so sánh ký tự. Đây là cùng lập luận khấu hao với Manacher: biên phải của vùng đã biết chỉ tiến, không lùi.

Ví dụ chạy tay

s= abacabab, n=8. Ban đầu l=r=0.

 i :  0  1  2  3  4  5  6  7
 s :  a  b  a  c  a  b  a  b

i=1 (i≥r): so s[0]=a với s[1]=b: khác. z[1]=0. Hộp giữ nguyên.

i=2 (i≥r): a=a khớp, rồi s[1]=b với s[3]=c: khác. z[2]=1, hộp [2,3).

i=3 (i≥r=3): a với c: khác. z[3]=0.

i=4 (i≥r): a=a, b=b, a=a, rồi s[3]=c với s[7]=b: khác. z[4]=3, hộp mới [4,7).

 i :  0  1  2  3  4  5  6  7
 s :  a  b  a  c  a  b  a  b
      [-----]     [-----]
      s[0..2]     hộp Z s[4..6] = [4, 7): l=4, r=7

i=5 (i<r): k=5−4=1, z[1]=0, r−i=2. Vì z[k]=0<2 nên z[5]=0 chính xác (vòng while chỉ làm một phép so sánh thất bại).

i=6 (i<r): k=2, z[2]=1, r−i=1. Lúc này z[k]≥r−i: đoạn khớp chạm biên, chỉ biết z[6]≥1.

 i :  0  1  2  3  4  5  6  7
 s :  a  b  a  c  a  b  a  b
            ^           ^  |
            k=2         i  r=7   <- biết chắc s[6]=s[0], từ r trở đi phải so

So tiếp từ vị trí r=7: s[1]=b với s[7]=b: khớp, z[6]=2. Hết xâu. Hộp mới [6,8), r tiến từ 7 lên 8.

i=7 (i<r): k=1, z[1]=0<r−i=1 nên z[7]=0.

Kết quả z=[8,0,1,0,3,0,2,0]. Tổng cộng có 5 phép so sánh thành công và 6 phép thất bại, dưới 2n=16.

Cài đặt

C++
#include <bits/stdc++.h>
using namespace std;

// z[i] = length of the longest common prefix of s and s[i..]; z[0] = n by convention
vector<int> z_function(const string& s) {
    int n = s.size();
    vector<int> z(n, 0);
    if (n == 0) return z;
    z[0] = n;
    int l = 0, r = 0;                 // Z-box [l, r): s[l..r-1] == s[0..r-l-1]
    for (int i = 1; i < n; i++) {
        if (i < r) z[i] = min(r - i, z[i - l]);   // reuse the mirrored value
        while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;  // extend past r
        if (i + z[i] > r) { l = i; r = i + z[i]; }
    }
    return z;
}

int main() {
    string s;
    cin >> s;
    vector<int> z = z_function(s);
    for (int x : z) cout << x << ' ';
    cout << '\n';
}

Hàm chỉ dùng phép == trên phần tử nên đổi string thành vector<int> là chạy được với dãy số.

Python
def z_function(s):
    n = len(s)
    z = [0] * n
    if n == 0:
        return z
    z[0] = n
    l = r = 0  # Z-box [l, r)
    for i in range(1, n):
        if i < r:
            z[i] = min(r - i, z[i - l])
        while i + z[i] < n and s[z[i]] == s[i + z[i]]:
            z[i] += 1
        if i + z[i] > r:
            l, r = i, i + z[i]
    return z


print(z_function("abacabab"))  # [8, 0, 1, 0, 3, 0, 2, 0]

Bản Python dùng được, nhưng vòng lặp thuần Python chậm hơn C++ nhiều lần; bài có n=106 và giới hạn thời gian chặt thì nên dùng C++.

Ứng dụng

Tìm mẫu: ghép p+#+t

Để tìm mọi vị trí p (độ dài m) xuất hiện trong t, tính Z của xâu S=p+#+t, với # là ký tự không có trong p và t. Vị trí i trong S (với i>m) ứng với vị trí i−m−1 trong t, và p xuất hiện tại đó khi và chỉ khi z[i]≥m (thực ra =m).

 p = aba,  t = abacababa
 i : 0 1 2 3 4 5 6 7 8 9 10 11 12
 S : a b a # a b a c a b a  b  a
 z : - 0 1 0 3 0 1 0 3 0 3  0  1
             ^       ^   ^
     z >= 3 tại i = 4, 8, 10  ->  t chứa p tại 0, 4, 6

Vì sao cần ký tự phân cách:

  • Chặn z không vượt m. Mọi đoạn khớp bắt đầu trong t phải dừng trước # của tiền tố, nên z[i]≤m và z[i] đúng bằng độ dài tiền tố dài nhất của p bắt đầu tại t[i−m−1]. Không có # thì z[i] có thể lớn hơn m, mọi chỗ dùng giá trị này (không chỉ phép so ≥m) phải tự cắt min(z[i], m). Đây là lợi ích chính: z[i] dùng được trực tiếp làm lcp với p (cho lcp/lcs, cho so sánh thứ tự).
  • Đoạn vắt qua ranh giới chỉ gây nhầm khi duyệt cả những i nằm trong p. Nếu chỉ xét các vị trí thuộc t (như vòng lặp dưới đây), thì kể cả khi không có #, điều kiện z[i]≥m vẫn cho đúng các vị trí khớp, vì đoạn bắt đầu trong t chỉ đọc tiếp trong t. Nhầm lẫn chỉ xảy ra khi duyệt i từ 1: với p = aa, t = ab, xâu ghép không phân cách aaab có z[1]=2≥m nhưng i=1 nằm trong p. Có # thì mọi i trong p đều có z[i]<m, nên duyệt từ 1 cũng an toàn.
// 0-indexed starting positions of p in t
vector<int> find_occurrences(const string& p, const string& t) {
    string s = p + '#' + t;           // '#' must not appear in p or t
    vector<int> z = z_function(s);
    int m = p.size();
    vector<int> pos;
    for (int i = m + 1; i < (int)s.size(); i++)
        if (z[i] >= m) pos.push_back(i - m - 1);
    return pos;
}

Thời gian O(n+m), bộ nhớ O(n+m) cho xâu ghép và mảng z.

Chu kỳ và biên của xâu

p (1≤p≤n) là chu kỳ của s nếu s[i]=s[i+p] với mọi i hợp lệ. Điều này đúng khi và chỉ khi hậu tố s[p..] trùng tiền tố cùng độ dài, tức

p+z[p]=n(hoặc p=n).

Tương tự, tiền tố độ dài ℓ là biên (vừa là tiền tố vừa là hậu tố) khi và chỉ khi z[n−ℓ]=ℓ. Chu kỳ p và biên n−p luôn đi thành cặp.

// all periods of s in increasing order
vector<int> all_periods(const string& s) {
    int n = s.size();
    vector<int> z = z_function(s);
    vector<int> res;
    for (int p = 1; p < n; p++)
        if (p + z[p] == n) res.push_back(p);
    res.push_back(n);
    return res;
}

Ví dụ s = abaababaab (n=10) có các chu kỳ 5,8,10. Chú ý: chu kỳ nhỏ nhất p0 không nhất thiết chia hết n. Xâu s viết được thành wk (k≥2) khi và chỉ khi p0<n và p0∣n; kiểm tra p0∣n là đủ vì nếu s có một chu kỳ d∣n, d<n thì theo định lý Fine–Wilf (p0+d≤n), gcd(p0,d) cũng là chu kỳ, suy ra p0∣d∣n.

Đếm số xâu con phân biệt trong O(n2)

Thêm lần lượt từng ký tự c vào cuối xâu hiện tại s. Các xâu con mới xuất hiện đều là hậu tố của s+c. Đảo ngược t=rev(s+c) thì các hậu tố đó thành tiền tố của t. Một tiền tố của t đã có từ trước khi và chỉ khi nó xuất hiện ở vị trí i≥1 nào đó, tức độ dài ≤zmax=maxi≥1zt[i]. Vậy số xâu con mới là |t|−zmax.

long long count_distinct_substrings(const string& s) {
    long long total = 0;
    string cur;
    for (char c : s) {
        cur += c;
        string t(cur.rbegin(), cur.rend());   // new substrings = prefixes of t
        vector<int> z = z_function(t);
        int zmax = 0;
        for (int i = 1; i < (int)t.size(); i++) zmax = max(zmax, z[i]);
        total += (int)t.size() - zmax;
    }
    return total;
}

Ví dụ abab cho 1+2+2+2=7: thêm a được a; thêm b được b, ab; thêm a được ba, aba (còn a đã có, zmax=1); thêm b được bab, abab (còn b, ab đã có, zmax=2). Thời gian O(n2), đủ cho n vài nghìn. Với n lớn hơn, dùng mảng hậu tố hoặc suffix automaton. Cùng lập luận (đổi vai trò tiền tố/hậu tố) cho số xâu con phân biệt thay đổi thế nào khi thêm ký tự vào đầu, hoặc xóa một ký tự ở đầu hay cuối, mỗi thao tác O(n).

So sánh một đoạn với tiền tố

z[i] chính là lcp(s[i..],s), nên với mọi i ta trả lời trong O(1):

  • Đoạn s[i..i+len−1] bằng tiền tố độ dài len khi và chỉ khi z[i]≥len.
  • Nếu không bằng, thứ tự từ điển của hai đoạn do ký tự đầu tiên khác nhau quyết định: so s[i+z[i]] với s[z[i]].

Muốn so một đoạn của t với tiền tố của p thì dùng xâu ghép p+#+t như trên. Muốn so hai đoạn bất kỳ (không phải tiền tố) thì Z không đủ; cần hash (tìm lcp bằng chặt nhị phân) hoặc mảng hậu tố + LCP.

Một mẹo hay dùng: tiền tố đối xứng. Tiền tố độ dài L của s là palindrome khi và chỉ khi nó trùng hậu tố độ dài L của rev(s). Tính Z của s+#+rev(s); tiền tố độ dài L đối xứng khi và chỉ khi giá trị Z tại vị trí n+1+(n−L) đạt ≥L.

Chuyển đổi giữa Z-function và hàm tiền tố

Hàm tiền tố π[i] là độ dài biên thực sự dài nhất của s[0..i]. Hai mảng mang cùng một lượng thông tin: cả hai đều mô tả tập các cặp (vị trí, độ dài) mà tiền tố xuất hiện, Z ghi theo điểm bắt đầu, π ghi theo điểm kết thúc.

Z sang π. Tiền tố độ dài j+1 xuất hiện kết thúc tại i+j với mọi j<z[i]. Duyệt i tăng dần; với mỗi i đi j từ z[i]−1 xuống 0 và gán π[i+j]=j+1 nếu ô đó chưa có giá trị. Khi gặp ô đã có giá trị thì dừng: ô đó được đặt bởi một điểm bắt đầu sớm hơn (cho biên dài hơn), và các ô bên trái nó cũng vậy. Mỗi ô được gán đúng một lần nên tổng O(n).

vector<int> z_to_pi(const vector<int>& z) {
    int n = z.size();
    vector<int> pi(n, 0);
    for (int i = 1; i < n; i++)
        for (int j = z[i] - 1; j >= 0; j--) {
            if (pi[i + j] > 0) break;     // already set by an earlier start (longer border)
            pi[i + j] = j + 1;
        }
    return pi;
}

π sang Z. Cách gọn nhất: dựng lại một dãy có đúng hàm tiền tố đó, rồi chạy Z trên dãy. Đặt t[i]=t[π[i]−1] nếu π[i]>0, ngược lại cho t[i] một ký hiệu mới chưa dùng (lấy luôn t[i]=i). Với π hợp lệ, dãy t có hàm tiền tố bằng π, và vì hai mảng cùng thông tin nên Z của t chính là Z cần tìm.

vector<int> pi_to_z(const vector<int>& pi) {
    int n = pi.size();
    vector<int> t(n);                  // a sequence with the same prefix function
    for (int i = 0; i < n; i++)
        t[i] = (pi[i] > 0) ? t[pi[i] - 1] : i;   // fresh symbol when there is no border
    vector<int> z(n, 0);
    if (n == 0) return z;
    z[0] = n;
    for (int i = 1, l = 0, r = 0; i < n; i++) {
        if (i < r) z[i] = min(r - i, z[i - l]);
        while (i + z[i] < n && t[z[i]] == t[i + z[i]]) z[i]++;
        if (i + z[i] > r) { l = i; r = i + z[i]; }
    }
    return z;
}

Cả hai hàm đã được kiểm tra đối chiếu với cách tính trực tiếp trên hàng chục nghìn xâu ngẫu nhiên.

Khi nào chọn Z, KMP hay hash
Tình huống Nên dùng Lý do
Đã có toàn bộ xâu, cần "khớp với tiền tố bao xa" tại mọi vị trí Z Đúng định nghĩa của z[i], code ngắn, dễ nhớ
Tìm mẫu một lần, p và t cho sẵn Z hoặc KMP Cả hai O(n+m), chọn cái quen tay
Xâu đến theo luồng (từng ký tự, không biết trước phần sau), hoặc cần automaton để quy hoạch động "không chứa mẫu" KMP π[i] chỉ phụ thuộc s[0..i]; Z cần nhìn về phía phải
Duyệt chuỗi biên lồng nhau (π[i],π[π[i]−1],…) KMP Hàm tiền tố cho sẵn cây biên
So sánh hai đoạn bất kỳ, lcp hai hậu tố bất kỳ, xâu 2 chiều, nhiều truy vấn Hash O(1) mỗi phép so, O(logn) cho lcp; đổi lại có xác suất va chạm
Nhiều truy vấn lcp/thứ tự giữa các hậu tố, đếm xâu con phân biệt với n lớn Mảng hậu tố Cấu trúc tổng quát hơn, đổi lại cài đặt dài hơn
Nhiều mẫu cùng lúc Aho-Corasick Z/KMP phải chạy lại cho từng mẫu

Z và hàm tiền tố tất định (không có rủi ro va chạm như hash), nên khi bài chỉ cần so với tiền tố thì chúng an toàn hơn.

Độ phức tạp

Thao tác Thời gian Bộ nhớ Lý do
Tính mảng z O(n) O(n) ≤n lần khớp (mỗi lần đẩy r tiến) cộng ≤n lần thất bại (một lần mỗi i)
Tìm mẫu p trong t O(n+m) O(n+m) Một lần Z trên xâu ghép dài n+m+1
Mọi chu kỳ / biên O(n) O(n) Một lần Z, rồi kiểm tra p+z[p]=n cho mỗi p
Đếm xâu con phân biệt O(n2) O(n) n lần Z trên xâu dài tối đa n
Z sang π O(n) O(n) Mỗi ô π được gán một lần, mỗi i dừng sau tối đa một lần gặp ô đã gán
π sang Z O(n) O(n) Dựng dãy O(n) rồi chạy Z

⚠️ Lỗi thường gặp

  • Quên min(r - i, ...). Viết z[i] = z[i - l] khi i<r là sai: giá trị ở tiền tố có thể "lan" ra ngoài hộp, nơi hai bản sao không còn giống nhau. Với s = aaaab: tại i=2, hộp là [1,4), z[1]=3 nhưng đáp án đúng z[2]=2. Triệu chứng: kết quả lớn hơn thực tế, thường chỉ lộ ra trên xâu có nhiều ký tự lặp.
  • Lẫn hộp nửa mở [l,r) với đoạn đóng [l,r]. Nhiều tài liệu dùng r là vị trí cuối (đóng), khi đó công thức thành min(r - i + 1, z[i - l]) và điều kiện là i <= r. Trộn hai quy ước gây lệch một. Chọn một quy ước và giữ nguyên trong cả hàm.
  • Quy ước z[0]. Code ở đây đặt z[0]=n; nhiều đề (và nhiều thư viện) đặt z[0]=0. Nếu lấy max trên cả mảng mà quên bỏ i=0 thì luôn nhận được n; nếu đề yêu cầu in z[0]=0 mà in n thì sai ngay test đầu.
  • Ký tự phân cách có mặt trong dữ liệu. Đề cho xâu gồm cả ký tự đặc biệt mà vẫn dùng # thì đoạn khớp có thể đi xuyên qua ranh giới. Chọn ký tự chắc chắn không xuất hiện (ví dụ '\0' hay '$' khi đề chỉ có chữ cái), hoặc chuyển sang vector<int> và dùng phân cách −1.
  • Tưởng O(n) nhưng thực ra O(n2). Chỉ đúng tuyến tính khi dùng lại giá trị từ hộp Z. Nếu quên cập nhật hộp (if (i + z[i] > r) { l = i; r = i + z[i]; }) hoặc bỏ nhánh dùng lại z[i - l] thì vòng while so lại những ký tự đã so, và xâu aaa...a với n=106 sẽ quá thời gian. Ngược lại, đếm xâu con phân biệt bằng Z vốn là O(n2); đừng dùng nó cho n=105.
  • Quên đảo xâu khi đếm xâu con phân biệt. Xâu con mới khi thêm ký tự vào cuối là hậu tố, còn Z nói về tiền tố. Tính Z trên s+c thay vì rev(s+c) cho số đếm sai.
  • Nhầm "có chu kỳ" với "là lũy thừa của một xâu". Mọi xâu đều có chu kỳ nhỏ nhất p0, nhưng chỉ viết được thành wk (k≥2) khi p0∣n và p0<n. Với abaab, p0=3 không chia hết 5 nên xâu không phải lũy thừa.
  • Ghép xâu lặp lại cho nhiều mẫu. Với n,m tới 106, xâu ghép và mảng int tổng cộng khoảng 10 MB, ổn. Nhưng ghép lại xâu trong vòng lặp nhiều truy vấn (mỗi mẫu một lần ghép với cả t) sẽ tốn O(k·n) thời gian; nhiều mẫu thì cân nhắc Aho-Corasick, hash hoặc mảng hậu tố.

Biến thể / Mở rộng

  • Lcp của mỗi hậu tố của t với p. Từ p+#+t ta có luôn mảng lcp(t[j..],p) cho mọi j, không chỉ các vị trí khớp trọn. Đảo cả hai xâu cho hậu tố chung dài nhất kết thúc tại mỗi vị trí. Ghép hai mảng này giải được bài "tìm mẫu cho phép sai đúng một ký tự": p khớp tại j với tối đa một chỗ sai khi lcp+lcs≥m−1.
  • Đếm số lần xuất hiện của mọi tiền tố. Tiền tố độ dài ℓ xuất hiện tại i khi z[i]≥ℓ. Đếm cnt[z[i]]++ rồi cộng dồn từ lớn xuống nhỏ để có số vị trí i với z[i]≥ℓ cho mọi ℓ, tổng O(n).
  • Chu kỳ vòng / xoay xâu. t là một phép xoay của s (cùng độ dài) khi và chỉ khi t xuất hiện trong s+s; dùng tìm mẫu với t+#+s+s.
  • Dãy số thay cho xâu. Z chỉ cần phép so sánh bằng, nên áp dụng cho dãy số, hoặc cho dãy hiệu ai+1−ai để tìm đoạn "cùng hình dạng" bất kể tịnh tiến.
  • Thuật toán Main-Lorentz. Tìm mọi xâu con dạng XX (lặp liên tiếp) theo dạng nén (từng nhóm) trong O(nlogn) bằng chia để trị, mỗi tầng dùng Z trên phần trái đảo ngược và phần phải. Số lần lặp dạng XX có thể tới Θ(n2) (ví dụ aaa...a), nên không thể liệt kê từng lần một trong O(nlogn); thuật toán xuất mỗi nhóm lần lặp cùng tâm và cùng độ dài dưới dạng một khoảng vị trí, đủ để đếm tổng số lần lặp hoặc tìm lần lặp dài nhất.
  • Liên hệ. KMP cho cùng thông tin theo điểm kết thúc và làm được trực tuyến; hash xâu linh hoạt hơn với đoạn bất kỳ; mảng hậu tố giải các bài lcp tổng quát; Manacher dùng đúng ý tưởng "hộp + đối xứng + biên phải chỉ tiến" cho palindrome.

Bài tập luyện

Luyện kỹ thuật
  • Khớp Xâu (strmatch) (Trung cấp) đếm số lần mẫu xuất hiện với n,m≤106: bài nhập môn cho kỹ thuật ghép p+#+t và đếm z[i]≥m.
  • Tìm Chu Kỳ (periods) (Nâng cao) in mọi độ dài chu kỳ: áp dụng trực tiếp điều kiện p+z[p]=n.
  • Tiền tố trùng Hậu tố (borderocc) (Kỳ cựu) tìm mọi biên (điều kiện z[n−ℓ]=ℓ) rồi đếm số lần mỗi tiền tố đó xuất hiện bằng mảng đếm cnt[z[i]] cộng dồn ngược.
  • Hàm Xâu (strfuncs) (Kỳ cựu) in cả Z-function và hàm tiền tố với n≤106; chú ý quy ước z(1)=0 của đề, và có thể lấy π từ z bằng phép chuyển đổi ở trên.
  • Bậc đối xứng (paldeg) (Kỳ cựu) cần biết mọi tiền tố nào là palindrome: dùng Z trên s+#+rev(s) (hoặc hash), sau đó bậc của tiền tố độ dài L suy ra từ bậc của tiền tố độ dài ⌊L/2⌋.
Đề VOI

Trong các đề VOI hiện có trên hệ thống, chưa có bài nào mà lời giải chính thức dùng Z-function. Bài dưới đây chỉ dùng được Z cho một subtask, như một cách giải thay thế:

  • Virus (voi17_virus) (Nâng cao) VOI 2017, chỉ subtask k=0: đoạn cần tìm là xâu dạng XX. Gợi ý: cố định vị trí bắt đầu p và tính Z của hậu tố T[p..]; mảng này cho biết nửa sau của một đoạn có lặp lại nửa đầu hay không với mọi độ dài cùng lúc, cho O(m2) tổng cộng. Lời giải chính thức cho mọi k dùng cửa sổ trượt trên mảng sai khác, không dùng Z.
Trang này có hữu ích không? Đăng nhập để trả lời

Bình luận

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

gnatmake 12.2.0 a68g 3.1.2 nasm 2.16.1 as_x64 2.47 awk 1.3.4 gcc 16.2.0 dotnet 10.0.402 g++ 16.2.0 g++-themis 16.2.0 g++17 16.2.0 g++20 16.2.0 g++23 16.2.0 clang++ 22.1.8 dmd 2.113.0 dart 3.13.6 gforth 0.7.3 gfortran 15.3.0 go 1.27.3 groovyc 5.1.3 javac 26.0.2.1 node 26.12.1 julia 1.13.2 kotlinc 2.4.21 lean 4.35.0 sbcl 2.2.9 lua 5.5.1 nim 2.2.12 fpc 3.2.2 fpc-themis 3.2.2 perl 5.42.3 php 8.5.12 pike 8.0 swipl 9.0.4 pypy3 7.3.23 python3 3.15.1 racket 8.7 ruby 4.0.8 rustc 1.99.1 csc 5.3.0 ctoj-scratch 0.0.1 sed 4.9 swiftc 6.4.1 tclsh 8.6 bun 1.4.4 deno 2.9.8 v 0.5.2 zig 0.17.0