3.8.6 · D4 · HinglishString Algorithms

ExercisesAho-Corasick — multiple pattern search, automaton

3,356 words15 min read↑ Read in English

3.8.6 · D4 · Coding › String Algorithms › Aho-Corasick — multiple pattern search, automaton

Ek chhota sa vocabulary reminder jo tum use karoge, plain words mein:

Related tools jo tum doosre tab mein open rakhna chahoge: Trie — prefix tree, KMP — single pattern matching, BFS — breadth-first search, Finite Automata — DFA/NFA.


Level 1 — Recognition

L1.1

In ek-ek sentence mein batao ki yeh chaar objects kya hain: str(v), fail[v], δ(v,c), dictionary chain.

Recall Solution
  • str(v): root se tak spell ki gayi string.
  • fail[v]: str(v) ke longest proper suffix ka node, jo ab bhi ek node hai (ek pattern prefix).
  • δ(v,c): c padhne par tum kahan jaate ho — real child agar exist kare, warna δ(fail[v], c).
  • Dictionary chain: se fail links follow karke mile terminal nodes ka sequence; sab yahan ek match report karte hain.

L1.2

Patterns {he, she, his, hers}. Batao ki yeh strings trie mein node hain ya nahi: h, sh, her, hi, sha, e?

Recall Solution

Ek string node hai iff woh kisi pattern ka prefix hai.

  • h ✓ (he, his, hers ka prefix)
  • sh ✓ (she ka prefix)
  • her ✓ (hers ka prefix)
  • hi ✓ (his ka prefix)
  • sha ✗ (koi pattern sha se start nahi hota)
  • e ✗ (koi pattern e se start nahi hota; note karo ki "e" "he" mein appear karta hai lekin woh prefix nahi hai)

Level 2 — Application

L2.1

Patterns {a, ab, bab, bc, bca, c, caa}. fail[bab] ko haath se compute karo, failure-chain search dikhate hue.

Recall Solution

str(bab) = "bab", parent p = ba character b ke zariye. Hum "bab" ka longest proper suffix dhundh rahe hain jo ek node ho. Last char b par end hone wale proper suffixes: "ab", "b", "". Parent p = ba ki failure chain walk karo, b par child dhundhte hue:

  • fail[ba] = "ba" ka longest proper suffix jo node hai = "a" (node ✓). Kya node a ka b par child hai? Haan: a → ab. Roko.
  • Toh fail[bab] = ab. Check karo: "ab" actually "bab" ka longest proper suffix hai jo node hai. ✓

L2.2

Same patterns. Text abcc ke liye scan table fill karo. Saare matches report karo, aur har character ke liye batao ki kyun tum wahan gaye jahan gaye.

Recall Solution
char state after reports why
a a a kyunki root ka a par real edge hai; a terminal hai
b ab ab kyunki a ka b par real edge hai; ab terminal hai
c bc bc, c kyunki ab ka c par real edge nahi, toh δ(ab,c)=δ(fail[ab]=b, c); b ka real edge cbc; dictLink[bc]=c se c fire hota hai
c c c kyunki bc ka c par real edge nahi, toh δ(bc,c)=δ(fail[bc]=c, c); c ka c edge nahi, toh δ(c,c)=δ(fail[c]=root, c)=root child c=c; c terminal hai

Matches (end position ke hisaab se grouped, ek position mein longest-first): a (pos 1), ab (pos 2), bc, c (pos 3), c (pos 4).

L2.3

Text abcc mein, teesre character (c) ne ek single state se do patterns kyun report kiye?

Recall Solution

Node bc (jo terminal hai) par land karne ke baad, dictionary chain hai: bc → fail[bc] = c → fail[c] = root. Node c bhi terminal hai, toh woh bhi fire karta hai. Dono patterns us same text position par end hote hain, ek doosre ka suffix hote hue. dictLink[bc] shortcut bc se seedha c par point karta hai, jisse hum dono emit kar sakte hain bina non-terminal nodes visit kiye.


Level 3 — Analysis

L3.1

Prove karo ki fail[v] hamesha se strictly smaller depth wale node ko point karta hai. Yeh ek fact BFS ordering ko justify kyun karta hai?

Recall Solution

Yaad karo (is page ke top par defined hai): root se tak spell ki gayi string mein characters ki count. Ab fail[v], str(v) ka proper suffix hai, toh , isliye — strictly smaller. Isliye jab BFS depth par pohonchta hai, depth ke har node (jinmein saare fail targets shamil hain jinki zaroorat hai) already finalize ho chuke hain. Exactly yahi wajah hai ki hum fail ko breadth-first order mein compute karte hain, recursively nahi.

L3.2

