Yazı algoritmaları, metinleri verimli şekilde işlemek için kullanılır. Pek çok yazı problemi $O(n^2)$’de kolayca çözülse de asıl hedef $O(n)$ veya $O(n \log n)$’e inmektir. Temel örnek örüntü bulma problemidir: $n$ uzunluğundaki yazıda $m$ uzunluğundaki örüntü nerede geçiyor? ``
26.1 Yazı Terminolojisi
$n$ uzunluğundaki $s$ yazısı sıfır tabanlı indislenir: $s[0], s[1], \ldots, s[n-1]$.
| Kavram | Tanım | Örnek (ABCD) |
|---|---|---|
| Alt dize (substring) | Ardışık karakterler; $s[a \ldots b]$ | A, AB, ABC, ABCD, … |
| Alt dizi (subsequence) | Ardışık olmak zorunda değil | A, AC, BD, … |
| Ön ek (prefix) | Baştan başlayan alt dize | A, AB, ABC, ABCD |
| Son ek (suffix) | Sonla biten alt dize | D, CD, BCD, ABCD |
| Rotasyon | Karakterleri sağa/sola kaydırma | ABCD, BCDA, CDAB, DABC |
| Periyot (period) | Yazıyı tekrarla oluşturan ön ek | ABCABCA‘nın en kısa periyodu ABC |
| Sınır (border) | Hem ön ek hem son ek olan alt dize | ABACABA‘nın sınırları: A, ABA, ABACABA |
Sözlük sıralaması (lexicographical order): $x < y$ için ya $x \neq y$ ve $x$, $y$’nin ön ekidir; ya da $x[k] < y[k]$ olan ilk $k$ pozisyonu vardır.
26.2 Trie Yapısı
Trie, bir yazı kümesini tutan köklü ağaçtır. Ortak ön ekli yazılar aynı dalı paylaşır.
Örnek: ${\text{CANAL, CANDY, THE, THERE}}$ kümesi için trie:
(kök)
├── C → A → N → A → L *
│ └── D → Y *
└── T → H → E *
└── R → E *
* o noktada bir yazının bittiğini gösterir (THE, THERE gibi iç içe yazılar için gereklidir).
İşlemler:
- $n$ uzunluğunda yazı arama: $O(n)$
- $n$ uzunluğunda yazı ekleme: $O(n)$
- Bir yazının en uzun ön eki kümede mi? → $O(n)$
- Her düğümde sayaç tutularak ön eke sahip yazı sayısı da bulunabilir
Dizi temsili:
int trie[N][A];
// N: maksimum düğüm sayısı, A: alfabe büyüklüğü
// trie[s][c]: s düğümünden c karakteriyle gidilen sonraki düğüm
// Kök düğüm: 0
26.3 Yazı Hashleme
Yazı hashleme, iki yazının aynı olup olmadığını verimli şekilde belirler: karakterleri tek tek karşılaştırmak yerine hash değerlerini karşılaştırırız.
Hash Değeri Hesaplama (Polinom Hashleme)
$n$ uzunluğundaki $s$ yazısının hash değeri:
\[H(s) = \Bigl(s[0] \cdot A^{n-1} + s[1] \cdot A^{n-2} + \cdots + s[n-1] \cdot A^0\Bigr) \bmod B\]$A$ ve $B$ önceden seçilen sabitlerdir. Örneğin ALLEY için karakter kodları $65, 76, 76, 69, 89$; $A=3$, $B=97$ ile:
Ön İşleme ile $O(1)$ Alt Dize Hashi
$h[k] = H(s[0 \ldots k])$ ve $p[k] = A^k \bmod B$ dizileri $O(n)$’de oluşturulur:
\(h[0] = s[0], \qquad h[k] = (h[k-1] \cdot A + s[k]) \bmod B\) \(p[0] = 1, \qquad p[k] = (p[k-1] \cdot A) \bmod B\)
$a > 0$ için $s[a \ldots b]$ alt dizesinin hash değeri $O(1)$’de:
\[H(s[a \ldots b]) = \bigl(h[b] - h[a-1] \cdot p[b-a+1]\bigr) \bmod B\]Bu sayede kaba kuvvet örüntü eşleştirme $O(n^2)$’den $O(n)$’e iner.
Ek uygulama: Hashleme + ikili arama ile iki yazının sözlük sırası $O(\log n)$’de bulunur (ortak ön ek uzunluğu ikili aramayla bulunur).
Çarpışmalar ve Parametre Seçimi
Farklı iki yazının aynı hash değerini almasına çarpışma denir ve yanlış sonuç üretir.
Üç senaryo ($n = 10^5$ için çarpışma olasılıkları):
| $B$ sabiti | 1 karşılaştırma | $x$ vs $y_1 \ldots y_n$ | Tüm çiftler |
|---|---|---|---|
| $10^3$ | 0.001 | 1.000 | 1.000 |
| $10^6$ | 0.000001 | 0.632 | 1.000 |
| $10^9$ | $\approx 0$ | $\approx 0$ | 0.393 |
| $10^{12}$ | $\approx 0$ | $\approx 0$ | 0.0005 |
- senaryo doğum günü paradoksudur: Tüm çiftler karşılaştırıldığında $B \approx 10^9$ ile çarpışma olasılığı yaklaşık %39’dur!
Önerilen sabitler:
A = 911382323
B = 972663749
Güvenli kullanım: İki farklı $(A, B)$ çiftiyle paralel hash — çarpışma ihtimali $\approx 10^{-18}$.
⚠️ $B = 2^{32}$ veya $B = 2^{64}$ gibi 2’nin kuvvetlerini kullanmayın; bu parametrelerle her zaman çarpışma üretebilen girdiler oluşturmak mümkündür.
26.4 Z-Algoritması
$n$ uzunluğundaki $s$ yazısının Z-dizisi $z$, her $k$ için $k$ pozisyonundan başlayan ve aynı zamanda $s$’nin ön eki olan en uzun alt dizenin uzunluğunu verir:
\[z[k] = p \iff s[0 \ldots p-1] = s[k \ldots k+p-1]\]Örnek: ACBACDACBACBACDA için Z-dizisi:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A | C | B | A | C | D | A | C | B | A | C | B | A | C | D | A |
| – | 0 | 0 | 2 | 0 | 0 | 5 | 0 | 0 | 7 | 0 | 0 | 2 | 0 | 0 | 1 |
Örneğin $z[6] = 5$ çünkü ACBAC ($= s[6 \ldots 10]$) yazının ön ekidir ama ACBACB değil.
Algoritmanın Çalışması
Z-algoritması bir $[x, y]$ aralığı tutar: $s[x \ldots y]$, $s$’nin bir ön ekidir ve $y$ mümkün olduğunca büyük tutulur.
Her $k$ pozisyonu için:
- $z[k-x]$ değerine bakılır.
- $k + z[k-x] < y$ ise → $z[k] = z[k-x]$ (karaktere bakmaya gerek yok).
- $k + z[k-x] \geq y$ ise → $y$’den devam ederek karakter karakter karşılaştır.
- Gerekirse $[x, y]$ güncellenir.
Her karakter en fazla bir kez yeni karşılaştırmaya girer → toplam süre $O(n)$.
Örüntü Bulma ile Kullanım
$p$ örüntüsünün $s$ yazısında geçtiği yerleri bulmak için:
\[t = p \# s\]oluşturulur; # karakteri ne $p$’de ne $s$’de geçer. $t$’nin Z-dizisinde $ |
p | $ değerine eşit olan pozisyonlar örüntünün başlangıç noktalarıdır. |
Örnek: $s = \text{HATTIVATTI}$, $p = \text{ATT}$
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A | T | T | # | H | A | T | T | I | V | A | T | T | I |
| – | 0 | 0 | 0 | 0 | 3 | 0 | 0 | 0 | 0 | 3 | 0 | 0 | 0 |
-
ve 10. pozisyonlar $z = 3 = p $ → ATT,HATTIVATTI‘da 1. ve 6. indislerde geçer.
Implementasyon
vector<int> z(string s) {
int n = s.size();
vector<int> z(n);
int x = 0, y = 0;
for (int i = 1; i < n; i++) {
z[i] = max(0, min(z[i-x], y-i+1));
while (i+z[i] < n && s[z[i]] == s[i+z[i]]) {
x = i; y = i+z[i]; z[i]++;
}
}
return z;
}
Hashleme vs Z-Algoritması
| Yazı Hashleme | Z-Algoritması | |
|---|---|---|
| Hız | $O(n)$ ön işleme, $O(1)$ sorgu | $O(n)$ tek geçiş |
| Güvenlik | Çarpışma riski var | Her zaman doğru |
| Kod kolaylığı | Daha basit | Daha karmaşık |
| Esneklik | İkili arama ile sözlük sırası | Yalnızca ön ek eşleşmeleri |
Kaynak: Rekabetçi Programcı (Competitive Programmer’s Handbook — Antti Laaksonen), Bölüm 26.
Yorumlar