אלגוריתם KMP

אלגוריתם KMP הוא אלגוריתם בתחום מדעי המחשב להתאמת תבניות הפועל בזמן ליניארי במקרה הגרוע. האלגוריתם התגלה בשנת 1970 על ידי דונלד קנות' ווואן פראת', ובאופן בלתי תלוי על ידי ג'יימס מוריס. השלושה פרסמו יחדיו את האלגוריתם בכתב העת SIAM בשנת 1977. 

מטרתו של האלגוריתם היא למצוא מופעים של מחרוזת P (תבנית, pattern) בתוך מחרוזת T (טקסט), כאשר <math>|T|=n, |P|=m</math>.
בעוד האלגוריתם הנאיבי לביצוע פעולה זו פועל בזמן <math>\Omicron (mn)</math> בזמן הגרוע, אלגוריתם KMP עובד בזמן <math>\Omicron (n)</math> בזמן גרוע.

==תיאור האלגוריתם==
נניח <math>T=t_0 t_1 ... t_{n-1}</math> וגם <math>P=p_0 p_1 ... p_{m-1}</math>

האלגוריתם הנאיבי להתאמת מחרוזות, יחפש עבור כל מיקום בטקסט האם הוא התחלה של המחרוזת. אלגוריתם זה עובד בזמן <math>\Omicron (mn)</math>. אלגוריתם KMP משתמש בטרנזיטיביות של התאמת מחרוזות על מנת לחסוך השוואות מיותרות. בכל פעם שהאלגוריתם יתקל בנקודת חוסר התאמה בטקסט, הוא יחזור להתאים את הרישא הארוכה ביותר של התבנית המתאימה לסיפא הארוכה ביותר של הטקסט בנקודה בו נתקלנו בחוסר התאמה. 

על מנת להתאים בין הרישא הארוכה ביותר לסיפא הארוכה ביותר, נבנה טבלה ייעודית ונוכל להציץ בה בזמן קבוע.

===הסבר ויזואלי===
כאשר ניתקל בתבנית ABCABCD ובטקסט ABCABCABCABCABCABCD, נקודת אי ההתאמה הראשונה תהיה באינדקס 6. 
תמונה 1

