Longest Increasing Path in a Matrix
ניתן לך matrix, רשת של מספרים שלמים עם m שורות ו-n עמודות, כרשימה של שורות. מסלול נע למעלה, למטה, שמאלה או ימינה (ללא אלכסונים וללא מעבר בין הקצוות), ובכל צעד חייבים להגיע לערך גדול יותר ממש. החזר את מספר התאים במסלול הארוך ביותר כזה. תא יחיד הוא מסלול באורך תא אחד.
פונקציה
- matrixinteger-2d-array
- רשת הערכים, כרשימה של שורות באורך שווה
- מחזירהinteger
- מספר התאים במסלול העולה ממש הארוך ביותר
אילוצים
1 ≤ m, n ≤ 100, כאשרm = matrix.lengthו-n = matrix[i].length- לכל שורה יש אותו אורך
n. 0 ≤ matrix[i][j] ≤ 231-1
דוגמאות
- קלט
- matrix = [[9, 8, 3], [2, 7, 4], [1, 6, 5]]
- פלט
- 7
- הסבר
- המסלול 3, 4, 5, 6, 7, 8, 9 יורד בעמודה הימנית, עובר לאורך השורה התחתונה ועולה בעמודה האמצעית עד ל־9: שבעה תאים. מהערך הקטן ביותר, 1, המסלולים הטובים ביותר כוללים חמישה תאים.
- קלט
- matrix = [[2, 2, 2], [2, 5, 2]]
- פלט
- 2
- הסבר
- ערכים שווים אינם צעד כלפי מעלה, ולכן אף מסלול אינו עובר לאורך ה־2. האפשרות הטובה ביותר היא צעד מאחד משלושת ה־2 שמסביב ל־5 אל ה־5: שני תאים.
- קלט
- matrix = [[4, 4], [4, 4], [4, 4]]
- פלט
- 1
- הסבר
- כל הערכים הם 4, ולכן שום צעד אינו מותר בשום מקום. כל תא בפני עצמו הוא מסלול באורך תא אחד, ו-1 הוא התשובה.
שאלת המשך
האם תוכל גם להחזיר את התאים של אחד המסלולים הארוכים ביותר, יחד עם אורכו?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
האם מסלול יכול לחזור אי פעם לתא שכבר ביקר בו? שימו לב לאופן שבו הערכים משתנים לאורך הדרך.
הערכים רק עולים, ולכן מסלול לא עובר שוב באותו תא, והמסלול הארוך ביותר שמתחיל בתא אינו תלוי באופן שבו הגעת אליו. אורכו הוא 1 ועוד אורך המסלול הארוך ביותר מהשכן הגדול ביותר שלו.
חשב את המספר הזה פעם אחת לכל תא ושמור אותו. אפשר למלא אותו באמצעות חיפוש לעומק על פני שכנים גדולים יותר, כשהמחסנית שלך מנהלת את החיפוש, או לקלף את הרשת מהפסגות שלה, שכבה אחת בכל פעם, ולספור את השכבות.
פתרון
צייר חץ מכל תא אל כל אחד משכניו הגדולים ממנו. הערכים עולים לאורך כל חץ, ולכן שום שרשרת חצים אינה יכולה ליצור לולאה: הרשת היא גרף מכוון חסר מעגלים, והתשובה היא המסלול הארוך ביותר בו. בהיעדר מעגלים, המסלול הארוך ביותר מתא תלוי רק באותו תא, ולכן מחשבים אותו פעם אחת לכל תא: O(m × n).
עקוב אחר כל מסלול עולה
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
התחילו מסלול הליכה בכל תא והמשיכו בכל אחד מהענפים אל שכנים בעלי ערכים גדולים יותר עד שלא נשאר אף אחד; מסלול ההליכה הארוך ביותר הוא התשובה. הערכים עולים בכל צעד, ולכן מסלול הליכה לעולם אינו חוזר לתא ואינו זקוק לקבוצה של תאים שבוקרו.
מסלול ההליכה מתא מסוים מחושב מחדש בכל פעם שמסלול הליכה אחר עובר בו. ברשת בגודל 100 × 100 שבה כל ערך הוא סכום השורה והעמודה שלו, כל צעד ימינה או למטה מטפס, ויותר מ-10^58 מסלולי הליכה יוצאים מהפינה השמאלית העליונה.
אלגוריתם
- עבור כל תא, דחפו (את התא, 1) למחסנית.
- שלפו רשומה (תא, אורך) ועדכנו את התשובה באורך.
- דחפו (שכן, אורך + 1) עבור כל שכן שנמצא בתוך הרשת וערכו גדול יותר באופן ממשי.
- חזרו על הפעולה עד שהמחסנית ריקה, ואז עברו לתא ההתחלה הבא.
- החזירו את האורך הגדול ביותר שנמצא.
def longestIncreasingPath(matrix):
rows, cols = len(matrix), len(matrix[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
answer = 0
for sr in range(rows):
for sc in range(cols):
# Each entry is one path in progress: the cell it ends on and
# how many cells it has.
stack = [(sr, sc, 1)]
while stack:
r, c, length = stack.pop()
answer = max(answer, length)
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
stack.append((nr, nc, length + 1))
return answerחיפוש לעומק עם זיכרון מטמון באמצעות מחסנית משלך
האינטואיציה
נניח ש־best[cell] הוא מספר התאים במסלול העולה הארוך ביותר שמתחיל שם: 1 + max(best[nb]) עבור השכנים הגדולים יותר nb, או 1 אם אין כאלה. בטוח לשמור את הערך הזה, כי התאים שמופיעים לפני cell במסלול קטנים ממנו, ולכן לעולם לא יופיעו אחריו. בדוגמה הראשונה, 9 מקבל 1, 8 מקבל 2, 7 מקבל 3, 6 ו־2 מקבלים 4, 5 ו־1 מקבלים 5, 4 מקבל 6, ו־3 מקבל 7.
מסלול יכול לעבור גם בכל 10,000 התאים, עומק גדול מדי לרקורסיה, לכן השתמשו במחסנית משלכם. התא שבראש המחסנית מנסה את הכיוון הבא שלו ודוחף שכן גדול יותר שעדיין לא הסתיים. לאחר שנוסו כל ארבעת הכיוונים, מוציאים אותו מהמחסנית ומגדירים את best שלו. אין צורך בסימון „בתהליך”: כל תא במחסנית גדול מהתא שמתחתיו.
אלגוריתם
- מלא את
bestב־0 (עדיין לא ידוע) ואת מונה הכיוונים ב־0 עבור כל תא. - עבור כל תא שבו
bestהוא 0, דחוף אותו למחסנית. - בתא שבראש המחסנית, קדם את המונה שלו ודחוף את השכן הזה אם הוא בתוך הרשת, גדול יותר ולא הסתיים.
- לאחר ארבעה כיוונים, הוצא את התא מהמחסנית והגדר את
bestל־1 ועוד הערך הגדול ביותר שלbestמבין השכנים הגדולים ממנו, או ל־1. - החזר את הערך הגדול ביותר של
best.
def longestIncreasingPath(matrix):
rows, cols = len(matrix), len(matrix[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
# best[r][c]: cells on the longest increasing path that starts at (r, c).
# 0 means not known yet.
best = [[0] * cols for _ in range(rows)]
# step[r][c]: how many of the 4 directions the search has tried from (r, c).
step = [[0] * cols for _ in range(rows)]
answer = 0
for sr in range(rows):
for sc in range(cols):
if best[sr][sc]:
continue
# Our own stack instead of recursion: a path can be thousands of
# cells long, past Python's limit of 1,000 nested calls.
stack = [(sr, sc)]
while stack:
r, c = stack[-1]
d = step[r][c]
if d < 4:
step[r][c] = d + 1
nr, nc = r + dirs[d][0], c + dirs[d][1]
# The stack only climbs, so a larger neighbour is never on it.
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c] and best[nr][nc] == 0:
stack.append((nr, nc))
continue
# Every larger neighbour is finished: build on the best of them.
stack.pop()
length = 1
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
length = max(length, best[nr][nc] + 1)
best[r][c] = length
answer = max(answer, length)
return answerקלפו את הרשת מהפסגות שלה
האינטואיציה
בנה את אותה טבלה מהערכים הגבוהים ביותר ומטה, כמו במיון טופולוגי של Kahn. פסגה, תא שאין לו שכן גדול ממנו, מתחילה מסלול באורך תא אחד: שכבה 1. הסר את כל הפסגות; התאים שאיבדו את השכן הגדול האחרון שלהם הם השכבה הבאה. חזור על הפעולה עד שהרשת ריקה. תא מופיע סיבוב אחד אחרי השכן הגדול האחרון שלו, ולכן השכבה שלו שווה ל־best, ומספר השכבות הוא התשובה. בדוגמה הראשונה השכבות הן 9; 8; 7; 2 ו־6; 1 ו־5; 4; 3.
ספור עבור כל תא כמה שכנים גדולים ממנו נותרו. הסרת תא מפחיתה את הספירה של כל שכן קטן ממנו ממש, וספירה של 0 מכניסה אותו לשכבה הבאה. כל תא מוסר פעם אחת: O(m × n), ללא מחסנית.
אלגוריתם
- עבור כל תא, ספור את השכנים בעלי ערך גדול יותר.
- הכנס לשכבה הנוכחית כל תא שהספירה שלו היא 0.
- כל עוד השכבה אינה ריקה, הוסף 1 למספר השכבות. עבור כל תא בה, הפחת את הספירה של כל שכן קטן ממנו ממש, והכנס לשכבה הבאה שכן שהספירה שלו מגיעה ל־0.
- הפוך את השכבה הבאה לנוכחית וחזור על הפעולה.
- החזר את מספר השכבות.
def longestIncreasingPath(matrix):
rows, cols = len(matrix), len(matrix[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
# higher[r][c]: neighbours of (r, c) with a larger value, not peeled yet.
higher = [[0] * cols for _ in range(rows)]
layer = []
for r in range(rows):
for c in range(cols):
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
higher[r][c] += 1
# A peak: no neighbour is larger, so a path from it has one cell.
if higher[r][c] == 0:
layer.append((r, c))
layers = 0
while layer:
layers += 1
next_layer = []
for r, c in layer:
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] < matrix[r][c]:
higher[nr][nc] -= 1
# Its last larger neighbour is peeled: it is a peak now.
if higher[nr][nc] == 0:
next_layer.append((nr, nc))
layer = next_layer
# Each layer is one step further from a peak; the count is the longest path.
return layers
מלכודות ומקרי קצה
- השוואה באמצעות
>=במקום>. כשיש שני ערכי 4 סמוכים, חיפוש בכוח גס נע הלוך ושוב לנצח, וחיפוש עם זיכרון מטמון קורא אורך שעדיין מחושב. - רקורסיה ב-Python. ערכים שמתפתלים על פני רשת בגודל 100 × 100 יוצרים מסלול של 10,000 תאים, פי עשרה מהמגבלה הרגילה של 1,000 קריאות.
- דילוג על תאים שהסתיימו, כפי שהיית עושה במילוי שטח. האורך השמור שלהם הוא בדיוק מה שהתא הנוכחי צריך.
- התחלה רק מהערך הקטן ביותר. בדוגמה הראשונה, ה-1 נותן 5 תאים, אבל התשובה, 7, מתחילה ב-3.
- התחלת האורכים ב-0. לרשת שבה כל הערכים שווים יש תשובה של 1.
שאלות נפוצות3
מהי סיבוכיות הזמן של המסלול העולה הארוך ביותר במטריצה?
זמן ומקום של O(m × n), באמצעות חיפוש לעומק עם שמירה במטמון או באמצעות קילוף: הטיפול בכל תא מסתיים פעם אחת, והוא בודק את 4 השכנים שלו. ניסיון לבדוק כל מסלול מכל תא הוא פעולה אקספוננציאלית.
האם מציאת המסלול העולה הארוך ביותר במטריצה היא בעיית תכנות דינמי או בעיית גרפים?
שניהם. זהו הנתיב הארוך ביותר בגרף מכוון חסר מעגלים, כלומר תכנות דינמי לפי סדר טופולוגי. מיון התאים מהערך הגדול ביותר לקטן ביותר נותן סדר תקף נוסף, במחיר של O(m × n × log(m × n)) עבור המיון.
במה זה שונה מתת־הרצף העולה הארוך ביותר?
תת־רצף יכול לדלג על איברים, אך שומר על הסדר שלהם; כאן, מסלול חייב לעבור לתא סמוך, באחד מארבעת הכיוונים. בשני המקרים, שרשרת עולה ממש לעולם לא יכולה לחזור על עצמה בלולאה.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def longestIncreasingPath(matrix):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
matrix = [[9, 8, 3], [2, 7, 4], [1, 6, 5]]
הפלט הצפוי
7