מספרי עבגד

פרוייקט "התלמיד והמחשב" בעיצומו, ואני רוצה בפוסט הזה לדבר גם כן על תכנות, אבל על בעיה לא קשורה שאינה מופיעה בספר, אלא צצה לה בדיון בדף הפייסבוק של עבגד יבאור. הבעיה היא כזו: קחו את המספר האהוב עליכם, נאמר 42, וכתבו אותו בעברית: “ארבעים ושתיים”. עכשיו, לזוג המילים “ארבעים ושתיים” יש ערך גימטרי, מהו? באופן די מדהים, 1089. מה מדהים פה? טוב, 42 הוא המספר ממדריך הטרמפיסט לגלקסיה, ואילו 1089 הוא המספר שמתקבל אם לוקחים מספר תלת ספרתי שאינו פלינדרום, הופכים את ספרותיו, מחסרים את הקטן מהגדול, הופכים את סדר הספרות של התוצאה ומחברים. למשל, אם ניקח את 281, נהפוך את הספרות ונקבל 182, נחסר ונקבל 099, נהפוך את הספרות ונקבל 990 ואז נחבר ונקבל 1089 (רואים מה עשיתי שם עם האפס?). שמעו, אני נדהם בדיוק כמוכם, סתם זרקתי את 42 כי זה ממדריך הטרמפיסט ולא ידעתי איזה ערך גימטרי נקבל. מדהים, פשוט מדהים!

אז כן, כפי שאתם בוודאי מבינים, אין שום חשיבות מיוחדת למספר שמתקבל מהמספר $latex n$ על ידי זה שכותבים את $latex n$ בעברית ואז לוקחים את הערך הגימטרי של התוצאה, מלבד העובדה שהתהליך הזה משעשע אותנו. צאו מנקודת הנחה שזה משעשע אותנו, ועכשיו בואו ננסה להבין קצת את הפונקציה $latex f(n)$ שמקבלת מספר $latex n$ ומחזירה את הערך הגימטרי של שם המספר בעברית (אז למשל, $latex f(42)=1089$).

הפונקציה הזו היא די כאוטית, כמובן, ולכן דווקא מעניין לשאול עליה שאלות. עבגד שאל ראשית כל האם קיימת נקודת שבת לפונקציה, כלומר מספר $latex n$ כך ש-$latex f(n)=n$, כלומר מספר שבצורה כלשהי “מתאר את עצמו” (קצת מזכיר את נוסחת טאפר). למספר שמקיים את התכונה הזו נקרא “מספר עבגד”. פרט לכך עבגד הצביע על כך שאם מפעילים את הפונקציה שוב ושוב על מספר מסוים, ואז על הפלט שלו, ואז על הפלט שלו וכן הלאה מקבלים שרשרת שיכולה או להימשך לנצח (כל הזמן נגיע למספרים חדשים) או שניכנס ללולאה על ידי כך שנפגע במספר ש”כבר ראינו קודם”. השאלה הייתה אם ניתן לגלות מי נכנס ללולאה, מי ממש נמצא על הלולאה (את ההבדל בין שני אלו אסביר בפירוט בהמשך) והאם יש מספר שהשרשרת שמתחילה ממנו ממשיכה עד אינסוף. כל השאלות הללו הן רלוונטיות עבור שלל פונקציות $latex f$ דומות, לאו דווקא זו של הגימטריה, ובקריפטוגרפיה יש לשאלות כאלו חשיבות גדולה כשהן נשאלות על פונקציות תמצות. אבל לא ארחיב על זה יותר מדי בפוסט הנוכחי.

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

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

[code language=”ruby”]

encoding: UTF-8

[/code]

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

[code language=”ruby”] class String def gematric_value self.split("").collect{|a| LETTER_VALUES[a] || 0}.inject(0){|sum, x| sum + x} end end [/code]

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

[code language=”ruby”] LETTER_VALUES = { "א" => 1, "ב" => 2, "ג" => 3, "ד" => 4, "ה" => 5, "ו" => 6, "ז" => 7, "ח" => 8, "ט" => 9, "י" => 10, "כ" => 20, "ך" => 20, "ל" => 30, "מ" => 40, "ם" => 40, "נ" => 50, "ן" => 50, "ס" => 60, "ע" => 70, "פ" => 80, "ף" => 80, "צ" => 90, "ץ" => 90, "ק" => 100, "ר" => 200, "ש" => 300, "ת" => 400 } [/code]

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

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

