1. Trang chủ
  2. » Công Nghệ Thông Tin

Cây hậu tố, mảng hậu tố, và ứng dụng

15 621 6

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 15
Dung lượng 540,29 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

Mảng hậu tố là một cấu trúc dữ liệu trong việc xử lý các bài toán về xâu. Nó hỗ trợ các thuật toán tìm kiếm xâu, thành lập từ điển, tìm xâu con chung một cách nhanh chóng và hiệu quả. Trong khuôn khổ thời gian của một cuộc thi lập trình, việc biết rõ về mảng hậu tố để giải quyết những bài toán là rất cần thiết, đặc biệt là tại kỳ thi ACMICPC, IOI, ...

Trang 1

SỞ GIÁO DỤC VÀ ĐÀO TẠO THÀNH PHỐ CẦN THƠ

TRƯỜNG THPT CHUYÊN LÝ TỰ TRỌNG

TỔ TIN HỌC

CHUYÊN ĐỀ

CÂY HẬU TỐ, MẢNG HẬU TỐ VÀ

ỨNG DỤNG

Học sinh thực hiện: Hoàng Văn Thiên

Lớp: 11A2-P

Năm học: 2015 – 2016

Trang 3

MỤC LỤC

I ĐẶT VẤN ĐỀ 4

II NỘI DUNG 5

1 Định nghĩa 5

a Hậu tố (suffix) 5

b Trie hậu tố (suffix trie) 5

c Cây hậu tố (suffix tree) 5

d Mảng hậu tố (suffix array) 7

2 Thuật toán và ứng dụng 7

a Thuật toán nhân đôi tiền tố 7

b Thuật toán tìm kiếm xâu trong ( ) 11

c Thuật toán tìm tiền tố chung dài nhất trong ( ) 12

d Bài toán tìm chuỗi con dài nhất được lặp lại (Longest Substring Repeated) 13

e Bài toán tìm chuỗi con chung dài nhất (Longest Common Substring) 13

3 Bài tập 14

4 Tài liệu tham khảo 15

III KẾT LUẬN 15

Trang 4

I ĐẶT VẤN ĐỀ

Xử lý chuỗi từ lâu đã trở thành một vấn đề quen thuộc, đặc biệt là trên những xâu có độ dài rất lớn, chẳng hạn như xử lý chuỗi DNA, đoạn văn bản, từ điển, v.v… Vì vậy, việc thành lập các cấu trúc dữ liệu riêng cho xâu và các thuật toán để thao tác trên xâu là vô cùng cần thiết Trong chuyên đề này, em xin giới thiệu một vài cấu trúc dữ liệu xâu tiêu biểu và hiệu quả đó là cây hậu tố, cùng với một dạng mô phỏng khác của cây là mảng hậu tố

Sức mạnh của cây hậu tố nằm ở chỗ chúng có thể biểu diễn tất cả các hậu tố của xâu và cung cấp nhiều phép toán làm nền tảng cho những thuật toán hiệu quả Bên cạnh đó, việc xây dựng cây hậu tố khá phức tạp và tốn kém bộ nhớ Trong một số trường hợp, người ta lại chuộng sử dụng mảng hậu tố hơn bởi tính ngắn gọn khi xây dựng, ít tốn kém Trong các kỳ thi ACM – ICPC, IOI, …

do tính đặc thù về khống chế thời gian nên dùng mảng hậu tố thay thế cho cây hậu tố là điều cần thiết

Theo thời gian, các nhà khoa học đã có nhiều đóng góp để tiến hóa cấu trúc

dữ liệu hậu tố này Ban đầu là Trie hậu tố, sau đó rút gọn lại thành cây hậu tố

và cuối cùng là mảng hậu tố Tuy nhiên, đã có nhiều thuật toán để hình thành trực tiếp mảng hậu tố mà không cần thông qua cây và còn dùng mảng để

“dựng ngược lại” về cấu trúc cây

Trong chuyên đề hôm nay, em sẽ giới thiệu sơ qua những định nghĩa về Trie hậu tố, Cây hậu tố và Mảng hậu tố Kèm theo đó, em sẽ nói thêm về giải thuật hình thành Mảng hậu tố trực tiếp và các dạng bài tập cơ bản để ứng dụng

