IP=PSPACE – ההוכחה (חלק ב')

בפוסט הקודם שעסק בהוכחה ש-\(\mbox{IP}=\mbox{PSPACE}\) הראיתי מערכת הוכחה אינטראקטיבית עבור השפה \(\overline{3\mbox{SAT}}\). זו הייתה הדוגמה הראשונה למערכת הוכחה "רצינית", כזו שמשתמשת בכל הכוח של האינטראקטיביות; במערכות האחרות שהראיתי תוך סיבוב או שניים המשחק נגמר, ואילו כאן ההוכחה הייתה דיאלוג ארוך ומתמשך, שבו לאט לאט המוכיח והמוודא התקרבו אל עצם העניין. הרעיון, כזכור, היה כזה: בהינתן פסוק לוגי \(\varphi\), אפשר היה לתרגם אותו לפולינום \(P_{\varphi}\left(X_{1},\dots,X_{n}\right)\) ב-\(n\) משתנים ואת שאלת הספיקות של \(\varphi\) לתרגם לשאלה האם \(\sum_{b_{1}\in\left\{ 0,1\right\} }\dots\sum_{b_{n}\in\left\{ 0,1\right\} }P_{\varphi}\left(b_{1},\dots,b_{n}\right)=K\) עבור \(K\) כלשהו. בפולינום הזה טיפלנו על ידי כך שהצלחנו לצמצם את הבדיקה של המשוואה לעיל לבדיקה של משוואה מהצורה \(\sum_{b_{1}\in\left\{ 0,1\right\} }\dots\sum_{b_{n-1}\in\left\{ 0,1\right\} }P^{\prime}\left(b_{1},\dots,b_{n-1}\right)=K^{\prime}\) – כלומר, צמצנו את הבעיה באופן רקורסיבי לבעיה קטנה יותר (עם משתנה אחת פחות בפולינום החדש) מאותו הסוג. הרעיונות הללו הם בדיוק הרעיונות שנשתמש בהם גם בהוכחה ש-\(\mbox{IP=PSPACE}\), שהיא פשוט הוכחה לכך שהשפה \(\mbox{TQBF}\) שייכת ל-\(\mbox{IP}\).

פסוק של \(\mbox{TQBF}\), כזכור, הוא פסוק מהצורה \(\exists x_{1}\forall x_{2}\exists x_{3}\dots\forall x_{n}\varphi\left(x_{1},\dots,x_{n}\right)\), כלומר כזה שבו מחליפים לסירוגין בין כמתי "קיים" ו"לכל". זה הבדל משמעותי מפסוק \(\mbox{CNF}\) רגיל שבו כל הכמתים היו "קיים". הערך \(\sum_{b_{1}\in\left\{ 0,1\right\} }\dots\sum_{b_{n}\in\left\{ 0,1\right\} }P_{\varphi}\left(b_{1},\dots,b_{n}\right)\) כבר לא ממש עוזר לנו – הוא אומר לנו מה מספר ההצבות של ערכים למשתני \(\varphi\) שיניבו \(\mbox{T}\), אבל המספר הזה לא עוזר לנו ממש – למשל, נניח ש-\(\varphi\) הוא הפסוק \(\varphi\left(x_{1},x_{2}\right)=\left(x_{1}\vee x_{2}\right)\wedge\left(\overline{x_{1}}\vee\overline{x_{2}}\right)\), שמוכר גם בסימון קצת יותר פופולרי כ-\(x_{1}\oplus x_{2}\) (פעולת XOR). לא קשה לראות כי \(\sum_{b_{1}\in\left\{ 0,1\right\} }\sum_{b_{2}\in\left\{ 0,1\right\} }\varphi\left(b_{1},b_{2}\right)=2\), אבל \(\exists x_{1}\forall x_{2}\varphi\left(x_{1},x_{2}\right)\) הוא בבירור בעל ערך \(\mbox{F}\) (כי לא קיים \(x_{1}\) שעבורו כל \(x_{2}\) יניב ערך אמת כשמציבים את שניהם ב-\(\varphi\)). לעומת זאת, \(\varphi\left(x_{1},x_{2}\right)=x_{1}\) הוא פסוק קטן ביותר ומטופש שגם בו הסכום יהיה בדיוק 2, אבל בו התכונה כן מתקיימת (הצבה של \(\mbox{T}\) ל-\(x_{1}\) מספקת את הפסוק בלי תלות ב-\(x_{2}\), כמובן).

למרבה המזל, טריק פשוט יחסית פותר את הבעיה הזו. כל מה שאנחנו רוצים לדעת בהינתן פסוק \(\mbox{TQBF}\) הוא אם ערך האמת שלו הוא \(\mbox{T}\) או \(\mbox{F}\). אז בואו נשים לב שערך האמת של \(\exists x_{1}\forall x_{2}\exists x_{3}\dots\forall x_{n}\varphi\left(x_{1},\dots,x_{n}\right)\) הוא \(\mbox{T}\) אם ורק אם \(\sum_{b_{1}\in\left\{ 0,1\right\} }\prod_{b_{2}\in\left\{ 0,1\right\} }\sum_{b_{3}\in\left\{ 0,1\right\} }\dots\prod_{b_{n}\in\left\{ 0,1\right\} }P_{\varphi}\left(b_{1},\dots,b_{n}\right)\ne0\). נסו להוכיח את זה לעצמכם – ההוכחה היא באינדוקציה פשוטה על מספר הכמתים, ומתבססת על כך ש-\(P_{\varphi}\) מקבל או 0 או 1, ושמכפלה של שני ערכים שהוא מקבל היא 1 רק אם שניהם היו 1, ושסכום של שני ערכים שהוא מקבל הוא 0 רק אם שניהם היו 0.

אז איך הפרוטקול של הפוסט הקודם יעבוד כאן? פשוט מאוד: המוכיח ישלח למוודא פולינום במשתנה יחיד \(g\left(X_{1}\right)\) שבתיאוריה מוגדר באמצעות הנוסחה \(g\left(X_{1}\right)=\prod_{b_{2}\in\left\{ 0,1\right\} }\sum_{b_{3}\in\left\{ 0,1\right\} }\dots\prod_{b_{n}\in\left\{ 0,1\right\} }P_{\varphi}\left(X_{1},b_{2},\dots,b_{n}\right)\), והמוודא יוודא ש-\(g\left(0\right)+g\left(1\right)\ne0\); וכעת המוודא ירצה להשתכנע ש-\(g\left(X_{1}\right)\) אכן הוגדר על פי הנוסחה הנ"ל ושהמוכיח לא סתם שלח לו פולינום מונפץ, אז הוא יגריל ערך \(a\) כלשהו מהשדה שמעליו עובדים (שהוא כזכור \(\mathbb{Z}_{p}\) עבור \(p\) ראשוני גדול מספיק), יחשב את \(K=g\left(a\right)\) ויתבע מהמוכיח שיוכיח לו כעת ש-\(\prod_{b_{2}\in\left\{ 0,1\right\} }\sum_{b_{3}\in\left\{ 0,1\right\} }\dots\prod_{b_{n}\in\left\{ 0,1\right\} }P_{\varphi}\left(a,b_{2},\dots,b_{n}\right)=K\), והמשחק יימשך. בסיבוב הבא, המוודא יבדוק שהפולינום \(g\left(X_{2}\right)\) שהוא קיבל מקיים דווקא \(g\left(0\right)\cdot g\left(1\right)=K\), אבל זה לא הבדל עקרוני.

פוף! סיימנו. זה היה קל!

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

אז מה הבעיה? הבעיה היא שלא בטוח שהמוכיח יהיה מסוגל לשלוח למוודא את \(g\left(X_{1}\right)\), כי הדרגה של \(g\left(X_{1}\right)\) עשויה להיות גבוהה מדי מכדי ששליחה שלו תאפשר לפרוטוקול להמשיך להיות יעיל מבחינת זמן הריצה. בואו נעשה סדר בדברים: זמן הריצה של הבעיה נמדד ביחס לגודל הקלט, כלומר גודל הנוסחה \(\varphi\). אפשר גם כאן להניח שזו נוסחת \(3\mbox{CNF}\) – נוסחה שמורכבת מפסוקיות כך שכל פסוקית מכילה שלושה משתנים בדיוק; נסמן ב-\(m\) את מספר הפסוקיות. את \(P_{\varphi}\) מייצגים בתור מכפלה של פולינומים שכל אחד מהם מייצג את אחת מהפסוקיות; פולינום שכזה, עם שיטת התרגום שהצגתי בפוסט הקודם, יהיה מדרגה 3, ולכן קל לייצוג (צריך לזכור את המקדמים של הפולינום, אבל יש מעט כאלו – כמה?). בסך הכל צריך כמות זכרון שהיא "בערך \(m\)" כדי לייצג את \(P_{\varphi}\), כש"בערך" פירושו "\(m\) כפול קבוע כלשהו" – מסמנים זאת \(O\left(m\right)\).

כעת, בפרוטוקול שהראינו בפוסט הקודם, שבו \(g\left(X_{1}\right)\) הוגדר בתור \(\sum_{b_{2}\in\left\{ 0,1\right\} }\sum_{b_{3}\in\left\{ 0,1\right\} }\dots\sum_{b_{n}\in\left\{ 0,1\right\} }P_{\varphi}\left(X_{1},b_{2},\dots,b_{n}\right)\), מה שקרה הוא ש-\(g\left(X_{1}\right)\) היה פולינום שהתקבל מחיבור הרבה פולינומים במשתנה יחיד שדרגתם לכל היותר \(3m\) (כי דרגת \(P_{\varphi}\) הייתה \(3m\)). חיבור פולינומים לא יכול להגדיל את הדרגה שלהם, ולכן \(3m\) נותר חסם גם על דרגת \(g\left(X_{1}\right)\), מה שאומר שכמות המקדמים שהמוכיח צריך לשלוח למוודא כדי לשדר אליו את \(g\left(X_{1}\right)\) הייתה סבירה ביחס לגודל הקלט של הפרוטוקול, והכל עבד יופי טופי.

בפרוטוקול החדש, לעומת זאת, הכשל הוא מוחץ, שכן \(g\left(X_{1}\right)\) נבנה לא רק באמצעות חיבור פולינומים אלא גם הכפלתם; והכפלה של שני פולינומים בהחלט יכולה להגדיל את הדרגה של התוצאה: \(\left(x+1\right)\left(x-1\right)=x^{2}-1\). מכיוון שהדרגה של פולינום התוצאה יכולה להכפיל את עצמה בכל פעם שבה אנחנו נתקלים בפעולת כפל בהגדרה של \(g\left(X_{1}\right)\), הדרגה של \(g\left(X_{1}\right)\) עשויה להיות אקספוננציאלית (ב-\(n\); שהוא בתורו עשוי בהחלט לפעמים להיות מאותו סדר גודל של \(m\)). בקיצור, רק לשלוח את כל המקדמים של \(g\left(X_{1}\right)\) מהמוכיח אל המוודא ייקח זמן רב מדי מכדי שהפרוטוקול ייחשב יעיל והכל הלך לעזאזל.

אז מה עושים? מכניסים לתמונה עוד טריק אחד, שפותר את הבעיה – לינאריזציה. לינאריזציה פירושה לקחת פולינום \(P\left(X_{1},\dots,X_{n}\right)\) כלשהו, שבו המשתנה \(X_{i}\) עשוי להופיע בדרגה גבוהה, ולהחליף אותו בפולינום חדש, \(Q\left(X_{1},\dots,X_{n}\right)\) שבו הדרגה של \(X_{i}\) היא 1 (כלומר, ב-\(Q\) לא מופיע \(X_{i}^{2}\), או \(X_{i}^{3}\) וכדומה), ועם זאת \(Q\) הוא "שקול" ל-\(P\) במובן זה שעל הצבה של ערכי 0 ו-1 למשתנים שלהם מתקבלת אותה התוצאה (וזה, כזכור, כל מה שחשוב לנו – אנחנו מנסים לברר מה מתקבל מ-\(P_{\varphi}\) כשמציבים בו 0 ו-1 בכל הצורות האפשריות ואז עושים מיש-מש מכל התוצאות).

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

\(L_{X_{i}}P\left(X_{1},\dots,X_{i},\dots,X_{n}\right)=X_{i}\cdot P\left(X_{1},\dots,1,\dots X_{n}\right)+\left(1-X_{i}\right)\cdot P\left(X_{1},\dots,0,\dots X_{n}\right)\)

כלומר, האופרטור לוקח את \(P\), מציב פעם 0 ופעם 1 במקום \(X_{i}\), מקבל שני פולינומים חדשים עם משתנה אחד פחות, כופל את האחד במשתנה \(X_{i}\) ואת השני במשתנה \(1-X_{i}\), ומחבר. הנה דוגמה: נניח ש-\(P\left(X_{1},X_{2}\right)=X_{1}^{5}+2X_{1}^{2}X_{2}+X_{2}^{3}\). אז נקבל:

\(L_{X_{1}}P=X_{1}\left(1+2X_{2}+X_{2}^{3}\right)+\left(1-X_{1}\right)X_{2}^{3}=X_{1}+2X_{1}X_{2}+X_{2}^{3}\)

שימו לב שקיבלנו את הפולינום המקורי, רק שבכל מקום שבו הופיע \(X_{1}\) בחזקה כלשהי, עכשיו הוא מופיע בלי חזקה (או פורמלית, עם החזקה 1). זה לא מקרי, כמובן; \(P\left(X_{1},\dots,1,\dots X_{n}\right)\) הוא הפולינום \(P\) על כל מחובריו, כשהמשתנה \(X_{i}\) פשוט נמחק ממנו. לכן \(X_{i}\cdot P\left(X_{1},\dots,1,\dots X_{n}\right)\) הוא הפולינום \(P\) כאשר כל מופע קיים של \(X_{i}\) בחזקה כלשהי שונה למופע בחזקה 1; אבל לרוע המזל גם מחוברים שבהם \(X_{i}\) לא הופיע כעת כוללים אותו. זה מתוקן עם \(\left(1-X_{i}\right)P\left(X_{1},\dots,0,\dots X_{n}\right)\) – נסו להסביר לעצמכם מדוע!

כעת הבה ונגדיר אופרטורים עבור \(\forall\) ו-\(\exists\):

\(\forall_{X_{i}}P\left(X_{1},\dots,X_{n}\right)=P\left(X_{1},\dots,0,\dots,X_{n}\right)\cdot P\left(X_{1},\dots,1,\dots,X_{n}\right)\)

\(\exists_{X_{i}}P\left(X_{1},\dots,X_{n}\right)=P\left(X_{1},\dots,0,\dots,X_{n}\right)+P\left(X_{1},\dots,1,\dots,X_{n}\right)\)

כעת אפשר לנסח מחדש את הבעיה שלנו: בהינתן פולינום \(P\left(X_{1},\dots,X_{n}\right)\), נשים לב ש-\(\exists_{X_{1}}\forall_{X_{2}}\dots\exists_{X_{n}}P\left(X_{1},\dots,X_{n}\right)\) הוא פולינום קבוע, ללא משתנים, כלומר הוא ערך מספרי; האתגר של המוכיח יהיה להוכיח כי \(\exists_{X_{1}}\forall_{X_{2}}\dots\exists_{X_{n}}P\left(X_{1},\dots,X_{n}\right)=K\) עבור \(K\) נתון כלשהו.

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

\(\exists_{X_{1}}L_{X_{1}}\forall_{X_{2}}L_{X_{1}}L_{X_{2}}\dots\exists_{X_{n}}L_{X_{1}}\dots L_{X_{n}}P\left(X_{1},\dots,X_{n}\right)=K\)

יש כאן בסך הכל \(1+2+\dots+n=O\left(n^{2}\right)\) הפעלות של אופרטור הלינאריזציה, כך שהוא לא מבזבז יותר מדי זמן ריצה.

הפרוטוקול כעת "מקלף" את הכמתים משמאל לימין באותה שיטה של הפוסט הקודם. למשל, שימו לב לכך ש-\(L_{X_{1}}\forall_{X_{2}}L_{X_{1}}L_{X_{2}}\dots\exists_{X_{n}}L_{X_{1}}\dots L_{X_{n}}P\left(X_{1},\dots,X_{n}\right)\) הוא פולינום במשתנה יחיד, \(X_{1}\), ושבגלל אופרטור הלינאריזציה בקצה השמאלי, זה פולינום ממעלה נמוכה (מעלה 1, למעשה!) ולכן המוכיח בוודאי יכול לשלוח למוודא פולינום \(g\left(X_{1}\right)\) שלכאורה אמור להיות שווה אליו. המוודא יבדוק ש-\(g\left(0\right)+g\left(1\right)=K\), ואז יציב אתגר חדש למוכיח, שמטרתו להוכיח ש-\(g\left(X_{1}\right)\) הוא מה שהוא מתיימר להיות: הוא יגריל \(a_{1}\) כלשהו מהשדה, יחשב את \(K^{\prime}=g\left(a_{1}\right)\), ויבקש מהמוכיח להוכיח לו ש-\(L_{X_{1}}\forall_{X_{2}}L_{X_{1}}L_{X_{2}}\dots\exists_{X_{n}}L_{X_{1}}\dots L_{X_{n}}P\left(a_{1},\dots,X_{n}\right)=K^{\prime}\) (חשבו על זה כעל הפולינום \(L_{X_{1}}\forall_{X_{2}}L_{X_{1}}L_{X_{2}}\dots\exists_{X_{n}}L_{X_{1}}\dots L_{X_{n}}P\left(X_{1},\dots,X_{n}\right)\), שהוא פולינום במשתנה היחיד \(X_{1}\), שאחר כך מציבים בו גם \(a_{1}\)).

מה יקרה עכשיו? המוכיח ישלח למוודא איזה \(g\left(X_{1}\right)\) שאמור להיות \(\forall_{X_{2}}L_{X_{1}}L_{X_{2}}\dots\exists_{X_{n}}L_{X_{1}}\dots L_{X_{n}}P\left(X_{1},\dots,X_{n}\right)\); המוודא יוודא ש-\(a_{1}g\left(1\right)+\left(1-a_{1}\right)g\left(0\right)=K^{\prime}\), כלומר שאם עושים ל-\(g\) לינאריזציה לפיה \(X_{1}\) ואז מציבים ב-\(X_{1}\) את הערך \(a_{1}\), אכן מקבלים את ה-\(K^{\prime}\) שאנו מצפים לקבל. כעת המוודא יבחן את המוכיח על ה-\(g\) הזה על ידי הגרלת ערך \(a\) חדש, ותביעה מהמוכיח להוכיח ש-\(g\left(a\right)=\forall_{X_{2}}L_{X_{1}}L_{X_{2}}\dots\exists_{X_{n}}L_{X_{1}}\dots L_{X_{n}}P\left(a,\dots,X_{n}\right)\) (הערך \(a_{1}\) הקודם נזרק לפח), וכן הלאה וכן הלאה. בסופו של דבר כל האופרטורים מקולפים, ולמוודא נשאר רק לוודא שמתקיים \(P\left(a_{1},a_{2},\dots,a_{n}\right)=K\) עבור ערכים \(a_{1},\dots,a_{n}\) ספציפיים, ואת זה הוא כמובן מסוגל לעשות בעצמו.

בואו נתאר את התהליך באופן כללי. בכל שלב של האלגוריתם יש למוודא פולינום כלשהו, \(h\left(X_{1},\dots,X_{n}\right)\) (שהוא \(P\left(X_{1},\dots,X_{n}\right)\) אחרי שהופעלו עליו חלק מהאופרטורים, מימין לשמאל) ותפקידו של המוודא בחיים הוא לבדוק ש-\(h\left(a_{1},\dots,a_{n}\right)=K\) עבור \(K\) קבוע וערכים \(a_{1},\dots,a_{n}\) מסויימים. שימו לב שהשימוש שלי בסימונים עשוי להיות מבלבל למדי כאן – למשל, בסיבוב הראשון של הפרוטוקול \(h\) הוא פשוט \(\exists_{X_{1}}L_{X_{1}}\forall_{X_{2}}L_{X_{1}}L_{X_{2}}\dots\exists_{X_{n}}L_{X_{1}}\dots L_{X_{n}}P\left(X_{1},\dots,X_{n}\right)\), וזה פולינום בלי משתנים חופשיים כלל. אז את הסימון \(h\left(X_{1},\dots,X_{n}\right)\) יש לקרוא כאומר שהמשתנים שעשויים להופיע ב-\(h\) הם \(X_{1},\dots,X_{n}\), וש-\(h\left(a_{1},\dots,a_{n}\right)\) הוא מה שמתקבל ב-\(h\) כשמציבים להם את הערכים \(a_{1},\dots,a_{n}\), כאשר משתנה שכלל לא מופיע ב-\(h\) מן הסתם לא משפיע על התוצאה. כמו כן, לא ברור מיהם \(a_{1},\dots,a_{n}\) בתחילת הפרוטוקול (ראינו שהם נקבעים במהלכו, ולפעמים עוברים מספר שינויים) – אז אפשר לאתחל את כולם להיות 0 או כל ערך שרירותי אחר, אבל אם אתם עדיין מצליחים לעקוב אחרי מה שהולך כאן בוודאי תראו שזה לא באמת משנה.

מה שהמוודא עושה הוא לקלף את האופרטור הבא, אם עוד קיים כזה: כלומר, לשים לב לכך ש-\(h\left(X_{1},\dots,X_{n}\right)=\Phi f\left(X_{1},\dots,X_{n}\right)\), כאשר \(\Phi\) הוא או אופרטור \(L\), או אופרטור \(\forall\), או אופרטור \(\exists\). כעת הפרוטוקול מתנהג בשלוש דרכים שונות, בהתאם לזהות האופרטור:

אם \(\Phi=\forall_{X_{i}}\) אז המוכיח שולח למוודא פולינום במשתנה יחיד \(g\left(X_{i}\right)\) שאמור להיות \(f\left(a_{1},\dots,X_{i},\dots,a_{n}\right)\). המוודא בודק ש-\(g\left(0\right)\cdot g\left(1\right)=K\). אם לא, הוא דוחה; אחרת, הוא מגריל ערך חדש של \(a_{i}\) ומבקש מהמוכיח להוכיח לו ש-\(f\left(a_{1},\dots a_{i},\dots,a_{n}\right)=g\left(a_{i}\right)\).

אם \(\Phi=\exists_{X_{i}}\) קורה אותו הסיפור כמו במקרה של \(\forall\), פרט לכך שהמוודא בודק ש-\(g\left(0\right)+g\left(1\right)=K\).

אם \(\Phi=L_{X_{i}}\) אז שוב, קורה אותו הסיפור פרט לכך שהמוודא בודק ש-\(a_{i}g\left(1\right)+\left(1-a_{i}\right)g\left(0\right)=K\).

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

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

IP=PSPACE – ההוכחה (חלק א')

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

משפט אימרמן שהצגתי לא מזמן הצטמצם בסופו של דבר לפתרון של בעיה אחת – בעיית אי-הישיגות בגרף, שהייתה מה שנקרא \(\mbox{coNL}\)-שלמה. גם הוכחה ש-\(\mbox{IP=PSPACE}\) היא בעיקרה פתרון של בעיה אחת ספציפית – \(\mbox{TQBF}\), שהוזכרה בפוסט הקודם. אם נציג מערכת הוכחה יעילה עבור בעיה זו, סיימנו, מכיוון שכל בעיה אחרת ב-\(\mbox{PSPACE}\) ניתן לתרגם לבעיית \(\mbox{TQBF}\) ולהסתפק במערכת ההוכחה עבור \(\mbox{TQBF}\) שכבר יש לנו. זה אולי ירגיז את חלקכם שרוצים לראות הוכחה שלמה ככל הניתן ל-\(\mbox{IP=PSPACE}\) ושמו לב שטרם הוכחתי ש-\(\mbox{TQBF}\) היא \(\mbox{PSPACE}\)-שלמה. זה נכון, וזה חוב שאחזיר מתישהו; אבל זה לא העיקר בהוכחה – כשנמצאה ההוכחה ל-\(\mbox{IP=PSPACE}\) הייתה עובדת היותה של \(\mbox{TQBF}\) שפה \(\mbox{PSPACE}\)-שלמה משהו ידוע ומוכר לגמרי.

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

כמו שבדרך כלל עושים עם הוכחות מסובכות, מאוד כדאי להתחיל עם הוכחה של טענה חלשה יותר, שעדיין כוללת את הרעיונות המרכזיים של הההוכחה האמיתית ובכך מקלה מאוד על העיכול שלה. במקרה שלנו, נרצה להראות מערכת הוכחה אינטראקטיבית לשפה שהיא \(\mbox{coNP}\)-שלמה – השפה \(\overline{\mbox{3SAT}}\). שפת כל פסוקי ה-CNF\(3\) שאינם ספיקים (פסוק CNF הוא "וגם" של הרבה פסוקיות, שכל אחת מהן היא "או" של 3 ליטרלים, שכל אחד מהם הוא משתנה או שלילה של משתנה, ופסוק הוא ספיק אם יש השמה למשתנים שנותנת לו ערך אמת). ברור שאם פסוק הוא ספיק אז קל להוכיח זאת – פשוט נותנים למוודא את ההשמה המספקת. אבל אם הוא לא ספיק לא ברור מה אפשר לעשות, ולכן מערכת הוכחה אינטראקטיבית כאן היא מעניינת. השפה 3CNF מזכירה קצת את TQBF בכך ששתיהן עוסקות בפסוקים לוגיים; ב-TQBF הפסוק עצמו גם מכיל כמתים בנוסף לכל הצרות. אבל כבר כעת ברור שאנחנו הולכים לפתור מקרה דומה יחסית.

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

איך אפשר לתרגם את משתנים לוגיים לפולינום, הרי \(\mbox{T,F}\) הם לא ערכים שבדרך כלל חוקי להציב בפולינום? ובכן, באופן טבעי יחסית נחשוב על 0 כמייצג \(\mbox{F}\) ועל 1 כמייצג \(\mbox{T}\). אם בפסוק הלוגי יש משתנה \(x_{1}\), אז בפולינום יהיה משתנה \(X_{1}\) מתאים (המעבר מאות קטנה לגדולה נועד רק כדי להבטיח שיהיה ברור מתי מדברים על פולינום ומתי מדברים על פסוק לוגי). פעולת "וגם" ניתנת לביצוע בידי כפל: \(x_{1}\wedge x_{2}\) הוא \(X_{1}\cdot X_{2}\), ובאופן כללי אם \(\varphi_{1},\varphi_{2}\) הן שתי פסוקיות שמיוצגות בידי הפולינומים \(P_{\varphi_{1}},P_{\varphi_{2}}\) אז \(\varphi_{1}\wedge\varphi_{2}\) מיוצגת על ידי \(P_{\varphi_{1}}\cdot P_{\varphi_{2}}\).

פעולת "לא" ניתנת לביצוע על ידי חיסור: \(\neg x_{1}\) מיוצג על ידי \(1-X_{1}\), ואם \(P_{\varphi}\) מייצג את \(\varphi\) אז \(1-P_{\varphi}\) מייצג את \(\neg\varphi\).

שתי הפעולות הללו מאפשרות ייצוג של כל פסוק שהוא כי אפשר "לסמלץ" איתן את "או" באמצעות כלל דה-מורגן: \(x_{1}\vee x_{2}=\neg\left(\neg x_{1}\wedge\neg x_{2}\right)\). במילים אחרות, את \(x_{1}\vee x_{2}\) מייצגים על ידי הפולינום \(1-\left(1-X_{1}\right)\left(1-X_{2}\right)\).

משלושת אלו נובע שכל נוסחת 3CNF עם \(n\) משתנים, \(\varphi\left(x_{1},\dots,x_{n}\right)\), ניתן לתרגם לפולינום \(P\left(X_{1},\dots,X_{n}\right)\) ב-\(n\) משתנים. לאלו מכם שאולי מכירים רק פולינומים במשתנה אחד נציין שפולינום מרובה משתנים הוא פשוט סכום של ביטויים מהצורה \(aX_{1}^{k_{1}}X_{2}^{k_{2}}\dots X_{n}^{k_{n}}\) – כלומר, מקדם כלשהו ואז מכפלה של חזקות של המשתנים. כל ביטוי כזה נקרא מונום. הדרגה של מונום היא סכום החזקות של כל המשתנים, והדרגה של הפולינום כולו היא הדרגה הגבוהה ביותר של מונום שבו. למשל, \(5xy+7xyz+2y^{2}z^{3}\) הוא פולינום בשלושה משתנים שדרגתו 5.

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

כעת, אם \(P_{\varphi}\left(X_{1},\dots,X_{n}\right)\) מייצג את הפסוק \(\varphi\left(x_{1},\dots,x_{n}\right)\), אז מספר ההשמות המספקות של \(\varphi\) נתון על ידי הנוסחה \(\sum_{b_{1}\in\left\{ 0,1\right\} }\sum_{b_{2}\in\left\{ 0,1\right\} }\dots\sum_{b_{n}\in\left\{ 0,1\right\} }P_{\varphi}\left(b_{1},\dots,b_{n}\right)\). זה נראה אולי קצת מפחיד במבט ראשון, אבל זה ממש לא נורא: אנחנו בסך הכל סוכמים הרבה מאוד איברים – לכל הצבה אפשרית של ערכי 0 ו-1 למשתנים \(X_{1},\dots,X_{n}\) אנחנו בודקים איזה ערך הפולינום קיבל ומוסיפים לסכום. אם כן, הפכנו את הבעיה שלנו לבעיה מסוג שונה, בעיה אלגברית יותר – בהינתן פולינום \(P\left(X_{1},\dots,X_{n}\right)\) ומספר \(K\), להוכיח ש-\(\sum_{b_{1}\in\left\{ 0,1\right\} }\dots\sum_{b_{n}\in\left\{ 0,1\right\} }P\left(b_{1},\dots,b_{n}\right)=K\). מכאן ואילך נתעסק רק בבעיה הזו.

עכשיו אתם אולי שואלים את עצמכם מה יצא לנו מכל העסק הזה – עברנו מבעיה פשוטה שמערבת פסוקים לוגיים, לבעיה יותר מסובכת עם פולינומים. הפאנץ' המרכזי כאן הוא זה: בפולינומים אפשר להציב ערכים נוספים פרט ל-0 ו-1. הצבה שכזו נראית במבט ראשון כמו ג'יבריש, כי מה קורה כשמציבים משהו שאינו אפס או אחד בפולינום שמנסה לייצג פסוק לוגי? מקבלים תוצאה שאין לה שום משמעות שקשורה לפסוק המקורי! אבל, תזכרו שאנחנו כבר לא מנסים לפתור בעיה שקשורה לפסוקים אלא בעיה כלשהי שקשורה לפולינומים, ולכן המשמעות המקורית ההיא לא חשובה לנו יותר. כפי שנראה בקרוב, היכולת להציב בפולינום ערכים שאינם 0 או 1 היא בדיוק מה שנותן למוודא את הכוח לבחון אינטראקטיבית את המוכיח.

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

צריך להיות זהירים כאן – הטענה הזו על פולינומים נכונה רק כשהפולינום מוגדר מעל שדה, כמו המספרים הרציונליים או הממשיים. למשל, בקבוצה \(\mathbb{Z}_{16}\) של המספרים השלמים מ-0 עד 15 עם חיבור וכפל מודולו 16 לפולינום \(x^{2}\) ישנם שורשים למכביר: 0, וגם 4, וגם 8. אם כן, אפשר היה לדבר על הפולינום מעל הרציונליים, או הממשיים, אבל אלו שדות גדולים מדי לצרכים שלנו – בהמשך הפרוטוקול המוודא יצטרך לבחור באקראי איברים מהם, ומכיוון שהם שדות אינסופיים בחירה שכזו היא בעייתית. לכן עוברים לדבר על הפולינום מעל שדה סופי פשוט – קבוצה מהצורה \(\mathbb{Z}_{p}\) כאשר \(p\) הוא ראשוני (עבור ראשוניים הקבוצה הזו היא אכן שדה – הרי לכם דוגמה אחת לאופן שבו הראשוניים צצים באופן טבעי באלגברה). לכן הפרוטקול נפתח בבחירה של \(p\) גדול – כמה גדול? נדבר על זה אחר כך – על ידי המוכיח, שליחה למוודא, ובדיקה של המוודא שאותו \(p\) הוא אכן ראשוני בעזרת אלגוריתם כלשהו לבדיקת ראשוניות (מילר-רבין שהזכרתי בבלוג בעבר מספיק טוב כאן; הוא אמנם הסתברותי אבל כל מערכת ההוכחה האינטראקטיבית היא הסתברותית).

ועכשיו הטריק הוא זה: בואו נגדיר פולינום \(g\left(X\right)\) על ידי כך שניקח את הסכום של \(P\) שלעיל ונותיר בו את המשתנה הראשון בלי ידוע. כלומר:

\(g\left(X\right)=\sum_{b_{2}\in\left\{ 0,1\right\} }\dots\sum_{b_{n}\in\left\{ 0,1\right\} }P\left(X,b_{2},\dots,b_{n}\right)\) – שימו לב שהסכומים התחילו מ-\(b_{2}\). אם למוודא היה ביד את הפולינום הזה, הוא היה יכול לבדוק בקלות האם הסכום המקורי יוצא \(K\) – הוא פשוט היה מחשב את \(g\left(0\right)+g\left(1\right)\) ובודק את התוצאה (למה זה עובד?). רק מה, חישוב של \(g\left(X\right)\) הוא בעייתי כמעט כמו חישוב של כל הסכומים, אם כי הוא טיפה פחות בעייתי שכן יש משתנה אחד פחות שמעורב במשחק. אז במקום לחשב את הפולינום בעצמו, המוודא יבקש מהמוכיח לשלוח לו אותו – כלומר, לשלוח למוודא את סדרת המקדמים של הפולינום. מעלת הפולינום היא אותה מעלה כשל \(P\) שכבר הסברנו שאינה גדולה, כך שמספר המקדמים שהמוכיח שולח אינו גדול. אחרי שהמוודא קיבל מהמוכיח את \(g\) הוא יכול לחשב את \(g\left(0\right)+g\left(1\right)\), ואם קיבל משהו שונה מ-\(K\), לדחות מייד. אבל אם הוא קיבל, כצפוי, \(K\), מה עכשיו? הוא לא יכול לקבל, מהטעם הפשוט שהוא לא סומך על המוכיח! מה אם המוכיח שלח לו סתם פולינום שלא קשור ל-\(P\) בשום צורה (והינדס את הפולינום כדי שכן יקיים \(g\left(0\right)+g\left(1\right)=K\))?

אם כן, המוודא עושה את המשחק הבא: הוא אומר למוכיח "אוקיי, אני סומך עלייך שאם \(g\) הוא אכן הפולינום שהיית אמור לשלוח לי, אז הסכום המקורי יצא \(K\) ואפשר לשכוח ממנו. עכשיו בוא תוכיח לי ש-\(g\) הוא אכן הפולינום שהיית אמור לשלוח לי". אבל איך המוכיח יכול להוכיח את זה?

כאן מגיע הפאנץ' המובטח. המוודא יגריל מספר כלשהו \(a\) מתוך השדה, יציב אותו ב-\(g\) ויקבל מספר, \(g\left(a\right)\). כעת הוא יפנה למוכיח את האתגר הבא: הוכח נא עבורי שמתקיים \(\sum_{b_{2}\in\left\{ 0,1\right\} }\dots\sum_{b_{n}\in\left\{ 0,1\right\} }P\left(a,b_{2},\dots,b_{n}\right)=g\left(a\right)\). אבל מה זה האתגר הזה? זה בדיוק אותו דבר שהתחלנו ממנו – המוכיח מנסה להוכיח למוודא שאם לוקחים פולינום, מציבים במשתנים שלו את כל הקומבינציות האפשריות של 0 ו-1 וסוכמים, מקבלים מספר מסויים. ההבדל היחיד הוא שהפולינום החדש הוא בעל פחות משתנים, כי "חיסלנו" את אחד המשתנים על ידי כך שהצבנו בו \(a\).

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

הדרך היחידה שבה המוכיח יוכל לעבוד על המוודא כאן תהיה אם הוא יגריל פולינום \(g\) כך שבאורח קסום מתקיים \(\sum_{b_{2}\in\left\{ 0,1\right\} }\dots\sum_{b_{n}\in\left\{ 0,1\right\} }P\left(a,b_{2},\dots,b_{n}\right)=g\left(a\right)\) וזאת למרות ש-\(g\) איננו הפולינום שמוגדר על ידי המשוואה \(\sum_{b_{2}\in\left\{ 0,1\right\} }\dots\sum_{b_{n}\in\left\{ 0,1\right\} }P\left(X,b_{2},\dots,b_{n}\right)\). מה הסיכוי שזה יקרה?

באופן כללי אם \(p,q\) הם שני פולינומים במשתנה יחיד, מה ההסתברות ש-\(p\left(a\right)=q\left(a\right)\) עבור \(a\) שנבחר באקראי? ובכן, זו ההסתברות ש-\(a\) יהיה שורש של הפולינום \(p\left(a\right)-q\left(a\right)\) – פולינום שאינו זהותית אפס, ולכן מספר שורשיו הוא לכל היותר כדרגת פולינום ההפרש – נאמר שהיא \(d\). אז ההסתברות להצלחה היא \(\frac{d}{p}\), וכאן צריך לחשוב על \(d\) כקטן יחסית ועל \(p\) כגדול. מכאן שההסתברות שהמוכיח לא יצליח לעבוד על המוודא בסיבוב מסויים היא \(\left(1-\frac{d}{p}\right)\), ואם יש \(n\) סיבובים סך הכל בפרוטוקול, ההסתברות שהמוכיח לא יצליח לעבוד על המוודא באף אחד מהם היא \(\left(1-\frac{d}{p}\right)^{n}\), ולא קשה לראות שזה מספר גדול מספיק כדי שהפרוטוקול יהיה מוצלח.

האם סיימנו? חס ושלום, לא. הראינו ש-\(\overline{3\mbox{SAT}}\in\mbox{IP}\), אבל לא ש-\(\mbox{TQBF}\in\mbox{IP}\). הפרוטקול שראינו כולל את מרבית הרעיונות של ההוכחה הכללית, אבל יש תעלול אחד נוסף שנזדקק לו. למה ומדוע? נסו לחזור על אותה הוכחה עבור \(\mbox{TQBF}\) ובדקו היכן אתם נתקעים. נסביר את הכל בפוסט הבא בנושא.

אז מהי PSPACE?

שני המשאבים המרכזיים שמדברים עליהם בתורת הסיבוכיות הם זמן וזכרון. בכל הנוגע לזמן, הפורמליסטיקה בוחרת להגדיר "זמן יעיל" כזמן שהוא פולינומי בגודל הקלט של האלגוריתם – כלומר, אם \(x\) הוא הקלט וב-\(\left|x\right|\) אנחנו מסמנים את אורכו (מספר הביטים שמקודדים אותו), אז אלגוריתם הוא "יעיל" בכל הנוגע לזמן הריצה אם קיים פולינום \(p\) כך שלכל \(x\), זמן הריצה של האלגוריתם על \(x\) אינו עולה על \(p\left(\left|x\right|\right)\). זו בחירה שנראית מוזרה למדי במבט ראשון – זמן ריצה של \(n^{1000}\) הוא ממש לא מה שהיינו חושבים עליו בתור יעיל מלכתחילה, בפרט בהתחשב בכך שבעולם מדעי המחשב נעשים מאמצים לא מבוטלים לרדת מזמן כמו \(n^{3}\) אל \(n^{2}\).

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

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

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

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

בשביל חימום, בואו נדבר על \(\mbox{NP}\). ההגדרה שאני אוהב ל-\(\mbox{NP}\) מדברת על שפות שיש "עדים" לשייכות מילים אליהן. פורמלית, יש אלגוריתם "מוודא" פולינומי \(V\) שמקבל זוג \(\left(w,\pi\right)\), ועונה כן או לא, כך שאם \(w\in L\) קיים \(\pi\) כך ש-\(V\) מקבל את \(\left(w,\pi\right)\), ואם \(w\notin L\) אז לכל \(\pi\) שלא יהיה, \(V\) ידחה את \(\left(w,\pi\right)\). המגבלה היחידה על \(\pi\) היא שיהיה פולינומי בגודלו ביחס ל-\(w\), אחרת \(V\) ממילא לא יוכל לקרוא את כולו.

מכונת \(\mbox{PSPACE}\) עבור \(L\in\mbox{NP}\) תשתמש בכוח הגס האלים והברוטלי ביותר שיש: בהינתן \(w\), היא תעבור סדרתית על כל הזוגות \(\left(w,\pi\right)\) עבור \(\pi\) שאינו גדול מדי (לא חורג מהחסם הפולינומי שמאפיין את \(L\)), ולכל אחד מהם היא תריץ את \(V\) על \(\left(w,\pi\right)\) ותקבל רק אם \(V\) מקבל מתישהו. מכיוון ש-\(V\) פולינומי בזמן, הסימולציה שלו דורשת זכרון פולינומי, ואת המקום הזה אפשר למחזר בין הרצות שונות. זה גורם לכך שכל האלגוריתם הזה, למרות שהוא עובר על מספר אקספוננציאלי של \(\pi\)-ים שיש לבדוק, דורש רק זכרון פולינומי, ומכריע את אותה השפה. זה מראה ש-\(\mbox{NP}\subseteq\mbox{PSPACE}\) (ולכן אם \(\mbox{P=PSPACE}\) זה גם גורר מיידית ש-\(\mbox{P=NP}\)).

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

את \(\mbox{NP}\) אפשר להכליל באופן הבא: אם אפשר לתאר כל שפה ב-\(\mbox{NP}\) בתור \(L=\left\{ x|\exists y:\left(x,y\right)\in S\right\} \) כאשר \(S\) הוא יחס כלשהו שניתן לזהות בזמן פולינומי, אפשר גם לדבר על שפות שאפשר לתאר, למשל, על ידי \(L=\left\{ x|\exists y_{1}\forall y_{2}\exists y_{3}\forall y_{4}:\left(x,y_{1},y_{2},y_{3},y_{4}\right)\in S\right\} \). המשחק הזה של הכמתים – מעבר מ"קיים" ל"לכל" וכן הלאה – מוסיף כוח חישובי כאשר מגדירים שפות ללא מגבלות סיבוכיות (כלומר, דורשים רק ש-\(S\) תהיה ניתנת לזיהוי, לא בהכרח בזמן פולינומי). כאשר אנחנו עוברים לעולם הפולינומי, לא ידוע לנו אם אכן כל החלפת כמתים מוסיפה כוח, אבל ההשערה היא שאכן כך המצב. ב-\(\Sigma_{n}^{p}\) מסמנים את כל השפות שאפשר לתאר בצורה כזו עם \(n\) חילופי כמתים (כך למשל \(\mbox{NP}=\Sigma_{1}^{p}\). לאוסף המחלקות \(\Sigma_{n}^{p}\) שמתקבל כך קוראים ההירכייה הפולינומית, ואת האיחוד של כולן מסמנים ב-\(\mbox{PH}\).

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

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

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

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

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

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

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

איך מוכיחים את זה? ובכן, בעקיפין. כמו שאף אחד לא מוכיח באופן ישיר ש-3-צביעה היא בעיה \(\mbox{NP}\)-שלמה, כך גם כאן – יש בעיה "קנונית" שיחסית קל להוכיח עבורה באופן ישיר שהיא \(\mbox{PSPACE}\)-שלמה, וכדי להראות שבעיות אחרות דוגמת \(\mbox{GG}\) הן \(\mbox{PSPACE}\)-שלמות, פשוט מראים איך פותרים את אותה בעיה קנונית באמצעותן. השפה הקנונית עבור \(\mbox{PSPACE}\) נקראת \(\mbox{TQBF}\) – ראשי תיבות של True Quantified Boolean Formulas.

נוסחה בוליאנית מורכבת ממשתנים שיכולים לקבל "אמת" ו"שקר" – \(\mbox{T}\) ו-\(\mbox{F}\) – ומחוברים ביניהם עם "וגם" (\(\wedge\)), "או" (\(\vee\)) ו"לא" (\(\neg\)). למשל, \(\left(x_{1}\vee x_{2}\right)\wedge\left(\neg x_{1}\vee\neg x_{2}\right)\) היא נוסחה שמקבלת ערך אמת רק אם מציבים ערך אמת לאחד מהמשתנים וערך שקר לשני (זו דרך לתאר את פונקציית ה-XOR באמצעות \(\wedge,\vee,\neg\)). השפה \(\mbox{SAT}\) שהזכרתי קודם מורכבת מאוסף כל הנוסחאות הללו שהן ספיקות – כלומר, שיש השמה שמספקת אותן. \(\mbox{TQBF}\) היא הכללה כלשהי של העסק: אם \(\varphi\left(x_{1},\dots,x_{n}\right)\) היא נוסחה בוליאנית כלשהי במשתנים \(x_{1},\dots,x_{n}\), אז פסוק ה-\(\mbox{QBF}\) המתאים הוא פסוק מהצורה \(\exists x_{1}\forall x_{2}\dots\exists x_{n}\varphi\left(x_{1},\dots,x_{n}\right)\). כלומר, פסוק שאומר "קיימת השמה ל-\(x_{1}\) כך שלכל השמה ל-\(x_{2}\) קיימת השמה ל-\(x_{3}\)… קיימת השמה ל-\(x_{n}\) כך ש-\(\varphi\) מקבל ערך אמת תחת ההשמה הזו". ה"לכל" הוא בדיוק מה שמסבך את העניינים כאן, וכמו שאמרתי קודם על ההיררכייה הפולינומית, הפינג-פונג הזה בין "לכל" ו"קיים" מגדיל עוד ועוד את כושר הביטוי של הפסוקים הללו. שימו לב שבניגוד לפסוקי \(\mbox{SAT}\) חסרי הכמתים, כאן לפסוק יש ערך אמת מוגדר – הוא או נכון או לא נכון.

\(\mbox{TQBF}\) היא בדיוק שפת כל הפסוקים המכומתים שערך האמת שלהם הוא \(\mbox{T}\). הבדל מהותי בינה לבין שפות שמופיעות בהיררכייה הפולינומית הוא שאין מגבלה על מספר ההחלפות של כמתים – בתוך \(\mbox{TQBF}\) אפשר למצוא פסוקים שבהם יש 100 החלפות, או זיליארד, או \(10^{100^{100^{100}}}\) וכל מה שתרצו. וכפי שודאי כבר ניחשתם, קל מאוד להראות ש-\(\mbox{TQBF}\) שייכת ל-\(\mbox{PSPACE}\) על ידי אותם שיקולי כוח גס שכבר דיברנו עליהם. הפאנץ' הוא שזהו סוף הדרך – \(\mbox{TQBF}\) היא שפה \(\mbox{PSPACE}\)-שלמה, ולכן "הקשה ביותר" ב-\(\mbox{PSPACE}\) (כפי שכבר הבנתם, התואר הזה לא מוחזק על ידי שפה יחידה; גם \(\mbox{GG}\) מחזיקה בו). במובן מסויים, \(\mbox{TQBF}\) היא משחק מאורך פולינומי בצורתו המזוקקת ביותר: בתור הראשון, השחקן הלבן בוחר ערך (\(\mbox{T}\) או \(\mbox{F}\)) למשתנה הראשון; בתור השני השחור בוחר ערך למשתנה השני; וכן הלאה. בסוף המשחק (כשנגמרים המשתנים) מחשבים את ערך האמת של \(\varphi\) ובודקים מי ניצח.

למה \(\mbox{TQBF}\) היא \(\mbox{PSPACE}\)-שלמה? זה שייך לפוסט נפרד. ההוכחה היא יפה ומחוכמת, גם אם לא מסובכת במיוחד. את הפוסט הזה אני רוצה לסיים בשכנוע ש-\(\mbox{GG}\) קשה לפחות כמו \(\mbox{TQBF}\), כלומר להראות איך אפשר לתרגם פסוק \(\mbox{TQBF}\) כללי לגרף שעליו משחקים \(\mbox{GG}\).

ראשית, אפשר להגביל קצת את \(\mbox{TQBF}\) ולדרוש ש-\(\varphi\) יהיה \(\mbox{CNF}\) – פסוק שהוא \(\wedge\) של פסוקיות שכל אחת מהן היא \(\vee\) של ליטרלים (ליטרל הוא משתנה או שלילה של משתנה) השמה מספקת פסוק \(\varphi\) שכזה רק אם בכל פסוקית, יש לפחות ליטרל אחד שמקבל \(\mbox{T}\). באופן שקול ומסובך יותר (אבל יש סיבה שאני מציין אותו), השמה מספקת פסוק \(\varphi\) אם לא ניתן למצוא פסוקית שבה אף אחד מהמשתנים לא קיבל \(\mbox{T}\) בהשמה. נניח גם שהכמת האחרון בפסוק ה-\(\mbox{TQBF}\) הוא תמיד \(\exists\), כלומר יש מספר אי זוגי של משתנים (למעשה, זו הייתה הצורה הכללית של הפסוקים שהצגתי קודם – כמה מכם שאלו את עצמם "רגע, האם אפשר שסדרת הכמתים תסתיים גם ב-\(\forall\)"?).

כעת, הגרף שעליו ישחקו את ה-\(\mbox{GG}\) (בהינתן פסוק ה-\(\mbox{TQBF}\) שהוא אמור לסמלץ) יהיה בנוי משני רכיבים – הראשון מסמלץ את השלב שבו השחקנים בוחרים השמה למשתנים של \(\varphi\), והשני יסמלץ נסיון של השחקן השני להראות לשחקן הראשון ש-\(\varphi\) לא סופק, כלומר להציג בפני השחקן הראשון פסוקית ב-\(\varphi\) שהשחקן הראשון, לכאורה, לא יכול לספק. כרגיל, תמונה אחת שווה אלף מילים אז הנה איך הגרף נראה, ואנסה גם להסביר קצת מה הולך פה במילים:

הרכיב הראשון (השמאלי) מסמלץ את הבחירות באופן הבא: לכל משתנה \(x_{i}\) יש צומת. מהצומת הזה יוצאות שתי קשתות, אל צמתים שמסומנים ב-\(x_{i}=\mbox{F}\) ו-\(x_{i}=\mbox{T}\). הבחירה של השחקן לאיזה משני הצמתים ללכת תתורגם, בסופו של דבר, להשמה למשתנה \(x_{i}\).

אחרי שמסתיים המשחק בחלק הראשון, עוברים בחלק השני לתת-גרף שבו יש צומת לכל פסוקית של \(\varphi\). השחקן השני בוחר לאיזה מהצמתים הללו ללכת. כעת, אם בפסוקית הזו מופיע המשתנה \(x_{i}\), אז תהיה קשת מהצומת של הפסוקית אל הצומת \(x_{i}=\mbox{F}\), ואילו אם יש בה את \(\neg x_{i}\), תהיה קשת ממנה אל \(x_{i}=\mbox{T}\). זה אולי נראה לכם הפוך ממה שאמור להיות, וזה לא מקרי – הנקודה היא שאם בחלק הראשון של המשחק נבחר, למשל \(x_{i}=\mbox{T}\), אז הצומת הזה "שרוף" ואי אפשר להיכנס אליו שוב. את הכניסה לצומת \(x_{i}=\mbox{F}\) מתוך הצומת של הפסוקית שמכילה את \(x_{i}\) צריך לראות לא בתור בחירה של \(\mbox{F}\) עבור \(x_{i}\), כי כבר בחרנו ש-\(x_{i}=\mbox{T}\) בתחילת המשחק; צריך לראות אותה בתור כניסה לצומת פנוי ותו לא. מכיוון שהצומת פנוי, שחקן 1 שרד את הסיבוב הזה – ושחקן 2 הפסיד, כי הצומת שאליו חייבים להיכנס אחרי \(x_{i}=\mbox{F}\) כבר נשרף. במילים אחרות, אם לכל צומת פסוקית ששחקן 2 יבחר, שחקן 1 יכול להמשיך לבצע עוד צעד אחד במשחק (והוא יכול רק אם הפסוקית הזו הסתפקה על ידי ליטרל כלשהו), אז שחקן 2 הפסיד, ואחרת הוא ניצח.

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

מערכות הוכחה אינטראקטיביות – דוגמאות

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

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

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

גרף הוא אחד מהמושגים הבסיסיים ביותר במדעי המחשב והצגתי אותו כאן כבר מספרים פעמים. הוא מורכב מקבוצה של צמתים \(V\), ומקשתות, שהן זוגות של צמתים: \(E\subseteq V\times V\). כל זוג צמתים \(u,v\) הוא או מחובר בקשת או שלא. זה הכל; אבל המושג הפשוט הזה ממדל אינספור דברים.

שני גרפים הם איזומורפיים אם הם זהים עד כדי הסימון שאנחנו נותנים לצמתים. אצלנו תמיד נסמן את הצמתים במספרים טבעיים, כלומר \(V=\left\{ 1,2,3,\dots,n\right\} \), ואז שני גרפים \(G_{1}=\left(V_{1},E_{1}\right)\) ו-\(G_{2}=\left(V_{2},E_{2}\right)\) הם איזומורפיים אם קיימת תמורה \(\sigma\) של הקבוצה \(\left\{ 1,\dots,n\right\} \) (תמורה היא סידור מחדש של אברי הקבוצה – פונקציה חד-חד ערכית ועל מהקבוצה לעצמה) כך ש-\(\left(u,v\right)\in E_{1}\) אם ורק אם \(\left(\sigma\left(u\right),\sigma\left(v\right)\right)\in E_{2}\). במילים: שני צמתים בגרף \(G_{1}\) מחוברים בקשת אם ורק אם שני הצמתים ב-\(G_{2}\) שאליהם \(\sigma\) מעבירה אותם גם הם מחוברים בקשת. כרגיל, תמונה אחת שווה אלף מילים:

כאן יש שני גרפים. בשניהם \(V=\left\{ 1,2,3,4\right\} \), אבל בראשון \(E_{1}=\left\{ \left(1,2\right),\left(1,3\right),\left(2,4\right),\left(3,4\right)\right\} \) ובשני \(E_{2}=\left\{ \left(1,2\right),\left(1,4\right),\left(2,3\right),\left(3,4\right)\right\} \). הגרפים הללו "נראים" שונה, אבל הם איזומורפיים: \(\sigma\left(1\right)=1,\sigma\left(2\right)=2,\sigma\left(3\right)=4,\sigma\left(4\right)=3\), ששווה ערך להחלפת צמתים 3 ו-4 (דמיינו שאתם עושים זאת לגרף הראשון בלי "לקרוע" את הקשתות) הוא האיזומורפיזם המתאים.

הבה ונגדיר שפה: \(\mbox{GI}=\left\{ \left(G_{1},G_{2}\right)|G_{1}\cong G_{2}\right\} \) – שפת כל זוגות הגרפים שאיזומורפיים זה לזה. מה אפשר להגיד על השפה הזו? בבירור \(\mbox{GI}\in\mbox{NP}\) – אם יש לנו זוג גרפים איזומורפיים, אז הוכחה לכך היא פשוט \(\sigma\) שמהווה איזומורפיזם; בהינתן \(\sigma\) כזו לא קשה לבדוק את התנאי על הקשתות. האם \(\mbox{GI}\in\mbox{P}\)? זוהי שאלה פתוחה. הייחוד של \(\mbox{GI}\) היא בכך שמאוד לא סביר שהיא \(\mbox{NP}\)-שלמה – יש הוכחה לכך שאם \(\mbox{GI}\) היא \(\mbox{NP}\)-שלמה אז "דברים רעים קורים" (שוב, ההיררכייה הפולינומית קורסת). הרבה יותר סביר שיתגלה ש-\(\mbox{GI}\in\mbox{P}\), אבל לעת עתה, למרות שלפני כמה שנים היה נדמה לרגע שנמצאה לכך הוכחה – זו עדיין שאלה פתוחה.

אבל \(\mbox{GI\ensuremath{\in}NP}\), אז איך היא רלוונטית לדיון בכלל? ובכן, היא לא, אבל השפה המשלימה שלה כן: \(\mbox{GNI}\), שפת כל זוגות הגרפים שאינם איזומורפיים, היא אמנם ב-\(\mbox{coNP}\) אך איננו חושבים שהיא \(\mbox{coNP}\)-שלמה (אם היא הייתה כזו אז \(\mbox{GI}\) הייתה \(\mbox{NP}\)-שלמה) וגם איננו יודעים אם היא ב-\(\mbox{P}\). זה הופך אותה לדוגמה המושלמת עבורנו. אם כן, איך תעבוד מערכת הוכחה אינטראקטיבית לשפה זו? איך להוכיח ששני גרפים אינם איזומורפיים?

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

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

התעלול של המוודא פשוט. בתחילת הדיאלוג, הוא מטיל מטבע בחשאי לעצמו – בחשאי, כלומר באופן שבו המוכיח אינו יודע מה המוודא קיבל בהטלת המטבע. על פי הטלת המטבע המוודא בוחר אחד משני הגרפים – או \(G_{1}\) או \(G_{2}\). כעת המוודא מגריל – שוב, בחשאי – פרמוטציה \(\sigma\), ומפעיל אותה על צמתי הגרף שהוא בחר. התוצאה היא גרף חדש, \(G_{3}\), שאיזומורפי אל… אל מי, בעצם? ברור ש-\(G_{3}\) איזומורפי לגרף שהמוודא בחר, אבל אם \(G_{1}\cong G_{2}\), אז \(G_{3}\) איזומורפי לשני הגרפים גם יחד.

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

כדי להקטין את ההסתברות לטעות, המוודא יכול לשחק את המשחק הזה כמה סיבובים שמתחשק לו. אם הוא משחק \(k\) סיבובים עם המוכיח, ההסתברות שהמוכיח יצליח לעבוד עליו בכל \(k\) הסיבובים היא \(\frac{1}{2^{k}}\) – והדבר הזה שואף מהר מאוד לאפס, בלי להגדיל משמעותית את זמן החישוב של המוודא (פורמלית – יש לנו הקטנה אקספוננציאלית של הטעות במחיר הגדלה פולינומית של זמן הריצה). בקיצור, קיבלנו מערכת הוכחה אינטראקטיבית בדיוק כפי שרצינו – שלמות מלאה, נאותות בהסתברות טובה כרצוננו, וזמן ריצה פולינומי.

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

מערכת הוכחה מאוד דומה באופיה לבעיה אחרת שגם היא ב-\(\mbox{coNP}\) אך איננו יודעים אם היא ב-\(\mbox{P}\) קשורה לבעיית שארית ריבועית. בהינתן מספר \(n\), אומרים ש-\(a\) הוא שארית ריבועית מודולו \(n\) אם קיים פתרון למשוואה \(x^{2}\equiv a\left(\mbox{mod n}\right)\). במילים אחרות, קיים \(x\) כלשהו שכאשר מעלים אותו בריבוע ומחלקים ב-\(n\), השארית המתקבלת שווה לשארית החלוקה של \(a\) ב-\(n\). בדרך כלל מסתפקים בדיון הזה במספרים שהם בין \(0\) ו-\(n-1\).

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

כעת, נניח שהמוכיח רוצה להוכיח למוודא ש-\(a\) כלשהו, שסימן יעקובי שלו הוא 1, איננו שארית ריבועית. אז מערכת הוכחה תתנהל כך: המוודא יגריל מספר כלשהו \(x\) בין \(0\) ל-\(n-1\),יגריל ביט \(b\in\left\{ 0,1\right\} \), ישלח למוכיח את \(a^{b}x^{2}\) וישאל אותו מהו הביט \(b\). איך המוכיח יכול לענות על כך?

ובכן, נניח ש-\(a\) אינו שארית ריבועית. אז \(a^{0}x^{2}=x^{2}\) הוא בוודאי שארית ריבועית; ואפשר להוכיח ש-\(a^{1}x^{2}\) איננו שארית ריבועית (מכיוון ששארית ריבועית כפול משהו שאינו שארית ריבועית, אינו שארית ריבועית). לכן המוכיח (הכל יכול מבחינה חישובית) רק צריך לבדוק אם מה ששלחו לו הוא שארית ריבועית או לא, ולענות 0 או 1 בהתאם. לעומת זאת, אם \(a\) הוא כן שארית ריבועית, אז גם \(x^{2}\) וגם \(ax^{2}\) הם שאריות ריבועיות, ומכיוון ש-\(x\) נבחר באקראי, גם \(x^{2}\) וגם \(ax^{2}\) שניהם שאריות ריבועיות אקראיות כלשהן, ולכן למוכיח לא יהיה מושג מה לענות. אני מניח שהדמיון למערכת ההוכחה של איזומורפיזם הגרפים ברור.

עכשיו אני לא יכול להתאפק ורוצה לעבור לדון בסוג מיוחד של הוכחות אינטראקטיביות – הוכחות באפס ידע. את המושג הזכרתי כבר בראשית ימי הבלוג (לינק), אבל עכשיו זו הזדמנות פז להציג עוד כמה דוגמאות. נתחיל מאיזומורפיזם גרפים – השפה \(\mbox{GI}\), של זוגות גרפים שהם כן איזומורפיים. האם יש דרך שבה המוכיח יוכיח למוודא ש-\(G_{1}\cong G_{2}\)? ובכן, כמובן, הוא יכול פשוט לתת לו את פונקציית האיזומורפיזם, \(\sigma\); אבל האם יש דרך שבה הוא יכול להוכיח זאת למוודא מבלי לגלות לו כלום? כלומר, שהמידע היחיד שהמוודא יסיק מתהליך ההוכחה הוא שאכן \(G_{1}\cong G_{2}\)?

התשובה חיובית, והפתרון פשוט ואלגנטי ביותר. הוא מעין היפוך להוכחה של \(\mbox{GNI}\). המוכיח יגריל פרמוטציה כלשהי \(\pi\), יבחר באקראי אחד משני הגרפים \(G_{1},G_{2}\), יפעיל את הפרמוטציה עליו ויקבל גרף \(G_{3}\), וישלח אותו למוודא. המוודא יגריל מספר \(b\) – או 1, או 2, וישלח אותו למוכיח. המוכיח ישלח לו בחזרה פרמוטציה שהופכת את \(G_{b}\) ל-\(G_{3}\). האם אתם רואים מדוע זו מערכת הוכחה אינטראקטיבית?

אולי אתם תוהים לעצמכם – רגע, מה אפס ידע כאן? הרי המוודא לומד משהו מהפרוטוקול – המוכיח נותן לו איזומורפיזם בין \(G_{b}\) ובין גרף חדש, \(G_{3}\). זה נכון, כמובן, אבל הנקודה היא שהידע הזה הוא משהו שהמוודא היה יכול לייצר בעצמו. מה כבר המוודא קיבל? איזומורפיזם בין אחד מהגרפים המקוריים ובין גרף אקראי כלשהו. גם המוודא יכל לייצר את זה בצורה פשוטה – להגריל \(\pi\), להפעיל אותה על אחד מהגרפים, ולקבל כתוצאה גרף חדש, שנבחר באקראי בין הגרפים שאיזומורפיים ל-\(G_{1},G_{2}\). זו המשמעות של אפס-ידע – המוודא לא לומד שום אינפורמציה שהוא לא יכל לייצר בעצמו. יש לכל זה גם הגדרה מתמטית מדויקת, כמובן, והצגתי את הרעיון שמאחוריה בעבר, אבל לא אחזור עליה כעת.

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

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

לא ארחיב יותר מכך כרגע על העניינים הללו כי זה ראוי לפוסט נפרד שעוסק בהוכחות אפס ידע; רק אעיר שניתן לפתור את הבעיה ולהציג מערכות אפס-ידע טובות גם עבור \(\mbox{GNI}\) ו-\(\mbox{QNR}\). לבינתיים נשכח מהוכחות אפס ידע – בפוסט הבא אתאר את \(\mbox{PSPACE}\) ולאחר מכן אעבור להוכחה ש-\(\mbox{IP=}\mbox{PSPACE}\).

אז מה זה IP=PSPACE?

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

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

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

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

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

בואו נביט שניה בהוכחה מתמטית "אמיתית" – הוכחת אוקלידס לקיום אינסוף ראשוניים. נניח שיש רק מספר סופי של ראשוניים, \(p_{1},\dots,p_{n}\). נתבונן במספר \(p_{1}\cdot p_{2}\cdots p_{n}+1\). הוא אינו מתחלק על ידי אף אחד מהראשוניים \(p_{1},\dots,p_{n}\) ולכן מתחלק על ידי ראשוני שונה מהם. זו כל ההוכחה, ועבור המתמטיקאים בקהל אני בטוח שהיא מספיקה, אבל האם זו הייתה "סדרה סופית של טענות הנובעות זו מזו בעזרת כללי היסק תוך שימוש באקסיומות"? היכן האקסיומות? מיהם כללי ההיסק? גרוע מכך – ההוכחה הזו מבצעת הרבה קפיצות לוגיות שעבור מתמטיקאי הן טריוויאליות אבל עבור מי שמעולם לא ראה אותן הן אסון. למה ש-\(p_{1}\cdots p_{n}+1\) לא יתחלק על ידי אף אחד מהראשוניים \(p_{1},\dots,p_{n}\)? ולמה שהוא כן יתחלק על ידי ראשוני שונה מהם? אלו דברים שדורשים הסבר – למעשה, הוכחה – בפני עצמם. ויקיפדיה העברית מכסה זאת בעזרת "ידע קודם שהוכח קודם לכן", אבל כשקוראים הוכחות אמיתיות לא תמיד ברור בכלל לאיזה ידע קודם ההוכחה מתייחסת. בהוכחות אמיתיות (כאלו שנמצאות בספרי לימוד מתקדמים; בספרי מבוא לרוב ההוכחות יותר ידידותיות) יש המון קפיצות מעל פרטים שרק קורא שבקיא בחומר מסוגל להשלים; קוראים אחרים נאלצים לפנות לעזרה לאתרים כמו MathOverflow ו-MathStackExchange.

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

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

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

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

  1. שלמות: אם \(w\in L\) אז קיימת הוכחה \(\pi\) כלשהי כך ש-\(V\) מסיים את ריצתו על \(\left(w,\pi\right)\) ואומר "כן".
  2. נאותות: אם \(w\notin L\) אז לכל \(\pi\) מתקיים ש-\(V\) מסיים את ריצתו על \(\left(w,\pi\right)\) ואומר "לא".

כלומר, אם \(V\) אומר "כן" הוא בעצם אומר "כן, \(w\in L\) וההוכחה \(\pi\) שכנעה אותי בכך", ואילו אם הוא אומר "לא", הוא בעצם אומר "\(\pi\) לא שכנעה אותי ש-\(w\in L\)" (שימו לב להבדל המהותי בין זה ובין לומר "\(w\notin L\), נקודה").

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

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

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

כעת, לבעיה העשירית של הילברט יש מערכת הוכחה פשוטה למדי: אם \(w\) משוואה דיופנטית בעלת פתרון, אז \(\pi\) יקודד את הפתרון הזה – ההצבה הנדרשת למשתנים. אם \(V\) מקבל \(\left(w,\pi\right)\), הוא פשוט מציב את \(\pi\) לתוך המשוואה ובודק אם המשוואה מתקיימת או לא. אם כן – האח, \(V\) עוצר ומקבל; אחרת, \(V\) עוצר ודוחה. בכל מקרה \(V\) יעצור, ובכל מקרה הוא לא יטעה – אם ל-\(w\) יש פתרון, אז יש \(\pi\) שעבורו \(V\) יקבל (ההצבה ה"נכונה" למשוואה) ואם ל-\(w\) אין פתרון, ברור שלכל \(\pi\) הוא ידחה.

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

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

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

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

אינטראקציה אומרת כך – הבה ונניח שלמשחק שלנו מצטרף מתמודד נוסף, \(P\), "המוכיח" (מלשון Prover). בהינתן \(w\) \(P\) ו-\(V\) מנהלים ביניהם דיאלוג כלשהו – \(P\) שולח ל-\(V\) הודעה, ו-\(V\) יכול לענות לו, ו-\(P\) יכול לענות לתשובה, וכן הלאה. בסופו של דבר (ואחרי זמן פולינומי) \(V\) צריך לעצור ולקבל או לדחות את \(w\), ואותם תנאים של קודם תקפים: אם \(w\in L\) אז יש דרך התנהגות של \(P\) שגורמת ל-\(V\) לקבל, ואילו אם \(w\notin L\) אז לא משנה איך \(P\) יתנהג, \(V\) ידחה. בדרך כלל במערכות כאלו אנחנו לא מניחים ש-\(P\) מוגבל חישובית בכלל – כמו שקודם ההוכחה \(\pi\) הגיעה באורח קסום מהשמיים, כך גם כאן \(P\) הוא יצור שמיימי בעל כוח לא מוגבל (בעולם האמיתי הכוח של \(P\) ינבע לרוב מכך שיש לו כבר ידע קודם שאין ל-\(V\); למשל, \(P\) יכיר פירוק לגורמים של מספר \(n=pq\) מסויים בגלל שהוא עצמו הגריל מלכתחילה את \(p,q\) והכפיל אותם לקבלת \(n\)) .

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

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

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

הדרך השניה להתקדם מכאן היא להרשות אקראיות. נניח שאנחנו מרשים למוודא לעשות הגרלות שמכתיבות אילו שאלות הוא ישאל את המוכיח. האם שינינו משהו? ובכן, אם כל מה שעשינו הוא להרשות למוודא להגריל שאלות, לא ממש. כי איך תיראה כעת הוכחה \(\pi\) לנכונות טענה? היא תהיה מהצורה "אם היית מגריל 0, אז היית שואל כך וכך, והייתי עונה לך: 42! ואז, אם היית מגריל 1, היית שואל כך וכך והייתי עונה לך: 17! ואחר כך…". כלומר, ההוכחה מתארת מה קורה בסדרת בחירות אקראיות אחת של המוודא ומתעלמת מכל שאר האפשרויות. למה זה עובד? כי בסוף הדיאלוג הזה, התשובה של המוודא חייבת להיות התשובה הנכונה. אם \(w\in L\), כל מה שאנחנו רוצים הוא לראות סדרת בחירות אקראיות אחת שגורמת למוודא להשיב "כן". לא אכפת לנו מכך שיש אולי עוד המון דרכים לגרום לו להשיב כך.

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

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

כמה כוח הוסיפה לנו האפשרות הן לבצע אינטראקיציה והן האקראיות? כלומר, מהי המחלקה \(\mbox{IP}\) של כל השפות שיש להן הוכחה אינטראקטיבית הסתברותית? זו הייתה שאלה פתוחה במשך זמן מה מאז שהוצע המודל. היה ברור לכולם ש-\(\mbox{IP}\subseteq\mbox{PSPACE}\), אבל מה פרט לכך? כשהסתכלו על המחלקות \(\mbox{IP}\left[k\right]\) של מערכות הוכחה שבהן האינטראקציה היא של \(k\) סיבובים בדיוק, התגלה שלא משנה כמה \(k\) גדול – כל עוד הוא קבוע בגודלו ביחס לגודל הקלט, מתקבלת המחלקה \(\mbox{IP}\left[2\right]\). רק כשמרשים למספר הסיבובים בפרוטוקול להיות פולינומי בגודלו ביחס לגודל הקלט יש סיכוי לקבל משהו גדול יותר. מצד שני, אף אחד לא הכיר בעיות שדרשו זאת – כל מה שמצאו לו פרוטוקול הוכחה אינטראקטיבית (ונראה לכך דוגמאות בפוסט הבא) יכל להסתפק במספר קבוע של סיבובים. בנוסף, לא נראה היה שהסתברות אמורה להוסיף הרבה כוח למערכת ההוכחה; הסתברות לא מוסיפה הרבה כוח גם לחישוב רגיל (המחלקה \(\mbox{BPP}\), שהיא המקבילה ההסתברותית של \(\mbox{P}\), איננה גדולה מ-\(\mbox{P}\) בהרבה ויש החושדים ש-\(\mbox{P=BPP}\), ובניגוד ל-\(\mbox{P=NP}\) זה גם לא מופרך).

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

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

NL=coNL ("משפט אימרמן") – מי, מה, כמה ולמה

בשעה טובה אנו עוברים לתיאור התוצאה השניה ב"תוצאות מפתיעות בסיבוכיות" – הטענה \(\mbox{NL=coNL}\), או בשמה הקליט יותר, משפט אימרמן-סזלפסני (Immerman–Szelepcsényi – אין לי מושג איך לתעתק נכון), ומכאן ואילך – משפט אימרמן. אז על מה מדובר בכלל?

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

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

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

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

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

בואו נעבור לקצת טרמינולוגיה מדוייקת. במקום לדבר כל הזמן על "בעיות", מדעני מחשב מדברים על "שפות". שפה \(L\) היא קבוצה של מילים – מחרוזות סופיות של תווים שמייצגות מידע כלשהו (יכול להיות קידוד של גרף, מספר, קוד של תוכנית מחשב וכדומה – תלוי בהקשר). למשל, אפשר לדבר על השפה שמכילה את כל השלשות \(\left(G,s,t\right)\) של גרף \(G\) עם צמתים \(s,t\) שיש מסלול ביניהם. השפה המשלימה ל-\(L\) מסומנת ב-\(\overline{L}\) – זו שפת כל המחרוזות שאינן ב-\(L\) (מראש אנחנו מניחים שמדובר רק על מחרוזות בינאריות, נאמר, כך שהשאלה מהי "קבוצת כל המחרוזות" שלוקחים משלים ביחס אליה איננה בעייתית). למשל, אם \(L\) היא שפת השלשות \(\left(G,s,t\right)\) שתיארתי קודם, אז \(\overline{L}\) היא שפת השלשות \(\left(G,s,t\right)\) של גרף \(G\) עם צמתים \(s,t\) שאין ביניהם מסלול. אולי תגידו עכשיו שיש גם הרבה מחרוזות שבכלל לא מייצגות שלשה \(\left(G,s,t\right)\) וגם הן אמורות להיות ב-\(\overline{L}\); כדי להיפרד מהמטרד הלא חשוב הזה נוח להניח שכל מחרוזת שמייצגת ג'יבריש תיחשב על ידינו ככזו שמייצגת איזה קלט טריוויאלי – למשל \(\left(G,s,t\right)\) כאשר \(G\) הוא גרף ששני הצמתים היחידים בו הם \(s,t\) ואין קשתות. הדברים הללו לא קריטייים ממילא ולא אחזור אליהם.

אומרים ש-\(L\in\mbox{NP}\) אם קיימת מכונת טיורינג \(M\) (ואם "מכונת טיורינג" לא אומר לכם כלום, תחשבו על "אלגוריתם" ותו לא; הסיבה שאומרים מכונת טיורינג במקום אלגוריתם היא אך ורק מכיוון שאין הגדרה מתמטית פורמלית ל"אלגוריתם") כך שאם \(x\in L\) אז קיים \(y\) כלשהו כך ש-\(M\left(x,y\right)=\mbox{ACCEPT}\), כלומר \(M\), כשהיא רצה על הקלטים \(x,y\) יחדיו, מסיימת את ריצה עם הפלט "Accept"; ואם \(x\notin L\) אז לכל \(y\) \(M\left(x,y\right)=\mbox{REJECT}\), כלומר \(M\) דוחה כל "נסיון הוכחה" לכך ש-\(x\in L\) במקרה שבו \(x\notin L\). יש על \(M\) דרישה אחת נוספת – שזמן הריצה שלה יהיה יעיל – פולינומי – ביחס לגודל של \(x\). הדרישה הזו אוטומטית גם מגבילה את \(y\); אם \(y\) הוא יותר מאשר פולינומי בגודל של \(x\), אז \(M\) פשוט לא תספיק לקרוא את כולו. לכן לפעמים כדי לפשט את החיים דורשים מראש ש-\(y\) יהיה פולינומי ב-\(x\).

כעת אפשר להציג את \(\mbox{coNP}\): בפשטות, זו המחלקה של כל המשלימות של שפות ב-\(\mbox{NP}\): \(\mbox{coNP}=\left\{ \overline{L}|L\in\mbox{NP}\right\} \). כפי שתיארתי לעיל, זו מחלקת השפות שיש עבורן תהליך-בדיקת-עדים כך שעבור תשובת "לא" קיים \(y\) שמוכיח זאת, ועבור תשובת "כן" אין אף \(y\) שיעבוד עלינו ויגרום לנו לחשוב שהתשובה היא "לא". וכעת לשאלת המחץ: האם \(\mbox{NP}=\mbox{coNP}\)?

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

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

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

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

כעת אפשר להכניס לתמונה סוף סוף את \(\mbox{NL}\)- זו מחלקת השפות שקיימת מכונה-בודקת-עדים עבורן שפועלת בסיבוכיות זכרון \(O\left(\log n\right)\). את \(\mbox{coNL}\) מגדירים באותו אופן כמו \(\mbox{coNP}\). לא מדויק עד הסוף לומר שאלו האנלוגים של \(\mbox{NP}\) ו-\(\mbox{coNP}\) כי גם סיבוכיות זכרון של \(O\left(\log^{k}n\right)\) ("פולי-לוגריתמית") נחשבת יעילה; אבל שתי המחלקות הללו הן הבסיס. ולכן זו הפתעה לא קטנה כשמתברר שהאנלוג לשאלת \(\mbox{NP=coNP}\) עבור סיבוכיות זכרון זוכה לתשובה, ותשובה חיובית דווקא – \(\mbox{NL=coNL}\), ויותר מכך – לכל מחלקה דומה של סיבוכיות זכרון עבור חסם סיבוכיות גדול יותר, השוויון עדיין מתקיים (פורמלית, \(\mbox{NSPACE}\left(f\left(n\right)\right)=\mbox{coNSPACE}\left(f\left(n\right)\right)\) לכל \(f\left(n\right)\ge\log n\) שהיא מה שנקרא Space Constructible, כלומר ניתן לחשב את \(f\left(n\right)\) מתוך \(1^{n}\) תוך שימוש ב-\(f\left(n\right)\) זכרון – זו דרישה טכנית שלא ניכנס כעת אליה). הטענה הוכחה בנפרד בידי אימרמן ובידי סזלפסני, ואין לי מושג מי מהם המציא את ההוכחה שאציג כעת (או אפילו אם מישהו מהם המציא אותה ולא שמדובר על המצאה מאוחרת יותר).

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

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

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

לבדוק שיש מסלול מ-\(s\) אל \(t\) בסיבוכיות זכרון יעילה, בהינתן עד לכך, את זה קל לעשות. העד יהיה תיאור המסלול עצמו, ולכן כל המידע שצריך לזכור בכל שלב הוא מה הצומת הנוכחי שלנו, לקרוא את הצומת הבא בתור מהעד, ולבדוק אם אכן יש קשת מהצומת הנוכחי שלנו אל הצומת הבא בתור שהעד מדבר עליו. מכיוון שלייצג צומת או שניים בודדים דורש רק כמות לוגריתמית של זכרון, זה מראה ש-\(\mbox{CON}\in\mbox{NL}\). האתגר הוא להראות ש-\(\mbox{CON}\in\mbox{coNL}\) – שיש עד לכך שלא קיים מסלול מ-\(s\) אל \(t\). איך עושים דבר כזה? התשובה פשוטה – אומרים את האמת, רק האמת, וכל האמת.

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

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

אז מה ראינו? שבזכרון לוגריתמי אפשר לבדוק הוכחה לטענה מהצורה "לפחות \(k\) הצמתים הללו ישיגים מ-\(s\) ב-\(n\) צעדים", ושאם זה משולב בטענה מהצורה "בדיוק \(k\) צמתים ישיגים מ-\(s\) ב-\(n\) צעדים" אפשר להוכיח שמשהו לא ישיג מ-\(s\) ב-\(n\) צעדים. אבל איך אפשר להראות שבדיוק \(k\) צמתים ישיגים מ-\(s\) ב-\(n\) צעדים? ובכן, באינדוקציה על \(n\).

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

בואו נעצור לרגע כדי לקחת אוויר ולסכם את מה שראינו עד כה. ראינו כי ניתן להוכיח (באופן שיהיה ניתן לבדיקה בזכרון לוגריתמי) שצומת כלשהו ישיג מ-\(s\) ב-\(n\) צעדים. כמו כן, ראינו כי אם \(C_{n}\) נתון, אז אפשר להוכיח שצומת כלשהו אינו ישיג מ-\(s\) ב-\(n\) צעדים. למעשה, אפשר גם להוכיח באופן דומה מאוד שצומת \(v\) אינו ישיג מ-\(s\) ב-\(n+1\) צעדים – פשוט מוכיחים שאף שכן של \(v\) אינו ישיג מ-\(s\) ב-\(n\) צעדים (ולמי שנזקק לפירוט – ההוכחה כוללת את רשימת כל \(C_{n}\) השכנים שכן ישיגים ואת המסלול שמוביל להם; בזמן שבודקים שההוכחה נכונה גם בודקים שכל אחד מהשכנים הללו אינו שכן של \(v\)).

עכשיו בואו נעצור לרגע ונחשוב – איך כל המרכיבים הללו פותרים לנו את הבעיה של חישוב \(C_{n+1}\)? תנסו לחשוב על זה בעצמכם לרגע. תזכרו שאנחנו כלל לא צריכים להיות חסכוניים בזמן ריצה, רק בזכרון.

התשובה פשוטה: ההוכחה לערך החדש של \(C_{n+1}\) תהיה מורכבת מרשימת כל הצמתים בגרף לפני הסדר, כשאחרי כל צומת ההוכחה או טוענת שהוא ישיג מ-\(s\) ב-\(n+1\) צעדים ומספקת תת-הוכחה שמראה זאת, או טוענת שהוא אינו ישיג מ-\(s\) ב-\(n+1\) צעדים, ומספקת תת-הוכחה שמראה זאת. הגאונות כאן היא בכך שאחרי שאנחנו גומרים עם צומת, אנחנו לא צריכים לזכור אם הוא היה ישיג או לא היה ישיג (זה ידרוש מאיתנו הרבה יותר מדי זכרון לתחזק רשימה כזו) אלא רק להוסיף או לא להוסיף 1 למונה של \(C_{n+1}\) שאנחנו מתחזקים. אחרי שנסיים לקרוא את ההוכחה (שכאמור, אומרת פרטנית מה קורה לכל אחד מהצמתים בגרף), יהיה ברשותנו הערך הנכון של \(C_{n+1}\).

מכאן הפתרון לבעיה טריוויאלי. ההוכחה לכך ש-\(t\) לא ישיג מ-\(s\) ראשית כוללת הוכחות לסדרת הערכים \(C_{1},C_{2},\dots,C_{\left|V\right|}\), ולבסוף היא כוללת הוכחה לכך ש-\(t\) אינו ישיג מ-\(s\) ב-\(\left|V\right|\) צעדים, שמסתמכת על \(C_{\left|V\right|}\). אם \(t\) לא ישיג מ-\(s\) ב-\(\left|V\right|\) צעדים, הוא לא ישיג בכלל (למה?).

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

משפט ברינגטון – ההוכחה

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

בתור חימום בואו נראה את הכיוון הקל. הוא אמנם קל, אבל גם כאן בוודאי אאבד קוראים – מי שיישבר, אני מציע לו לפחות לקפוץ לתחילת ההוכחה של הכיוון השני כדי לראות את "הרעיון הגדול" של ההוכחה.

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

את \(R\left(a,b\right)\) קל לבנות במקרה שבו \(a,b\) הם צמתים משכבות סמוכות (הגרף של התוכנית המתפצלת הוא גרף שכבות, זוכרים?). במקרה כזה יש מסלול מ-\(a\) אל \(b\) אם ורק אם יש קשת מ-\(a\) ל-\(b\), מה שמצריך שני דברים: ראשית, שבתוכנית המתפצלת המקורית הייתה קשת מ-\(a\) אל \(b\) (שמסומנת או ב-0 או ב-1) ושנית, שהיא "שרדה" את ההשמה. אם הצומת \(a\) מסומן במשתנה \(x_{i}\), אז נגדיר את הנוסחה באופן הבא: \(R\left(a,b\right)=x_{i}\) אם הקשת \(a\to b\) מסומנת ב-1, \(R\left(a,b\right)=\neg x_{i}\) אם הקשת \(a\to b\) מסומנת ב-0, ו-\(R\left(a,b\right)=0\) אם אין קשת.

נוסחה כללית עבור \(a,b\) שיכולים להיות מרוחקים זה מזה יותר מאשר שכבה אחת נבנית ברקורסיה. הטיעון הבסיסי הוא זה: אם המרחק בין \(a\) ל-\(b\) גדול מ-1 אבל יש מסלול ביניהם, אז יש צומת \(c\) שנמצא "באמצע הדרך" ביניהם. במילים אחרות, יש מסלול מ-\(a\) אל \(b\) אם ורק אם קיים צומת \(c\) בשכבה שהיא באמצע הדרך בין \(a\) ל-\(b\), כך ש-\(R\left(a,c\right)\) וגם \(R\left(c,b\right)\). לאלו מכם שמכירים קצת סיבוכיות כל זה ודאי מזכיר את ההוכחה של משפט סביץ', שמשתמש בתעלול דומה.

מה לכאורה הבעיה? שבשכבת הביניים בין \(a\) ל-\(b\) יכולים להיות המון \(c\)-ים ואז הנוסחה שלנו, שצריכה לקחת בחשבון את כולם, תהיה ענקית. אלא שאנחנו מתעסקים כאן עם תוכניות שהרוחב שלהם חסום, ולכן יש רק \(d\) צמתים \(c\) אפשריים לכל היותר, מה שאומר שהנוסחה שלנו לא תהיה עד כדי כך גדולה. נכתוב אותה במפורש: \(R\left(a,b\right)=\bigvee_{c}\left(R\left(a,c\right)\wedge R\left(c,b\right)\right)\).

נותר להבין מהו גודל הנוסחה. נניח שהמרחק בין \(a\) ו-\(b\) הוא \(k\). נסמן ב-\(S\left(k\right)\) את הגודל המקסימלי של נוסחה שנבנית בשיטה שלנו עבור צמתים במרחק \(k\). אז ראינו כי \(S\left(1\right)\) הוא קבוע קטן, וכי \(S\left(k\right)\le2d\cdot S\left(\frac{k}{2}\right)\) (כי בנוסחה עבור \(R\left(a,b\right)\) שכתבנו, כל \(R\left(a,c\right)\) ו-\(R\left(c,b\right)\) שמופיעים שם עוסקים בזוג צמתים שהמרחק ביניהם הוא בערך \(\frac{k}{2}\) – זאת מכיוון שבחרנו את \(c\) להיות באמצע הדרך). פתרון נוסחת הנסיגה שלעיל מניב ש-\(S\left(k\right)=O\left(\left(2d\right)^{\lg k}\right)=O\left(k^{\lg\left(2d\right)}\right)\), ולכן נקבל שגודל הנוסחה \(R\left(s,t\right)\) עבור התוכנית המתפצלת כולה, גודל המעגל המתאים יהיה \(O\left(t^{\lg\left(2d\right)}\right)\) – זהו פולינום ב-\(t\), שכן \(d\) קבוע. זה סוף הכיוון הזה – עד כה לא ראינו רעיונות מעניינים חריגים.

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

בואו נעשה קצת סדר בעניינים. אצל ברינגטון, כל שכבה בתוכנית המתפצלת כוללת חמישה צמתים בדיוק. נמספר אותם \(1,2,3,4,5\). עכשיו, מהצומת \(1\) יוצאות שתי קשתות – אחת שמסומנת ב-0 ואחת שמסומנת ב-1. הקשת שמסומנת ב-0 נכנסת לאחד מהצמתים בשכבה הבאה, שגם בה כל הצמתים מסומנים ב-1 עד 5. נניח שהקשת נכנסת ל-3, אז אנחנו אומרים ש-"1 עובר ל-3 עם הקשת 0". באותו אופן כל אחד מחמשת המספרים עובר למספר בשכבה הבאה עם קשת ה-0 שלו. ברינגטון מוודא שלא יהיו שני מספרים שעוברים לאותו צומת בשכבה הבאה, ולכן אכן מתקבלת כאן פרמוטציה. באותו אופן, אם שוכחים מהקשתות שמסומנות ב-0 ומסתכלים רק על אלו שמסומנות ב-1, מקבלים פרמוטציה אחרת, אולי שונה מהראשונה.

על כן, אומר ברינגטון, אפשר לאפיין כל שכבה בגרף באמצעות השלשה הבאה: \(\left(k,\pi^{0},\pi^{1}\right)\). \(k\) הוא מספרו של המשתנה \(x_{k}\) אשר מופיע על כל צומת בשכבה הזו. \(\pi^{1}\) היא הפרמוטציה שמתאימה לקשתות שמסומנות ב-0, ו-\(\pi^{1}\) היא הפרמוטציה שמתאימה לקשתות שמסומנות ב-1. בצורה הזו אפשר לבנות את כל התוכנית, ולקבל סדרה של שלשות: \(\left(k_{1},\pi_{1}^{0},\pi_{1}^{1}\right),\left(k_{2},\pi_{2}^{0},\pi_{2}^{1}\right),\dots,\left(k_{t},\pi_{t}^{0},\pi_{t}^{1}\right)\). זוהי תוכנית מאורך \(t\).

כעת, השמה של ערכים למשתנים כמוה בעצם כבחירה של פרמוטציה אחת מכל זוג, והכפלת כל הפרמוטציות שנבחרו כדי לקבל פרמוטציה שמייצגת את הגרף כולו. אם \(\pi\) היא מה שהתקבל מהכפל הזה, אז \(\pi\left(1\right)\) הוא מספרו של הצומת שאליו מגיעים בשכבה האחרונה אם מתחילים את הטיול מצומת 1 בשכבה הראשונה והולכים עם הקשתות שההשמה הותירה בחיים עד שמגיעים לשכבה האחרונה. בדומה \(\pi\left(2\right)\) זה אותו הדבר אם מתחילים מצומת 2 בשכבה הראשונה, וכן הלאה. פורמלית זה מוגדר כך: \(\pi=\prod_{i=1}^{t}\pi_{i}^{x_{k_{i}}}\).

אם כן, אפשר לחשוב על תוכנית מתפצלת \(P\) כעל פונקציה שמקבלת קלט \(\overline{x}\) של ביטים ופולטת פרמוטציה ב-\(S_{5}\): \(P\left(\overline{x}\right)=\pi\). אבל איך כל זה קשור לפלט של 0 או 1? פשוט מאוד: נבחר מראש פרמוטציה ספציפית \(\sigma\in S_{5}\) ששונה מפרמוטציית הזהות, שאותה אסמן ב-\(e\) (הפרמוטציה שמעבירה כל איבר לעצמו). אז נגדיר ש-\(P\) מחזירה 1 על קלט \(\overline{x}\) אם \(P\left(\overline{x}\right)=\sigma\), ושהיא מחזירה 0 אם \(P\left(\overline{x}\right)=e\). בניסוח שיהיה נוח בהמשך – \(P\) \(\sigma\)-מחשבת את \(f\) אם מתקיים הדבר הבא: עבור ערכים של \(\overline{x}\) שעבורם מתקיים \(f\left(\overline{x}\right)=1\), \(P\) מקיימת ש-\(P\left(\overline{x}\right)=\sigma\), ואילו עבור ערכים שעבורם מתקיים \(f\left(\overline{x}\right)=0\), \(P\) מקיימת ש-\(P\left(\overline{x}\right)=e\).

זה שהגדרנו את זה כך זה טוב ויפה אבל לא מתאים להגדרה המקורית של תוכנית מתפצלת – כזכור, תוכנית מתפצלת מחזירה 1 אם יש מסלול מ-\(s\) ל-\(t\) אחרי ההשמה למשתנים, ו-0 אם אין מסלול כזה. אצלנו טרם בחרנו את \(s\) ו-\(t\), והרעיון הוא לבחור אותם באופן שמתאים ל-\(\sigma\). מכיוון ש-\(\sigma\ne e\) קיים \(i\) כך ש-\(\sigma\left(i\right)\ne i\); אם כן, נגדיר את \(s\) להיות הצומת מס' \(i\) בשכבה הראשונה, ואת \(t\) להיות הצומת מס' \(\sigma\left(i\right)\) בשכבה האחרונה, וסיימנו: אם \(P\left(\overline{x}\right)=\sigma\) אז אכן יש מסלול מ-\(i\) אל \(\sigma\left(i\right)\), ואם \(P\left(\overline{x}\right)=e\) אז המסלול היחיד מ-\(i\) הוא אל \(e\left(i\right)=i\ne\sigma\left(i\right)\), כלומר אין מסלול מ-\(s\) אל \(t\).

מסקנת ביניים, למי שאיבד אותי: אנחנו יכולים לשכוח מתוכניות מתפצלות ולעבור לדבר על "תוכניות פרמוטציה" – תוכניות שמתוארת על ידי שלשות של משתנה וזוג פרמוטציות כפי שתיארתי לעיל. מספיק להראות שהן מסוגלות לסמלץ כל מעגל \(\mbox{NC}^{1}\) ולעשות זאת תוך שמירה על אורך לא גדול במיוחד (כלומר, \(t\) קטן).

אולי קצת יותר ברור כעת המספר 5 – הוא אומר שהפרמוטציות שמשתתפות במשחק כולן מ-\(S_{5}\). אבל למה דווקא \(S_{5}\) ולא \(S_{4}\) או \(S_{6}\)? ובכן, עוד מעט נראה.

אמרתי שהסיבה שפרמוטציות מועילות לנו היא כי הן מצד אחד יצורים פשוטים ומצד שני מורכבים – בואו ננסה להבין איך זה מתבטא. ראשית, כמו שכבר ראינו, קיימת פעולת "כפל" של פרמוטציות שפירושה הוא פשוט להפעיל אותן בזו אחר זו: \(\sigma\tau\) זו הפרמוטציה שבה מפעילים את \(\tau\) ואחר כך את \(\sigma\). באופן כללי זה לא חייב להיות שווה ל-\(\tau\sigma\) – נסו למצוא דוגמה שבה אכן \(\tau\sigma\ne\sigma\tau\). כמו כן לכל פרמוטציה קיימת \(\sigma^{-1}\) פרמוטציה הפוכה שעבורה \(\sigma\sigma^{-1}=\sigma^{-1}\sigma=e\). לא קשה לראות שהפרמוטציות מהוות מה שנקרא חבורה עם פעולת הכפל המדוברת, אבל זה לא יהיה קריטי יותר מדי עבורנו כאן.

פרמוטציות פשוטות במיוחד הן מעגלים: אם למשל \(\sigma\left(1\right)=3\) ו-\(\sigma\left(3\right)=5\) ו-\(\sigma\left(5\right)=1\) אז \(\sigma\) כוללת בתוכה את המעגל \(\left(1\ 3\ 5\right)\) ("1 עובר ל-3 שעובר ל-5 שעובר ל-1"). לא קשה להוכיח שכל פרמוטציה אפשר לכתוב כמכפלה של מעגלים זרים, כלומר כאלו שכל מספר מופיע רק באחד מהם. למשל, \(\left(1\ 4\right)\left(2\ 5\right)\left(3\right)\) היא הפרמוטציה שמעבירה את 1 ל-4, את 4 ל-1, את 2 ל-5, את 5 ל-2, ואת 3 לעצמו. אנחנו מדברים על פרמוטציות על חמישה איברים, אז לפרמוטציה שהיא מעגל על כל חמשת האיברים הללו אקרא "5-מעגל" (צריך לא להתבלבל עם המושג השני של מעגל שעליו אנחנו מדברים פה – מעגל בוליאני, שהוא יצור שונה לגמרי – אבל אני בטוח שיהיה בסדר).

פעולה בסיסית בחבורות, ולכן גם בפרמוטציות, היא הצמדה. אם \(\sigma,\tau\) הן שתי פרמוטציות, אז הצמדה של \(\sigma\) על ידי \(\tau\) היא הפרמוטציה \(\tau\sigma\tau^{-1}\). אפשר לחשוב על זה כעל "מה שקורה כשמפעילים את \(\sigma\), אבל לא על הדבר המקורי שעליו רצינו להפעיל אותה אלא עליו אחרי שהוא "מוזז" ובסוף מתקנים את ה"הזזה" הזו" (באלגברה לינארית, למשל, הצמדה של מטריצות היא בעלת המשמעות של שינוי מערכת הקוארדינטות, הפעלת הטרנספורמציה הלינארית שהמטריצה האמצעית מייצגת בתוך מערכת הקוארדינטות החדשה, ואז חזרה למערכת הקוארדינטות הישנה – מי שכל זה נשמע לו כמו ג'יבריש, לא נורא; אני מקווה שהפלתי למישהו אסימון כרגע).

להצמדת פרמוטציות יש פירוש קל ונחמד. בואו נניח לשניה ש-\(\sigma\) היא מעגל: \(\sigma=\left(a_{1}\ a_{2}\ \dots a_{k}\right)\). אז \(\tau\sigma\tau^{-1}=\left(\tau\left(a_{1}\right)\ \tau\left(a_{2}\right)\ \dots\tau\left(a_{k}\right)\right)\). כלומר, עדיין קיבלנו מעגל, רק שבמקום המספרים המקוריים של \(\sigma\) אנחנו מקבלים את המספרים הללו אחרי שערבבנו אותם באופן ש-\(\tau\) מתאר. אם למשל \(\sigma=\left(1\ 4\ 2\right)\) ו-\(\tau=\left(1\ 3\right)\left(2\ 5\right)\) אז \(\tau\sigma\tau^{-1}=\left(3\ 4\ 5\right)\). נסו והיווכחו בעצמכם. לא אוכיח את הטענה הזו כעת למרות שהיא אינה מסובכת במיוחד (האינטואיציה גם כאן היא לחשוב על \(\tau\) כמעין "שינוי קוארדינטות").

המסקנה שמעניינת אותנו כאן היא זו: אם \(\sigma_{1},\sigma_{2}\) הן שתי פרמוטציות ששתיהן 5-מעגל, אז יש \(\tau\) כך ש-\(\tau\sigma_{1}\tau^{-1}=\sigma_{2}\) (למה? האם אתם יכולים להגיד איך מוצאים את \(\tau\)?). בהתבסס על המסקנה הזו, אפשר סוף סוף להתחיל ולראות את הכוח והגמישות שהשימוש בפרמוטציות נותן לנו. נניח ש-\(P\) היא תוכנית פרמוטציות אשר \(\sigma\)-מחשבת איזו פונקציה \(f\), כאשר \(\sigma\) היא 5-מעגל, אז קיימת תוכנית פרמוטציות \(P^{\prime}\) מאותו אורך כמו \(P\) אשר \(\sigma^{\prime}\)-מחשבת את \(f\) לכל \(\sigma^{\prime}\) שהיא 5-מעגל. זה מפחית מאוד את התלות שלנו ב-\(\sigma\)הספציפית שאיתה אנחנו מחשבים את \(f\), וכפי שנראה בקרוב, זה מאוד מועיל.

ההוכחה של הטענה הזו היא פשוט מקסימה. אז נניח ש-\(\sigma^{\prime}=\tau\sigma\tau^{-1}\). בואו נכתוב במפורש את התוכנית \(P\): \(P=\left(k_{1},\pi_{1}^{0},\pi_{1}^{1}\right),\left(k_{2},\pi_{2}^{0},\pi_{2}^{1}\right),\dots,\left(k_{t},\pi_{t}^{0},\pi_{t}^{1}\right)\). מה שנעשה יהיה "לתקן" את \(P\) באופן הבא: \(P^{\prime}=\left(k_{1},\tau\pi_{1}^{0},\tau\pi_{1}^{1}\right),\left(k_{2},\pi_{2}^{0},\pi_{2}^{1}\right),\dots,\left(k_{t},\pi_{t}^{0}\tau^{-1},\pi_{t}^{1}\tau^{-1}\right)\) – כלומר, השינוי הוא רק בפרמוטציות הראשונות והאחרונות. בפרט לא שינינו את אורך התוכנית.

כעת, מה קורה? אם \(f\left(\overline{x}\right)=1\) אז \(P\left(\overline{x}\right)=\prod_{i=1}^{t}\pi_{i}^{x_{k_{i}}}=\sigma\). ומהו \(P^{\prime}\left(\overline{x}\right)\)? ובכן, על פי ההגדרה, זהו \(P^{\prime}\left(\overline{x}\right)=\tau\left(\prod_{i=1}^{t}\pi_{i}^{x_{k_{i}}}\right)\tau^{-1}=\tau\sigma\tau^{-1}=\sigma^{\prime}\).

ואם \(f\left(x\right)=0\), מה אז? ובכן, \(P^{\prime}\left(\overline{x}\right)=\tau\left(\prod_{i=1}^{t}\pi_{i}^{x_{k_{i}}}\right)\tau^{-1}=\tau e\tau^{-1}=\tau\tau^{-1}=e\), שכן כאשר מצמידים את \(e\) תמיד מקבלים את \(e\). קיבלנו בדיוק את מה שרצינו.

הטענה הזו תעזור לנו עוד מעט בהמשך, אבל עכשיו אפשר סוף סוף לגשת להוכחה עצמה. המטרה שלנו היא לבצע סימולציה של מעגל \(\mbox{NC}^{1}\) באמצעות תוכנית פרמוטציות. הרעיון יהיה לתקוף את המעגל באופן אינדוקטיבי – נראה כיצד ניתן להמיר צומת כניסה למעגל לתוכנית פרמוטציות, וכיצד ניתן להמיר את צמתי השערים הלוגיים בתוכניות פרמוטציות. בכל אחד מהמקרים העיקרון יהיה שעל קלטים שעבורם השער פולט \(1\), תוכנית הפרמוטציות תפלוט \(\sigma\) עבור פרמוטציה כלשהי שהיא 5-מעגל, ועל קלטים שעבורם הוא פולט \(0\), תוכנית הפרמוטציות תפלוט \(e\).

כדי לפשוט לנו את החיים, נניח שבמעגל הבוליאני יש רק שני סוגי שערים: שער \(\neg\) ושער \(\wedge\). שער \(\vee\) ניתן לסימולציה בעזרת כללי דה-מורגן: \(x\vee y=\neg\left(\neg x\wedge\neg y\right)\). אמנם, זה יגדיל קצת את עומק המעגל, אבל לא באופן משמעותי – רק פי 2, כלומר הכפלה בקבוע.

נתחיל, אם כן, משער קלט של המעגל. שער כזה תמיד מסומן במשתנה \(x_{i}\) ומקבל 1 או 0 בהתאם לערך ש-\(x_{i}\) מקבל בהשמה. תוכנית פרמוטציות עבור שער כזה היא פשוטה מאוד – מאורך 1: \(P=\left(i,e,\sigma\right)\). נסו להבהיר לעצמכם למה זה עובד.

שער \(\neg\) מסובך מעט יותר ויש בו תעלול נחמד. זכרו שלשער \(\neg\) יש כניסה אחת בלבד, ולכן אפשר לתאר את הסיטואציה כך: יש לנו שער שמחשב פונקציה \(f\) כלשהי וכבר בנינו עבורו תוכנית פרמוטציות \(P\) אשר \(\sigma\)-מחשבת את \(f\) עבור \(\sigma\) שהיא 5-מעגל כלשהו. כעת אנו רוצים לבנות תוכנית פרמוטציות \(\neg P\) אשר \(\tau\)-מחשבת את \(\neg f\) עבור \(\tau\) שגם היא 5-מעגל (לא בהכרח שווה ל-\(\sigma\)). הרעיון הוא זה: אם \(P=\left(k_{1},\pi_{1}^{0},\pi_{1}^{1}\right),\dots,\left(k_{t},\pi_{t}^{0},\pi_{t}^{1}\right)\) אז \(\neg P=\left(k_{1},\pi_{1}^{0},\pi_{1}^{1}\right),\dots,\left(k_{t},\pi_{t}^{0}\sigma^{-1},\pi_{t}^{1}\sigma^{-1}\right)\). כלומר, שינינו במעט את השלב האחרון בתוכנית – שימו לב כי לא שינינו את אורך התוכנית כלל. למה זה עובד? כי אם \(f\left(\overline{x}\right)=1\) אז \(P\left(\overline{x}\right)=\sigma\) ולכן \(\neg P\left(\overline{x}\right)=\sigma\cdot\sigma^{-1}=e\), בהתאם לכך ש-\(\neg f\left(\overline{x}\right)=0\); ואילו אם \(f\left(\overline{x}\right)=0\) אז \(P\left(\overline{x}\right)=e\) ולכן \(\neg P\left(\overline{x}\right)=e\cdot\sigma^{-1}=\sigma^{-1}\). כלומר, \(\neg P\) \(\sigma^{-1}\)-מקבלת את \(\neg f\), וזה מצויין כי אם \(\sigma\) היא 5-מעגל, כך גם \(\sigma^{-1}\) (כלומר, \(\sigma^{-1}\) היא ה-\(\tau\) שלנו).

עכשיו הגענו לקליימקס – שער \(\wedge\). נניח שיש לנו את \(f_{1},f_{2}\) אשר \(\sigma_{1},\sigma_{2}\)-מחושבות בידי \(P_{1},P_{2}\) בהתאמה – ושוב, \(\sigma_{1},\sigma_{2}\) שתיהן 5-מעגלים. אנחנו רוצים לבנות \(P\) אשר \(\sigma\)-תחשב את \(f_{1}\wedge f_{2}\). מה עושים?

כאן נשלף תעלול נוסף: ב-\(S_{5}\) יש שתי פרמוטציות \(\sigma_{1},\sigma_{2}\) שהן 5-מעגלים וגם \(\sigma=\sigma_{1}\sigma_{2}\sigma_{1}^{-1}\sigma_{2}^{-1}\) – מה שנקרא הקומוטטור של \(\sigma_{1},\sigma_{2}\) – הוא 5-מעגל. לעובדה הזו יש חשיבות קריטית, והיא הסיבה שבגללה מתעסקים עם \(S_{5}\) דווקא – עבור \(S_{n}\) עם \(n\le4\) פשוט לא קיימות שתי פרמוטציות שהן מעגלים וגם הקומוטטור שלהן הוא מעגל מאותו האורך.

מיהן אותן תמורות שמקיימות את התכונה המדוברת? ובכן, למשל: \(\left(1\ 2\ 3\ 4\ 5\right)\) ו-\(\left(1\ 3\ 5\ 4\ 2\right)\) שהקומוטטור שלהן הוא \(\left(1\ 3\ 2\ 5\ 4\right)\). לא צריך יותר מזה.

אנחנו יכולים להניח כי \(P_{1},P_{2}\) אכן מחשבות את הפונקציות שלהן בעזרת אותן \(\sigma_{1},\sigma_{2}\) המדוברות בזכות הטיעון שהראינו קודם – שאפשר להחליף בחופשיות את ה-\(\sigma\) של התוכנית כל עוד ממשיכים לדבר על 5-מעגלים. יותר מזה: אנחנו גם יכולים לבנות מ-\(P_{1}\) תוכנית חדשה, \(P_{1}^{-1}\), שאורכה זהה לזה של \(P_{1}\) אבל היא \(\sigma_{1}^{-1}\)-מקבלת את \(f_{1}\). כנ"ל עבור \(P_{2}\). וכעת אפשר להציג את התוכנית עבור \(f_{1}\wedge f_{2}\): היא תהיה פשוט \(P=P_{1}P_{2}P_{1}^{-1}P_{2}^{-1}\). כלומר, שרשור של הסדרות של כל ארבע התוכניות הללו. כאן, סוף כל סוף, אורך התוכנית שאנו בונים גדל.

מדוע זה עובד? ובכן, אם \(f_{1}\left(\overline{x}\right)=f_{2}\left(\overline{x}\right)=1\) אז \(P\left(\overline{x}\right)=\sigma_{1}\sigma_{2}\sigma_{1}^{-1}\sigma_{2}^{-1}=\sigma\), כפי שרצינו. אבל אם למשל \(f_{1}\left(\overline{x}\right)=0\) ו-\(f_{2}\left(\overline{x}\right)=1\) אז \(P_{1}\left(\overline{x}\right)=P_{1}^{-1}\left(\overline{x}\right)=e\) ולכן \(P\left(\overline{x}\right)=e\cdot\sigma_{2}\cdot e\cdot\sigma_{2}^{-1}=e\). באופן דומה מקבלים \(e\) גם עבור הסיטואציות שבהן \(f_{2}\left(\overline{x}\right)=0\). זה הסוף. אני לא יודע מה איתכם, אבל לדעתי הטריק הזה ממש מקסים, ואלוהים יודע מאיפה הוא הגיע.

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

כפי שכבר ראינו, אורך תוכנית שמתאימה לשער קלט היא 1, ושער שלילה לא משנה כלל את אורך התוכנית. רק שער \(\wedge\) מאריך אותה. בואו נסמן ב-\(S\left(d\right)\) את האורך המקסימלי של תוכנית עבור מעגל מעומק \(d\).

אם יש לנו בתוכנית שער \(\wedge\) בעומק \(d\), אז שני הצמתים שנכנסים אליו מהווים כל אחד מעגל בעומק \(d-1\). לכן גודל התוכניות \(P_{1},P_{2}\) עבורם הוא לכל היותר \(S\left(d-1\right)\) לכל אחד מהם. מכיוון שאנו משרשרים את \(P_{1},P_{2}\) ואז גם את שתי התוכניות עם הפרמוטציה ההופכית, אנחנו בסך הכל מגדילים פי 4 את האורך לכל היותר, כלומר \(S\left(d\right)=4\cdot S\left(d-1\right)\). אם נפתור את נוסחת הנסיגה הזו נקבל \(S\left(d\right)=4^{d}\). שזה, חייבים להודות, לא נראה מרשים במבט ראשון – זה נראה אקספוננציאלי.

אלא מה, צריך לזכור שאנו מדברים כאן על מעגל \(\mbox{NC}^{1}\) – מעגל שהעומק שלו הוא לוגריתמי ב-\(n\), כלומר במספר הביטים שעליהם פועלת הפונקציה שמחשבים. לכן \(4^{d}\) הוא בעצם \(O\left(4^{\log n}\right)=O\left(n^{\log4}\right)\) – פולינומי לגמרי (אבל, אם ניקח מעגל \(\mbox{NC}^{2}\), שבו העומק הוא \(O\left(\log^{2}n\right)\), כבר לא נקבל אורך פולינומי). זה מסיים את ההוכחה.

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

תוכניות מתפצלות ומשפט ברינגטון

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

כמו שמעגל בוליאני הוגדר כגרף, כך גם תוכנית מתפצלת מוגדרת כגרף מכוון וחסר מעגלים. אלא שבמעגל בוליאני חשבנו על הצמתים בתור יחידות עיבוד למידע: מידע (בדמות ביטים – אפסים ואחדים) נכנס לכל צומת, עבר עיבוד כלשהו (למשל, פונקציית "וגם"), ונפלט מהיציאה של הצומת. בתוכנית מתפצלת אנחנו חושבים על הצמתים כמעין "שורות קוד" בתוכנית מחשב. כל שורה בתוכנית מכילה פקודות לקפיצה לאחת מכמה שורות אפשריות אחרות, כשההחלטה לאן לקפוץ תלויה בקלט. יותר במדויק, כל צומת מסומן על ידי משתנה \(x_{i}\), והקשתות שיוצאות מהצומת מסומנות ב-0 וב-1. יש לתוכנית שני צמתים מיוחדים: צומת התחלה \(s\) (שגם הוא מסומן במשתנה) וצומת סיום \(t\). עבור השמה כלשהי למשתנים, אנחנו מוחקים קשתות בהתאם להשמה – אם למשל המשתנה \(x_{3}\) קיבל 0, אז מכל צומת שמסומן ב-\(x_{3}\) (יכולים להיות הרבה כאלו) אנו מוחקים את כל הקשתות היוצאות שמסומנות ב-1. פלט התוכנית על קלט הוא 1 אם לאחר מחיקת הקשתות הזו עדיין קיים מסלול מ-\(s\) ל-\(t\) בגרף, ואחרת הפלט שלה הוא 0.

כרגיל, תמונה אחת שווה אלף מילים. הנה תוכנית מתפצלת עבור הפונקציה \(\mbox{MAJORITY}\left(x_{1},x_{2},x_{3}\right)\) שמחזירה את הערך הבוליאני שמצוי אצל רוב הקלטים. נסו "לסמלץ" את הריצה של התוכנית על כמה קלטים ובדקו מה קורה.

אולי אתם תוהים למה הגדרתי הגדרה מסובכת שכזו לתוכנית מתפצלת – כל העסק עם מחיקת הקשתות ועם "אם קיים מסלול…". למה לא אמרתי פשוט "בהינתן קלט נצא לטיול בגרף – כאשר אנחנו בצומת \(x_{i}\) נתבונן בערך המשתנה \(x_{i}\) והצעד הבא שלנו ייקבע על פי ערך זה, ואם אין לנו קשת יוצאת מתאימה ניתקע"? ובכן, זה אכן מספיק עבור מה שנתעסק בו, אבל באופן כללי ייתכן שמאותו צומת תצא יותר מקשת אחת שמסומנת ב-1 (או יותר מקשת אחת שמסומנת ב-0) וכל מה שיעניין אותו הוא שקיימת דרך כלשהי להגיע מ-\(s\) אל \(t\). מי שזה מבלבל אותו – לא נורא, אתם בהחלט יכולים לחשוב על ההגדרה שהצגתי כרגע כאילו היא הגדרה שקולה.

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

כדי להוכיח חסמים תחתונים על מודל חישובי, צריך קודם כל להגדיר מדדי סיבוכיות כלשהם עבורו. אם אנחנו רוצים להגיד "קשה לחשב את \(f\) בעזרת תוכניות מתפצלות" צריך קודם כל להבהיר מה זה בכלל אומר, "קשה", בהקשר של תוכניות מתפצלות. המדד האלמנטרי הוא הגודל של התוכנית – מספר הצמתים שבה. מחשב אמיתי שרוצה להריץ תוכנית מתפצלת צריך לזכור את כל הצמתים שלה – ולכן מספר אקספוננציאלי של צמתים (כתלות במספר הביטים של הקלט לפונקציה שאותה רוצים לחשב) הוא לא ריאלי – חשבו על תוכנית שכדי לחשב את הפלט של פונקציה על 50 ביטים דורשת \(1,125,899,906,842,624\) שורות קוד! (\(2^{50}\)). לכן הדרישה האלמנטרית מתוכנית מתפצלת הוא שגודלה יהיה פולינומי. כמו שקרה עם מעגלים בוליאניים, כך גם כאן, תוכנית מתפצלת תמיד מוגדרת עבור מספר נתון של ביטי קלט. לכן מדברים לרוב על משפחות של פונקציות, שמחושבות על ידי משפחות של תוכניות מתפצלות, והשאלה ששואלים היא כמה מהר צומח הגודל של התוכנית המתפצלת עבור קלט מאורך \(n\) כאשר מגדילים את \(n\). לא אחזור שוב על הדיון הזה בשלמותו.

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

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

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

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

כדי להבהיר את הקושי כדאי לשכוח שנייה מתוכניות מתפצלות ולדבר על מה שקורה למודל החישובי הסטנדרטי, מכונת טיורינג, אם דורשים מהזכרון שלה להיות חסום באופן שאינו תלוי בקלט. מה שמקבלים הוא שקילות למודל פשוט בהרבה, שמכונה אוטומט סופי דטרמיניסטי. לא אציג את המודל הזה כרגע, אבל רק אבהיר עד כמה הוא חלש: אם נותנים לו מספר, הוא לא מסוגל לבדוק האם המספר ראשוני. אם נותנים לו מחרוזת מהצורה \(0^{*}1^{*}\) (סדרת אפסים ואז סדרת אחדות – הכוכבית מציינת "0 או 1 או 2 או…") הוא לא מסוגל לבדוק האם מספר האפסים שווה למספר האחדות. אם נותנים לו שלשה \(\left(a,b,c\right)\) הוא לא מסוגל לבדוק האם \(c=a-b\) (זה בעצם נובע מיידית ממה שאמרתי קודם…). זה מודל מאוד, מאוד חלש. ותוכניות מתפצלות מרוחב קבוע הן בדיוק האנלוג המתאים לאוטומטים סופיים דטרמיניסטיים בתוך עולם התוכניות המתפצלות.

כדי להראות חסם תחתון על תוכניות מתפצלות, כדאי לתקוף קודם כל פונקציה קונקרטית אחת ולהראות שהיא קשה. עבור מעגלים בוליאניים הצליחו להשתמש ב-\(\bigoplus\) (שמבצעת XOR של כל הביטים שלה) בתור פונקציה שכזו – הוכיחו כי היא אינה ב-\(\mbox{AC}^{0}\) (מעגלים בוליאניים מעומק קבוע – שוב ה"קבוע" הזה – עם דרגת כניסה לא מוגבלת של הצמתים). אחרי שיש דוגמה קונקרטית אחת ביד, אפשר לרוב להראות עבור פונקציות רבות אחרות שהן לא שייכות למחלקה על ידי טיעון בסגנון "אם הן כן היו שייכות, אפשר היה להשתמש בהן כדי לחשב גם את \(\bigoplus\)". לרוע המזל, את \(\bigoplus\) קל מאוד לחשב עם תוכנית מתפצלת מרוחב 2: קוראים ביט ביט, וכל המידע שצריך לזכור הוא "האם סכום הביטים עד כה היה 0 או 1 מודולו 2". הנה התוכנית המתפצלת המתאימה עבור \(\mbox{\ensuremath{\bigoplus\left(x_{1},x_{2},x_{3},x_{4},x_{5}\right)}}\):


ובכן, מי כן מועמד טוב להיות פונקציה שתוכניות מתפצלות לא יכולות לחשב? רצוי שזו תהיה פונקציה פשוטה, כזו שאפשר לחשב ביעילות במודלים פשוטים אחרים – זכרו שבסופו של דבר אנחנו רוצים להצביע על קושי של בעיות שנראות פשוטות יחסית (זהו למשל העניין בשאלת \(\mbox{P=NP}\); המחלקה \(\mbox{NP}\) מורכבת מבעיות שבמובן מסויים הן קלות, ולכן חשוב לנו להוכיח שלמרות זאת הן קשות; את זה שקיימות בכלל בעיות שאינן ב-\(\mbox{P}\) קל להוכיח). לצורך העניין, מחלקה פשוטה מספיק שעדיין נראית כמו כר מתאים לחיפוש אחר פונקציות קשות לחישוב היא \(\mbox{NC}^{1}\) – מה שבא בתור אחרי \(\mbox{AC}^{0}\). ב-\(\mbox{NC}^{1}\), כזכור, נמצאות כל הפונקציות שניתנות לחישוב על ידי מעגל מעומק לוגריתמי וגודל פולינומי, שבו דרגת הכניסה של כל שער היא לכל היותר 2. הרבה פונקציות פשוטות (למשל, הפונקציות האריתמטיות) נמצאות שם; וגם הפונקציה \(\mbox{MAJORITY}\) שהזכרתי בהתחלה. אותה \(\mbox{MAJORITY}\) נראתה כבעלת פוטנציאל רב לתקיפת תוכניות מתפצלות; כמו שאפשר לראות בתוכנית המתפצלת שציירתי בהתחלה, נראה שהרוחב תלוי בצורה חזקה בכמות המשתנים. ובאמת, צריך "לזכור" כמה משתנים היו 0 וכמה משתנים היו 1 עד כה, או לפחות מה היה ההפרש ביניהם, והוא יכול להיות גדול באופן שרירותי, לא? זה לא נראה כמו משהו שאפשר לתפוס עם תוכניות מתפצלות מרוחב קבוע. זה הוביל להשערה בראשית שנות השמונים לפיה כל תוכנית מתפצלת מרוחב חסום עבור \(\mbox{MAJORITY}\) לא תהיה פולינומית באורכה. די מהר הוכיחו גם חסם תחתון נאיבי יחסית – שתוכנית מתפצלת מרוחב חסום עבור פונקציה זו לא יכולה להיות לינארית באורכה (כלומר, \(O\left(n\right)\)). על פניו כיוון המחקר הזה נראה מבטיח.

ואז, בשנת 1989 בא ברינגטון ופוצץ את הכל כשהוכיח שלא רק את \(\mbox{MAJORITY}\) אפשר לחשב באמצעות תוכניות מתפצלות מרוחב חסום, אלא כל פונקציה ב-\(\mbox{NC}^{1}\). ולא רק שאפשר, אלא שמספיק שהרוחב החסום יהיה 5 (לכל פונקציה ב-\(\mbox{NC}^{1}\)). אני מקווה שביצעתי מספיק עבודת הכנה כדי שתתקבל תחושה כלשהי עד כמה זו תוצאה מפתיעה. גם אני, למרות שאני מכיר את ההוכחה, עדיין מתקשה להבין איך בכלל אפשר לתפוס משהו כמו \(\mbox{MAJORITY}\) עם רוחב חסום. וכמובן, יש את עניין מספר הקסם 5 – מאיפה הוא צץ? למה דווקא 5 ולא 4 או 6? גם לשאלה הזו יש תשובה מצויינת. כל התשובות – בפוסט הבא, שבו אציג במפורט את ההוכחה.

מעגלים בוליאניים – מה זה בכלל?

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

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

ראשית, המטרה שלנו. אנחנו הולכים לדבר רק על פונקציות בוליאניות – כאלו שמקבלות קלט שהוא סדרה של 0 ו-1, ומוציאות פלט שהוא ביט בודד – או 0 או 1. פורמלית, \(f:\left\{ 0,1\right\} ^{n}\to\left\{ 0,1\right\} \). בהקשר של מדעי המחשב מספיק לדבר על פונקציות כאלו כי כל פונקציה אחרת שניתן לחשב במחשב ניתנת לתיאור באמצעות פונקציות בוליאניות. על ערכים בוליאניים אוהבים להגדיר פעולות של "וגם" ו"או", מתוך מחשבה על ערך 1 כמייצג "אמת" וערך 0 כמייצג "שקר". אם \(x,y\) הם שני ביטים, אז \(x\wedge y\) (\(x\) וגם \(y\)) הוא 1 אם ורק אם \(x=y=1\), אחרת הוא 0; ואילו \(x\vee y\) (\(x\) או \(y\)) הוא 0 אם ורק אם \(x=y=0\) ואחרת הוא 1. ה"או" הזה קצת שונה מהמשמעות שלו בחיי היום יום, כי הוא יכול לקבל "אמת" גם אם שני המשתנים קיבלו "אמת" ("או שאלך לים או שאקרא ספר" נשמע כאילו הוא אומר "אעשה אחד משני אלו אך לא את שניהם בו זמנית"). יש אופרטור אחר, Exclusive Or, או בשמו המקוצר XOR, ובסימון \(x\oplus y\), שמקבל 1 אם ורק אם בדיוק אחד משני המשתנים מכיל 1, שממדל את ה"או" היומיומי. כמו כן יש אופרטור שלילה שפועל רק על משתנה יחיד: \(\neg x\) הוא 1 אם ורק אם \(x\) היה 0.

אלו פונקציות בסיסיות, אבל אפשר להגדיר פונקציות מורכבות יותר על הרבה יותר ביטים. החל ממשהו מטופש כמו \(f\left(x_{1},\dots,x_{n}\right)=\bigwedge x_{i}\) שמקבל 1 אם ורק אם כל \(n\) הקלטים הם 1, עבור בדברים יותר מתוחכמים כמו \(\mbox{MAJORITY}\left(x_{1},\dots,x_{n}\right)\) שמקבלת 1 אם ורק אם רוב הביטים בקלט הם 1, וכלה בדברים ממש מתוחכמים כמו הפונקציה שמוציאה 1 רק אם הביטים של הקלט שלה, כשמפרשים אותם על ידי קידוד כלשהו בתור גרף, מייצגים גרף שמכיל מעגל המילטוני. למעשה, אפשר לכמת ולהגיד בדיוק כמה פונקציות יש מ-\(n\) ביטים אל שני ביטים: \(2^{2^{n}}\). כבר עבור ערכים לא גדולים של \(n\) המספר הזה הוא עצום – יש עושר רב של פונקציות בינאריות. השאלה שאנו שואלים את עצמנו היא – אילו פונקציות ניתן לתאר באמצעות שימוש בפונקציות בסיסיות ופשוטות בלבד, ובאופן חסכוני במשאבים?

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

בואו נעבור להגדרה של מעגל בוליאני – מה זה בכלל? כרגיל בעניינים כאלו, תמונה אחת שווה אלף מילים:


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

בהינתן השמה כלשהי למשתנים, כלומר סדרה של 0 ו-1 לכל אחד מהמשתנים שבצמתי הכניסה של המעגל, הערך של צומת הפלט נקבע באופן הבא: כל צומת שטרם קבענו את ערכו אבל כן קבענו את ערכם של כל הבנים שלו – הצמתים שיש קשת מהם אליו – הערך שלו מחושב בהתאם לערך הבנים ולסימן שלו. למשל, צומת שמסומן ב-\(\wedge\) מקבל את הערך 1 אם ורק אם כל בניו קיבלו 1, ואחרת הוא מקבל 0. זה הכל. פלט המעגל על ההשמה הוא ערך של צומת הפלט. בדרך כלל מסמנים מעגל ב-\(C\) ואת הערך שלו על השמה מסויימת \(x\) ב-\(C\left(x\right)\) (כאן צריך לחשוב על \(x\) כעל מחרוזת ביטים שאורכה כמספר המשתנים של המעגל).

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

אפשר היה לעקוף את הקושי הזה ולהגיד שמראש אנחנו רוצים לדבר רק על \(n\) קונקרטי אחד וחסל. אלא מה, כל מהותה של תורת הסיבוכיות הוא לעסוק בחסמים אסימפטוטיים על סיבוכיות – כלומר, לשאול שאלות כמו "עבור \(n\)-ים הולכים וגדלים, כמה מהר יגדל זמן החישוב שנדרש למכונת טיורינג כדי להתמודד עם קלט בגודל \(n\) כשהיא מנסה לחשב את הפונקציה הזו והזו?". לכן כשמדברים על בעיה חישובית שאנו רוצים לפתור באמצעות מעגלים, אנחנו לא מדברים על פונקציה \(f\) בודדת שאנו רוצים לחשב אלא על משפחת פונקציות \(f_{1},f_{2},\dots,f_{n},\dots\) כאשר \(f_{n}\) היא פונקציה על \(n\) קלטים. מן הסתם בדרך כלל כל הפונקציות \(f_{n}\) הללו יהיו דומות זו לזו באופיין – אבל הן לא זהות, כי מספר הקלטים שלהן שונה. בהתאם לכך, לא ניתן יהיה להציג מעגל בודד שמטפל בכל משפחת הפונקציות, אלא משפחת מעגלים \(C_{1},C_{2},\dots,C_{n},\dots\) כך שהמעגל \(C_{n}\) מחשב את הפונקציה \(f_{n}\). לכאורה זה רעיון פשוט ומובן, ולא ברור למה אני מייחס לו כל כך הרבה חשיבות, עד שמתברר עד כמה החוסר-יוניפורמיות הזה גורם לתופעות מוזרות. הנה הדוגמה הקלאסית לעניין. שפה אונרית היא קבוצה של מילים (מילה, לצורך העניין, היא רצף של ביטים) שכל המילים בה הן סדרה של 1-ים, כלומר 0 לא מופיע בכלל. מה שמאפיין את המילים בשפה, אם כן, הוא רק האורך שלהן. לכל \(n\), אחד משניים: או ש-\(1^{n}\) (סדרה של \(n\) פעמים 1) מופיע בשפה, או שלא. זה אומר שלכל שפה אונרית קיימת משפחת מעגלים ש"מזהה" אותה (מוציאה 1 על מילים ששייכות לשפה ו-0 על מילים שלא). למה? כי קל לבנות מעגל שמוציא 1 על הקלט \(1^{n}\) – פשוט מבצעים \(\wedge\) על כל ביטי הקלט. גם קל לבנות מעגל שמוציא \(0\) על כל הקלטים – למשל \(x_{1}\wedge\neg x_{1}\). לכן לכל \(n\) קיים מעגל, ומעגל קטן ופשוט שעונה את התשובה הנכונה לכל המילים מאורך \(n\). אבל זה לחלוטין לא המצב עם מכונות טיורינג – קיימות שפות אונרית שמכונת טיורינג לא יכולה לזהות (כלומר, לעצור ולומר "כן" או "לא, בהתאם לשייכות המילה לשפה או לא). זה נובע מכך שבכלל קיימות שפות כלשהן שמכונת טיורינג לא יכולה לזהות, ואפשר לקודד שפות כאלו בעזרת מילים אונריות (איך? תרגיל). בקיצור, המודל הלא-יוניפורמי של מעגלים מסוגל לעשות דברים שמכונת טיורינג לא מסוגלת (זה לא אומר שהוא חזק יותר – אם המעגלים מוגבלים, ונראה דוגמאות להגבלות בקרוב, יהיו דברים שמכונת טיורינג יכולה לעשות ומעגלים לא).

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

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

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

וכאן מתחיל להתברר לנו שצריך לחזק קצת את המגבלות שלנו אחרת לא נצליח לתאר שום דבר מעניין. נתחיל מכך שכל פונקציה בוליאנית – כל פונקציה – ניתן לחשב עם מעגל שעומקו המביך הוא 2. הסיבה לכך היא שכל פונקציה בוליאנית ניתן לתאר בצורה קנונית: לתאר אותה בתור \(\vee\) אחד גדול על המון \(\wedge\)-ים. מה הרעיון? אם \(f\) היא פונקציה בוליאנית, אפשר לחלק את הקלטים בעולם לשניים – אלו שמחזירים 0 ואלו שמחזירים 1. אז אפשר לתאר את הפונקציה בתור "החזירי אחד אם הקלט שלך הוא \(a\), או אם הוא \(b\), או אם הוא \(c\), או…" כש-\(a,b,c\) וכדומה כולן סדרות ביטים שעבורן \(f\) מקבלת 1. את זה מתארים בתור \(\vee\). כדי לתאר את "הקלט \(x\) שווה ל-\(a\)" צריך בעצם להגיד משהו בסגנון "הביט הראשון של \(x\) שווה לביט הראשון של \(a\), וגם הביט השני של \(x\) שווה לביט השני של \(a\), וגם…" – את זה מתארים בתור \(\wedge\). זה הכל. לצורה הקנונית המתקבלת קוראים DNF.

בתור דוגמה, הנה דרך לתאר את \(\oplus\) כ-DNF. כזכור, זו פונקציה שמקבלת 1 אם ורק אם שני הקלטים (שנסמן כ-\(x_{1},x_{2}\)) שונים זה מזה. כלומר, הקלט הוא \(01\) או \(10\). הצורה הנורמלית המתאימה היא \(\left(x_{1}\wedge\neg x_{2}\right)\vee\left(\neg x_{1}\wedge x_{2}\right)\).

אם כן, יש לנו כאן מעגל מעומק 2: בשכבה הראשונה יהיו לנו שערי \(\wedge\) עבור כל ההשמות שנותנות 1, ובשכבה השניה יהיה לנו שער בודד, שער הפלט, עם \(\vee\) עליו. אז למה הסתבכנו כל כך עד כה? הסיבה היא שהמעגל שלנו אולי לא עמוק, אבל הוא מאוד לא יעיל: עבור רוב הפונקציות, מספר שערי ה-\(\wedge\) שנזדקק להם הוא אקספוננציאלי ב-\(n\), כי יהיה מספר אקספוננציאלי ב-\(n\) של קלטים עליהם \(f\) מחזירה 1. בעצם אמרנו שאם יש לנו המון, המון מעבדים, אז אפשר לחשב כל פונקציה מהר. טוב ויפה, אבל אז המודל שלנו לא ריאליסטי; אנחנו רוצים לבדוק מה מעגלים יכולים לבצע גם בזמן יעיל וגם בזכרון יעיל. לכן מראש בכל הדיון שלנו מגבילים את גודל המעגלים להיות פולינומי, ואז כל הבניה שהצגנו לא שווה כלום, ועבור רוב הפונקציות לא נצליח למצוא מעגל מעומק 2, או אפילו מעומק קבוע.

חישוב מקבילי הוא מעניין רק אם הוא משיג שיפורים משמעותיים בזמן הריצה אל מול חישובים לא מקביליים. אם חישוב לא מקבילי יעיל הוא כזה שזמן הריצה שלו הוא פולינומי, אין טעם לקרוא גם לחישוב מקבילי "יעיל" אם זמן הריצה שלו פולינומי; לעומת זאת, זמן ריצה לוגריתמי זה כבר משהו מרשים. גם לוגריתמי-בריבוע זה עדיין טוב, ובאופן כללי זמן ריצה פולי-לוגריתמי (שהוא פולינום, אבל לא ב-\(n\) אלא ב-\(\log n\) – למשל \(\log^{7}n+3\log^{3}n+7\) הוא דוגמה לפולינום ב-\(\log n\)) הוא זמן ריצה מקבילי טוב. כמובן, ככל שהחזקה של הלוגריתם גבוהה יותר כך החישוב טוב פחות. זה מוביל אותנו להגדרה הבאה: \(\mbox{AC}^{k}\) הוא אוסף כל הפונקציות שניתן לחשב על ידי מעגל בוליאני (בעצם משפחת מעגלים בוליאניים אבל כבר הבנתם את העיקרון) מגודל פולינומי ב-\(n\) ובעומק שהוא \(O\left(\log^{k}n\right)\). כך למשל \(\mbox{AC}^{0}\) הוא הפונקציות שניתן לחשב על ידי מעגל מגודל פולינומי ועומק קבוע; \(\mbox{AC}^{1}\) הוא הפונקציות שניתן לחשב על ידי מעגל פולינומי מעומק לוגריתמי, וכן הלאה. ב-\(\mbox{AC}\) מסמנים את איחוד כל המחלקות הללו.

גם המחלקה \(\mbox{AC}\) היא לא ריאליסטית עד הסוף, מהטעם הפשוט שלשערי ה-\(\wedge\) ו-\(\vee\)יכולים להיכנס כמה קלטים שרק נרצה. בפועל, לחשב \(\wedge\) של 100 קלטים לוקח יותר זמן מלחשב \(\wedge\) של 2 כאלו. לכן מוגדרת מחלקה מקבילה ל-\(\mbox{AC}\), שנקראת \(\mbox{NC}\). \(\mbox{NC}^{k}\) זהה ל-\(\mbox{AC}^{k}\) בהגדרתו פרט לכך שדורשים שלשערי \(\wedge,\vee\) ייכנסו ביטים משני שערים בדיוק (מה שמכונה Fan-in 2). זו לא מגבלה עד כדי כך חמורה: אפשר לסמלץ שער \(\vee\) שנכנסים אליו המוני קלטים באמצעות סדרה של שערי \(\vee\) שפועלים הדרגתית על זוגות מתוך הקלטים הללו. דבר כזה יוצר ניפוח לוגריתמי בעומק של המעגל (כי כל צעד קבוע מוחלף על ידי סדרת צעדים שגודלה עשוי להיות פולינומי) ומכאן מתקבלת סדרת ההכלות הבאה: \(\mbox{NC}^{0}\subseteq\mbox{AC}^{0}\subseteq\mbox{NC}^{1}\subseteq\mbox{AC}^{1}\subseteq\mbox{NC}^{2}\subseteq\dots\). כדי לראות שכן יש הבדל כלשהו שימו לב ש-\(\mbox{NC}^{0}\) מכיל רק פונקציות שתלויות במספר קבוע (שאינו תלוי ב-\(n\)) של ביטים מהקלט שלהן, בעוד שב-\(\mbox{AC}^{0}\) זה לא כך.

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

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

פרוייקט "תוצאות מפתיעות בסיבוכיות" יוצא לדרך!

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

גרף המציג דירוג "הפתעה" של משפטים במדעי המחשב; ה"משפט" P=NP מדורג בצורה קיצונית מעבר למספר משפטים ידועים ומפתיעים במדעי המחשב

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

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

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

משפט ברינגטון:

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

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

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

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

משפט ברינגטון אומר שכל פונקציה שניתן לחשב על ידי מעגלים בוליאניים יעילים (שוב, אני מרמה פה – פורמלית, הוא מדבר על המחלקה \(\mbox{NC}^{1}\) שאתאר בפירוט בפוסט המתאים) ניתן לחישוב על ידי תוכניות מתפצלות מאורך פולינומי ורוחב 5. כלומר, אפשר "תמיד" לוותר על רוחב גדול – 5 זה כל מה שצריך. וכפי שקורה בדרך כלל במשפטים עם ניסוח מוזר שכה, המספר 5 אינו מקרי כלל; עבור 4 ומטה זה לא עובד, והסיבה לכך היא שחבורת התמורות \(S_{5}\) היא עשירה ומעניינת מספיק כדי שתקרה בה תופעה מסויימת שלא מתרחשת ב-\(S_{4}\) (הדבר מזכיר מאוד את האופן שבו משוואות ממעלה חמישית ומעלה אינן ניתנות לפתרון כללי באמצעות רדיקלים – שוב, בגלל ש-\(S_{5}\) עשירה מספיק כדי שתקרה בה תופעה כלשהי שלא מתרחשת בחבורות הקטנות יותר). הבניה עצמה משתמשת באופן מחוכם ויפהפה בתמורות; היא מסוג הדברים שעליהם אני חושב בו זמנית גם "אלוהים, כמה זה פשוט", גם "אלוהים, איך לעזאזל הוא חשב על זה?" ו"אלוהים אדירים!" באופן כללי.

NL=coNL:

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

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

שאלה פתוחה מרכזית במדעי המחשב היא האם \(\mbox{NP=coNP}\). זה אחד מהדברים שאותם \(\mbox{P=NP}\) יוכיח מייד, אבל הכיוון ההפוך אינו נכון; ייתכן ש-\(\mbox{P\ensuremath{\ne}NP}\) ועדיין \(\mbox{NP=coNP}\), כך שזו טענה "סבירה קצת יותר" מאשר \(\mbox{P=NP}\). עם זאת, היא עדיין נחשבת לבלתי סבירה בעליל ויש תחומים בסיבוכיות ש(אני קצת מגזים עכשיו) עצם קיומם מתבסס על ההנחה ש-\(\mbox{NP\ensuremath{\ne}coNP}\) (למשל Proof Complexity שעוסק בשאלה כמה מסובכת צריכה להיות הוכחת "כן" לטענה ב-\(\mbox{coNP}\)).

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

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

IP=PSPACE:

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

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

ואז התגלה שהן שוות – \(\mbox{IP=PSPACE}\). זה אומר שהכוח של הוכחות אינטראקטיביות הוא גדול בהרבה ממה שניתן היה לשער – ולכן גם עושה את התחום הזה למעניין יותר. וכרגיל, גם ההוכחה עצמה משתמשת ברעיונות מתוחכמים ויפים; היא מהווה דוגמה אלגנטית שחביבה עלי מאוד להוכחה ש"יוצאת מהמסגרת". אבל בואו לא נקדים את המאוחר…

האלגוריתם של שור:

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

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

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

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

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

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