Is Graph Bipartite
בגרף יש n צמתים, המסומנים מ-0 עד n-1. כל זוג [a, b] בתוך edges מייצג קשת לא מכוונת בין הצומת a לצומת b. הגרף עשוי להתחלק לכמה חלקים נפרדים, ולחלק מהצמתים ייתכן שאין אף קשת.
גרף הוא דו-צדדי אם אפשר לחלק את הצמתים שלו לשתי קבוצות כך שכל קשת מחברת צומת מקבוצה אחת לצומת מהקבוצה השנייה. יש להחזיר true אם הגרף דו-צדדי, ו-false אחרת.
פונקציה
- ninteger
- מספר הצמתים, הממוספרים מ־0 עד n-1
- edgesinteger-2d-array
- הקשתות הלא־מכוונות, שכל אחת מהן היא זוג [a, b]
- מחזירהboolean
- נכון אם אפשר לחלק את הצמתים לשתי קבוצות כך שכל הקשתות יהיו בין הקבוצות, ולא נכון אחרת
אילוצים
2 ≤ n ≤ 1041 ≤ edges.length ≤ 5000edges[i] = [a, b]כאשר0 ≤ a, b ≤ n-1וגםa ≠ b- אף קשת אינה מופיעה פעמיים, בשום כיוון.
דוגמאות
- קלט
- n = 4edges = [[0, 1], [1, 2], [2, 3], [3, 0]]
- פלט
- true
- הסבר
- שים את הצמתים 0 ו-2 בקבוצה אחת ואת הצמתים 1 ו-3 בקבוצה השנייה. כל אחת מארבע הקשתות מחברת בין שתי הקבוצות.
- קלט
- n = 3edges = [[0, 1], [1, 2], [2, 0]]
- פלט
- false
- הסבר
- צמתים 0 ו-1 חייבים להיות בקבוצות שונות, וצומת 2 חייב להיות שונה משניהם, אבל יש רק שתי קבוצות. אי אפשר לחלק מחזור של שלוש צלעות.
- קלט
- n = 6edges = [[0, 1], [2, 3], [3, 4], [4, 2]]
- פלט
- false
- הסבר
- הקשת
[0, 1]מתפצלת היטב ולצומת 5 אין קשתות, אבל הצמתים 2, 3 ו-4 יוצרים משולש. מספיק שחלק אחד ייכשל כדי שהתשובה תהיהfalse.
שאלת המשך
כאשר הגרף אינו דו־צדדי, האם תוכל להחזיר מחזור אחד באורך אי־זוגי כהוכחה, ולציין את הצמתים שלו לפי הסדר?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
שים את הצומת 0 בקבוצה הראשונה. לאן חייבים ללכת כל השכנים שלו? ומה לגבי השכנים שלהם?
לאחר שממקמים צומת אחד של רכיב קשיר, לכל צומת אחר באותו רכיב אין ברירה: שני הקצוות של קשת משתייכים לקבוצות מנוגדות. הגרף נכשל רק כאשר השיבוץ הכפוי הזה מציב את שני הקצוות של קשת כלשהי באותה קבוצה, וזה קורה בדיוק כאשר יש ברכיב מעגל באורך אי־זוגי.
צבע באמצעות חיפוש לרוחב: תן לצומת הראשון של חלק צבע 0, תן לכל שכן לא צבוע את הצבע ההפוך לצבע של הצומת הנוכחי, והחזר
falseכשלאחד השכנים כבר יש אותו צבע. התחל חיפוש חדש מכל צומת שעדיין לא צבוע, כדי שלא לדלג על אף חלק.
פתרון
בחירת קבוצה עבור צומת אחד כופה את הקבוצה של שאר חלקיו המחוברים: שכן עובר לקבוצה האחרת. צבעו כל חלק באמצעות חיפוש לרוחב שמחליף בין שני צבעים; הגרף הוא דו־צדדי בדיוק כאשר אין קשת ששני קצותיה צבועים באותו צבע. הגרף עשוי להיות לא קשיר, ולכן כל צומת שלא נצבע מתחיל חיפוש חדש.
נסו כל פיצול
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
נסו כל חלוקה. התייחסו לקבוצה של כל צומת כביט אחד, כך ש־n הביטים יוצרים מספר בינארי, והספירה מ־0 עד 2^n-1 עוברת על כל חלוקה פעם אחת. עבור כל חלוקה, בדקו כל קשת, והחזירו true אם יש חלוקה כלשהי שבה כל הקשתות חוצות בין הקבוצות. מספר החלוקות מוכפל בכל צומת: 20 צמתים נותנים כמיליון.
אלגוריתם
- עבור כל מספר מ-0 עד 2^n-1, שייך את הצומת
iלקבוצה שמציינת הסיביתi. - אם כל קשת מחברת בין שתי הקבוצות, החזר
true. - אם אף חלוקה לא עובדת, החזר
false.
def isBipartite(n, edges):
side = [0] * n # side[x] is 0 or 1: the group of node x in the current split
while True:
# Does every edge cross between the two groups?
if all(side[u] != side[v] for u, v in edges):
return True
# Move to the next split, counting in binary over side.
i = 0
while i < n and side[i] == 1:
side[i] = 0
i += 1
if i == n:
return False # all 2^n splits have been tried
side[i] = 1איחוד־חיפוש בין השכנים של כל צומת
האינטואיציה
לאיזו קבוצה שלא ישתייך צומת, כל שכניו ישתייכו לקבוצה השנייה, ולכן הם חולקים קבוצה. עבור כל צומת, מאחדים את כל שכניו לקבוצת union-find אחת; קבוצה מחזיקה אז צמתים שנכפה עליהם להיות באותו צד. קשת ששני קצותיה נמצאים באותה קבוצה חייבת להישאר בתוך קבוצה אחת, ולכן הגרף אינו דו־צדדי.
הבדיקה מזהה כל כשל. סביב מעגל אי־זוגי, איחוד של שתי קשתות בכל פעם מחבר את הצומת הראשון שלו עם האחרון, והקשת שסוגרת את המעגל מחברת בין שניהם. ללא מעגל אי־זוגי, כל איחוד מחבר שני צמתים שמרחקם זוגי, ולכן הם נמצאים באותו צד. השכן הראשון של צומת, עוגן הצומת, מגדיר את קבוצת שכניו, כך שמאחדים תוך כדי מעבר הקשתות, בלי רשימות שכנויות. עם איחוד לפי גודל וחציית מסלולים, העלות של 2m האיחודים ו־m הבדיקות היא O(m α(n)).
אלגוריתם
- קבע
parent[x] = xואתanchor[x] = -1עבור כל צומת. - עבור כל קשת
[u, v], הגדר את הקצה השני כעוגן של כל קצה שאין לו עדיין עוגן. - מזג את
vלתוך הקבוצה שלanchor[u], ואתuלתוך הקבוצה שלanchor[v]. - החזר
falseאם לקשת כלשהי שני קצוות באותה קבוצה, אחרת החזרtrue.
def isBipartite(n, edges):
parent = list(range(n)) # every node starts as its own group
size = [1] * n
anchor = [-1] * n # the first neighbour seen for each node
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
def union(a, b):
a, b = find(a), find(b)
if a == b:
return
# Hang the smaller group under the larger one.
if size[a] < size[b]:
a, b = b, a
parent[b] = a
size[a] += size[b]
# All neighbours of a node must sit on one side: the side opposite the node.
for u, v in edges:
if anchor[u] == -1:
anchor[u] = v
if anchor[v] == -1:
anchor[v] = u
union(anchor[u], v) # v joins the other neighbours of u
union(anchor[v], u) # u joins the other neighbours of v
# An edge whose two ends were forced onto one side cannot cross.
for u, v in edges:
if find(u) == find(v):
return False
return Trueצביעה בשני צבעים בחיפוש לרוחב של כל חלק
האינטואיציה
סתירה היא סופית. הצבע הראשון בחלק הוא בחירה חופשית, והיפוך שלו הופך את הצבעים של כל החלק; כל צבע אחר מוכתב על ידי קשת. החיפוש צובע צמתים לפי השאלה אם המרחק שלהם מנקודת ההתחלה זוגי או אי־זוגי, כך ששני שכנים חולקים צבע רק כאשר יש בחלק מעגל באורך אי־זוגי, ואז לא קיימת חלוקה.
חיפוש רואה רק את החלק שלו, לכן עוברים על כל הצמתים ומתחילים חיפוש חדש מכל צומת שעדיין לא נצבע. בדוגמה השלישית, החיפוש מצומת 0 צובע את הצמתים 0 ו־1. החיפוש מצומת 2 נותן לצומת 2 את הצבע 0 ולצמתים 3 ו־4 את הצבע 1, ואז מוצא את [3, 4] עם צבע 1 בשני הקצוות: false. כל צומת נצבע ומוכנס לתור פעם אחת, וכל אחת מ־2m הכניסות ברשימה מושווית פעם אחת: זמן ומקום O(n + m).
אלגוריתם
- בנו רשימות שכנות, שבהן כל קשת מופיעה תחת שני הקצוות שלה, והגדירו את הצבע של כל צומת ל־-1.
- עברו על הצמתים לפי הסדר. לכל צומת שעדיין לא נצבעו, תנו צבע 0 והכניסו אותו לתור.
- הוציאו צומת מהתור. תנו לכל שכן שעדיין לא נצבע את הצבע ההפוך והכניסו אותו לתור; אם לשכן יש אותו צבע, החזירו
false. - לאחר צביעת כל החלקים, החזירו
true.
from collections import deque
def isBipartite(n, edges):
graph = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
color = [-1] * n # -1: not colored yet, otherwise 0 or 1
for start in range(n):
if color[start] != -1:
continue
# A new component: its first node can take either color.
color[start] = 0
queue = deque([start])
while queue:
node = queue.popleft()
for nxt in graph[node]:
if color[nxt] == -1:
# A neighbour must take the other color.
color[nxt] = 1 - color[node]
queue.append(nxt)
elif color[nxt] == color[node]:
return False # this edge stays inside one group
return True
מלכודות ומקרי קצה
- חיפוש רק מ־node 0. הדוגמה השלישית תקינה בסביבת node 0 ונכשלת בין nodes 2, 3 ו־4.
- הוספת כל קשת לרשימה אחת בלבד. עם הקשתות
[1, 0]ו־[2, 0], node 0 מקבל צבע 0 ולא רואה שכנים; לאחר מכן גם node 1 מקבל צבע 0, והקשת שלו ל־node 0 נראית כמו התנגשות, אף על פי שהמסלול מתפצל כראוי. - צביעת צומת כשמוציאים אותו מהתור. כך צומת יכול להיכנס לתור פעמיים, והביקור השני דורס את הראשון. יש לצבוע אותו כשמכניסים אותו לתור.
- בלבול בין דו־צדדי לחסר מעגלים. לריבוע יש מעגל והוא דו־צדדי; רק מעגלים באורך אי־זוגי שוברים את החלוקה.
שאלות נפוצות3
מהי סיבוכיות הזמן של Is Graph Bipartite?
צביעה בחיפוש לרוחב או לעומק פועלת בזמן O(n + m) עבור m קשתות: כל צומת נצבע פעם אחת, וכל קשת מושווית פעם אחת מכל אחד מקצותיה. רשימות הסמיכויות והצבעים תופסים מקום של O(n + m). מבנה איחוד-חיפוש מגיע לאותה תשובה בזמן O(n + m α(n)).
למה מחזור אי־זוגי גורם לכך שגרף אינו דו־צדדי?
עוברים לאורך המעגל, והקבוצות חייבות להתחלף: ראשונה, שנייה, ראשונה, שנייה. מעגל עם מספר אי־זוגי של צלעות מחזיר אותך לנקודת ההתחלה אחרי מספר אי־זוגי של מעברים, ולכן נקודת ההתחלה הייתה צריכה להשתייך לשתי הקבוצות. גרף הוא דו־צדדי אם ורק אם אין בו מעגל באורך אי־זוגי.
האם כדאי להשתמש ב-BFS או ב-DFS כדי לבדוק אם גרף הוא דו־צדדי?
שניהם מתאימים: כלל הצביעה זהה, ורק סדר הביקור משתנה. בגרפים גדולים כתבו DFS עם מחסנית מפורשת, כי גרסה רקורסיבית עלולה להגיע לעומק של קריאה אחת לכל צומת.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def isBipartite(n, edges):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנטים
n = 4 edges = [[0, 1], [1, 2], [2, 3], [3, 0]]
הפלט הצפוי
true