Trang 5

II NỘI DUNG

1 Định nghĩa

a Hậu tố (suffix)

Hậu tố i của một xâu là xâu con tính từ vị trí thứ i đến vị trí cuối cùng

của xâu đó

Ví dụ, hậu tố 4 của xâu ‘BALALAIKA’ là ‘LAIKA’ (vị trí đầu tiên của chuỗi được đánh số thứ tự là 0)

b Trie hậu tố (suffix trie)

Trie được lấy từ chữ ‘Information Retrieval’ (truy hồi thông tin)

Cho S là một tập hợp các xâu Suffix Trie của S là một cây gồm tất cả các

hậu tố viết ra được từ S Cây này tuân thủ một vài nguyên tắc như sau:

- Mỗi cạnh được gán nhãn bằng một ký tự

- Mỗi đỉnh được gán nhãn bằng một xâu được hình thành bằng cách

ghép tất cả các nhãn của các cạnh trên đường đi từ đỉnh gốc đến đỉnh đang xét

- Mỗi đỉnh liên kết với tối đa 26 đỉnh (giả thiết chỉ xét chuỗi gồm các

ký tự tiếng Anh in hoa)

- Mỗi đỉnh có thêm hai nhãn boolean để đánh dấu xâu trên đỉnh đó

có phải là một hậu tố hay không, và có phải là một phần tử trong S hay không

Ví dụ, S = {‘DOG’, ‘JOG’, ‘JOB’}

thì các hậu tố sẽ bao gồm

{‘DOG’, ‘JOG’, ‘JOB’, ‘OG’, ‘OB’,

‘G’, ‘B’} Hình bên mô tả một

Suffix Trie của tập S Các đỉnh

tô màu trắng mang xâu là một

hậu tố trong S Các đỉnh được

viền đen mang xâu là một

phần tử trong S Hiển nhiên vì

một xâu là hậu tố của chính

nó nên nếu đỉnh nào được

viền đen thì sẽ tô màu trắng

c Cây hậu tố (suffix tree)

Bây giờ chúng ta hãy thử chuyển sang thao tác trên chuỗi dài hơn để thấy rõ sự thiếu hiệu quả trong phân vùng dữ liệu của Trie

Xét xâu S = ‘BALALAIKA$’ Ký tự cuối ‘$’ mang ý nghĩa kết thúc một xâu Trong bảng mã ASCII, ký tự này đứng trước ký tự ‘A’, nói cách khác,

Root

DO

DOG

JO

JOG

O

G

O

G JOB B OG

G OB B

Trang 6

đứng trước tất cả các ký tự trong xâu S Ký tự này đảm bảo rằng mọi hậu tố của S đều nằm ở nút lá trên cây Nút lá này sẽ được gán nhãn

bởi một số i, mô tả rằng hậu tố khi đi từ root đến nút lá này trùng với hậu tố thứ i của S

Nếu biểu diễn trong Suffix Trie, có thể thấy sự thiếu hiệu quả nằm ở chỗ: Xâu S càng dài

thì càng nhiều đỉnh bị

lặp lại

Suffix Tree được hình thành bằng cách “gộp” các đỉnh chỉ có một nút con lại với nhau Như vậy Suffix Tree cho xâu S nói trên được biểu diễn gọn gàng như hình bên

Khi đó, nhãn của mỗi cạnh không chỉ là một ký tự mà

là một chuỗi Tương tự như ở Trie, nhãn của một nút là chuỗi ghép của các chuỗi nằm trên các cạnh khi đi từ gốc xuống nút đó

Trong suffix tree của một chuỗi độ dài n, số nút (kí hiệu là V) sẽ không

các nút trên cây Mà:

- Nút gốc có ít nhất 1 con

root

K

L

A

L

A

L

A

I

K

A

A

I

K

A L

1

$

I

K

A

3

$

I

5

K A

A

I

K A L

2

$

I

K

A

4

$

K

A

6

$

A

7

$

8

$

root

9

0

6 7

$ A

BALALAIKA$

LA IKA$

KA$

LA

1

LAIKA$

3

IKA$

5 IKA$

2

LAIKA$

4 IKA$

8

$

Trang 7

- Nút nhánh ngoại trừ nút gốc có ít nhất 2 con

