קודים לתיקון שגיאות (שניתנים לבדיקה מקומית)

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

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

אלא שהעולם האמיתי הוא מקום מלוכלך – לקווי תקשורת יש "רעש", דיסקים יכולים להישרט, וכן הלאה. בפועל המשמעות של זה מבחינתנו היא שחלק מהמידע שאנחנו שולחים (מכאן ואילך "לשלוח" פירושו גם "לשים מידע על דיסק"; תחשבו על זה כאילו אנחנו שולחים כך מידע אל גרסה עתידית שלנו שתנסה לקרוא את הדיסק) ייהרס. מה זה "ייהרס" מבחינתנו אם כל מה שיש הוא אפסים ואחדים? שבמקום לקרוא 0 נקרא 1, ולהפך. למשל, נשלח את המילה 0010 ובצעד השני תתקבל המילה 0110. כאן הביט השני "התקלקל". זה יכול לגרום לשינוי עצום במשמעות של המידע שנשלח ולהיות בעל תוצאות הרסניות – בקיצור, אסור להתיר לדברים כאלו לקרות. הבעיה היא שבעולם האמיתי הם קורים כל הזמן. לא סביר לבנות ערוץ ממשי שבו תקלות כאלו לא יתרחשו כל הזמן. בקצרה – אנו נזקקים לפתרון לבעיה שאינו תלוי ערוץ; אנחנו צריכים לקבל את הקלקולים בתור חלק מהחיים ולמצוא דרך לטפל בהם בכל זאת.

גישה נאיבית לבעיה היא זו – במקום לשדר את המידע סיבית-סיבית, נשכפל כל סיבית מספר פעמים כלשהו – נאמר, שלוש – ונשלח את השכפולים ברצף. כך למשל במקום לשלוח את המידע 0010, נשלח 000000111000. מי שמקבל את המידע בצד השני יפרק את הקלט שקיבל לשלשות, ויחליט מה הסיבית שכל שלשה מייצגת על פי "הכרעת רוב". למשל, אם קיבלנו 111, אפשר להניח שהשלשה הזו מייצגת את הסיבית 1; ואם קיבלנו 100, אפשר להניח שהשלשה מייצגת את הסיבית 0. כמובן, ייתכן שנשלח את 000 ובדרך כל שלוש הסיביות יתפקששו, בצד השני יתקבל 111 והמפענח יחשוב שניסיתי לשדר לו 1; אבל הסיכוי לכך שזה יקרה הוא נמוך יחסית. כמובן שכדי לבצע חישוב מדוייק צריך לדעת מהי ההסתברות לכך שסיבית תתפקשש, ובהתאם אפשר לשלוח 5 עותקים של הספרה שרוצים להעביר, או 7, וכו' – איני רוצה להיכנס לניתוחים הללו כעת.

הנה עוד דוגמה לדבר מה שניתן לעשות – אם אני רוצה לשלוח רצף של סיביות, נניח 01010, אני אוסיף לסוף הרצף עוד סיבית אחת, שתתקבל מ"חיבור" כל הסיביות (עם הכלל לפיו \(1+1=0\)). הסיבית הזו נקראת "סיבית הזוגיות", שכן היא 0 אם יש מספר זוגי של 1 ברצף שאני שולח (למשל, ברצף שנתתי בדוגמה) והיא 1 אם יש מספר אי זוגי שלהן (למשל, ברצף 11100). בשביל מה זה טוב? אם בצד השני יקבלו 110001, יבינו שמשהו השתבש, כי סיבית הסימן היא 1 ועם זאת בין שאר חמש הסיביות (שאמורות לייצג את המידע האמיתי) יש רק שתיים שהן מגודל 1. זה טוב לזיהוי השגיאה, אבל לא ממש עוזר לתקן אותה במקרה הזה (כמובן שזיהוי שגיאה הוא חשוב מספיק לכשעצמו – אם שגיאות הן נדירות, אז כשמזוהה שגיאה אפשר לבקש מהשולח שישלח את פיסת המידע שוב).

באופן כללי אפשר לתאר קוד באמצעות שלושה פרמטרים – ראשית, מספר הסיביות שיש בכל מילת קוד; שנית, קבוצת המילים בקוד; שלישית, המרחק המינימלי שבין המילים בקוד. בואו נחזור לרגע לקוד השלשות שתיארתי קודם – זה קוד שהכיל בדיוק שתי מילות קוד, 111 ו-000; כל מילה אחרת הייתה "לא חוקית". אם כן, זה קוד שבו המילים הן מאורך 3 ויש בדיוק שתי מילים. הקוד הזה מאפשר לנו, אם כן, לשלוח שני "סוגי מידע שונים"; אני הגדרתי שרירותית ש-000 ייצג את 0 וש-111 ייצג את 1 אבל זה לא הכרחי; הייתי יכול גם לקבוע ש-000 מייצג את 1 ו-111 מייצג את 0, או שהם מייצגים בכלל את 012 ואת 4325, או כל זוג מוזר אחר. נותר להסביר מהו עניין ה"מרחק".

המילה 000 נראית לנו שונה מהמילה 111 הרבה יותר מאשר 110 שונה מ-111. ההבדל ברור - מספר המקומות השונים בין 000 ו-111 הוא 3, בעוד שמספר המקומות השונים בין 110 ו-111 הוא 1 בלבד. זה מוביל להגדרה של מרחק המינג בין שתי מחרוזות מאורך זהה – מספר המקומות השונים זה מזה במחרוזת, או אם תרצו: מספר השגיאות שצריכות להתרחש כדי שבמקום המחרוזת א' תתקבל המחרוזת ב'.

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

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

כעת אני רוצה לעזוב את הנושא הכללי הזה, שלא התחלתי אפילו לדגדג את קצה-קצהו, ולדבר על קוד מאוד ספציפי, עם תכונה מאוד ספציפית – קוד הדמר (Hadamard), עם תכונה מאוד ספציפית – הוא ניתן לבדיקה מקומית (Locally Testable). הכוונה ב"בדיקה מקומית" היא שבאמצעות דגימה של מספר קטן מאוד – סופי – של ביטים ממילת הקוד ניתן יהיה לזהות בהסתברות טובה האם היא חוקית או לא. ליתר דיוק – אם היא חוקית, תמיד נזהה זאת; אם היא לא חוקית, אז ככל שהיא רחוקה מלהיות מילת קוד חוקית ההסתברות שלנו לזהות זאת תגדל משמעותית (מובן מאליו שאם נפלה שגיאה בביט בודד במילת הקוד אין סיכוי לזהות זאת בהסתברות גדולה על ידי בדיקה של מספר סופי של ביטים – הרי חייבים לקלוע "בול" לביט שהתקלקל כדי שיהיה בכלל סיכוי להבין שמשהו השתבש).

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

שדה המשחק שלנו הוא מרחבים וקטוריים מעל \(\mathbb{F}_{2}\) – השדה הסופי בן שני איברים (0 ו-1, עם כללי החיבור והכפל הרגילים, כשב"רגילים" הכוונה לכך ש-\(1+1=0\)). "מרחב וקטורי מעל השדה הזה" הוא בסך הכל אוסף של סדרות מאורך נתון כלשהו של איברים מ-\(\mathbb{F}_{2}\); למשל, אם המרחב הוקטורי הוא ממימד 3, אז אברי המרחב הם \(\left(0,0,0\right),\left(0,1,0\right)\) וכן הלאה. בקיצור, טרם חידשנו משהו. כעת יש להכניס לתמונה את שאר התכונות של מרחב וקטורי: אם \(v_{1},v_{2}\) הם איברים של המרחב הוקטורי, כך גם \(v_{1}+v_{2}\) (כשחיבור הוא "רכיב-רכיב", כלומר \(\left(1,0,1\right)+\left(1,1,0\right)=\left(0,1,1\right)\) ). מי שמכיר אלגברה לינארית יודע שיש גם דרישה של כפל בסקלר, אבל במקרה הזה היא לא מעניינת במיוחד (למה?)

בואו נחשוב לרגע שוב על קודים באופן כללי. קוד פשוט יכול להיות קוד שלוקח מילה \(w\) וממיר אותה במילת הקוד \(\left(w,f\left(w\right)\right)\) כש-\(f\) היא פונקציה שמקבלת את \(w\) ופולטת ביט בודד. אם נרצה שהקוד יהיה לינארי (ואנחנו רוצים, שכן קודים לינאריים הם קלים מאוד לניתוח), אז חיבור של שתי מילות קוד צריך לתת בעצמו מילת קוד, כלומר צריך שיתקיים ש-\(\left(w,f\left(w\right)\right)+\left(v,f\left(v\right)\right)=\left(w+v,f\left(w\right)+f\left(v\right)\right)\) יהיה בעצמו מילת קוד חוקית, כלומר שיתקיים \(f\left(w+v\right)=f\left(w\right)+f\left(v\right)\). התכונה הזו נקראת "לינאריות של \(f\)". נסכם – אם אנחנו רוצים לבנות את הקוד שלנו על ידי כך שנוסיף ל-\(w\) עוד ביטים לאחריו, שמחושבים באופן מסויים מ-\(w\), אז כל ביט שכזה חייב להיות מחושב באמצעות פונקציה לינארית.

קוד הדמר לוקח את הגישה הזו עד הסוף – הוא אומר "בואו נוסיף אחרי \(w\) את כל הביטים האפשריים שיכולים להתקבל מחישוב של פונקציה לינארית כלשהי". זה מייד מעלה את השאלה איך בכלל אפשר למצוא ולייצג את כל הפונקציות הלינאריות הללו. זה שוב עניין לסטודנטים לאלגברה לינארית; אבל אפשר להראות שאם \(v=\left(v_{1},v_{2},\dots,v_{n}\right)\) הוא וקטור, אז כל פונקציה לינארית היא מהצורה \(f\left(v\right)=\sum_{i=1}^{n}a_{i}v_{i}\), כשכל \(a_{i}\) הוא או 0 או 1. זו הפשטה נוראית של מה שקורה במקרה הכללי יותר, של מרחב מעל שדה גדול יותר – אבל אני רוצה לא לגלוש לדיון הכללי כרגע.

