Graph Valid Tree
لديك n من العقد، مرقّمة من 0 إلى n-1، وقائمة edges من الحواف غير الموجّهة. يصل كل زوج [a, b] بين العقدة a والعقدة b.
أعِد true إذا كانت الحواف تجعل العقد n كلها شجرة واحدة: بحيث يمكن لكل عقدة الوصول إلى كل عقدة أخرى، ولا توجد أي دورة. أعِد false خلاف ذلك.
الدالة
- ninteger
- عدد العُقد، المرقّمة من 0 إلى n-1
- edgesinteger-2d-array
- الحواف غير الموجّهة، كلٌّ منها زوج [a, b]
- تُرجعboolean
- true إذا كانت الحواف تصل جميع العقد الـ n في شجرة واحدة، وfalse خلاف ذلك
القيود
2 ≤ n ≤ 1041 ≤ edges.length ≤ 5000edges[i] = [a, b]حيث0 ≤ a, b ≤ n-1وa ≠ b- لا تظهر أي حافة مرتين، في أيٍّ من الاتجاهين.
أمثلة
- المدخلات
- n = 5edges = [[0, 1], [0, 2], [0, 3], [1, 4]]
- المخرجات
- true
- الشرح
- ترتبط العقدة 0 بالعقد 1 و2 و3، وتتفرع العقدة 4 من العقدة 1. تربط أربعة أضلاع العقد الخمس جميعها من دون أي دورة، لذا فهذا رسم شجري.
- المدخلات
- n = 5edges = [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]]
- المخرجات
- false
- الشرح
- تشكّل الحواف
[1, 2]و[2, 3]و[1, 3]الحلقة 1، 2، 3، 1. خمسة أضلاع على خمس عقد تزيد بواحد على العدد الذي يمكن أن تحتويه شجرة.
- المدخلات
- n = 4edges = [[0, 1], [1, 2], [2, 0]]
- المخرجات
- false
- الشرح
- هناك 3 حواف، وهو
n-1، لكنها تُشكّل دورة 0، 1، 2، 0، ولا توجد أي حافة للعقدة 3.
سؤال إضافي
عندما لا يكون الرسم البياني شجرة، فكم حافة يجب أن تحذف، وكم حافة يجب أن تضيف، ليصبح شجرة؟ هل يمكنك حساب كليهما في مرور واحد؟
تلميحات
افتحها واحدًا تلو الآخر. كل تلميح يكشف أكثر قليلًا.
ارسم بعض الرسوم البيانية الصغيرة التي ليست أشجارًا. هناك طريقتان فقط للفشل: ما هما؟
يفشل الرسم البياني إذا احتوى على دورة أو إذا ترك بعض العقد منقطعة. تحتوي الشجرة التي تضم
nعقد علىn-1حافة بالضبط، ومع وجودn-1حافة بالضبط، يحدث هذان الأمران معًا دائمًا. لذا تحقّق من العدد، ثم تحقّق من أحد الأمرين.ابدأ بكل عقدة منفردة. ولكل حافة، اسأل عمّا إذا كان طرفاها ينتميان بالفعل إلى المجموعة نفسها. إن كانا كذلك، تُغلق الحافة دورة؛ وإلا، فاجمع مجموعتيهما معًا. تجيب بنية الاتحاد-والبحث عن هذا السؤال في زمن شبه ثابت.
الحل
الشجرة متصلة وليس فيها أي دورة. وبوجود n-1 حافة بالضبط، يشكّل هذان الشرطان شرطًا واحدًا: يكون هذا الرسم البياني متصلًا إذا وفقط إذا لم يكن فيه أي دورة. لذا تحقّق أولًا من عدد الحواف، ثم ابحث مرة واحدة عن الاتصال، أو دع بنية الاتحاد-والإيجاد تكتشف حافة تصل بين عقدتين مرتبطتين بالفعل.
ابحث عن مسار قبل كل حافة
صحيحة، لكنها لا تنتهي في أكبر الاختبارات
الفكرة
أضف الحواف واحدةً تلو الأخرى. قبل إضافة [u, v]، ابحث انطلاقًا من u عبر الحواف التي أُضيفت حتى الآن. إذا كان الوصول إلى v ممكنًا، فالحافة الجديدة تمثل مسارًا ثانيًا بينهما وتُغلق دورة، كما تفعل [1, 3] في المثال الثاني. إذا لم تُغلق أي حافة دورة، يكون الرسم البياني غابة، ويُبيّن بحث أخير انطلاقًا من العقدة 0 ما إذا كانت شجرة واحدة.
يفحص كل تحقق الرسم البياني بأكمله حتى تلك اللحظة. في مسار مكوّن من 5001 عقدة تصل حوافه بالترتيب، تزور عمليات البحث 1 + 2 + ... + 5000 عقدة، أي نحو 12.5 مليونًا.
الخوارزمية
- ابدأ بقائمة تجاور فارغة لكل عقدة.
- لكل حافة
[u, v]، ابحث انطلاقًا منuعبر الحواف التي أُضيفت حتى الآن. إذا وصلت إلىv، فأعِدfalse. - وإلا، فأضِف الحافة إلى قائمتي الطرفين.
- في النهاية، أعد ما إذا كان البحث انطلاقًا من العقدة 0 يصل إلى جميع العقد
n.
def reachable(graph, start):
# Every node a search from start can reach, using the edges added so far.
seen = [False] * len(graph)
seen[start] = True
stack = [start]
while stack:
node = stack.pop()
for nxt in graph[node]:
if not seen[nxt]:
seen[nxt] = True
stack.append(nxt)
return seen
def validTree(n, edges):
graph = [[] for _ in range(n)]
for u, v in edges:
# A path from u to v already exists, so this edge closes a cycle.
if reachable(graph, u)[v]:
return False
graph[u].append(v)
graph[v].append(u)
# No cycle anywhere. A tree also needs every node reachable from node 0.
return all(reachable(graph, 0))عدّ الحواف، ثم ابحث مرة واحدة
الفكرة
ابدأ من n عقد منفصلة وأضف الحواف واحدة تلو الأخرى. كل حافة إما أن تصل بين مجموعتين، فتقلل عدد المجموعات بمقدار مجموعة واحدة، أو تقع داخل مجموعة وتغلق دورة. يتطلب الوصول إلى مجموعة واحدة n-1 عملية وصل. لذا فإن عدد الحواف الأقل من n-1 لا يمكنه وصل الرسم البياني، والعدد الأكبر لا بد أن يغلق دورة: يفشل المثال الثاني، الذي يحتوي على 5 حواف و5 عقد، بسبب العدد وحده.
عندما يكون عدد الحواف n-1 بالضبط، ينتهي الرسم البياني بمجموعة واحدة فقط إذا كانت كل حافة عملية وصل، أي إذا لم تغلق أي حافة دورة. لذا، بعد التأكد من صحة العدد، تحقّق من الاتصال فقط: يجب أن يصل بحث واحد من العقدة 0 إلى جميع العقد n. في المثال الثالث، لا يصل إلا إلى 0 و1 و2. عندما يكون m = n-1، تكون كلفة القوائم والبحث O(n).
الخوارزمية
- إذا لم تكن
edges.lengthمساويةً لـn-1، فأعِدfalse. - أنشئ قائمة مجاورة، وخزّن كل حافة عند كلا طرفيها.
- ادفع العقدة 0 إلى مكدس وعلّمها بأنها تمت زيارتها.
- أزل العقد من المكدس وادفع كل جار لم تتم زيارته بعد، مع تعليمه وعدّه عند دفعه.
- أعِد ما إذا كان العدد يساوي
n.
def validTree(n, edges):
# A tree on n nodes has exactly n-1 edges.
if len(edges) != n - 1:
return False
graph = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
# With n-1 edges, the graph is a tree exactly when it is connected.
seen = [False] * n
seen[0] = True
stack = [0]
reached = 1
while stack:
node = stack.pop()
for nxt in graph[node]:
if not seen[nxt]:
seen[nxt] = True
reached += 1
stack.append(nxt)
return reached == nعدّ الحواف وبنية الاتحاد-البحث
الفكرة
تجيب بنية الاتحاد والبحث عن سؤال الدورة لكل حافة دون إجراء بحث. تبدأ كل عقدة كمجموعة مستقلة. بالنسبة إلى حافة [u, v]، اعثر على جذر كل طرف. إذا كان الجذران مختلفين، فإن الحافة تصل بين مكوّنين، لذا ادمجهما. وإذا كان الجذر واحدًا، فهذا يعني أن u وv متصلتان بالفعل، وأن الحافة تُغلق دورة. إذا كانت هناك n-1 حافة، وكلها تدمج المجموعات، فستنتهي المجموعات n كمجموعة واحدة: شجرة.
تحافظ قاعدتان على قِصر سلاسل الآباء. تربط عملية الدمج جذر المجموعة الأصغر بجذر المجموعة الأكبر، لذا مع 10^4 عقدة لا يتجاوز أي تسلسل 13 خطوة. أثناء البحث، وجّه كل عقدة تمر بها إلى جدّها. معًا، تجعل القاعدتان كلفة m عملية O(m α(n))، حيث إن α، دالة أكرمان العكسية، لا تتجاوز 4 هنا. المساحة هي O(n).
الخوارزمية
- إذا لم تكن
edges.lengthمساويةً لـn-1، فأعِدfalse. - عيّن
parent[x] = xوحجم المجموعة 1 لكل عقدة. - لكل حافة
[u, v]، اتبع الآباء بدءًا منuومنvحتى تصل إلى جذريهما، واجعل كل عقدة تمر بها تشير إلى جدّها. - إذا كان الجذران متساويين: فأعِد
false. - إذا كان الجذران مختلفين: ألحِق جذر المجموعة الأصغر بالأخرى، وأضف حجمها.
- إذا دمجت كل حافة مجموعتين، فأعِد
true.
def validTree(n, edges):
# A tree on n nodes has exactly n-1 edges.
if len(edges) != n - 1:
return False
parent = list(range(n)) # every node starts as its own group
size = [1] * n
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)
# The two ends are already joined: this edge closes a cycle.
if a == b:
return False
# Hang the smaller group under the larger one.
if size[a] < size[b]:
a, b = b, a
parent[b] = a
size[a] += size[b]
# n-1 merges with no cycle turn the n groups into one.
return True
أخطاء شائعة وحالات حدّية
- التحقق من وجود دورة فقط. ثلاث حواف منفصلة على ست عقد لا تحتوي على دورة، لكنها تشكّل ثلاث قطع.
- التحقق من عدد الحواف فقط. يحتوي المثال الثالث على
n-1حافة، لكن الحلقة 0, 1, 2 تستنفد الحافة التي تحتاج إليها العقدة 3. - تخزين كل حافة تحت أحد طرفيها فقط. تربط
[1, 0]العقدتين 0 و1 أيضًا، ويُغفل البحث من العقدة 0 العقدة 1. - اختبار دورة باستخدام البحث بالعمق أولًا من دون تخطّي العقدة الأم. تؤدي كل حافة مباشرةً إلى العقدة التي أتيت منها، لذا تُفعَّل عبارة «العثور على جار تمت زيارته» في كل شجرة.
- استخدام الاستدعاء الذاتي لمسار طويل. يصل عمق الاستدعاءات إلى 5000 عند وجود خط من 5001 عقدة، متجاوزًا الحد الافتراضي في Python البالغ 1000؛ استخدم مكدسًا صريحًا.
أسئلة شائعة3
ما هو التعقيد الزمني لمسألة Graph Valid Tree؟
O(n + m) باستخدام البحث بالعمق، حيث إن m هو عدد الحواف، وO(n + m α(n)) باستخدام بنية الاتحاد-والإيجاد. يتحقق كلاهما من عدد الحواف أولًا، لذا تُرفض أي رسم بياني ذي عدد غير صحيح قبل إجراء أي بحث.
لماذا تحتوي الشجرة ذات n عقدة على n-1 حافة؟
أنشئ الشجرة بدءًا من عقدة واحدة، ثم أضف العقد الأخرى واحدة تلو الأخرى. تصل كل عقدة جديدة ومعها حافة واحدة بالضبط: فبدون أي حافة ستُقطع عن الشجرة، وبوجود حافتين ستُغلق دورة. لذا تحتاج n عقد إلى n-1 حافة.
كيف تكتشف وجود دورة في رسم بياني غير موجّه؟
في بنية الاتحاد-والإيجاد، تُغلق الحافة التي يشترك طرفاها بالفعل في جذر دورةً. أما في البحث بالعمق أولًا، فالدورة هي جار تمت زيارته غير العقدة التي وصلت منها، لذا تتبّع والد كل عقدة. لا تحتاج بنية الاتحاد-والإيجاد إلى الوالد ولا إلى قائمة تجاور.
مسائل مشابهة
مسائل تعتمد على الأفكار نفسها. حلّ اثنتين أو ثلاث منها يثبّت النمط.
def validTree(n, edges):
# اكتب الكود هنااختبار #1
اختبار #2
اختبار #3
الوسائط
n = 5 edges = [[0, 1], [0, 2], [0, 3], [1, 4]]
الإخراج المتوقع
true