- Nút lá không có con

d Mảng hậu tố (suffix array)

Trong quyển Algorithmica của Esko Ukkonen có trình bày cách xây

dựng cây hậu tố trong ( ), tuy nhiên rất phức tạp và khó cài đặt trong các kỳ thi lập trình Một sự thay thế gần như hoàn hảo đó là mảng hậu

tố, được thiết kế bởi Udi Manber và Gene Meyers, và đơn giản hơn rất nhiều so với cây hậu tố

Gọi A là tập hợp các hậu tố của chuỗi S, sắp xếp mảng A theo thứ tự từ

điển Với mỗi A[i] có thể xác định nó là hậu tố thứ SA[i] của chuỗi S Như vậy mảng SA được hình thành như thế gọi là mảng hậu tố Ví

dụ, S=’BALALAIKA$’ thì mảng hậu tố sẽ là SA = {9, 8, 5, 3, 1, 0, 4, 2, 6, 7} Xem bảng dưới đây:

Cách hình thành trên đơn thuần chỉ định nghĩa mảng hậu tố là gì Nếu

dựa vào thuật toán trên, sẽ tốn ( ) cho mỗi lần so sánh, và tốn ( log ) cho công việc sắp xếp Như vậy độ phức tạp cuối cùng là

Trong phần tiếp theo của chuyên đề, em xin được trình bày thuật toán thành lập mảng hậu tố hiệu quả, cùng với các bài toán cơ bản đi kèm

2 Thuật toán và ứng dụng

a Thuật toán nhân đôi tiền tố

Thuật toán này dùng để lập mảng hậu tố trong thời gian ( log )

root

9

0

6 7

$ A

BALALAIKA$

LA

IKA$

KA$

LA

1

LAIKA$

3

IKA$

5 IKA$

2 LAIKA$

4 IKA$

8

$

Trang 8

Trong giới hạn của chuyên đề này, khi nói đến “sắp xếp”, độc giả hãy ngầm hiểu rằng em đang nói đến “sắp xếp tăng dần” theo giá trị (đối với số) hoặc theo thứ tự từ điển (đối với chuỗi)

Ý tưởng của thuật toán như sau:

- Gán mỗi hậu tố một hạng (rank): hai hậu tố có ký tự đầu bằng nhau

có hạng bằng nhau, hậu tố nào có ký tự đầu nhỏ hơn có hạng nhỏ hơn Việc gán hạng có thể tiến hành trong ( ) Hạng đại diện cho

ký tự đầu của hậu tố Các hậu tố nếu được sắp xếp theo hạng cũng đồng nghĩa với việc được sắp xếp theo ký tự đầu tiên

- Giả sử chúng ta đã có dãy các hậu tố sắp xếp theo k ký tự đầu tiên, thuật toán sẽ xây dựng dãy các hậu tố sắp xếp theo 2k ký tự đầu tiên

nếu ta đã xếp được k ký tự đầu, thì tại hậu tố thứ i cần xem hậu

tố thứ i+k như là một tiêu chí tiếp theo để sắp xếp Vậy mỗi hậu

tố thứ i cần có thêm một hạng thứ cấp, chính bằng hạng sơ cấp

của hậu tố thứ i+k Trường hợp chuỗi S không có hậu tố i+k (tức

là i+k ≥ |S|) thì xem như hạng thứ cấp của hậu tố i bằng 0

sắp xếp, vị trí của các hậu tố thay đổi theo cặp số này Hay nói cách khác, chúng ta sắp xếp hậu tố một cách gián tiếp, thông qua việc sắp xếp cặp hạng này

tố có cặp hạng cũ bằng nhau thì hạng mới bằng nhau, hậu tố nào

có cặp hạng cũ nhỏ hơn thì hạng mới nhỏ hơn

tiên Thuật toán có thể được ngưng lại khi hạng sơ cấp của các hậu tố đôi một khác nhau (tức là việc sắp xếp hậu tố đã hoàn toàn xác định)

Có tổng cộng tối đa log bước lặp, mỗi bước lặp lại có thao tác gán hạng tốn và thao tác sắp xếp ( log ) Vậy cơ bản thì thuật toán nhân đôi tiền tố tốn ( (log ) ) Tuy nhiên thuật toán sắp xếp cho số nhỏ