Parent claim karta hai ki scanning cost karta hai jahan = reported matches ki count. "Potential argument" explain karo: poore scan mein failure-chain walking ki total cost kyun nahi badhti, bhale hi ek single step kaafi links jump kare?

Recall Solution

Ek potential define karo = current state ki . Ek text character padhna depth ko zyada se zyada badhata hai (ek downward edge), toh characters mein depth total zyada se zyada badhti hai. Har failure jump depth ko kam se kam ghatata hai. Kyunki depth se shuru hoti hai, rehti hai, aur total sirf baar badhti hai, poore scan mein failure jumps ki count hai. Isliye saara jumping amortized hai — kisi single step ki cost matter nahi karti, sirf total matter karta hai. (Full δ table ke saath har character aur bhi simple hai: ek lookup.) Extra unavoidable hai: tumhe matches emit karne mein waqt toh lagana hi padega.

L3.3

Patterns {he, she, his, hers}. Ek student claim karta hai ki fail[hers] = s. Kya yeh sahi hai? Agar nahi, toh sahi jawab reasoning ke saath do.

Recall Solution

str(hers) = "hers". "hers" ka longest proper suffix jo ek node ho: "ers" check karo (✗), "rs" (✗), "s" (✓ node). Koi longer suffix jo node ho? "ers", "rs" kisi bhi pattern ke prefix nahi hain. Toh fail[hers] = s. Student sahi hai. (Trap defused: bahut log zyada fancy target expect karte hain, lekin s genuinely yahan longest matching suffix hai.)


Level 4 — Synthesis

L4.1

Patterns {ab, bab, ba} ke liye automaton scratch se banao: saare nodes list karo, har node ke liye fail[v] do, phir failure links draw karo.

Neeche figure finished automaton dikhata hai jisse tum khud check kar sako: solid teal arrows trie edges hain (har ek ek character add karta hai, neeche greater depth ki taraf jaata hai), dashed plum arrows failure links hain (har ek upar strictly shorter suffix ko point karta hai), aur orange nodes terminals hain jahan ek pattern end hota hai. Visually notice karo ki har plum arrow ek shallower node ko rise karta hai — L3.1 ka depth-strictly-decreases fact concrete ho gaya.

Figure — Aho-Corasick — multiple pattern search, automaton
Recall Solution

Nodes (strings ke roop mein): root, a, ab(✓ab), b, ba(✓ba), bab(✓bab). BFS mein fail compute karo (increasing depth):

  • depth 1: fail[a]=root, fail[b]=root (root ke children root par fail hote hain).
  • depth 2:
    • fail[ab]: parent a, char b. fail[a]=root walk karo; root ka child b par? haan b. Toh fail[ab]=b.
    • fail[ba]: parent b, char a. fail[b]=root walk karo; root ka child a par? haan a. Toh fail[ba]=a.
  • depth 3:
    • fail[bab]: parent ba, char b. fail[ba]=a walk karo; node a ka child b par? haan ab. Toh fail[bab]=ab. Yeh upar figure mein plum arrows se match karta hai.

L4.2

L4.1 ke automaton ko use karke text babab scan karo aur har transition ki reason ke saath har match report karo.

Recall Solution
char state reports why
b b kyunki root ka b par real edge hai
a ba ba kyunki b ka a par real edge hai; ba terminal hai
b bab bab, ab kyunki ba ka b par real edge hai → bab terminal; dictLink[bab]=ab se ab fire hota hai
a ba ba kyunki bab ka a par real edge nahi, toh δ(bab,a)=δ(fail[bab]=ab, a); ab ka a edge nahi, toh =δ(fail[ab]=b, a); b ka real edge aba terminal hai
b bab bab, ab kyunki ba ka b par real edge hai → bab terminal; dictLink[bab]=ab se ab fire hota hai

Matches end position ke hisaab se grouped (longest-first): ba(pos 2), bab, ab(pos 3), ba(pos 4), bab, ab(pos 5).

L4.3

Aisi patterns design karo (kam se kam teen) jisme kisi node ki dictionary chain ek position par teen patterns report kare. Patterns aur position do.

Recall Solution

Lo {c, bc, abc}. Text abc scan karo.

  • a → node a (koi report nahi), kyunki root ka real edge a hai
  • bab? real edge exist karta hai → ab (koi report nahi)
  • cabc (✓abc), kyunki ab ka real edge c hai. Dictionary chain: abc → fail = bc (✓) → fail = c (✓) → root. Final c par, teen patterns fire hote hain: abc, bc, c — har ek pichle ka suffix. Position: index 3 (last char).

Level 5 — Mastery

L5.1

