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مجموعات منفصلة. الحافة الأولى التي يكون طرفاها في المجموعة نفسها تُغلق الدورة، وقد سبقتها كل حافة أخرى في الدورة. وتجد بنية الاتحاد-والبحث تلك الحافة في مرور واحد.
الحل
تؤدي إزالة حافة إلى ترك شجرة بالضبط عندما تقع الحافة على الدورة الوحيدة في الرسم البياني، لذا فالإجابة هي حافة الدورة التي تأتي أخيرًا في القائمة. تعثر عليها بنية الاتحاد-البحث بتمريرة واحدة: أضف الحواف بالترتيب، وتكون أول حافة طرفاها متصلين مسبقًا هي الإجابة.
جرّب إزالة كل حافة، بدءًا بالأخيرة
صحيحة، لكنها لا تنتهي في أكبر الاختبارات
الفكرة
أزل حافة واحدة وتحقق مما إذا كانت الحواف n-1 الأخرى تكوّن شجرة. بوجود n عقد وn-1 حافة، يتحقق ذلك بالضبط عندما تصل عملية بحث واحدة من العقدة 1 إلى جميع العقد n. جرّب الحواف من نهاية القائمة، بحيث تكون أول إزالة تنجح هي الأحدث. خزّن فهرس كل حافة في قوائم التجاور، وستتمكن عملية البحث من تجاوز الحافة المحذوفة دون إعادة بناء أي شيء.
كل محاولة هي عملية بحث كاملة بتعقيد O(n)، وقد يصل عدد المحاولات إلى n: ما يصل إلى 2.5 × 10^7 خطوة عندما يكون n = 5000.
الخوارزمية
- أنشئ قوائم تجاور تحتفظ، لكل جار، بفهرس الحافة التي تؤدي إليه.
- من آخر فهرس إلى 0، كرّر قيمة
skipوابحث بدءًا من العقدة 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
ما هو التعقيد الزمني لمسألة الاتصال الزائد؟
تحل بنية الاتحاد-الإيجاد المسألة بمرور واحد على n حافة: بزمن O(n · α(n))، حيث تكون α(n) على الأكثر 4 عمليًا، وبمساحة O(n). كما أن تقشير الأوراق يستغرق O(n). أما تجربة كل إزالة مع بحث جديد فتكلف O(n²).
ما الحواف التي يمكن إزالتها لترك شجرة؟
الحواف الواقعة على الدورة تحديدًا. لكل واحدة منها مسار ثانٍ بين طرفيها يمر عبر بقية الدورة، لذا فإن إزالتها تُبقي الرسم البياني متصلًا. أما أي حافة أخرى فهي الوصلة الوحيدة بين جزأين من الرسم البياني، وإزالتها تفصلهما.
هل يمكن حل مسألة الاتصال الزائد باستخدام البحث بالعمق أولًا (DFS)؟
نعم. قبل إضافة كل حافة، ابحث من أحد الطرفين عبر الحواف التي أُضيفت حتى الآن؛ فأول حافة يكون طرفها الآخر قابلاً للوصول إليه هي الإجابة. يستغرق ذلك O(n) لكل حافة، وO(n²) إجمالاً، بينما يجيب هيكل الاتحاد-والإيجاد عن السؤال نفسه في زمن شبه ثابت.
مسائل مشابهة
مسائل تعتمد على الأفكار نفسها. حلّ اثنتين أو ثلاث منها يثبّت النمط.
def findRedundantConnection(edges):
# اكتب الكود هنااختبار #1
اختبار #2
وسيط
edges = [[1, 2], [1, 3], [2, 3]]
الإخراج المتوقع
[2, 3]