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الاتحاد-إيجاد على جيران كل عقدة
الفكرة
أيًّا كانت المجموعة التي تقع فيها عقدة، تقع جميع جيرانها في المجموعة الأخرى، لذا فهم يشتركون في مجموعة. لكل عقدة، ادمج جميع جيرانها في مجموعة واحدة ضمن بنية الاتحاد-البحث؛ وعندئذٍ تضم المجموعة عقدًا مفروضًا عليها أن تكون في الجانب نفسه. إذا كان طرفا حافة في مجموعة واحدة، فلا بد أن تبقى داخل مجموعة واحدة، وبالتالي فالرسم البياني ليس ثنائيَّ التقسيم.
يكشف هذا الفحص كل حالات الإخفاق. حول دورة فردية، يؤدي دمج حافتين في كل مرة إلى ضم عقدتها الأولى إلى عقدتها الأخيرة، ثم تضم الحافة التي تغلق الدورة هاتين العقدتين. في غياب دورة فردية، يضم كل دمج عقدتين تفصل بينهما مسافة زوجية، فتقعان على الجانب نفسه. يسمّي جار العقدة الأول، وهو مرساتها، مجموعة جيرانها؛ لذا تدمج العقد أثناء مرور الحواف، دون قوائم تجاور. باستخدام الاتحاد حسب الحجم وتقسيم المسار إلى النصف، تستغرق عمليات الدمج 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
أخطاء شائعة وحالات حدّية
- البحث انطلاقًا من العقدة 0 فقط. المثال الثالث مناسب قرب العقدة 0، لكنه يفشل بين العقد 2 و3 و4.
- إضافة كل حافة إلى قائمة واحدة فقط. مع الحافتين
[1, 0]و[2, 0]، تحصل العقدة 0 على اللون 0 ولا ترى أي جيران؛ ثم تحصل العقدة 1 أيضًا على اللون 0، فتبدو حافتها إلى العقدة 0 كأنها تعارض، رغم أن المسار ينقسم بشكل صحيح. - تلوين العقدة عند إخراجها من قائمة الانتظار. عندئذٍ قد تدخل العقدة قائمة الانتظار مرتين، وتستبدل الزيارة الثانية اللون الأول. لوّنها عند إضافتها إلى قائمة الانتظار.
- الخلط بين كون الرسم البياني ثنائي التقسيم وكونه لا دوريًا. للمربع دورة وهو ثنائي التقسيم؛ الدورات الفردية وحدها هي التي تُفسد التقسيم.
أسئلة شائعة3
ما هو التعقيد الزمني لمسألة «هل الرسم البياني ثنائي التقسيم»؟
يعمل التلوين بالبحث بعرض الشجرة أو بعمقها في زمن 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