có thể dùng đếm phân phối (counting sort) và sắp xếp cơ số (radix sort)

để giảm độ phức tạp còn ( )

Mô phỏng qua ví dụ S = ‘BALALAIKA$’ như sau:

(Để thuận tiện cho việc trình bày, em xin dùng ký tự ‘_’ để mô tả ký tự

“rỗng” trong chuỗi.)

Trang 9

- Xếp hạng các ký tự đầu tiên Ví dụ, các ký tự đầu tiên của các hậu tố gồm {$, A, B, I, K, L} tương ứng với các hạng {1, 2, 3, 4, 5, 6} Hậu tố bắt đầu bởi ký tự nào thì được gán hạng sơ cấp là một số nguyên dương tương ứng được mô tả ở trên

- Sắp xếp lại cặp số (RA[SA[i]], RA[SA[i]+1])

- Đặt lại hạng sơ cấp dựa theo cặp (<hạng sơ cấp>, <hạng thứ cấp>) Hai hậu tố có cặp hạng cũ bằng nhau thì hạng mới bằng nhau Hậu

tố nào có cặp hạng cũ nhỏ hơn thì hạng mới nhỏ hơn Ví dụ hậu tố 1

và 3 bảng trên có cùng cặp hạng (2, 5) nên ở bảng dưới chúng có cùng hạng sơ cấp (hạng 4)

Trang 10

- Sắp xếp bảng trên theo cặp số (<hạng sơ cấp>, <hạng thứ cấp>) và đặt hạng sơ cấp mới, ta có bảng dưới đây:

- Từ trên xuống dưới, 10 hạng đã điền đủ cho 10 hậu tố nên đây là kết quả sắp xếp cuối cùng Mảng SA = {9, 8, 5, 3, 1, 0, 6, 7, 4, 2} là mảng hậu tố, mô tả các hậu tố tương ứng sắp xếp theo thứ tự từ điển

Pseudo Code sau sử dụng SA là mảng hậu tố, RA[SA[i]] là hạng sơ cấp của hậu tố SA[i], tempSA[.] và tempRA[.] là hai mảng tạm thời để lưu

vị trí mới của SA và RA khi thực hiện counting sort và radix sort

Ghi chú: mảng hạng đang xét có thể là mảng hạng sơ cấp hoặc thứ cấp

int c[N], RA[N], tempRA[N], SA[N], tempSA[N];

void countingSort(int k) { // sắp xếp SA theo RA[i+k]

fill c with zero;

∀i∈[0 n-1]: if (i+k >= n) c[0]++; else c[RA[i+k]]++;

// lúc này, c[x] = số lần xuất hiện của x trong mảng hạng đang xét;

∀i∈[0 max(300,n)): {

int t = c[i]; c[i] = sum; sum += t;

}

// lúc này, c[x] = số phần tử trong mảng hạng đang xét có giá trị nhỏ hơn x

// thứ tự xếp của RA[i+k] cũng là của i

∀i∈[0 n-1]:

if (i+k >= n) tempSA[c[0]++] = i;

else tempSA[c[RA[i+k]]++] = i;

// cập nhật SA

∀i∈[0 n-1]: SA[i] = tempSA[i];

}

void radixSort(int k) {

countingSort(k);

countingSort(0);

}

void construct(int k) {

Trang 11

∀i∈[0 n-1]: SA[i] = i;

∀i∈[0 n-1]: RA[i] = S[i]; // tận dụng mã ASCII trở thành hạng của ký tự đầu tiên

int k = 1, r;

while (k <= n) {

radixSort(k);

// SA đã được sắp xếp theo 2k ký tự đầu

tempRA[SA[0]] = r = 1;

∀i∈[1 n-1]:

if (hạng sơ cấp và thứ cấp của hậu tố SA[i] và SA[i-1] đều bằng nhau)

tempRA[SA[i]] = r; // chúng vẫn giữ hạng cũ

else tempRA[SA[i]] = ++r; // ngược lại, có hạng mới

∀i∈[0 n-1]: RA[i] = tempRA[i]; // cập nhật

if (RA[SA[n-1]] == n-1) ngắt vòng lặp;

}

}

b Thuật toán tìm kiếm xâu trong ( )