כעת, הנה אבחנה מעניינת נוספת – \(a=\left(a_{1},a_{2},\dots,a_{n}\right)\) הוא בעצמו איבר המרחב הוקטורי ממימד \(n\) מעל \(\mathbb{F}_{2}\), כלומר אפשר לייצג כל פונקציה לינארית מהמרחב הוקטורי הזה באמצעות איבר מהמרחב הוקטורי הזה! (גם זו תוצאה כללית בהרבה מכפי שאני מציג אותה כאן). כדי לפשט את הסימונים, מגדירים \(a\cdot v=\sum_{i=1}^{n}a_{i}v_{i}\) – לדבר הזה קוראים "מכפלה סקלרית". מכפלה סקלרית דומה למדי לכפל "רגיל", למשל \(a\left(v_{1}+v_{2}\right)=av_{1}+av_{2}\) (התכונה הזו תהיה חשובה בקרוב). נסו להוכיח את התכונה הזו – זה קל למדי, ומסתמך באופן חזק על התכונה הדומה עבור כפל "רגיל".

כעת, אם יש לנו מילה \(w\in\mathbb{F}_{2}^{n}\) (כלומר, רצף של \(n\) ספרות שהן אפס או אחד), אפשר להתעלל קצת בסימונים ולהגיד שנשתמש ב-\(w\) גם כדי לתאר את המילה המקודדת, ולסמן בתור \(w_{a}\) את הביט המתאים במילה המקודדת שמתקבל על ידי חישוב \(a\cdot w\) (ובפירוט יתר: הביט שמתאים להפעלה של הפונקציה הלינארית שמיוצגת על ידי \(a\) על המילה \(w\)). עכשיו אפשר לשים לב לכך שההפרדה המקורית שלנו את מילת הקוד ל"קודם כל המילה המקורית, ואז התוצאה של הפעלת כמה פונקציות לינאריות עליה" היא מלאכותית למדי; גם הביטים של מילת הקוד עצמה יכולים להתקבל מהפעלה של פונקציות לינאריות. למשל, הביט הראשון של \(w\) מתקבל על ידי כפל עם המילה \(e_{1}=\left(1,0,0,\dots,0\right)\), הביט השני על ידי כפל עם \(e_{2}=\left(0,1,0,0,\dots,0\right)\) וכן הלאה. מעתה אמרו – הקידוד של \(w\) הוא פשוט אוסף ההפעלות של כל הפונקציות הלינאריות האפשריות על \(w\). לקידוד הזה יש מחיר – אנחנו מקודדים \(n\) ביטים באמצעות \(2^{n}\) ביטים (כי יש \(2^{n}\) פונקציות לינאריות אפשריות – למה?) – ניפוח אקספוננציאלי של המידע המקורי.

למה זה מועיל? כי במובן מסויים, זה לוקח את המילה \(w\) ו"מורח" את הביטים שלה טוב-טוב. כל קומבינציה אפשרית של ביטים של \(w\) באה לידי ביטוי איפה שהוא במילת הקוד. לכן לדגום באקראי ביט מתוך מילת הקוד בעצם שקול לדגימה אקראית של מספר כלשהו של ביטים מתוך \(w\); יותר במדוייק, אם אני בוחר ביט במילת קוד באקראי, אני בעצם מגריל אינדקס \(a\) באקראי וקורא את \(w_{a}\); והגרלה של \(a\) באופן אקראי (בהתפלגות אחידה) פירושו שכל קוארדינטה של \(a\) מוגרלת בהסתברות \(\frac{1}{2}\) ל-0 ו-\(\frac{1}{2}\) ל-1. כלומר, בדגימה אקראית של ביט ממילת הקוד אנחנו מגרילים כל ביט מ-\(w\) בהסתברות חצי. האבחנה הזו היא המפתח לכך שקוד הדמר ניתן לבדיקה מקומית.

בואו ננסח במדוייק כעת מה מטרתו של בודק מקומי עבור הקוד. הדרישה היא שבהינתן מילת קוד חוקית, הבודק יגיד "כן", ובהינתן מילה שאינה מילת קוד, הבודק יגיד "לא" בהסתברות סבירה, שתלויה במרחק המילה ממילת קוד חוקית. נאמר שהמילה \(w\) רחוקה עד כדי \(\delta\) ממילת קוד חוקית, כש-\(0<\delta\le1\), אם צריך לשנות \(\delta\) מהכניסות של \(w\) כדי לקבל מילת קוד חוקית (למשל, אם \(\delta=\frac{1}{4}\) זה אומר שצריך לשנות רבע מהכניסות של \(w\)). לאלו מכם שרוצים פורמליזם, ההגדרה המדוייקת היא \(\delta=\min_{w^{\prime}\in C}\frac{d\left(w,w^{\prime}\right)}{\left|w\right|}\). זהו בעצם המרחק במשמעותו המקורית כשהוא מנורמל באופן שמתחשב באורך המילים – "המרחק היחסי" שבין המילים. מכניסים אותו לתמונה כי הוא מפשט את הניסוחים הטכניים.

אם כן, מה שאראה הוא בודק מקומי שעל מילה שאיננה מילת קוד חוקית, מזהה זאת בהסתברות של \(\frac{\delta}{2}\) לפחות, אך לא יותר מ-\(\frac{2}{9}\). זאת אומרת שאם \(\delta<\frac{4}{9}\), אז ההסתברות שתתגלה שגיאה היא \(\frac{\delta}{2}\) (ולכן עבור \(\delta\) קטן היא לא גדולה; אבל היא לינארית ב-\(\delta\), וזה טוב), ועבור ערכים גדולים יותר יש הסתברות קבועה של \(\frac{2}{9}\) לגלות שגיאה ולכן על ידי הפעלות נשנות של הבודק אפשר "לנפח" את ההסתברות לגילוי שגיאה עד שתתקרב כרצוננו ל-1. ה"מחיר" יהיה קריאה של בדיוק שלושה ביטים מהקלט; כמובן שה"ניפוח" מצריך קריאה של עוד ביטים, אבל עדיין מספר קבוע שאינו תלוי ב-\(n\). כאן אנחנו מתחילים להרגיש את ה-PCP שהזכרתי בפוסט הקודם; גם שם הרעיון הוא קריאה של מספר קבוע של ביטים.

ואיך הבודק עובד? באופן כמעט מגוחך. הוא בוחר באקראי וקטורים \(a,b\) וקורא את \(w_{a},w_{b}\). כזכור, הערכים הללו הם \(a\cdot w,b\cdot w\), ועל פי כללי הכפל נובע ש-\(aw+bw=\left(a+b\right)w\), מה שמוביל אותנו בקלות לביט הנוסף שיש לדגום – הוא קורא את \(w_{a+b}\) ובודק האם \(w_{a+b}=w_{a}+w_{b}\). אם כן – הוא מקבל (או מתחיל סיבוב בדיקה חדש); אם לא, הוא דוחה. כל כך פשוט.

טוב, אבל למה זה עובד? התשובה הקצרה היא "זה טכני". התשובה הארוכה היא "זה טכני, אבל אראה את זה בכל זאת ובתקווה לא אאבד יותר מדי אנשים, כי זו אחת מההוכחות האהובות עלי". אני מקווה שברור מדוע אם \(w\) היא מילת קוד חוקית הבדיקה עובדת – נימקתי זאת לעיל. השאלה היא מה קורה אם \(w\) אינה מילת קוד חוקית.

הבה ונסמן את ההסתברות שהבודק ידחה את המילה \(w\) ב-\(\varepsilon\). אם \(\varepsilon\ge\frac{2}{9}\), "ניצחנו" – הראנו שהבודק עובד טוב על \(w\). לכן ההנחה היא ש-\(\varepsilon<\frac{2}{9}\). מה שרוצים להראות הוא שמכך נובע שהמרחק היחסי של \(w\) ממילת הקוד החוקית הקרובה ביותר (שהוא, כזכור, \(\delta\)) לא עולה על \(2\varepsilon\) (כלומר – \(\delta\le2\varepsilon\), או \(\varepsilon\ge\frac{\delta}{2}\)). בקיצור – רוצים למצוא איכשהו מילת קוד חוקית שקרובה יחסית ל-\(w\). אה, אבל זה בדיוק מה שקודים לתיקון שגיאות עושים – בהינתן מילה "מקולקלת", מראים כיצד לתקן אותה.

כאן מגיע הרעיון המרכזי בהוכחה, שהוא מקסים. כיצד תיבנה אותה מילת קוד, שאסמן בתור \(v\)? באופן הבא. הכניסה \(v_{a}\) במילה תוגדר בתור "הצבעת הרוב" של \(w\). למה הכוונה? אנחנו יודעים שאם \(w\) הייתה מילה חוקית, אז היה מתקיים \(w_{a}+w_{b}=w_{a+b}\) לכל \(b\) אפשרי, או בניסוח אחר – \(w_{a}=w_{a+b}-w_{b}\). לרוע המזל \(w\) איננה מילת קוד חוקית ולכן זה לא מתקיים עבורה תמיד – כלומר, ייתכן ש-\(w_{a+b}-w_{b}\) לא תמיד מחזיר את אותו ערך, כאשר משנים את הערכים ש-\(b\) מקבל. עם זאת, הערך שמופיע מספר רב יותר של פעמים הוא כנראה הערך ה"נכון" עבור \(w\). אם כן, התיקון של \(w\) יהיה \(v\) כך ש-\(v_{a}=\mbox{majority}_{b}\left\{ w_{a+b}-w_{b}\right\} \), כאשר \(\mbox{majority}\) היא פונקציה שמחזירה את האיבר השכיח יותר ב"קבוצה" שהיא מקבלת ("קבוצה" במרכאות כי בדרך כלל בקבוצות כל איבר נספר פעם אחת, וכאן למספר המופעים יש חשיבות מכרעת).

