אלגוריתם KMP

אלגוריתם KMP הוא אלגוריתם בתחום מדעי המחשב להתאמת תבניות הפועל בזמן ליניארי במקרה הגרוע. האלגוריתם התגלה בשנת 1970 על ידי דונלד קנות' ווואן פראת', ובאופן בלתי תלוי על ידי ג'יימס מוריס. השלושה פרסמו יחדיו את האלגוריתם בכתב העת SIAM בשנת 1977. מטרתו של האלגוריתם היא למצוא מופעים של מחרוזת P (תבנית, pattern) בתוך מחרוזת T (טקסט), כאשר | T | = n , | P | = m {\displaystyle |T|=n,|P|=m} . בעוד האלגוריתם הנאיבי לביצוע פעולה זו פועל בזמן O ( m n ) {\displaystyle \mathrm {O} (mn)} בזמן הגרוע, אלגוריתם KMP עובד בזמן O ( n ) {\displaystyle \mathrm {O} (n)} בזמן גרוע. נניח T = t 0 t 1 . . . t n − 1 {\displaystyle T=t_{0}t_{1}...t_{n-1}} וגם P = p 0 p 1 . . . p m − 1 {\displaystyle P=p_{0}p_{1}...p_{m-1}} האלגוריתם הנאיבי להתאמת מחרוזות, יחפש עבור כל מיקום בטקסט האם הוא התחלה של המחרוזת. אלגוריתם זה עובד בזמן O ( m n ) {\displaystyle \mathrm {O} (mn)} . אלגוריתם KMP משתמש בטרנזיטיביות של התאמת מחרוזות על מנת לחסוך השוואות מיותרות. בכל פעם שהאלגוריתם יתקל בנקודת חוסר התאמה בטקסט, הוא יחזור להתאים את הרישא הארוכה ביותר של התבנית המתאימה לסיפא הארוכה ביותר של הטקסט בנקודה בו נתקלנו בחוסר התאמה. על מנת להתאים בין הרישא הארוכה ביותר לסיפא הארוכה ביותר, נבנה טבלה ייעודית ונוכל להציץ בה בזמן קבוע. כאשר ניתקל בתבנית ABCABCD ובטקסט ABCABCABCABCABCABCD, נקודת אי ההתאמה הראשונה תהיה באינדקס 6. ניתן לשים לב, שהאלגוריתם הנאיבי יתחיל לחפש מהאינדקס הבא (1), למרות שניתן לדלג מידית לאינדקס 3. KMP יודע לדלג לאינדקס 3, מכיוון שלאחר שהתאמנו את A באינדקס 0, B באינדקס 1 ו-C באינדקס 2, ברור שלא נוכל להתאים את A באינדקסים 1 או 2. בנוסף, לאחר שנתחיל את הבדיקה מחדש באינדקס 3, אין טעם לבדוק ששלושת התווים הראשונים זהים, מכיוון שווידאנו זאת כבר קודם, כלומר המחרוזת שהותאמה עד כה היא ABCABC, ומחרוזת זו נגמרת ב-ABC. לכן, יש סיפא באורך 3 (ABC) שמתאימה לרישא באורך 3 (גם כן, ABC), ולכן ניתן לחסוך את ההתאמה של שלושת התווים הראשונים. על ידי התאמת הרישא לסיפא, אנחנו הלכה למעשה מוודאים שלא ייווצר מצב בו אנו מדלגים על התאמה. ניתן לחלק לשני מקרים: אם קיימת סיפא שמתאימה לרישא, אז אי אפשר לדלג על כל אזור אי ההתאמה, אבל בתוך הדילוג אנחנו כבר יודעים שהרישא שווה לסיפא, ולכן אין צורך לבדוק אותה. אם לא קיימת רישא שמתאימה לסיפא, אזי בכל מקום אליו נקפוץ בטווח שכרגע בדקנו לא תמצא התאמה, ולכן ניתן לדלג ישירות על כל החלק המאומת. מהגדרה זאת ניתן לקבל את ההרגשה שהאלגוריתם פועל בזמן ליניארי, בהנחה שפעולות הבדיקה על הרישא והסיפא פועלות בזמן קבוע. קלט - T, P אורך תבנית - m, אורך טקסט - n בנה טבלת סיפות A עבור התבנית P 0 → i {\displaystyle 0\to i} 0 → j {\displaystyle 0\to j} כל עוד i < n {\displaystyle i<n} בצע: כל עוד T [ i ] = P [ j ] {\displaystyle T[i]=P[j]} וגם j < m , i < n {\displaystyle j<m,i<n} בצע: i + 1 → i {\displaystyle i+1\to i} j + 1 → j {\displaystyle j+1\to j} אם j=m, דווח על התאמה באינדקס i-j אם i=n, סיים i − m i n ( 0 , A [ j ] ) → i {\displaystyle i-min(0,A[j])\to i} m a x ( A [ j ] , 0 ) → j {\displaystyle max(A[j],0)\to j} קלט - T באורך n צור מערך A בגודל n עם אפסים שים באיבר הראשון במערך -1 לכל i = 1... n {\displaystyle i=1...n} בצע: i − 1 → i ′ {\displaystyle i-1\to i'} כל עוד i ′ > 0 {\displaystyle i'>0} בצע אם T [ i − 1 ] = T [ A [ i ′ ] ] {\displaystyle T[i-1]=T[A[i']]} צא מהלולאה אחרת A [ i ′ ] → i ′ {\displaystyle A[i']\to i'} A [ i ′ ] + 1 → A [ i ] {\displaystyle A[i']+1\to A[i]} נוכיח שכל התאמה אפשרית במחרוזת נמצאת על ידי האלגוריתם, בהנחה שהטבלה הנוצרת נכונה. תהא התאמה באינדקס k, כלומר p 0 = t k , p 1 = t k + 1 . . . p m − 1 = t k + m − 1 {\displaystyle p_{0}=t_{k},p_{1}=t_{k+1}...p_{m-1}=t_{k+m-1}} . על מנת שתימצא התאמה באינדקס k, צריך להתחיל את הלולאה הפנימית עם ערכי i ,j המקיימים i − j = k {\displaystyle i-j=k} . נשים לב שההפרש בין i ל-j משתנה בשתי אפשרויות - m i n ( A [ j ] , 0 ) ≠ 0 {\displaystyle min(A[j],0)\neq 0} m a x ( A [ j ] , 0 ) ≠ j {\displaystyle max(A[j],0)\neq j} קל לראות שבדיוק אחד מהמקרים האלה מתרחש בכל פעם. במקרה הראשון, ההפרש גדל בדיוק ב-1, ולכן לא נדלג על שום התאמה. במקרה השני, ההפרש גדל בדיוק ב- j − A [ j ] {\displaystyle j-A[j]} וגם j לא שווה 0. נניח בשלילה שקיים אינדקס התאמה k המקיים i − j < k < i − A [ j ] {\displaystyle i-j<k<i-A[j]} , זאת אומרת ש p 0 = t k , p 1 = t k + 1 . . . p m − 1 = t k + m − 1 {\displaystyle p_{0}=t_{k},p_{1}=t_{k+1}...p_{m-1}=t_{k+m-1}} . בנוסף, מכיוון שהאלגוריתם הגיע לאינדקס j, מתקיים p 0 = t i − j , p 1 = t i − j + 1 . . . , p j − 1 = t i − 1 {\displaystyle p_{0}=t_{i-j},p_{1}=t_{i-j+1}...,p_{j-1}=t_{i-1}} . לכן, מתקיים p 0 = p k − i + j , p 1 = p k − i + j + 1 , . . . , p i − k − 1 = p j − 1 {\displaystyle p_{0}=p_{k-i+j},p_{1}=p_{k-i+j+1},...,p_{i-k-1}=p_{j-1}} , כלומר אורך הרישא הארוכה ביותר שמתאימה לסיפא הארוכה ביותר באינדקס j היא לכל הפחות i − k {\displaystyle i-k} , ולפי הגדרת הטבלה A [ j ] ≥ i − k → k ≥ i − A [ j ] {\displaystyle A[j]\geq i-k\to k\geq i-A[j]} בסתירה להנחת השלילה. ההוכחה שהאלגוריתם לא מחזיר התאמות שאינן נכונות פשוט למדי - מכיוון שהאלגוריתם משתמש בטרנזיטיביות של שוויון תווים, אזי אם התאמנו כבר סיפא אין צורך לבדוק מחדש את הרישא, ועל מנת שהאלגוריתם יודיע על התאמה יש לבדוק את כל האיברים שאחרי הרישא איבר-איבר, וזהו האלגוריתם הנאיבי. נרצה להוכיח שבהינתן מחרוזת האלגוריתם מוציא את טבלת הסיפות הנכונה, ו-(-1) במקום הראשון. הערה - קל לראות ∀ i A [ i ] < i {\displaystyle \forall iA[i]<i} . נניח שהאלגוריתם עובד עבור ערכים קטנים מ-i, נוכיח שהוא עובד עבור i - מקרה ראשון - אם מתקיים T [ i − 1 ] = T [ A [ i ′ ] ] {\displaystyle T[i-1]=T[A[i']]} וגם i ′ {\displaystyle i'} הוא אינדקס שהסיפא הארוכה ביותר הנגמרת אצלו המתאימה לרישא הארוכה ביותר זהה לסיפא באינדקס i − 1 {\displaystyle i-1} . נוכיח שאכן A [ i ] = A [ i ′ ] + 1 {\displaystyle A[i]=A[i']+1} . הערה - לפי ההגדרה, i − 1 {\displaystyle i-1} מקיים את זה. קל לראות שמתקיים A [ i ] ≥ A [ i ′ ] + 1 {\displaystyle A[i]\geq A[i']+1} . נניח שמתקיים A [ i ] > A [ i ′ ] + 1 {\displaystyle A[i]>A[i']+1} , זאת אומרת A [ i ] ≥ A [ i ′ ] + 2 {\displaystyle A[i]\geq A[i']+2} . אבל אם נוריד את התו האחרון מהסיפא והתו האחרון מהרישא, אזי יש לנו התאמה באורך לכל הפחות A [ i ] − 1 {\displaystyle A[i]-1} באינדקס i ′ {\displaystyle i'} , בסתירה להנחה שהאלגוריתם צודק עבור ערכים קטנים מ-i ובפרט i ′ {\displaystyle i'} . מקרה שני - i ′ = 0 {\displaystyle i'=0} . במקרה זה, מכיוון שלפי הגדרה ∀ i > 0 A [ i ] ≥ 0 {\displaystyle \forall i>0A[i]\geq 0} וגם i ′ {\displaystyle i'} הוא חסם עליון לאורך הסיפא המקסימלית, אזי מתקיים בוודאות A [ i ] = 0 {\displaystyle A[i]=0} . ואכן, A [ i ′ ] + 1 = A [ 0 ] + 1 = ( − 1 ) + 1 = 0 {\displaystyle A[i']+1=A[0]+1=(-1)+1=0} . מקרה שלישי - אם מתקיים T [ i − 1 ] ≠ T [ A [ i ′ ] ] {\displaystyle T[i-1]\neq T[A[i']]} . במקרה זה, התו החדש שנוסף אינו המשך של הרישא הקודמת, ולכן נחפש רישא קצרה יותר. מכיוון שהרישא הקודמת שווה לסיפא הקודמת, אזי הסיפא הארוכה ביותר המתאימה לרישא הארוכה ביותר ברישא הקודמת, היא גם סיפא של הסיפא הקודמת. מההנחה שהאלגוריתם פועל עבור קלטים קטנים, מתקיים שאורך הסיפא החדשה המתקבלת הוא המקסימלי, ולכן כל A [ i ′ ] < k < i ′ {\displaystyle A[i']<k<i'} לא יכול להיות אורך הסיפא שאינה כוללת את התו האחרון. כלומר - גם אם האלגוריתם לא ימצא בשלב זה את אורך הסיפא הוא יתחיל באיטרציה הבאה עם i ″ < i ′ {\displaystyle i''<i'} , ולכן האלגוריתם תמיד יגיע בסופו של דבר למקרה הראשון או השני. נוכיח שלא קיים i ″ = A [ i ′ ] < k < i ′ {\displaystyle i''=A[i']<k<i'} כך ש-k הוא האורך של הסיפא הארוכה ביותר הנגמרת ב- i − 1 {\displaystyle i-1} ומתאימה לרישא. אם זה אכן היה מתקיים, אז לפי הגדרה p 0 = p i − 1 − k , p 1 = p i − k , . . . p k − 1 = p i − 2 {\displaystyle p_{0}=p_{i-1-k},p_{1}=p_{i-k},...p_{k-1}=p_{i-2}} , אבל מכיוון ש- i ″ {\displaystyle i''} הוא אורך הסיפא הארוכה ביותר המתאימה ל- i ′ {\displaystyle i'} , וגם לפי הטרנזיטיביות הסיפא הנגמרת ב- i − 1 {\displaystyle i-1} זהה לסיפא הנגמרת ב- i ′ {\displaystyle i'} , אז למעשה p i − 2 = p i ′ − 1 = p k − 1 , p i − 3 = p i ′ − 2 = p k − 2 , . . . {\displaystyle p_{i-2}=p_{i'-1}=p_{k-1},p_{i-3}=p_{i'-2}=p_{k-2},...} , וזאת אומרת שאורך הסיפא המקסימלית הנגמרת ב- i ′ {\displaystyle i'} היא לפחות k, בסתירה לכך שהנחנו שהאלגוריתם צודק עבור קלטים קטנים. זאת גם למעשה ההוכחה ש- i ′ {\displaystyle i'} הוא חסם עליון לאורך הסיפא, שהרי A [ i ] = A [ i ′ ] + 1 ≤ i ′ − 1 + 1 = i ′ {\displaystyle A[i]=A[i']+1\leq i'-1+1=i'} עבור i ′ {\displaystyle i'} כלשהו. זמן הריצה הכולל של האלגוריתם הוא O ( m + n ) {\displaystyle \mathrm {O} (m+n)} , כאשר הזמן הדרוש לבניית טבלה של התבנית הוא O ( m ) {\displaystyle \mathrm {O} (m)} וזמן החיפוש הדרוש הוא O ( n ) {\displaystyle \mathrm {O} (n)} . נוכיח: כאשר אנחנו מבצעים את החיפוש עצמו, בכל איטרציה של כל אחת מהלולאות יש לפחות קידום אחד של i, כלומר לא קיימת איטרציה בה i לא קודם. בנוסף, כל איטרציה של הלולאות מפעילות O ( 1 ) {\displaystyle \mathrm {O} (1)} פעולות, ולכן בסך הכל יש לכל היותר O ( n ) {\displaystyle \mathrm {O} (n)} פעולות. נסמן מונה C1 הסופר כמה פעמים עודפות האלגוריתם ביקר בתא בודד, ומונה C2 הסופר כמה פעמים האלגוריתם ביקר פעם בודדת. מתקיים C 2 ≤ n + 1 {\displaystyle C2\leq n+1} מכיוון שיש n+1 תאים בטבלה. כפי שצוין קודם, מתקיים A [ i ] ≤ A [ i − 1 ] + 1 {\displaystyle A[i]\leq A[i-1]+1} , וגם A [ i ] = A [ i − 1 ] + 1 {\displaystyle A[i]=A[i-1]+1} אם ורק אם קיימת איטרציה אחת בדיוק (כלומר C2 מקודם). בנוסף, מתקיים A [ i ] < i {\displaystyle A[i]<i} לפי הגדרת הרישא. לכן, ניתן לכתוב A [ i ] ≤ i − 1 {\displaystyle A[i]\leq i-1} , כלומר במקרה הגרוע יש לולאה יורדת. נחבר את התנאים - על מנת שיתקיים A [ i ] > a {\displaystyle A[i]>a} צריך שיתקיים C 2 > a {\displaystyle C2>a} (לפי התנאי הראשון), ועל מנת שנוכל לקדם את C1, צריך שיתקיים A [ i ] > 0 {\displaystyle A[i]>0} . בסך הכל, על מנת לקדם את C1 יש לקדם את C2, ולכן מתקיים C 1 ≤ C 2 ≤ n + 1 {\displaystyle C1\leq C2\leq n+1} , ומספר האיטרציות הכולל קטן מ- 2 n + 2 {\displaystyle 2n+2} , או O ( n ) {\displaystyle \mathrm {O} (n)} , כנדרש. פרק זה לוקה בחסר. אנא תרמו לוויקיפדיה והשלימו אותו. ייתכן שתמצאו פירוט בדף השיחה. פרק זה לוקה בחסר. אנא תרמו לוויקיפדיה והשלימו אותו. ייתכן שתמצאו פירוט בדף השיחה. KMP visualization אם הייתה התאמה בנקודה כלשהי, אותה התאמה בהכרח מכילה כל רישא של התבנית, אבל מכיוון שהיא בטווח שסיימנו לבדוק, אז היא מכילה גם סיפא מכיוון שאם המינימום הוא לא 0, אזי j=0 בוודאות, בעוד התנאי השני מתקיים רק כאשר j אינו שווה ל-0, דבר הנובע ישירות מהגדרת הטבלה מכיוון שהתו האחרון נבדק ידנית, אז הסיפא הקודמת בצירוף התו החדש היא סיפא חוקית, והרישא בצירוף תו היא גם כמובן רישא חוקית הוכחה בהמשך דרשנו במפורש ש-k יהיה גדול מהסיפא, אבל k עצמו הוא סיפא