Cho xâu S độ dài n và mảng hậu tố SA đã được xây dựng từ S Dùng kỹ

thuật tìm kiếm nhị phân trên một mảng hậu tố đã sắp xếp, chúng ta dễ tìm ra được sự tồn tại của xâu P độ dài m nào đó trên xâu S trong tối

đa (log ) phép so sánh Mỗi phép so sánh xâu có độ phức tạp ( ) Như vậy thuật toán hoạt động hiệu quả với ( log )

Do xâu P có thể xuất hiện nhiều lần trong S, tức là nó sẽ là tiền tố của một dãy các hậu tố liên tiếp trong mảng SA đã xếp theo thứ tự từ điển Nếu tìm cận dưới (lower bound) và cận trên (upper bound) của P trong

mảng các hậu tố đã sắp xếp thì ta dễ dàng đưa ra tất cả các lần xuất hiện của P trong S chứ không đơn thuần là kiểm tra sự xuất hiện nữa

II stringMatching() {

int l = 0, r = n-1, mid;

while (l <= r) {

mid = (l+r)/2;

if (strncmp(S+SA[mid], P, m) >= 0) r = mid; else l = mid+1; }

if (strncmp(S+SA[l], P, m) < 0) return mp(-1, -1);

II ans = mp(l, -1);

l = 0, r = n-1;

while (l <= r) {

mid = (l+r)/2;

if (strncmp(S+SA[mid], P, m) > 0) r = mid; else l = mid+1; }

if (strncmp(S+SA[r], P, m) != 0) –-r;

ans.S = r;

return ans;

}

Trang 12

Hàm trên trả về một pair, mô tả rằng tất cả các hậu tố từ SA[pair.first] đến SA[pair.second] đều có một tiền tố trùng với P

c Thuật toán tìm tiền tố chung dài nhất trong ( )

Khái niệm:

- Cho hai xâu x[0 m] và xâu y[0 n] Tiền tố chung dài nhất (Longest

Common Prefix) của hai xâu x và y là số L lớn nhất sao cho x[0 L]

== y[0 L] Ký hiệu:

L = pcp(x, y) Nhận xét: Hậu tố i có tiền tố chung dài nhất với một hậu tố j của chuỗi S thì i và j nằm kề nhau trong mảng hậu tố, nói cách khác, hai hậu tố i và j nằm liền kề nhau trong thứ tự từ điển

- Cho xâu S[0 n] có mảng hậu tố SA[0 n], mảng LCP[1 n] được hình thành theo nguyên tắc:

LCP[j] = lcp(S[SA[j] n], S[SA[j-1] n]) (LCP[j] = tiền tố chung dài nhất của hậu tố SA[j] và hậu tố SA[j-1])

- Mảng LCP được sắp xếp lại (Permuted LCP Array) – PLCP[0 n-1] được hình thành theo nguyên tắc:

PLCP[SA[j]] = LCP[j]

- Mảng Phi, Phi[i] mô tả tiền tố đứng trước tiền tố i trong mảng SA

Dựa vào định nghĩa, có thể tính được mảng LCP:

void computeLCP() {

LCP[0] = 0;

for (int i = 1; i < n; ++i) {

int L = 0;

while (T[SA[i]+L] == T[SA[i-1]+L]) ++L;

LCP[i] = L;

}

}

với mục đích dựng mảng hậu tố Chúng ta sẽ có bước tiếp cận khác

Ta thừa nhận định lý sau:

Định lý trên đã được chứng minh bởi Kesai trong tài liệu [2]

Dựa vào định lý này, thay vì đặt L = 0, có thể đặt L = PLCP[i – 1] + 1 Khi

đó, câu lệnh while chỉ được thực hiện tối đa n lần

Trang 13

void computeLCP_PLCP() {

Phi[SA[0]] = -1;

for (i=1; i<n; ++i) Phi[SA[i]] = SA[i-1];

for (i=0; i<n; ++i) {

if (Phi[i] == -1) PLCP[i] = 0;

while (S[i+L] == S[Phi[i]+L]) ++L;

PLCP[i] = L;

L = max(L-1, 0);

}

for (i=0; i<n; ++i) LCP[i] = PLCP[SA[i]];

}

