Redundant Connection
רשת התחילה כעץ: n צמתים, שממוספרים מ־1 עד n, ומחוברים באמצעות n-1 קשתות לא מכוונות ללא מעגל. לאחר מכן נוספה קשת נוספת בין שני צמתים שלא היו מחוברים ישירות, ולכן הרשימה edges מכילה כעת n קשתות ומעגל אחד בדיוק.
החזר קשת שאפשר להסיר כדי ש־n-1 הקשתות האחרות יהוו שוב עץ. אם כמה קשתות מתאימות, החזר את זו שמופיעה אחרונה ב־edges, בתור הזוג [a, b] בדיוק כפי שהוא כתוב שם.
פונקציה
- edgesinteger-2d-array
- את n הקשתות הלא מכוונות, שכל אחת מהן היא זוג [a, b] של צמתים שממוספרים החל מ־1
- מחזירהinteger-array
- הקשת במחזור שמופיעה אחרונה ב־edges, כפי שהיא כתובה שם
אילוצים
3 ≤ n ≤ 5000, כאשרn = edges.lengthedges[i] = [a, b]כאשר1 ≤ a, b ≤ nוגםa ≠ b- אף קשת אינה מופיעה פעמיים, באף אחד מהכיוונים.
- הקשתות מחברות את כל
nהצמתים ומכילות בדיוק מחזור אחד.
דוגמאות
- קלט
- edges = [[1, 2], [1, 3], [2, 3]]
- פלט
- [2, 3]
- הסבר
- שלוש הצלעות יוצרות את המשולש 1, 2, 3, כך שהסרה של כל אחת מהן משאירה עץ.
[2, 3]מופיע אחרון ברשימה.
- קלט
- edges = [[1, 2], [2, 3], [3, 4], [1, 4], [1, 5]]
- פלט
- [1, 4]
- הסבר
- המחזור הוא 1, 2, 3, 4, 1. הקשת
[1, 5]אינה חלק ממנו: הסרתה תנתק את צומת 5. מבין ארבע הקשתות במחזור,[1, 4]מופיעה אחרונה.
שאלת המשך
אם הקשתות היו מכוונות והגרף המקורי היה עץ מושרש, איזו קשת היית מסיר כשהקשת הנוספת מעניקה לצומת שני הורים?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
הסרת הקשת חייבת להשאיר את כל הצמתים
nמחוברים. אילו קשתות אפשר להסיר בלי לחלק את הגרף לשניים?רק הקשתות שבמחזור: לכל אחת מהן יש מסלול שני מסביב למחזור, וכל קשת אחרת היא הקישור היחיד לחלק כלשהו בגרף. לכן התשובה היא קשת המחזור שמופיעה אחרונה ברשימה.
הוסף את הקשתות לפי סדר הרשימה ל־
nקבוצות נפרדות. הקשת הראשונה ששני קצותיה כבר נמצאים באותה קבוצה סוגרת את המעגל, וכל קשת אחרת במעגל הופיעה לפניה. מבנה union-find מוצא את הקשת הזאת במעבר אחד.
פתרון
הסרת קשת משאירה עץ בדיוק כאשר הקשת נמצאת במחזור היחיד של הגרף, ולכן התשובה היא הקשת במחזור שמופיעה אחרונה ברשימה. מבנה Union-find מוצא אותה במעבר אחד: מוסיפים את הקשתות לפי הסדר, והקשת הראשונה שקצותיה כבר מחוברים היא התשובה.
נסה להסיר כל קשת, מהאחרונה לראשונה
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
הסר קשת אחת ובדוק אם n-1 הקשתות האחרות יוצרות עץ. עם n צמתים ו-n-1 קשתות, הן יוצרות עץ בדיוק כאשר חיפוש אחד מהצומת 1 מגיע לכל n הצמתים. נסה את הקשתות מסוף הרשימה, כך שההסרה הראשונה שעוברת את הבדיקה תהיה המאוחרת ביותר. שמור את האינדקס של כל קשת ברשימות השכנות, וחיפוש יכול לדלג על הקשת שהוסרה בלי לבנות מחדש דבר.
כל ניסיון הוא חיפוש מלא של O(n), ויכולים להיות n ניסיונות: עד 2.5 × 10^7 צעדים כאשר n = 5000.
אלגוריתם
- בנה רשימות שכנות ששומרות, עבור כל שכן, את האינדקס של הקשת המובילה אליו.
- עבור
skip, מהאינדקס האחרון עד 0, חפש החל מצומת 1 והתעלם מהקשת שהאינדקס שלה הואskip. - ספור את הצמתים שהחיפוש מגיע אליהם.
- אם הוא מגיע לכל
nהצמתים, החזר אתedges[skip].
def findRedundantConnection(edges):
n = len(edges)
# graph[x] holds (neighbour, index of the edge) for every edge at node x.
graph = [[] for _ in range(n + 1)]
for i, (u, v) in enumerate(edges):
graph[u].append((v, i))
graph[v].append((u, i))
# Try leaving out each edge, the last one first.
for skip in range(n - 1, -1, -1):
# Without edge skip, can node 1 still reach all n nodes?
seen = [False] * (n + 1)
seen[1] = True
stack = [1]
reached = 1
while stack:
node = stack.pop()
for nxt, i in graph[node]:
if i != skip and not seen[nxt]:
seen[nxt] = True
reached += 1
stack.append(nxt)
# n-1 edges that connect n nodes form a tree.
if reached == n:
return edges[skip]
return []קלפו את העלים
האינטואיציה
עלה, צומת עם קשת אחת, אינו יכול להיות חלק ממחזור, כי מחזור נכנס לכל אחד מהצמתים שלו ויוצא ממנו. הסרת עלה יכולה להפוך את השכן שלו לעלה, לכן ממשיכים להסיר עלים עד שלא נשארים כאלה. מה שנותר הוא בדיוק המחזור, כי כל צומת במחזור שומר על שתי הקשתות שלו במחזור. בדוגמה השנייה מסירים את צומת 5, צומת 1 שומר על שתי קשתות, וצמתים 1 עד 4 נשארים.
כל קשת בין שני צמתים שנותרו היא קשת במחזור, כי קישור שני בין צמתים במחזור היה יוצר מחזור נוסף. לכן סורקים את edges מהסוף ומחזירים את הקשת הראשונה ששני הקצוות שלה נותרו.
אלגוריתם
- בנו רשימות שכנויות וספרו את הדרגה של כל צומת.
- דחפו למחסנית כל צומת שדרגתו 1.
- הוציאו עלה מהמחסנית, קבעו את דרגתו ל־0 והפחיתו את דרגת השכן שנותר; דחפו את השכן הזה כשהדרגה שלו יורדת ל־1.
- כשהמחסנית ריקה, הצמתים שדרגתם גדולה מ־0 מרכיבים את המחזור.
- סרקו את
edgesמהסוף והחזירו את הקשת הראשונה ששני קצותיה עדיין נותרים.
def findRedundantConnection(edges):
n = len(edges)
graph = [[] for _ in range(n + 1)]
degree = [0] * (n + 1)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
degree[u] += 1
degree[v] += 1
# A node with one edge cannot lie on the cycle. Remove it, and repeat.
stack = [x for x in range(1, n + 1) if degree[x] == 1]
while stack:
leaf = stack.pop()
degree[leaf] = 0
for nxt in graph[leaf]:
if degree[nxt] > 0:
degree[nxt] -= 1
if degree[nxt] == 1:
stack.append(nxt)
# Only the cycle is left. Its edge that comes last in the list is the answer.
for u, v in reversed(edges):
if degree[u] > 0 and degree[v] > 0:
return [u, v]
return []איחוד-חיפוש: הקשת הראשונה שסוגרת לולאה
האינטואיציה
הוסף את הקשתות לפי סדר הופעתן ברשימה ועקוב אחר הצמתים שמחוברים זה לזה. עד שהמחזור נסגר, כל קשת מחברת בין שתי קבוצות שאין ביניהן מסלול. הקשת הראשונה שקצותיה כבר מחוברים סוגרת את המחזור. שאר הקשתות של המחזור נוספו לפניה, ולכן זו הקשת במחזור שמופיעה אחרונה. בדוגמה השנייה [1, 4] מוצאת שהצמתים 1 ו־4 כבר מחוברים, ו־[1, 5] אף פעם לא נקראת.
מבנה union-find שומר מערך אחד, parent; מעקב אחר המצביעים מצומת מסתיים בשורש של הקבוצה שלו. הצמדת הקבוצה הקטנה יותר מתחת לגדולה יותר, והפניית צמתים לסבים שלהם במהלך find, שומרות על שרשראות קצרות. לכן העלות הממוצעת של כל פעולה היא O(α(n)), כאשר α, פונקציית אקרמן ההפוכה, לכל היותר 4 עבור כל קלט מציאותי.
אלגוריתם
- הפנה כל צומת מ-1 עד
nאל עצמו,parent[x] = x, והגדר את הגודל שלו ל-1. - עבור על
edgesלפי הסדר. עבור[u, v], מצא את השורש שלuואת השורש שלv. - אם השורשים זהים,
[u, v]סוגרת את המעגל: החזר אותה כפי שנכתבה. - אם לא, החלק הגדול סופח את החלק הקטן: השורש שלו הופך להורה של השורש הקטן, והגדלים מתחברים.
def findRedundantConnection(edges):
n = len(edges)
parent = list(range(n + 1)) # every node starts as its own group
size = [1] * (n + 1)
def find(x):
# Walk up to the root, halving the path on the way.
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
for u, v in edges:
a, b = find(u), find(v)
# u and v are already connected: this edge closes the cycle.
if a == b:
return [u, v]
# Hang the smaller group under the larger one.
if size[a] < size[b]:
a, b = b, a
parent[b] = a
size[a] += size[b]
return []
מלכודות ומקרי קצה
- החזרת הקשת האחרונה ברשימה. בדוגמה השנייה זו
[1, 5], והסרתה מנתקת את צומת 5. - סריקה מההתחלה אחרי הקילוף. כך מוחזרת הקשת המוקדמת ביותר במעגל,
[1, 2]בדוגמה השנייה. - מיון או היפוך של הזוג. אם הרשימה מכילה
[3, 1], יש להחזיר[3, 1], ולא[1, 3]. - הקצאת מערכים עבור צמתים 0 עד
n-1. הצמתים ממוספרים החל מ-1, ולכן צומתnזקוק למערך עםn+1מקומות. - רקורסיה מסביב למעגל. היא עשויה לעבור בכל 5000 הצמתים, פי חמישה ממגבלת הרקורסיה המוגדרת כברירת מחדל ב-Python, לכן השתמשו במחסנית מפורשת ובלולאה בתוך
find.
שאלות נפוצות3
מהי סיבוכיות הזמן של Redundant Connection?
Union-find פותר זאת במעבר אחד על פני n הקשתות: זמן של O(n · α(n)), כאשר α(n) לכל היותר 4 בפועל, ומקום של O(n). הסרת העלים היא גם O(n). ניסיון של כל הסרה עם חיפוש מחדש עולה O(n²).
אילו צלעות אפשר להסיר כדי להשאיר עץ?
בדיוק הקשתות שעל המעגל. לכל אחת מהן יש מסלול שני בין קצותיה לאורך שאר המעגל, ולכן הסרתה משאירה את הגרף קשיר. כל קשת אחרת היא הקישור היחיד בין שני חלקי הגרף, והסרתה מפרידה ביניהם.
האם אפשר לפתור את בעיית החיבור המיותר באמצעות DFS?
כן. לפני הוספת כל קשת, חפשו מאחד הקצוות לאורך הקשתות שנוספו עד כה; הקשת הראשונה שהקצה השני שלה כבר נגיש היא התשובה. העלות היא O(n) לכל קשת, O(n²) בסך הכול, בעוד שמבנה union-find עונה על אותה שאלה בזמן כמעט קבוע.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def findRedundantConnection(edges):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
ארגומנט
edges = [[1, 2], [1, 3], [2, 3]]
הפלט הצפוי
[2, 3]