אז מה עושים? לוקחים את המספר ומפרקים אותו לחלקים: למשל, את 1503 נפרק ל-1000 ו-500 ו-0 ו-3. כל אחד מהחלקים נמיר לשם שלו, באמצעות עוד מילון; לסיום, נחבר את החלקים ונוסיף “ו” לפני האחרון שבהם. היוצא מן הכלל היחיד הוא מספרים כדוגמת 1911 שבהם ה-11 שבסוף צריך להיות מומר ל-“אחד עשרה” ולא ל”עשר ואחד” - אני מטפל במקרה הזה במפורש. הצרה הזו חוזרת על עצמה גם באלפים (11000 הוא לא “עשרת אלפים ואלף” אלא “אחד עשרה אלף”) אבל לא טרחתי לטפל בזה וגם לא טרחתי בכלל לכתוב שמות מספרים עבור יותר מ-10000; הרעיון היה קודם לכתוב משהו שעובד עד 10000 ואז לראות אם בכלל יש צורך במשהו מעבר לכך. לא היה, אבל שימו לב שזה אומר שהקוד שלי כרגע הוא לא מלא. נדרשת עוד עבודה כדי שהוא באמת יעבוד עבור כל מספר אפשרי.

ראשית, המילון - שוב, עבודה של דקה בערך והוא היה כתוב:

[code language=”ruby”] HEBREW_NUMBER_NAMES = { 1 => "אחת", 2 => "שתיים", 3 => "שלוש", 4 => "ארבע", 5 => "חמש", 6 => "שש", 7 => "שבע", 8 => "שמונה", 9 => "תשע", 10 => "עשר", 11 => "אחת עשרה", 12 => "שתים עשרה", 13 => "שלוש עשרה", 14 => "ארבע עשרה", 15 => "חמש עשרה", 16 => "שש עשרה", 17 => "שבע עשרה", 18 => "שמונה עשרה", 19 => "תשע עשרה", 20 => "עשרים", 30 => "שלושים", 40 => "ארבעים", 50 => "חמישים", 60 => "שישים", 70 => "שבעים", 80 => "שמונים", 90 => "תשעים", 100 => "מאה", 200 => "מאתיים", 300 => "שלוש מאות", 400 => "ארבע מאות", 500 => "חמש מאות", 600 => "שש מאות", 700 => "שבע מאות", 800 => "שמונה מאות", 900 => "תשע מאות", 1000 => "אלף", 2000 => "אלפיים", 3000 => "שלושת אלפים", 4000 => "ארבעת אלפים", 5000 => "חמשת אלפים", 6000 => "ששת אלפים", 7000 => "שבעת אלפים", 8000 => "שמונת אלפים", 9000 => "תשעת אלפים", 10000 => "עשרת אלפים" }[/code]

וכעת לפונקציה עצמה, שהיא הרחבה של המחלקה של מספרים שלמים:

[code language=”ruby”] class Integer def to_hebrew_name return HEBREW_NUMBER_NAMES[self] if HEBREW_NUMBER_NAMES[self] name_parts = [] digits = self.to_s.split("").reverse.collect_with_index{|x,i| x.to_i * 10**i}.reverse if digits[-2] == 10 and digits[-1] != 0 digits[-2] += digits[-1] digits.pop end digit_names = digits.reject{|d| d == 0}.collect{|d| d.to_hebrew_name} digit_names[-1] = "ו" + digit_names[-1] name = digit_names.join(" ") name end end [/code]

כאן שורות 6-9 מטפלות ביוצא מן הכלל של 11 ודומיו, ושורה 11 משתילה “ו” לפני המספר האחרון. פרט לכך אני מקווה שכל הקוד ברור.

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

[code language=”ruby”] class Array def collect_with_index result = [] self.each_index{|i| result << yield(self[i], i)} result end end [/code]

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