d Bài toán tìm chuỗi con dài nhất được lặp lại (Longest Substring Repeated)

Cho một chuỗi S, tìm một chuỗi con xuất hiện ít nhất 2 lần trong S sao cho chuỗi này là dài nhất có thể Hai lần xuất hiện được xem là khác nhau nếu vị trí xuất hiện đầu tiên của chúng là khác nhau

Từ mảng LCP đã xây dựng, dễ thấy phần tử lớn nhất của mảng này cũng chính là độ dài lớn nhất của chuỗi cần tìm Khi biết được LCP[i] mang giá trị lớn nhất, ta còn có thể xuất ra chuỗi con đó bằng cách lấy ra i ký

tự đầu tiên của hậu tố SA[i]

void printLSR() {

int L = 0, pos;

for(int i = 0; i < n; ++i)

if (L < LCP[i]) L = LCP[i], pos = i;

for(int i = 0; i < L; ++i) cout << T[SA[pos]+i];

}

e Bài toán tìm chuỗi con chung dài nhất (Longest Common Substring)

của hai chuỗi phải khác nhau — $ và #) Ghép hai chuỗi này lại để có chuỗi S = ‘MALAI#BALALAIKA$’

Trong chuyên đề này, để tiện cho việc mô tả, ta gọi gốc của một hậu tố

x là chuỗi S1 nếu x < strlen(S1), là chuỗi S2 nếu x ≥ strlen(S1)

hành như sau:

- Xây dựng mảng hậu tố, mảng LCP cho chuỗi S;

Trang 14

- Khởi tạo biến L = 0 (độ dài chuỗi con chung dài nhất)

- Duyệt qua mảng SA, nếu hậu tố SA[i] và SA[i – 1] thuộc hai chuỗi khác nhau thì cập nhật L = max(L, LCP[i])

Việc kiểm tra hai hậu tố thuộc hai chuỗi khác nhau rất đơn giản Nếu

Tại sao chúng ta chỉ quan tâm hai hậu tố kề nhau trong từ điển?

Chúng ta sẽ chứng minh, khi xét hai hậu tố khác gốc không kề nhau, với mọi trường hợp đều quy về xét hai hậu tố có khoảng cách nhỏ dần cho đến khi chúng kề nhau

dài nhất độ dài k với một hậu tố SA[j] (j < i – 1) khác gốc của hậu tố SA[i] Do sắp xếp theo thứ tự từ điển, tiền tố chung dài nhất của hậu tố SA[j] so với một hậu tố SA[p] nào đó (j < p < i) sẽ không nhỏ hơn k Vậy

sẽ có lợi hơn nếu xét giữa SA[j] và SA[p] Nếu p == j + 1, khoảng cách không thể rút ngắn nữa Ngược lại, ta tiếp tục rút gọn khoảng cách này Lưu ý hậu tố SA[p] phải có gốc khác hậu tố SA[j]

3 Bài tập

a UVa 760 – DNA Sequencing

Tóm tắt: Cho hai chuỗi DNA, mỗi chuỗi được biểu diễn bằng dữ liệu kiểu string, chỉ gồm các ký tự a, t, g, c Tìm tất cả đoạn DNA chung dài nhất của hai chuỗi

Hướng dẫn: Giải bằng phương pháp nêu ở II.2.e

b SPOJ – SARRAY

Hướng dẫn: Bài tập thuần về mảng hậu tố Xem cách làm bằng phương pháp nhân đôi tiền tố tại II.2.a

c UVa 11512 – GATTACA

Hướng dẫn: Bài tập thuần về tìm chuỗi con lặp lại dài nhất Xem cách làm tại II.2.b

d SPOJ OI – PRINTER (trích từ đề thi IOI năm 2008)

Hướng dẫn: Lập mảng SA và LCP, từ đó dựng Trie hậu tố Bài toán trở thành độ dài đường đi ngắn nhất để thăm tất cả các nút lá trên một cây Mỗi bước đi xuống nút con tương ứng với việc viết nhãn nút con đó ra Mỗi lần kết thúc việc thăm nút lá tương ứng với lệnh Print (‘P’) Mỗi

Ngày đăng: 10/05/2016, 16:40

TỪ KHÓA LIÊN QUAN

🧩 Sản phẩm bạn có thể quan tâm

w