צריך כעת להראות שני דברים: ראשית ש-\(v\) היא בכלל מילת קוד חוקית; שנית, שהיא קרובה ל-\(w\) עד כדי \(2\varepsilon\) – פורמלית נסמן זאת \(d\left(w,v\right)<\varepsilon\). נתחיל דווקא מהתוצאה השניה. האינטואיציה כאן אינה קשה – אם \(w_{a}\ne v_{a}\), אז אם הבודק המקומי יבחר את \(a\) כאחת משתי הקוארדינטות שהוא בודק, יהיה לו סיכוי של לפחות חצי לעלות על שגיאה, שהרי \(w_{a}\) אינו מתאים לכלל הצבעת הרוב, ולכן עבור רוב הערכים של \(b\) שהבודק יגריל, יתקיים ש-\(w_{a}\ne w_{a+b}-w_{b}\) (כלומר, \(w_{a}+w_{b}\ne w_{a+b}\)). כעת, שימו לב שהסיכוי של הבודק ליפול על \(a\) שמקיים \(w_{a}\ne v_{a}\) הוא בדיוק המרחק היחסי שלהם; ואם הבודק כבר נפל על \(a\) שכזה, ההסתברות שלו ליפול על \(b\) שיכשיל את \(w\) היא לפחות חצי; ולכן ההסתברות של הבודק לדחות את \(w\) היא לפחות \(\frac{1}{2}d\left(w,v\right)\); אבל סימנו ב-\(\varepsilon\) את ההסתברות של הבודק לדחות את \(w\), כלומר \(\frac{1}{2}d\left(w,v\right)\le\varepsilon\), כלומר \(\delta\le d\left(w,v\right)\le2\varepsilon\).

אם כן, נותר אתגר בודד – להראות ש-\(v\) היא מילת קוד חוקית. כפי שניתן לנחש, זה המקום שבו \(\frac{2}{9}\) המסתורי יבוא לידי ביטוי. ברשותכם, ארשה לעצמי להמשיך להיות טכני ולהציג את ההוכחה במלואה. ראשית כל צריך להבהיר לעצמנו כיצד ניתן להוכיח שמילה כלשהי \(v\) היא מילת קוד חוקית. דרך אחת היא להראות איך מילה בת \(n\) ביטים ממופה ל-\(v\) על ידי הקוד; אבל זה די מסורבל. הרבה יותר נחמד להשתמש בקריטריון הבדיקה שכבר הצגנו, ומסתבר שזה אכן מספיק: אם \(v_{a}+v_{b}=v_{a+b}\) לכל \(a,b\), אז \(v\) היא מילת קוד חוקית. האבחנה הזו טבעית למדי – הרי כפי שראינו, גם הביטים של "המילה המקורית" שממנה התקבל הקוד מקודדים בתוך \(v\) (הביט ה-\(i\) מקודד כ-\(v_{e_{i}}\)) ומתכונת החיבוריות הזו נובע שלכל \(a=\sum\alpha_{i}e_{i}\) מתקיים \(v_{a}=v_{\sum\alpha_{i}e_{i}}=\sum\alpha_{i}v_{e_{i}}\), כנדרש.

אם כן, מספיק לקחת \(a,b\) שרירותיים ולהראות שעבורם מתקיים \(v_{a}+v_{b}=v_{a+b}\). באופן די מפתיע, ההוכחה היא כמעט מיידית אם רק נראה עוד משהו אחד – ש"הכרעת הרוב" שמגדירה את \(v_{a}\) היא לא סתם הכרעת רוב, אלא הכרעת רוב מוחץ: שלפחות \(\frac{2}{3}\) מה"הצבעות" \(w_{a+b}-w_{a}\) נתנו את הערך של \(v_{a}\) ולא את הערך השני האפשרי. קודם כל אסביר מדוע זה מסיים את העניין ואז אוכיח זאת.

הטריק הוא להשתמש בשיטה ההסתברותית – להוכיח שמשהו קיים על ידי כך שמראים שההסתברות למציאתו במרחב החיפוש שלנו גדולה מאפס. מה שאנו מחפשים הוא מילה \(c\) שתקיים את שלוש התכונות הבאות בו זמנית:

  1. \(v_{a}=w_{c}-w_{a+c}\)
  2. \(v_{b}=w_{b+c}-w_{c}\)
  3. \(v_{a+b}=w_{b+c}-w_{a+c}\)

אם מצאנו \(c\) כזה, סיימנו. למה? שכן אז \(v_{a}+v_{b} = \left(w_{c}-w_{a+c}\right)+\left(w_{b+c}-w_{c}\right)=w_{b+c}-w_{a+c}=v_{a+b}\) כנדרש. אז למה קיים \(c\) כזה? ובכן, בואו נביט לרגע על המשוואה הראשונה, \(v_{a}=w_{c}-w_{a+c}\). מכיוון שפעולת החיבור מתבצעת מעל \(\mathbb{F}_{2}\) (כלומר, \(1+1=0\), או במילים אחרות \(1=-1\)), זו בדיוק אותה משוואה כמו \(v_{a}=w_{a+c}-w_{c}\)

מה ההסתברות, אם מגרילים \(c\), שהיא לא מתקיימת? בדיוק ההסתברות ש-\(c\) יהיה אחד מקולות המיעוט בהצבעה שקבעה את \(v_{a}\). מכיוון שאמרנו שהרוב בהצבעה היה לפחות \(\frac{2}{3}\), נובע מכך שההסתברות ש-\(c\) יהיה "מקלקל" שכזה היא לכל היותר \(\frac{1}{3}\). באופן דומה גם עבור שני התנאים האחרים אפשר להראות שההסתברות לכך ש-\(c\) לא יקיים אותם היא לכל היותר \(\frac{1}{3}\). כעת, חסם טריוויאלי בהסתברות (שמכונה Union bound) אומר כי אם יש לנו קבוצת מאורעות, אז ההסתברות שלפחות אחד מהם יתרחש היא לכל היותר סכום ההסתברויות של כולם. כאן יש לנו שלושה מאורעות שההסתברות של כל אחד מהם קטנה מ-\(\frac{1}{3}\), ולכן ההסתברות שלפחות אחד מהם יקרה קטנה מ-\(1\) – כלומר, קיים \(c\) שלא מקיים אף אחד מהמאורעות. מכיוון שהמאורעות היו "1 מתקלקל", "2 מתקלקל" ו-"3 מתקלקל", קיבלנו שיש \(c\) שעבורו אף אחד משלושת התנאים אינו מתקלקל – כלומר, סיימנו.

נותר רק להראות שאכן מתקיימת "הכרעת רוב מוחץ", כלומר שעבור לפחות \(\frac{2}{3}\) מה-\(b\)-ים האפשריים מתקיים \(v_{a}=w_{a+b}-w_{b}\).

הבה ונסמן את גודל הרוב ב-\(p\), כלומר \(\mbox{Pr}\left[v_{a}=w_{a+b}-w_{b}\right]=p\) (\(\mbox{Pr}\) הוא סימון סטנדרטי להסתברות של המאורע שבסוגריים; ההסתברות נלקחת על פני הבחירות האקראיות של \(b\)). כעת נשדרג את המשחק – נניח שאנחנו בוחרים באקראי שני "מצביעים", \(b,c\) ושואלים אותם לדעתם; מה ההסתברות שהם יגידו את אותו הדבר? כלומר, מהו \(\mbox{Pr}\left[w_{a+b}-w_{b}=w_{a+c}-w_{c}\right]\)? ובכן, אחד משניים: או ששניהם הצביעו לערך שהרוב בחרו, בהסתברות \(p\) כל אחד ולכן \(p^{2}\) לשניהם יחד; או ששניהם הצביעו עבור הערך השני, בהסתברות \(\left(1-p\right)\) כל אחד ולכן \(\left(1-p\right)^{2}\) לשניהם יחד. סה"כ ההסתברות שהם מסכימים היא \(p^{2}+\left(1-p\right)^{2}\). אם נצליח למצוא חסם תחתון כלשהו על ההסתברות הזו, נוכל לחלץ מהמשוואה (ומכך ש-\(p\ge\frac{1}{2}\)) חסם תחתון על \(p\). כאן גם מתחילה ה"הנדסה לאחור" שלבסוף תניב את ה-\(\frac{2}{9}\) הידוע לשמצה – אנחנו מחפשים חסם תחתון על \(p^{2}+\left(1-p\right)^{2}\) שיגרור \(p>\frac{2}{3}\); קצת משחק בפרמטרים ופתרון משוואה ריבועית יראה כי החסם התחתון הזה הוא \(\frac{5}{9}\) (נסו זאת בבית!).

נסכם: עלינו להראות כי \(\mbox{Pr}\left[w_{a+b}-w_{b}=w_{a+c}-w_{c}\right]>\frac{5}{9}\). הטכניקה דומה לטכניקה שכבר השתמשתי בה. ראשית נשים לב לכך ש-\(\mbox{Pr}\left[w_{a+b}-w_{b}=w_{a+c}-w_{c}\right] = \mbox{Pr}\left[w_{a+b}+w_{c}=w_{a+c}+w_{b}\right]\)

כעת שני האגפים הם "מאוזנים" במידת מה – בכולם מופיעים גם \(a\), גם \(b\) וגם \(c\). יותר מכך – אם \(w\) הייתה מילת קוד חוקית, שני האגפים היו שווים ל-\(w_{a+b+c}\). לכן ההסתברות של המאורע שלמטה היא בעצם ההסתברות שלא חל "קלקול" בחישוב של \(w_{a+b+c}\) לא באמצעות \(w_{a+b}+w_{c}\), וגם לא באמצעות \(w_{a+c}+w_{b}\). לכן שוב נשאל את עצמנו – מה ההסתברות שכן חל קלקול באחד משני המקרים הללו?