ניתן לשים לב, שהאלגוריתם הנאיבי יתחיל לחפש מהאינדקס הבא (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
# <math>0\to i</math>
# <math>0\to j</math>
# כל עוד <math>i < n</math> בצע:
## כל עוד <math>T[i]=P[j]</math> וגם <math>j<m, i<n</math> בצע:
### <math>i+1\to i</math>
### <math>j+1\to j</math>
## אם j=m, דווח על התאמה באינדקס i-j
## אם i=n, סיים
## <math>i-min(0, A[j])\to i</math>
## <math>max(A[j], 0)\to j</math>

===בניית טבלה===
קלט - T באורך n
# צור מערך A בגודל n עם אפסים
# שים באיבר הראשון במערך -1
# לכל <math>i=1...n</math> בצע:
## <math>i-1\to i'</math>
## כל עוד <math>i' > 0</math> בצע
### אם <math>T[i-1]=T[A[i']]</math> צא מהלולאה
### אחרת <math>A[i']\to i'</math>
## <math>A[i']+1 \to A[i]</math>

==הוכחת נכונות==
===אלגוריתם===
נוכיח שכל התאמה אפשרית במחרוזת נמצאת על ידי האלגוריתם, בהנחה שהטבלה הנוצרת נכונה.

תהא התאמה באינדקס k, כלומר <math>p_0 = t_k, p_1 = t_{k+1}...p_{m-1}=t_{k+m-1}</math>.

על מנת שתימצא התאמה באינדקס k, צריך להתחיל את הלולאה הפנימית עם ערכי i ,j המקיימים <math>i-j=k</math>.
נשים לב שההפרש בין i ל-j משתנה בשתי אפשרויות - 
# <math>min(A[j], 0)\neq 0</math>
# <math>max(A[j], 0)\neq j</math>

קל לראות שבדיוק אחד מהמקרים האלה מתרחש בכל פעם. במקרה הראשון, ההפרש גדל בדיוק ב-1, ולכן לא נדלג על שום התאמה.

במקרה השני, ההפרש גדל בדיוק ב-<math>j-A[j]</math> וגם j לא שווה 0. נניח בשלילה שקיים אינדקס התאמה k המקיים <math>i-j<k<i-A[j]</math>, זאת אומרת ש<math>p_0 = t_k, p_1 = t_{k+1}...p_{m-1}=t_{k+m-1}</math>. בנוסף, מכיוון שהאלגוריתם הגיע לאינדקס j, מתקיים <math>p_0=t_{i-j}, p_1=t_{i-j+1}...,p_{j-1}=t_{i-1}</math>. לכן, מתקיים <math>p_0=p_{k-i+j},p_1=p_{k-i+j+1},...,p_{i-k-1}=p_{j-1}</math>, כלומר אורך הרישא הארוכה ביותר שמתאימה לסיפא הארוכה ביותר באינדקס j היא לכל הפחות <math>i-k</math>, ולפי הגדרת הטבלה <math>A[j] \ge i-k\to k\ge i-A[j]</math> בסתירה להנחת השלילה.

ההוכחה שהאלגוריתם לא מחזיר התאמות שאינן נכונות פשוט למדי - מכיוון שהאלגוריתם משתמש בטרנזיטיביות של שוויון תווים, אזי אם התאמנו כבר סיפא אין צורך לבדוק מחדש את הרישא, ועל מנת שהאלגוריתם יודיע על התאמה יש לבדוק את כל האיברים שאחרי הרישא איבר-איבר, וזהו האלגוריתם הנאיבי.

===טבלה===
נרצה להוכיח שבהינתן מחרוזת האלגוריתם מוציא את טבלת הסיפות הנכונה, ו-(-1) במקום הראשון.

הערה - קל לראות <math>\forall i A[i]<i</math>.
נניח שהאלגוריתם עובד עבור ערכים קטנים מ-i, נוכיח שהוא עובד עבור i - 

מקרה ראשון - אם מתקיים <math>T[i-1]=T[A[i']]</math> וגם <math>i'</math> הוא אינדקס שהסיפא הארוכה ביותר הנגמרת אצלו המתאימה לרישא הארוכה ביותר זהה לסיפא באינדקס <math>i-1</math>. נוכיח שאכן <math>A[i]=A[i']+1</math>. הערה - לפי ההגדרה, <math>i-1</math> מקיים את זה.
קל לראות שמתקיים <math>A[i]\ge A[i']+1</math>. נניח שמתקיים <math>A[i] > A[i']+1</math>, זאת אומרת <math>A[i] \ge A[i']+2 </math>. אבל אם נוריד את התו האחרון מהסיפא והתו האחרון מהרישא, אזי יש לנו התאמה באורך לכל הפחות <math>A[i]-1</math> באינדקס <math>i'</math>, בסתירה להנחה שהאלגוריתם צודק עבור ערכים קטנים מ-i ובפרט <math>i'</math>.

מקרה שני - <math>i'=0</math>. במקרה זה, מכיוון שלפי הגדרה <math>\forall i>0 A[i]\ge0</math> וגם <math>i'</math> הוא חסם עליון לאורך הסיפא המקסימלית, אזי מתקיים בוודאות <math>A[i]=0</math>. ואכן, <math>A[i']+1=A[0]+1=(-1)+1=0</math>.

מקרה שלישי - אם מתקיים <math>T[i-1]\neq T[A[i']]</math>. במקרה זה, התו החדש שנוסף אינו המשך של הרישא הקודמת, ולכן נחפש רישא קצרה יותר. מכיוון שהרישא הקודמת שווה לסיפא הקודמת, אזי הסיפא הארוכה ביותר המתאימה לרישא הארוכה ביותר ברישא הקודמת, היא גם סיפא של הסיפא הקודמת. מההנחה שהאלגוריתם פועל עבור קלטים קטנים, מתקיים שאורך הסיפא החדשה המתקבלת הוא המקסימלי, ולכן כל <math>A[i'] <k<i'</math> לא יכול להיות אורך הסיפא שאינה כוללת את התו האחרון. כלומר - גם אם האלגוריתם לא ימצא בשלב זה את אורך הסיפא הוא יתחיל באיטרציה הבאה עם <math>i<i'</math>, ולכן האלגוריתם תמיד יגיע בסופו של דבר למקרה הראשון או השני.

נוכיח שלא קיים <math>i=A[i']<k<i'</math> כך ש-k הוא האורך של הסיפא הארוכה ביותר הנגמרת ב-<math>i-1</math> ומתאימה לרישא. אם זה אכן היה מתקיים, אז לפי הגדרה <math>p_0=p_{i-1-k},p_1=p_{i-k},...p_{k-1}=p_{i-2}</math>, אבל מכיוון ש-<math>i</math> הוא אורך הסיפא הארוכה ביותר המתאימה ל-<math>i'</math>, וגם לפי הטרנזיטיביות הסיפא הנגמרת ב-<math>i-1</math> זהה לסיפא הנגמרת ב-<math>i'</math>, אז למעשה <math>p_{i-2}=p_{i'-1}=p_{k-1}, p_{i-3}=p_{i'-2}=p_{k-2},...</math>, וזאת אומרת שאורך הסיפא המקסימלית הנגמרת ב-<math>i'</math> היא לפחות k, בסתירה לכך שהנחנו שהאלגוריתם צודק עבור קלטים קטנים. זאת גם למעשה ההוכחה ש-<math>i'</math>הוא חסם עליון לאורך הסיפא, שהרי <math>A[i]=A[i']+1\le i'-1+1=i'</math> עבור <math>i'</math> כלשהו.

==הוכחת זמן ריצה==
זמן הריצה הכולל של האלגוריתם הוא <math>\Omicron(m+n)</math>, כאשר הזמן הדרוש לבניית טבלה של התבנית הוא <math>\Omicron(m)</math> וזמן החיפוש הדרוש הוא <math>\Omicron(n)</math>. נוכיח:
===אלגוריתם===
כאשר אנחנו מבצעים את החיפוש עצמו, בכל איטרציה של כל אחת מהלולאות יש לפחות קידום אחד של i, כלומר לא קיימת איטרציה בה i לא קודם. בנוסף, כל איטרציה של הלולאות מפעילות <math>\Omicron(1)</math> פעולות, ולכן בסך הכל יש לכל היותר <math>\Omicron(n)</math> פעולות.
===טבלה===
נסמן מונה C1 הסופר כמה פעמים עודפות האלגוריתם ביקר בתא בודד, ומונה C2 הסופר כמה פעמים האלגוריתם ביקר פעם בודדת. מתקיים <math>C2 \le n+1</math> מכיוון שיש n+1 תאים בטבלה. כפי שצוין קודם, מתקיים <math>A[i]\le A[i-1]+1</math>, וגם <math>A[i]=A[i-1]+1 </math> אם ורק אם קיימת איטרציה אחת בדיוק (כלומר C2 מקודם). בנוסף, מתקיים <math>A[i]<i</math> לפי הגדרת הרישא. לכן, ניתן לכתוב <math>A[i]\le i-1 </math>, כלומר במקרה הגרוע יש לולאה יורדת. נחבר את התנאים - על מנת שיתקיים <math>A[i]>a</math> צריך שיתקיים <math>C2>a</math> (לפי התנאי הראשון), ועל מנת שנוכל לקדם את C1, צריך שיתקיים <math>A[i]>0</math>. בסך הכל, על מנת לקדם את C1 יש לקדם את C2, ולכן מתקיים <math>C1\le C2\le n+1</math>, ומספר האיטרציות הכולל קטן מ-<math>2n+2</math>, או <math>\Omicron(n)</math>, כנדרש.

==דוגמה==

==הרחבות==

==קישורים חיצוניים==
* [http://whocouldthat.be/visualizing-string-matching/ KMP visualization]

==הערות שוליים==

KMP