[code language=”ruby”] (1..8000).each do |n| puts "#{n} => #{n.to_hebrew_name} => #{n.to_hebrew_name.gematric_value}" if (n == n.to_hebrew_name.gematric_value) end [/code]

מריצים, ו… כלום.

אוף.

אז מה עכשיו? קודם כל, בואו ננסה לקבל רושם כללי לגבי מה המספרים שבכלל עשויים להתקבל כפלט של $latex f$ עבור קלטים שהם עד 10,000:

[code language=”ruby”] puts (1…10000).collect{|n| n.to_hebrew_name.gematric_value}.max [/code]

הקוד הזה מחזיר 4144, וזה מעניין - זה אומר שכל מספר גדול מ-4144 עד 10,000 בהכרח החזיר פלט שקטן יותר מעצמו. למה זה מעניין? כי זה מעלה לי את הניחוש לפיו כל מספר גדול מ-4144 יחזיר פלט קטן מעצמו. למעשה, חקירה טיפה יותר מדוקדקת מראה ש-3999 (“שלושת אלפים תשע מאות תשעים ותשע”) הוא המספר הגדול ביותר עד 10,000 שמחזיר מספר גדול מעצמו (4010). ההשערה הלא מוכחת שלי היא שזה המספר הגדול ביותר שמחזיר מספר שאינו קטן ממנו. בכלל. זו לא השערה מוכחת והיא כמובן תלויה בשאלה מה סגנון הכתיבה שאנחנו הולכים לפיו, אבל זה נראה לי סביר מאוד. למה? ובכן, קל לקבל הערכה גסה של כמה שווה כל ספרה, בגימטריה: “שלושת אלפים” הוא 1197. אז גם אם נניח שכל ספרה במספר, כשכותבים אותה בעברית מחזירה ערך גימטרי של 1500, אז בעוד שמספר בן 4 ספרות יוכל להביא אותנו אל ערך גימטרי של 6,000, מספר בן 5 ספרות יוכל להביא אותנו רק אל 7,500 שהוא כמובן קטן ממנו. ומספר בן 8 ספרות - רק אל 9,000, וכן הלאה וכן הלאה. בלשון מתמטית, המספרים גדלים אקספוננציאלית כשמגדילים את מספר הספרות שלהם, אבל הערך הגימטרי של הכתיב שלהם בעברית גדל לינארית, עם קבוע גדול יחסית (חסום על ידי ה-1,500 הזה, אבל בעצם גם על ידי מספרים קטנים יותר). בגלל שהקבוע הזה גדול יחסית בכלל יש לנו “מרחב משחק” בהתחלה.

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

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

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

[code language=”ruby”] def find_cycles nums = (1..5000).to_a cycles = [] while not nums.empty? n = nums.shift cycle = [n] while true next_n = cycle.last.to_hebrew_name.gematric_value if not nums.include?(next_n) cycles << cycle if cycle.include?(next_n) break end cycle << next_n nums.delete(next_n) end end new_cycles = [] for c in cycles do new_c = [c.last] while new_c.last.to_hebrew_name.gematric_value != new_c.first new_c << new_c.last.to_hebrew_name.gematric_value end new_cycles << new_c end new_cycles end [/code]

זה החזיר חמישה מעגלים: [2706, 1596, 2332] [2370, 1682] [813, 2065, 1185, 957, 2003] [2450, 1305, 1548, 1636] [1786, 1982, 2540, 1295]

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

אז מה, שנתייאש? חלילה. אם לא הצלחנו למצוא מספר עבגד בצורה ישרה, נתחיל לרמות ולשנות את חוקי המשחק! למשל, אבינעם יגור מצא ש-“אלף מאה חמש ושש” הוא 1111 בגימטריה. זו לא צורת כתיבה חוקית; אז מה, למה שזה יהיה פחות מגניב? למה הדיקטטורה הזו של צורת הכתיבה החוקית של מילים בעברית? עבגד גם הציע לקרוא בשם “מספרי אבגד” למספרים ששווים לגימטריה של צורת כתיבה לא תקנית של עצמם. התחושה שלי, שתכף אסביר אותה, הייתה שכל צורת כתיבה שבה מערבים שני מספרים תיתן לנו שלל תוצאות אפשריות, ואכן לא התבדיתי. כך מצאתי חיש קל את “המספר שאם מוסיפים לו מאה מקבלים אלפיים ושש מאות” (הניסוח המסורבל הוא מכוון לצורך השעשוע) וגם המון זוגות אחרים שעבורם זה עובד (מה עובד? הערך הגימטרי של המשפט הזה הוא 2500, וזה גם בדיוק המספר שאם מוסיפים לו 100 מקבלים 2600…). אפשר כמובן גם דברים פשוטים יותר, למשל “ארבע מאות ועוד אלף שבע מאות וחמישים” שיוצא גם בגימטרית וגם חשבונית 2150. הנה למשל הקוד ששימש אותי במציאת הזוג האחרון:

[code language=”ruby”] def find_for_pair max = 2100 nums = (1..max).collect{|n| n.to_hebrew_name.gematric_value} nums = [nil] + nums

for x in (1..max) do for y in (1…x) do puts "#{x}, #{y}" if (x+y == nums[x] + nums[y] + 86) end end end [/code]

אשאיר לכם להבין מה בדיוק עשיתי בו. למה הייתי כל כך בטוח שאמצא המון תוצאות? ובכן, זה די דומה למה שקורה בדילוגי אותיות בתורה. נניח שיש לי ביטוי מהצורה “המספר שאם מוסיפים לו X מקבלים Y”. הביטוי הזה מערב שני מספרים X,Y שאפשר לבחור בחופשיות מתוך טווח גדול יחסית - נאמר, עד 4000. עכשיו, לצורך האינטואיציה נוח לחשוב שהערך הגימטרי של הביטוי הזה נקבע אקראית (בפועל הוא לא נקבע אקראית אלא נלקח מתחום קטן יותר וזה בעיקר עוזר לי) להיות משהו בין, נאמר, 1 ו-8000? עכשיו, מבין 8000 הערכים האפשריים הללו, עבור כל X,Y יש בדיוק ערך אחד שאנחנו רוצים לפגוע בו - הערך שהוא המספר שהביטוי “המספר שאם מוסיפים לו X מקבלים Y” מתאר. ההסתברות שלנו להצליח? 1 ל-8000. אם אנחנו עושים 4000 ניסויים כאלו, יש סיכוי לא רע שנצליח באחד מהם, בערך. אבל אנחנו עושים בערך 4000 בריבוע ניסויים כאלו, ולכן המון הצלחות זה עניין כמעט בטוח. כך גם בצופן התנ”כי - עושים שם בסופו של דבר מספר ריבועי של ניסויים.

ייתכן שכל זה עדיין לא מפיס את דעתכם. אל דאגה! הלל גרשוני הגיע לדיון והסביר שהדרך התקנית, התנ”כית, לכתוב מספרים היא בכלל כן לשים “ו” לפני כל מספר פרט לראשון. למשל, “מאה ועשרים ואחת”. זה מוביל לפונקציה הבאה: [code language=”ruby”] def to_hebrew_name_bible_style return HEBREW_NUMBER_NAMES[self] if HEBREW_NUMBER_NAMES[self] name_parts = [] digits = self.to_s.split("").reverse.collect_with_index{|x,i| x.to_i * 10**i}.reverse if digits[-2] == 10 and digits[-1] != 0 digits[-2] += digits[-1] digits.pop end digit_names = digits.reject{|d| d == 0}.collect{|d| d.to_hebrew_name} (1…digit_names.length).each{|i| digit_names[i] = "ו" + digit_names[i]} name = digit_names.join(" ") name end [/code]

קיוויתי שכשנריץ אותה נמצא תוצאה אחת, אולי שתיים. שומו שמיים! מצאנו לא פחות מאשר חמש תוצאות! 1046, 1134, 1702, 1910, 2499! שמעו חברים, אמנם אמרתי שהכל משחק ואין לייחס לו משמעות, ואמנם כתבתי פה פוסטים ארוכים על למה הצופן התנ”כי זה הבל ולמה גימטריה זה הבל, אבל לא עוד. היום הפכתי לאדם מאמין.


נהניתם? התעניינתם? אם תרצו, אתם מוזמנים לתת טיפ:

Buy Me a Coffee at ko-fi.com