אם כן, מה ההסתברות ש-\(w_{a+b}+w_{c}\ne w_{a+b+c}\)? זה טיפה מבלבל מכיוון שהן \(b\) והן \(c\) נבחרו באקראי ואילו \(a\) נקבע מראש, אבל אפשר לפשט קצת את העניינים: אם \(b\) נבחר באקראי ובהתפלגות אחידה מבין כל הערכים האפשריים, ו-\(a\) קבוע, אז גם \(a+b\) מוגרל בהתפלגות אחידה מבין כל הערכים האפשריים (למה?). לכן אפשר לסמן לצורך פשטות \(a^{\prime}=a+b\) ולשאול את עצמנו את השאלה הפשוטה יותר: מהי ההסתברות ש-\(w_{a^{\prime}}+w_{c}\ne w_{a^{\prime}+c}\) כאשר הן \(a^{\prime}\) והן \(c\) נבחרים באקראי? ואת התשובה לשאלה הזו אנחנו יודעים: ההסתברות הזו חסומה על ידי ההסתברות שהבודק ידחה את \(w\) – שהרי זה בדיוק מה שהבודק עושה – מגריל שני אינדקסים ומבצע את בדיקת השוויון שלעיל.

כזכור, בתחילת הדיון הזה הנחנו שהסתברות הדחייה של הבודק היא נמוכה יחסית – נמוכה מ-\(\frac{2}{9}\) (כי במקרה שהיא גבוהה יותר אין מה להראות). מכאן שההסתברות ש-\(w_{a+b}+w_{c}\ne w_{a+b+c}\) קטנה מ-\(\frac{2}{9}\), וגם ההסתברות ש-\(w_{a+c}+w_{b}\ne w_{a+b+c}\) קטנה מ-\(\frac{2}{9}\), ולכן על פי ה-Union bound נקבל שההסתברות שאף אחד משני מאורעות אלו אינו מתרחש, ולכן \(w_{a+b}+w_{c}=w_{a+c}+w_{b}\), היא לפחות \(\frac{5}{9}\), כמו שרצינו. זה מסיים, סוף כל סוף, את ההוכחה כולה.

תרגיל בית למי שרוצה לוודא שהוא אכן הבין את כל החישובים הקטנים והקטנוניים האחרונים, ושה-\(\frac{2}{9}\) שהופיע בהתחלה לא נפל משמיים אלא אפשר להגיע אליו בדרך טבעית: נניח שהיינו רוצים להראות ש-\(p>\frac{3}{4}\) ולא סתם \(p>\frac{2}{3}\); מהו החסם החדש על \(\varepsilon\) שהיה עלינו לדרוש?

משפט ה-PCP או: איך למדתי להפסיק לדאוג ולאהוב הוכחות הניתנות לבדיקה הסתברותית

אחד מהנושאים הלוהטים ביותר במדעי המחשב המודרניים הוא משפט ה-PCP, הרחבותיו והשלכותיו. אלא שכל הקשור ל-PCP עשוי להישמע מוזר מאוד כששומעים לראשונה מה ראשי התיבות הללו מסמלים: Probabilistically Checkable Proofs, הוכחות הניתנות לבדיקה הסתברותית. מה זה בכלל, ואיך אפשר לזהם מושג טהור כמו "הוכחה" שהיא תמיד ודאית ב-100 אחוז (לפחות כשמדובר על הוכחה "מתמטית") עם המושג הבזוי של "הסתברות"?

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

במדעי המחשב נוקטים גישה אחרת, שבמובן מסויים היא "טבעית" יותר. הבה ונחשוב לרגע על ספר מתמטיקה. ההוכחות שנמצאות בו למשפטים אינן, על פי רוב, הוכחות במובן המתמטי הפורמלי; כמעט כל הוכחה בספרים נכתבת בשפה טבעית, מסבירה דברים ב"נפנוף ידיים", משאירה חלק מההוכחה כתרגיל לקורא, מבצעת מעברים מורכבים בלי שום נימוק של סדרת הצעדים שבאמצע (שלעתים נדרש קורס שלם כדי להכיר אותם) וכן הלאה. עם זאת, על פי רוב ההוכחות הלו נחשבות ללגיטימיות ואפילו להכרחיות, שכן כל נסיון לכתוב הוכחה פורמלית יגרום להוכחה להיות ארוכה בהרבה וקריאה הרבה פחות על ידי בני אדם.

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

במקום לדבר על הוכחה של טענה בשפה מסדר ראשון או כל דבר דומה, מפשיטים; טענה, בשורה התחתונה, היא רצף של אותיות מעל אלפבית כלשהו, שהוא או נכון או לא נכון תחת פירוש מסויים שאנחנו נותנים לעולם. יותר במובהק, אם אנחנו קובעים את האלפבית שלנו מראש ואת הפרשנות שלנו לעולם מראש, אנחנו נשארים עם שלושה סוגים שונים של רצפי אותיות – כאלו שאין להם שום משמעות (למשל "עגכדהא"), כאלו שיש להם משמעות והם נכונים ("יש אינסוף מספרים ראשונים") וכאלו שיש להם משמעות והם אינם נכונים ("יש מספר סופי של ראשוניים"). על כל טענה כזו אפשר לחשוב בתור "מילה" – רצף אותיות (במקרה שלנו, כולל האות "רווח"), ועל אוסף הטענות הנכונות אפשר לחשוב בתור שפה (שפה היא פשוט אוסף של מילים). כלומר, צמצמנו את הבעיה שלנו לבעיה של זיהוי מתי מילה שייכת לשפה כלשהי. זה גם בדיוק המושג שהופיע בפוסטים הקודמים שלי, שעסקו באוטומטים; מה שחשוב להבין הוא שדרך ההתבוננות הזו לא גורעת מהכלליות של הדיון על הוכחה אלא רק מרחיבה אותו.

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

עכשיו, בהינתן גרף המילטוני, איך אפשר להשתכנע בכך שהוא אכן המילטוני? הבה ונקרא למי שמנסה להשתכנע "המוודא" (כי הוא מוודא שהטענה נכונה). המוודא יכול לנסות באופן שרירותי את כל המסלולים בגרף עד שימצא אחד המילטוני (או שלא ימצא, ואז ידע בודאות שהגרף אינו המילטוני). עם זאת, אפשר לעשות לו את החיים קלים יותר ולספק לו מראש תיאור של מסלול המילטוני בגרף – ואז כל מה שיישאר לו לעשות הוא לוודא שאכן התיאור הזה חוקי ומתאים לגרף. אם מישהו רמאי ייתן לו תיאור שקרי, המוודא יעלה על זה בקלות. זה מביא אותנו סוף סוף אל ההגדרה הפורמלית של "מערכת הוכחה עבור השפה \(L\)": היא מורכבת ממוודא \(V\), שאפשר לחשוב עליו בתור אלגוריתם מתוחכם (אבל עדיין אלגוריתם – כזה שכל צעד שלו מוגדר בצורה מדוייקת) שמקבל שני "קלטים" – טענה \(w\), והוכחה \(\pi\). הן הטענה והן ההוכחה הן פשוט מילים; המוודא רץ על שני הקלטים הללו ופולט לבסוף "מקבל" או "דוחה". פורמלית ניתן לכתוב זאת \(V\left(w,\pi\right)=\mbox{acc}\) ו-\(V\left(w,\pi\right)=\mbox{rej}\). הציפיות שלנו ממערכת הוכחה "טובה" (כלומר, ממוודא "טוב") הן שיתקיימו שלוש הדרישות הבאות:

  1. שלמות: לכל \(w\in L\) קיימת הוכחה \(\pi\) כך ש-\(V\left(w,\pi\right)=\mbox{acc}\).
  2. נאותות: לכל \(w\notin L\) ולכל הוכחה \(\pi\) מתקיים \(V\left(w,\pi\right)=\mbox{rej}\).
  3. יעילות: צריכת המשאבים של \(V\) על הקלטים \(w,\pi\) היא יעילה ביחס לגודל הייצוג של \(w\).

שתי הדרישות הראשונות הן מתמטיות "קלאסיות", בזמן שהשלישית היא מדעי-המחשבניקית "מודרנית". זו גם הדרישה הכי מעורפלת, ולכן אתמקד מעכשיו בפורמליזציה הסטנדרטית שלה- אנו דורשים כי זמן הריצה של \(V\) יהיה פולינומי בגודל \(w\). פולינומי, שכן זה מה שנחשב לזמן ריצה סביר בדרך כלל. כעת נשאלת השאלה מהן השפות שמקיימות את שלוש התכונות הללו. התשובה היא שאלו הן בדיוק השפות שבמחלקה המפורסמת \(\mbox{NP}\) שהזכרתי כאן לא אחת. אין כאן שום תובנה מחוכמת – ה"הוכחה" שמקבל \(V\) היא אותו דבר במהותו כמו ה"רמז" שבדרך כלל נהוג לומר שהמכונה לשפת ה-\(\mbox{NP}\) מקבלת (אם חושבים על \(\mbox{NP}\) כעל אוסף השפות שיש מכונה אי דטרמיניסטית שמכריעה אותן השקילות מעט פחות ברורה – ה"הוכחה" במקרה זה תהיה תיאור של הבחירות האי דטרמיניסטיות שהמכונה מבצעת עד לקבלת המילה השייכת לשפה).אם כן, במובן מסויים \(\mbox{NP}\) מסמלת עבורנו את מחלקת כל ה"תורות" שקיימת עבורן מערכת הוכחה יעילה. לאן ממשיכים מכאן?

כיוון אחד שניתן להמשיך אליו הוא הרחבה של מושג ה"הוכחה". במקום סתם הוכחה כתובה \(\pi\), אפשר לדבר על "מוכיח" \(P\) שמנהל שיחה מנומסת עם \(V\) ומנסה לשכנע אותו שהוא צודק. למערכת הוכחה כזו קוראים "מערכת הוכחה אינטראקטיבית". לרוע המזל, ניתן להוכיח כי השינוי הזה לא מוסיף כוח למערכת – גם מערכת הוכחה אינטראקטיבית מסוגלת להוכיח רק שפות ב-\(\mbox{NP}\). אם כן, צריך להקל איכשהו על אחת משלוש הדרישות שלנו. על יעילות לא רוצים לוותר בשום פנים ואופן, ולכן נותרו השלמות והנאותות. אם נלך לפי הגישה לפיה עדיפים עשרה פושעים מחוץ לכלא מאשר איש חף מפשע אחד בתוכו, הרי שעלינו לפגוע בנאותות, לא בשלמות; פגיעה בשלמות פירושה שייתכן שטענות נכונות לא יהיו ניתנות להוכחה; פגיעה בנאותות פירושה שיהיה אפשרי להשתכנע בטעות שטענה שגויה היא נכונה. החוכמה היא להגביל את ה"אפשרי" הזה ככל הניתן.