Tumhare paas patterns hain jinka total length hai, alphabet size hai, aur total matches wala length ka text hai. Ek colleague propose karta hai: "bas KMP ek baar har pattern ke liye run karo." Dono costs batao aur ek precise inequality do jab Aho-Corasick asymptotically cheaper ho.

Recall Solution
  • KMP-per-pattern total cost: — saare prefix functions build karna hai, patterns mein se har ek poora text scan karta hai (), plus report karna.
  • Aho-Corasick: build (full table), scan ; total . Precise criterion. Aho-Corasick asymptotically cheaper tab hoga jab uska total smaller ho: Rearrange karne par ( terms cancel ho jaate hain): Toh kisi bhi ke liye, Aho-Corasick tab jeet jaata hai jab text se longer ho. Jaise-jaise badhta hai yeh threshold shrink hoti hai, aur maps ke saath (build , toh term ban jaata hai) condition relax hokar simply ho jaati hai jab bhi ho.

L5.2

Amortized bound prove karo: poore scan mein, outputs emit karne ke liye use hone wale failure-link hops ki total count hai sirf tabhi agar tum dictLink use karo; naively yeh ho sakta hai. Ek aisi family of inputs do jahan naive output-walk quadratic ho.

Recall Solution

Naive quadratic family: patterns {a, aa, aaa, …, a^k} (aⁿ ke saare prefixes), text aⁿ. Text position par, state node a^i hai, aur uski failure chain hai a^i → a^{i-1} → … → a → root. Outputs emit karne ke liye naive walk chain ke har node ko visit karta hai, position par cost lagti hai, aur — quadratic. dictLink fix: dictLink[v] = failure chain par ke strictly upar nearest terminal node. Emit karna phir sirf ek reported match se doosre terminal tak hop karta hai, toh emit work matches ki count ke proportional hai plus per character, giving . Key: output ke dauran kabhi bhi non-terminal node mat touch karo.

L5.3

Full construction challenge. Patterns {ab, ba, bab, aba}, alphabet {a,b}. (a) Nodes aur fail list karo. (b) Saare nodes aur dono characters ke liye full transition table compute karo, har cell justify karte hue. (c) ababa scan karo aur matches list karo.

Recall Solution

(a) Nodes: root, a, ab(✓ab), aba(✓aba), b, ba(✓ba), bab(✓bab). Fail (BFS):

  • fail[a]=root, fail[b]=root
  • fail[ab]: parent a,b; fail[a]=root, root child bfail[ab]=b
  • fail[ba]: parent b,a; fail[b]=root, root child afail[ba]=a
  • fail[aba]: parent ab,a; fail[ab]=b, b child a? haan bafail[aba]=ba
  • fail[bab]: parent ba,b; fail[ba]=a, a child b? haan abfail[bab]=ab

(b) Full δ — har cell batata hai ki real edge exist karta hai ya nahi, warna fail se recurse karta hai:

node δ(·,a) why δ(·,b) why
root a real edge root→a b real edge root→b
a a real edge nahi; δ(fail[a]=root,a)=a ab real edge a→b
ab aba real edge ab→a b real edge nahi; δ(fail[ab]=b,b)=b (b ka b-edge nahi, δ(root,b)=b)
aba a real edge nahi; δ(fail[aba]=ba,a): ba ka a-edge nahi, δ(fail[ba]=a,a)=a bab real edge nahi; δ(fail[aba]=ba,b)=bab (ba ka real edge b→bab hai)
b ba real edge b→a b real edge nahi; δ(fail[b]=root,b)=b
ba a real edge nahi; δ(fail[ba]=a,a)=a bab real edge ba→b
bab aba real edge nahi; δ(fail[bab]=ab,a)=aba (ab ka real edge a hai) b real edge nahi; δ(fail[bab]=ab,b)=b

(c) Scan ababa (full δ table use karke):

  • a: root→a. reports: none.
  • b: a real edge b → ab. reports: ab.
  • a: ab real edge a → aba. reports aba; dictLink[aba]=baba.
  • b: aba ka b-edge nahi → δ(aba,b)=bab. reports bab; dictLink[bab]=abab.
  • a: bab ka a-edge nahi → δ(bab,a)=aba. reports aba; dictLink[aba]=baba.

Matches end position ke hisaab se grouped (longest-first): ab(pos 2), aba, ba(pos 3), bab, ab(pos 4), aba, ba(pos 5).


Recall Self-test checklist (sirf khatam karne ke baad kholna)

Kya tumne, har problem ke liye, (1) confirm kiya ki har claimed node real prefix hai, (2) verify kiya ki , (3) output ke liye sirf terminal flag se nahi balki dictionary chain (ya dictLink) follow ki, aur (4) fail/δ BFS order mein build kiya? Agar haan, toh tumne mechanics master kar li hai.