לא מזמן דיברתי על אלגוריתמים הסתברותיים. הדגשתי שם שהדבר החשוב באלגוריתם הסתברותי הוא שההסתברות תילקח על פני כל הריצות האפשריות של האלגוריתם על קלט מסויים, ולא על פני כל הקלטים. ההבדל מהותי: במקום שיהיו קלטים שעבורם האלגוריתם תמיד נכשל אבל "ההסתברות שהם יתקבלו" היא נמוכה (במרכאות, כי זה מושג מאוד בעייתי), האלגוריתם מבטיח הסתברות הצלחה כלשהי לכל הקלטים. כך גם במערכת הוכחה – אנחנו דורשים שלכל מילה \(w\notin L\) ולכל "הוכחה" \(\pi\) עבור \(w\) ("הוכחה" כזו היא בעצם נסיון להטעות את המוודא), ההסתברות שיתקיים \(V\left(w,\pi\right)=\mbox{rej}\) היא לפחות \(\frac{2}{3}\) או קבוע דומה (בפוסט על אלגוריתמים הסתברותיים הסברתי מדוע בחירת הקבוע אינה כה מהותית). כמובן שזה אומר, בפרט, שהמוודא פועל באופן הסתברותי.

אם מקבלים את ההקלה הזו של תנאי 2, פתאום מערכות ההוכחה שלנו מקבלות כוח רב הרבה יותר. אם הולכים בכיוון של מערכות הוכחה אינטראקטיביות, הרי שמתברר כי מערכת הוכחה אינטראקטיבית עם ההקלה ההסתברותית שהצגנו הופכת להיות חזקה כמו \(\mbox{PSPACE}\): כל בעיה שניתנת לפתרון בזכרון פולינומי, ניתנת להוכחה במערכת כזו. השלב הבא, והמשעשע עוד יותר, הוא הרחבת המערכת האינטראקטיבית כך שיהיו שני מוכיחים במקום אחד, כך שהמוודא מדבר עם כל אחד מהמוכיחים בנפרד, מבלי שהם יוכלו לתקשר זה עם זה. הדבר מזכיר קצת שני פושעים שנחקרים בנפרד והשקרים שהם מספרים אינם מתואמים, כך שקל לעלות עליהם. במדעי המחשב התחושה האינטואיטיבית הזו תורגמה לתוצאה מדוייקת – מערכות הוכחה שכאלו תופסות את כל \(\mbox{NEXP}\) – אוסף הבעיות שניתנות לפתרון אי דטרמיניסטי בזמן אקספוננציאלי. מחלקה זו גדולה לפחות כמו \(\mbox{PSPACE}\) (ככל שידוע לי, השאלה האם הן שוות או לא עודנה פתוחה).

עם זאת, הכיוון שאני רוצה לדבר עליו כעת אינו הכיוון האינטראקטיבי – נמשיך לדבר על הסיטואציה שבה המוודא מקבל הוכחה כתובה ותו לא. מכיוון שההסתברות מוסיפה לו כוח רב, מעניין לשאול איך אפשר "להצטמצם" כעת באופן אחר ועדיין לתפוס את מה שתפסה ההגדרה ה"קלאסית" להוכחה, כלומר את \(\mbox{NP}\). זה בדיוק המקום שבו נכנס משפט ה-\(\mbox{PCP}\) לתמונה.

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

זה גם בדיוק הרעיון שמנחה את ה-PCP: להגביל את כמות המידע שניתן לקרוא מתוך ההוכחה עצמה. זה גם מעלה את השאלה, שעד כה די הסתרתי, מה צריך להיות אורכה של ההוכחה. עד כה לא דרשתי שום דרישה על אורך ההוכחה, אבל כן דרשתי שהמוודא ירוץ בזמן פולינומי באורך \(w\); מכיוון שעד כה המוודא היה דטרמיניסטי, נבע מכך שגם מההוכחה עצמה הוא יוכל לקרוא מספר תווים שהוא לכל היותר פולינומי ב-\(w\), ולכן לא היה טעם לדבר על הוכחות ארוכות יותר; דה-פקטו, ההוכחת היו מאורך פולינומי ב-\(w\). לרוב בדיונים על \(\mbox{NP}\) הדרישה הזו מבוצעת במפורש.

אלא שכעת איננו חייבים לקרוא את כל ההוכחה – אנחנו יכולים לדגום תווים ממנה באקראי. למשל, להגריל את תו 13, תו 57 ותו 42,423 ולהחליט שרק אותם נקרא. גם כאן יש עניינים טכניים כלשהם של "איך קוראים את התו במקום ה-1,000,000,000 במהירות?" שלא אכנס אליהם (מה שעושים בפועל הוא להניח שההוכחה נתונה כ"קופסה שחורה" – בהינתן קלט של מקום בהוכחה, הקופסה מחזירה את התו שנמצא באותו מקום). מכאן שאנו מאפשרים להוכחות להיות ארוכות מאוד, אבל מרשים קריאה רק של חלק מצומצם מהן.

הדבר הנוסף שאותו ניתן להגביל הוא כמות האקראיות שבה מותר למוודא להשתמש (מספר ה"מטבעות" שיהיה מותר לו להטיל). שימו לב לקשר עדין ולא ברור מיידית בין נתון זה ובין גודל ההוכחה – אם לא ניתן להגריל יותר מדי ביטים, גם לא ניתן להגריל אינדקסים יותר מדי גדולים של מקומות בהוכחה; לכן מגבלה על מספר הביטים האקראיים שזמינים לנו כופה מגבלה דה-פקטו על הגודל האפקטיבי של ההוכחה.

וכעת מגיע הפאנץ'. נסמן ב-\(\mbox{PCP}\left(r\left(n\right),q\left(n\right)\right)\) את אוסף הבעיות שקיימת עבורן מערכת הוכחה מהסוג שתיארתי, שבה על קלט מאורך \(n\) ניתן להשתמש לכל היותר ב-\(O\left(r\left(n\right)\right)\) ביטים של אקראיות ולקרוא לכל היותר \(O\left(q\left(n\right)\right)\) ביטים מההוכחה. כך למשל \(\mbox{NP}=\mbox{PCP}\left(0,\mbox{poly}\left(n\right)\right)\) מתקיים בבירור (\(\mbox{poly}\) פירושו שאנו מרשים ל-\(q\left(n\right)\) להיות פולינום מכל חזקה שהיא; בפועל פירוש הדבר הוא גישה חופשית להוכחה כולה כל עוד עומדים במגבלות הזמן). התוצאה המפתיעה של משפט ה-\(\mbox{PCP}\) היא שמתקיים גם \(\mbox{NP}=\mbox{PCP}(\log(n),1)\).

במילים – אם מרשים שימוש ב-\(O\left(\log n\right)\) ביטים של אקראיות (וזה לא הרבה בכלל), אפשר להסתפק בקריאה של מספר קבוע של ביטים מההוכחה. קבוע, כלומר לא תלוי בכלל באורך \(w\). פירוש הדבר הוא, לדוגמה, שבבעיית המעגל ההמילטוני מספיק תמיד לקרוא 13 ביטים (שנבחרים באקראי) מההוכחה ואז לבצע כמה חישובים, ואין זה משנה כלל אם הגרף הנבדק הוא בעל 10 צמתים או בעל \(10^{100}\) צמתים. לטעמי האישי זוהי תוצאה מדהימה.

כמובן, יש דברים שמסתתרים כאן מאחורי הקלעים; בפרט, יש מספר גרסאות שונות למשפט (כולן אומרות כי \(\mbox{NP}=\mbox{PCP}\left(\log\left(n\right),1\right)\) אך נבדלות בקבועים ובפרמטרים נוספים שמאחורי הקלעים – למשל, דרישות השלמות והנאותות נפגעות בצורות שונות ומשונות). בגרסה שאני מתמקד בה, גודלה של ההוכחה יהיה עצום (אך כאמור, לא נצטרך לקרוא את כולה). בפוסטים הבאים אתאר (בעיקר בנפנופי ידיים, כי מדובר בנושא טכני למדי) את ההוכחה של גרסה מוחלשת של המשפט בכדי לתת תחושה של "מה הולך כאן בכלל", ולהציג את הנושא היפהפה בזכות עצמו שעליו ההוכחה מסתמכת – קודים לתיקון שגיאות. לעת עתה, אני מקווה שהתוצאה מצליחה להיות מעניינת בזכות עצמה.

אז מה הקשר בין אוטומטים ופונקציות יוצרות?

בפוסטים הקודמים תיארתי אוטומטים סופיים ותיארתי פונקציות יוצרות. עכשיו הגיע הזמן להציג את הקשר היפה (לדעתי) בין השניים. שני הפוסטים הקודמים היו מעין מבוא מרפרף, ולכן בפוסט הזה ארשה לעצמי להיות פחות "מבואי" ולדבר יותר לעניין – ולכן הפוסט יהיה גם יותר טכני מהקודמים. ראו הוזהרתם.

ובכן, אם יש לנו שפה רגולרית \(L\), אפשר לשכוח מהמבנה הפנימי העשיר שלה ולהתמקד בשאלה כמותית – כמה מילים יש מכל אורך? נסמן ב-\(a_{n}\) את מספר המילים ב-\(L\) מאורך \(n\). אפשר כעת להתאים ל-\(L\) פונקציה יוצרת \(f_{L}\left(x\right)\) שמתארת את הסדרה \(a_{n}\) (למי שתוהה, יש מילה אחת מאורך 0 – "המילה הריקה", כך ש-\(a_{0}\) יכול להיות שווה 1). כל זה טוב ויפה – כעת השאלה היא כיצד ניתן לחשב את \(f_{L}\left(x\right)\). אציג שיטה כללית לעשות זאת, אם נתון אוטומט \(A\) שמקבל את \(L\); זה מכתיב דרך פעולה כללית בהתמודדות עם בעיה קומבינטורית שאנחנו רוצים למצוא עבורה פונקציה יוצרת – קודם כל לבדוק האם הבעיה הזו ניתנת לניסוח בתור שפה רגולרית. אם כן, קיבלנו את הפונקציה היוצרת "בחינם".

מכיוון שאנחנו מתמקדים בשאלה כמותית ולא במבנה של כל מילה, אפשר להתעלם מההבדלים שבין אותיות שונות, ולהתמקד בשאלה יותר פשוטה הנוגעת לאוטומט – בכמה מסלולים שונים מאורך \(n\) אפשר להגיע מהמצב ההתחלתי למצב המקבל? מספר המסלולים הזה הוא בדיוק \(a_{n}\). עם זאת, כפי שקורה בדרך כלל בהוכחות שעוסקות באוטומטים, צריך לשאול שאלה "חזקה" יותר – נניח שאנחנו מתחילים ממצב כלשהו של האוטומט, לאו דווקא המצב ההתחלתי. כמה מסלולים מאורך \(n\) שמגיעים למצב מקבל יהיו כעת? אם \(q_{i}\) הוא המצב הזה, אפשר לסמן את המספר בתור \(a_{n}^{i}\), ואת הפונקציה היוצרת המתאימה בתור \(f_{i}\left(x\right)\). כדי לפשט עוד יותר את הסימונים אפשר לדבר על וקטור של פונקציות יוצרות: \(\overline{f}=\left(f_{0},f_{2},\dots,f_{k}\right)\) (אני מסמן את מספר המצבים הכולל של האוטומט ב-\(k+1\) כדי שהוקטור יראה "נחמד").

מכיוון שויתרנו על זהות האותיות השונות ואנחנו מתמקדים רק במספר הדרכים להגיע ממצב אחד באוטומט לאחר, אפשר להפסיק לדבר על פונקצית המעברים של האוטומט ולהסתפק במעין טבלת מעברים מופשטת – מטריצה מגודל \(\left(k+1\right)\times\left(k+1\right)\) שהן שורותיה והן עמודותיה מייצגות מצבים, ובתא ה-\(\left(i,j\right)\) שלה כתוב מספר הדרכים לעבור מהמצב \(q_{i}\) למצב \(q_{j}\) (בצורה אולטרה פורמלית, זהו \(\left|\left\{ \sigma\in\Sigma:\delta\left(q_{i},\sigma\right)=q_{j}\right\} \right|\)). דרך הייצוג הזו, של גרף באמצעות מטריצה, היא מאוד, מאוד נפוצה. אקרא למטריצה הזו \(T\). אלו שבקיאים באלגברה לינארית ומכירים כפל מטריצות יכולים לוודא לעצמם ש-\(T^{r}\) היא המטריצה שבכניסה \(\left(i,j\right)\) שלה רשום מספר הדרכים להגיע מ-\(i\) אל \(j\) בדיוק ב-\(r\) צעדים.

האבחנה הבסיסית בכל הסיפור הזה היא פשוטה ביותר. מספר המסלולים מאורך \(n\) שמובילים מ-\(q_{i}\) אל מצב מקבל כלשהו ניתן לחישוב באופן רקורסיבי. אם \(n=0\) אז הוא 1 אם \(q_{i}\) הוא בעצמו מצב מקבל ואחרת הוא 0. אפשר לסמן זאת בקומפקטיות באמצעות וקטור: \(\overline{u}\)יהיה וקטור שבכניסה ה-\(i\) שלו יש 1 אם המצב \(q_{i}\) הוא מקבל, ואחרת 0. אם \(n>0\) אז אפשר להפעיל רקורסיה: לכל מצב \(q_{j}\) אפשר לדבר על "מספר המסלולים מאורך \(n\) מ-\(q_{i}\) למצב מקבל, שבצעד הראשון נכנסים ל-\(q_{j}\)". מספר המסלולים הללו הוא בדיוק המכפלה של מספר האפשרויות לעבור מ-\(q_{i}\) אל \(q_{j}\) (כלומר, \(T_{ij}\)) במספר המסלולים מאורך \(n-1\) מ-\(q_{j}\) שמגיעים למצב מקבל. מכיוון שכל מסלול מ-\(q_{i}\) שמגיע למצב מקבל חייב לעבור דרך \(q_{j}\) כלשהו בצעד הראשון, מספר המסלולים הכולל הוא סכום של המכפלות הללו, עבור כל ה-\(j\)-ים האפשריים. מי שבקיא באלגברה לינארית אולי רואה כבר כפל מטריצות מול העיניים, אז אכתוב במפורש את הנוסחה: \(f_{i}\left(x\right) = u_{i}+\sum_{j}T_{ij}\cdot xf_{j}\left(x\right)=u_{i}+\left[T\right]_{i}\cdot x\overline{f}\left(x\right)\)

ההכפלה ב-\(x\) של \(f_{j}\left(x\right)\) היא הדרך שלנו לבטא את העובדה שמסלול מאורך \(n\) של \(f_{i}\) נבנה באמצעות מסלולים מאורך \(n-1\) של אגף ימין. ה-\(u_{i}\) שמתווסף לסכום בא לתאר בדיוק את תנאי העצירה של הרקורסיה (הוא יהיה המקדם החופשי בטור של \(f_{i}\left(x\right)\)).

המשוואה שלמעלה היא משוואה כללית לכל \(i\); היא בעצם מייצגת מערכת של \(k+1\) משוואות. את כל המערכת הזו אפשר לתאר באופן עוד יותר קומפקטי:

\(\overline{f} = u+T\cdot x\overline{f}\)

ועל ידי העברת אגף מקבלים:

\(\overline{f}\left(I-xT\right) = u\)

ומכאן:

\(\overline{f} = \left(I-xT\right)^{-1}u\)

וזה נותן לנו את וקטור הפונקציות היוצרות עבור כל המצבים. אם רוצים "לבודד" את הפונקציה היוצרת רק עבור המצב \(q_{0}\), צריך לכפול את שני האגפים בוקטור \(\left(1,0,0,\dots,0\right)\) (הוקטור שכולו אפסים פרט לכניסה במקום ה-0). נסמן וקטור זה כ-\(v\), ונקבל ש-\(f_{0}\left(x\right)=v\left(I-xT\right)^{-1}u\). זו הנוסחה.

חישוב של כל העסק הזה הוא לא מסובך, בהינתן שאנחנו כבר יודעים לעשות חשבון עם פולינומים. יש מערכות אלגברה ממוחשבות שעושות את זה בדיוק – אני משתמש ב-Sage לצורך כך, ובה החישוב שלעיל הוא מיידי. התוצאה המתקבלת היא תמיד פונקציה רציונלית (מנה של שני פולינומים), כך שמציאת נקודות הסינגולריות שלה (הנקודות שבהן "משהו מתקלקל" ולכן אפשר ללמוד מהן על קצב הגידול של \(a_{n}\)) היא פשוטה למדי (אלו בדיוק הנקודות שמאפסות את המכנה).

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

פונקציות יוצרות

אחת מהבעיות הבסיסיות בקומבינטוריקה היא בעיית הספירה של אובייקטים. יש קבוצה של אובייקטים מסוג מסויים – נניח, מספרים שמקיימים תכונה מסויימת, גרפים שנראים כך וכך, דרכים לחלק גלידה לילדים, קלטים האפשריים לאלגוריתם וכו', ואנחנו רוצים לדעת כמה בדיוק יש. לרוב התשובה הפשוטה לשאלות הללו היא "אינסוף" (כמה מספרים ראשוניים יש? אינסוף. לא הכי אינפורמטיבי) וצריך לחדד את השאלה – למשל, לשאול "כמה אובייקטים מכל גודל יש", כש"גודל" מוגדר בהתאם לשאלה. את שאלת המספרים הראשוניים אפשר לחדד ל"כמה מספרים ראשוניים בני \(n\) ספרות יש" (אם כי בפועל, השאלה שבה מתעניינים היא בעיקר "כמה מספרים ראשוניים הקטנים מ-\(n\) קיימים", וזה ראוי לפוסט נפרד). ב"דרכים לחלק גלידה לילדים" אפשר לדבר על הדרכים לחלק גלידה ל-\(n\) ילדים. כשמדברים על גרפים בעלי תכונה מסויימת, אפשר לדבר על גרפים בעלי \(n\) צמתים שמקיימים את התכונה, וכדומה. בשורה התחתונה, לכל אוסף אובייקטים שכזה אנחנו מתאימים סדרה אינסופית \(a_{0},a_{1},a_{2},\dots,a_{n},\dots\) של מספרים טבעיים, שמתארת כמה אובייקטים מכל "גודל" קיימים.

אלא מה? מכיוון שזוהי סדרה אינסופית, לא בהכרח קל לתת לה תיאור קומפקטי. כמובן שכל בעיה קומבינטורית מתחילה מהגדרה מילולית כלשהי של הקבוצה שאותה מונים, אבל זה לא בהכרח מספק מידע כלשהו על הכמות שלהם. אתן דוגמה מייצגת אחת, שאשתמש בה בהמשך – מסלולי מוצקין. חשבו שאתם מטיילים על דף משבצות – מתחילים מנקודה שאתם מסמנים שרירותית בתור \(\left(0,0\right)\), ובכל פעם הולכים לנקודה שמימין לנקודה הנוכחית. יש לכם שלוש אפשרויות – או ללכת באלכסון ימינה ולמעלה (כלומר, לזוז מ-\(\left(0,0\right)\) אל \(\left(1,1\right)\)), או ללכת ימינה בלבד (אל \(\left(1,0\right)\)) או ללכת באלכסון ימינה ולמטה (אל \(\left(1,-1\right)\)). כעת מבצעים עוד צעד, ועוד אחד, וכן הלאה וכן הלאה. באופן כללי נגיע לקוארדינטות מהצורה \(\left(a,b\right)\) כאשר \(a,b\) שניהם שלמים ו-\(a\) חיובי (ליתר דיוק, אחרי \(n\) צעדים נגיע לקוארדינטה \(\left(n,b\right)\), כלומר רק "הגובה" שבו אנו נמצאים אחרי \(n\) צעדים יכול להיות שונה מטיול לטיול).

הבה נשית עוד מגבלה על המסלולים – נאסור על \(b\) להיות שלילי, בכלל, בכל שלב של הטיול. לכן הצעד הראשון כבר לא יכול להיות "ימינה ולמטה"; רק אחרי שנבצע צעד ימינה ולמעלה, ירידה למטה תהיה בכלל לגיטימית. מגבלה אחרונה היא שהמסלול חייב להסתיים בגובה 0. כעת אפשר להגדיר במדוייק – מסלול מוצקין מאורך \(n\) הוא מסלול בן \(n\) צעדים שעונה לדרישות הללו (ולכן מסתיים ב-\(\left(n,0\right)\)). כעת נשאלת השאלה – כמה מסלולים כאלו יש? אם תשחקו קצת עם דף משבצות תגלו שיש מסלול בודד מאורך 1 ("רק ימינה"), שני מסלולים מאורך 2 ("פעמיים ימינה" או "למעלה וימינה ואז למטה וימינה), ארבעה מסלולים מאורך 3 וכן הלאה. נסמן ב-\(a_{n}\) את מספר המסלולים מאורך \(n\) – מהו \(a_{n}\)? האם יש נוסחה עבורו?

וכאן הגענו לבעיה – אמנם, את \(a_{n}\) עלה בידינו להגדיר מילולית, אבל נוסחה אין. זו בעיה נפוצה למדי – לפעמים קשה למצוא את הנוסחה, ולפעמים אפילו לא קיימת נוסחה שאפשר לכתוב במדוייק. אם כן, כדאי לחפש דרכי ייצוג נוספות לכל הסדרה \(a_{0},a_{1},a_{2},\dots\), רצוי דרכים שבהן קל לבצע מניפולציות לייצוג ולהפיק ממנו מידע על הסדרה. פונקציה יוצרת היא אחת מהדרכים המעניינות לעשות זאת.

ישנם מספר סוגים שונים של פונקציות יוצרות, ואציג כאן את הסטנדרטית. אתחיל מהגדרה פורמלית ורק אחר כך אעבור להצדקות. פורמלית, אם כן, פונקציה יוצרת היא הפונקציה \(f\left(x\right)=\sum_{n=0}^{\infty}a_{n}x^{n}\). כלומר, טור אינסופי של חזקות של \(x\), כך שהמקדם של החזקה ה-\(n\)-ית הוא בדיוק המספר \(a_{n}\) שאנחנו מנסים "לקודד". מה המשמעות של לקרוא ליצור הזה "פונקציה"? פונקציה היא בדרך כלל יצור שאם מציבים לו ערכים של \(x\) מקבלים פלטים – האם זה כך במקרה של \(f\left(x\right)\)? התשובה היא "כן ולא", וזה אולי מה שהופך את הפונקציות היוצרות לעסק די מבלבל.

התשובה הבסיסית היא "לא". אנחנו לא חושבים על הפונקציה היוצרת כמשהו שמחוייב לתת תשובה ל"מה אני מקבל אם אני מציב כך וכך במקום \(x\)". אנחנו חושבים עליה בתור אובייקט "פורמלי", בתור אוסף הסימנים \(\sum_{n=0}^{\infty}a_{n}x^{n}\) ותו לא. לאוסף הזה ניתן לבצע מניפולציות – למשל, לכפול אותו בפונקציה יוצרת אחרת – אבל אנחנו לא מטריחים את עצמנו בשאלה של הצבת ערכים ל-\(x\). דרך התבוננות זו עשויה להיראות מוזרה מאוד בתחילה, ולכן כדאי להזכיר שזו בדיוק הגישה של המתמטיקה לפולינומים. במתמטיקה מתייחסים ליצורים כמו \(x^{3}+2x+5\) בתור יצור עם זכות קיום עצמאית – אפשר לכפול אותו בפולינום \(x+1\) ולקבל פולינום חדש; אפשר אפילו לחלק אותו בפולינום הזה ולקבל שארית; אפשר לחבר ולחסר אותו; אפשר אפילו "לגזור" אותו (הנגזרת תהיה \(3x^2+2\)) מבלי להשתמש כלל במושגים מחשבון אינפיניטסימלי. כל הדברים הללו הם פשוט הגדרות; אפשר להגדיר במפורש שסכום של שני פולינומים הוא הפולינום שבו כל מקדם הוא סכום של מקדמי הפולינומים המחוברים (יש כאן הסתמכות על כך שמושג ה"סכום של מקדמים" כבר קיים). ההבדל היחיד הוא שפולינום הוא יצור סופי, ואילו הפונקציה היוצרת מתוארת על ידי סכום אינסופי – "פולינום אינסופי" אם תרצו. זה הבדל פחות משמעותי משזה עשוי להיראות.

עם זאת, אעדיף לא לדבוק בדרך ההצגה הזו, כי ממילא אני מחבב את השניה וארצה לדבר עליה הרבה בהמשך – אז בואו נחשוב על הפונקציה היוצרת כן בתור פונקציה, ונשאל את עצמנו – עבור אילו ערכים של \(x\) היא בכלל מוגדרת? מה בכלל המשמעות של טור אינסופי שכזה?

אם נציב ערך כלשהו ב-\(x\), נאמר מספר ממשי \(t\), נקבל טור של מספרים: \(\sum_{n=0}^{\infty}a_{n}t^{n}\). בחשבון האינפיניטסימלי נותנים הגדרה סבירה לסכום של טור שכזה – דיברתי על כך בעבר. עם זאת, לא מובטח כי לכל ערך של \(t\) ההגדרה "תעבוד" (בניסוח פורמלי – "הטור יתכנס"). לכן הפונקציה \(f\left(x\right)=\sum_{n=0}^{\infty}a_{n}x^{n}\) אינה מוגדרת בהכרח לכל \(t\). למרבה המזל, בגלל הצורה המיוחדת של הטור הזה (סכום של מכפלת מקדם בחזקה של \(x\)), ניתן להגיד דברים בצורה יותר מסודרת על הטור – בפרט, או שהוא מתכנס לכל \(x\), או שקיים מספר ממשי \(R\) כך שלכל \(\left|x\right|<R\) הטור מתכנס. ל-\(R\) הזה קוראים "רדיוס ההתכנסות של הטור" מסיבה שאסביר בקרוב.

לדוגמה, הטור \(1+x+\frac{x^{2}}{2}+\frac{x^{3}}{6}+\dots\), שאפשר לכתוב באופן קומפקטי כ-\(\sum_{n=0}^{\infty}\frac{x^{n}}{n!}\) (ובמילים אחרות – \(a_{n}=\frac{1}{n!}\)) מתכנס לכל ערך של \(x\). מתברר כי הפונקציה שטור זה מייצג היא הפונקציה האקספוננציאלית, \(e^{x}\). לעומת זאת, הטור \(1+x+x^{2}+x^{3}+\dots\) (כלומר, \(a_{n}=1\)) מתכנס לכל \(\left|x\right|<1\) אך לא עבור \(x=1\) או ערכים גדולים יותר. הטור הזה מייצג את הפונקציה \(\frac{1}{1-x}\) (ניתן לראות זאת באמצעות הנוסחה לטור הנדסי אינסופי), שבה אכן קל לראות "בעיניים" כי יש לנו בעיה כלשהי בנקודה 1. עם זאת, וזה הדבר המעניין כאן, הטור אינו מתכנס גם עבור \(x=-1\), למרות שהפונקציה \(f\left(x\right)=\frac{1}{1-x}\) דווקא "מתנהגת יפה" באותה נקודה (היא שווה \(\frac{1}{2}\)). מכאן שהטור והפונקציה מפסיקים להתנהג באותו האופן ברגע שבו מגיעים אל רדיוס ההתכנסות, ולא בהכרח רק בנקודות שהן בעייתיות מבחינת הפונקציה.

עד כה התעסקנו רק במספרים ממשיים, אבל זה לא הכרחי – כל התורה של טורים מתכנסים עוברת היטב גם למספרים מרוכבים, עם אותן תוצאות – לטור מהצורה \(\sum_{n=0}^{\infty}a_{n}x^{n}\) (עכשיו כבר אפשר לומר שיש לו שם מיוחד – "טור חזקות", Power series) יהיה רדיוס התכנסות \(R\) כך שלכל מספר מרוכב המקיים \(\left|z\right|<R\), הטור יתכנס. מכיוון שאפשר לחשוב על המספרים המרוכבים כעל נקודות במישור דו ממדי, שציר ה-\(x\) שלו הוא המספרים הממשיים, יש משמעות ברורה כעת למונח "רדיוס התכנסות" – לטור חזקות בעל רדיוס התכנסות \(R\) יש עיגול ברדיוס \(R\) שמרכזו בראשית הצירים ובכל נקודה בו (למעט נקודות על השפה) הטור מתכנס. ההרחבה הזו למרוכבים היא מאירת עיניים למדי. לדוגמה, נתבונן בטור \(1-x^{2}+x^{4}-x^{6}+\dots\), שמתכנס לפונקציה \(\frac{1}{1+x^{2}}\); הטור לא מתכנס לא עבור \(1\) ולא עבור \(-1\), ועם זאת הפונקציה \(f\left(x\right)=\frac{1}{1+x^{2}}\) בכלל לא רומזת שיהיו בעיות בנקודות אלו (שהרי ערכה בהן הוא \(\frac{1}{2}\)). התובנה מגיעה רק כשחושבים על הפונקציה הזו כפונקציה מרוכבת ושמים לב לכך שבנקודה \(x=i\) היא בכלל לא מוגדרת – והרי \(\left|i\right|=\left|1\right|=\left|-1\right|=1\). הדוגמה הזו זכורה לי כאחת הדוגמאות הראשונות שנתקלתי בהן כהמחשה לצורה שבה המספרים המרוכבים הם הרחבה טבעית של הממשיים, ולטעמי היא עדיין אחת מהיפות שבהן.

טוב, חזרה לענייננו. הטור \(f\left(x\right)=\sum_{n=0}^{\infty}a_{n}x^{n}\) מתכנס תמיד בנקודה \(0\) אז אפשר להיות רגועים ולחשוב על \(\sum_{n=0}^{\infty}a_{n}x^{n}\) תמיד כפונקציה חוקית, פשוט עם רדיוס התכנסות לא ידוע. כעת ניתן להתייחס לפונקציה הזו כפונקציה מרוכבת לכל דבר ולהתעלל בה – לכפול אותה באחרות, לגזור אותה וכו' – ומאחורי כל הדברים הללו יהיה הגיון מתמטי כלשהו ולא רק פורמליזם. כעת אפשר לעבור לשאלה הבאה – איך מוצאים תיאור נוח של הפונקציה הזו, ומה עושים איתו?

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

המשוואה שאציג מתבססת על אבחנה פשוטה: אם \(f\left(x\right)\) היא הפונקציה היוצרת של הסדרה \(a_{n}\), אז \(x\cdot f\left(x\right)\) הוא הפונקציה היוצרת של הסדרה \(b_{n}\) שמוגדרת על ידי \(b_{0}=0\) ו-\(b_{n}=a_{n-1}\) לכל \(n>0\) (למה?). כלומר, אם \(a_{n}\) הוא "מספר האיברים מגודל \(n\)", אז \(b_{n}\) יהיה דווקא "מספר האיברים מגודל \(n-1\)". כעת לפאנץ': מהו מספר מסלולי מוצקין מאורך \(n\)? אפשר לחלק את בעיית הספירה הזו לשתי אפשרויות, על פי הצעד הראשון במסלול. אם הצעד הראשון היה פשוט ימינה, אז מספר הדרכים להמשיך את המסלול ולקבל מסלול חוקי הוא כמספר מסלולי מוצקין מאורך \(n-1\) (שוב, מדוע?). הפונקציה שמתארת את הסדרה הזו היא בדיוק \(xf\left(x\right)\) שלעיל.

אם הצעד הראשון היה למעלה, הרי שמתישהו בהמשך הדרך חייב להיות צעד למטה שיחזיר אותנו לגובה 0. מה קורה בין שני הצעדים הללו? יש תת מסלול, שגם הוא נראה כמו מסלול מוצקין, אם ממקמים את ראשית הצירים שלנו דווקא בנקודה \(\left(1,1\right)\) שאליה הגיע המסלול אחרי העליה. ומה קורה אחרי הירידה? שוב, יהיה לנו מסלול מוצקין חדש, מנקודת הירידה ועד לנקודת הסיום. כלומר, מה היה לנו? שני צעדים "מבוזבזים", ושני מסלולי מוצקין, כך שכל זוג של שני מסלולים אפשריים ייתן לנו את המסלול המשולב. הפונקציה שמתארת את זה היא \(x^{2}f^{2}\left(x\right)\) (\(x^{2}\) כדי "להפחית" את שני הצעדים שבוצעו; \(f^{2}\left(x\right)\) כי כל מסלול בנפרד הוא \(f\left(x\right)\), ולכן השילוב של שניהם בזה אחר זה נותן \(f\left(x\right)\cdot f\left(x\right)\) – כאן אנחנו מבצעים בדיוק את התעלול שלא היה ניתן לבצע אם היינו מסתכלים על כל \(a_{n}\) לבדו ולא על "כולם ביחד" כמו שמאפשרת לנו \(f\left(x\right)\)). מחיבור שתי האפשרויות (או צעד ימינה, או צעד באלכסון) מקבלים את המשוואה \(f\left(x\right)=xf\left(x\right)+x^{2}f^{2}\left(x\right)+1\), כאשר ה-\(+1\) מגיע מכך שאנו סופרים גם את המסלול מאורך 0 (זה "תנאי העצירה" של ההסתכלות הרקורסיבית שלנו על הבעיה). קיבלנו משוואה ריבועית ב"נעלם" \(f\left(x\right)\), שניתן לחלץ ממנה את \(f\left(x\right)\) בטכניקות אלגבריות רגילות – מקבלים \(f\left(x\right)=\frac{1-x-\sqrt{1-2x-3x^{2}}}{2x^{2}}\) (למה בחרתי את הפתרון הזה מבין שני פתרונות המשוואה? כי כפי שאראה בהמשך, בסביבות \(x=0\) הוא מתנהג נחמד ואילו הפתרון השני לא, ולכן לא ייתכן שהפתרון השני מייצג את הטור שלנו).

אוקיי, לנשום עמוק. לא חייבים להבין עד הסוף את מה שעשיתי כאן – זה תרגיל בקומבינטוריקה. מה שמעניין הוא שעשיתי טריקים שלא ניתן לבצע אם מחפשים נוסחת נסיגה "רגילה" עבור \(a_{n}\) – הייתה לי יותר גמישות, ולכן בסוף עלה בידי להגיע לנוסחה סגורה (זה לא תמיד כך – בוריאציות יותר מורכבות על מסלולים אפשר לקבל משוואות ממעלה גבוהה שלא קיים להן פתרון סגור – אבל לא תמיד חייבים אפילו פתרון סגור כדי להפיק מידע על הפונקציה היוצרת).

משמצאנו את הפונקציה היוצרת, השאלה הבאה היא תמיד "מה הלאה?". מה כבר יצא לנו ממציאת הפונקציה? האם זה אומר שנוכל בעזרתה לחשב את \(a_{n}\)? לפעמים זה אכן כך, אבל לא במקרה שלפנינו. מה כן אפשר לעשות? להשתמש בפונקציה היוצרת כדי לקבל הערכה כלשהי לגבי סדר הגודל של \(a_{n}\), בפרט "כמה מהר" הוא גדל. לא אציג כאן את כל התיאוריה של מדידת קצב הגידול אלא אסתפק במדד בסיסי יחסית. נתחיל בדוגמה פשוטה: נניח כי \(a_{n}=2^{n}\), כלומר כשמגדילים את \(n\) ב-1, \(a_{n}\) גדל "פי שתיים". עבור \(a_{n}=3^{n}\) הגדלה ב-1 גוררת גידול "פי 3". ומה קורה עבור סדרות שבהן הגידול אינו תמיד במדוייק פי משהו? האם עדיין ניתן להגיד כי "הסדרה גדלה בערך פי \(\lambda\) כשמגדילים את \(n\) ב-1"? התשובה חיובית אם הגבול \(\lambda=\lim_{n\to\infty}\left(a_{n}\right)^{\frac{1}{n}}\) קיים. המשמעות של הגבול כאן היא פשוט "כש-\(a_{n}\) גדול, הערך שלו מאוד קרוב לערך של \(\lambda^{n}\)" (עם פורמליסטיקה שהופכת את "מאוד קרוב" למשהו מדוייק מתמטית). לכן האתגר הוא לחשב את ה-\(\lambda\) הזה. אם ידועה לנו הפונקציה היוצרת של \(a_{n}\), העסק נהיה פשוט יותר – משפט של קושי על טורי חזקות מראה כי \(\lambda=\frac{1}{R}\) כאשר \(R\) הוא רדיוס ההתכנסות של טור החזקות – לכן כל מה שצריך לעשות הוא לגלות את רדיוס ההתכנסות הזה. כאן הפונקציה היוצרת נכנסת לעניין – מסתכלים עליה כעל פונקציה מרוכבת, ומחפשים את הנקודות שלה שבהן יש "תקלה" – למשל, חלוקה באפס, או הוצאת שורש ממספר שלילי, וכדומה. לתקלות כאלו קוראים "סינגולריות" בתורת הפונקציות המרוכבות – להסביר במדוייק איך הן מוגדרות ואיך מסווגים אותם זה בהחלט משהו שלא ניתן לבצע במסגרת הפוסט הזה. עם זאת, השורה התחתונה פשוטה – לרוב ניתן להסיק את רדיוס ההתכנסות מנקודות הסינגולריות. במקרה של מספרי מוצקין זה פשוט יחסית. הנקודות ה"בעייתיות" הן \(x=0\) (שמאפס את המכנה) והשורשים של \(1-2x-3x^{2}\) (למה ה"פיצוץ" קורה דווקא בשורשים ולא בנקודות שנותנות כבר ערך שלילי? שוב, זה מסובך מדי מכדי שאוכל להסביר זאת על רגל אחת; רק אעיר שהשורשים הללו הם מה שנקרא "נקודות הסתעפות" של הפונקציה).

הנקודה \(x=0\) אינה בעייתית באמת – שוב, קשה להסביר מדוע על רגל אחת, אבל כאשר \(x\) שואף לאפס, \(f\left(x\right)\) שואף ל-\(1\) בצורה "רגועה" כך שהאפס במכנה לא באמת גורם להתפוצצות – היה אפשר להגדיר \(f\left(0\right)=1\) וחסל. לכן נשאר לטפל בנקודות שמאפסות את \(1-2x-3x^{2}\). למצוא אותן זה פתרון משוואה ריבועית – אלו הן \(\frac{1}{3}\) ו-\(-1\). שתיהן בעייתיות "באמת". רדיוס ההתכנסות הוא כמרחק הנקודה הבעייתית הראשונה מהראשית – כלומר, \(R=\frac{1}{3}\), ולכן קצב הגידול של מספרי מוצקין הוא \(\lambda=3\). זו תוצאה מעניינת, כי היא מראה שהמגבלה של "אסור לרדת מתחת לציר \(x\), חייבים לסיים בגובה 0" אינה מהותית כל כך – מספר המסלולים הכולל מאורך \(n\) שבהם מותרים שלושת הצעדים של מוצקין אבל בלי שום מגבלה אחרת הוא \(3^{n}\), כלומר גם לו אותו קצב גידול.

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

וכאן חוזרות לתמונה השפות הרגולריות של הפוסט הקודם.