Kehendak Soalan dan Skop
Terdapat n nod yang dilabelkan daripada 0 hingga n - 1. Pada mulanya, setiap nod adalah komponen bersambungnya sendiri. Laksanakan UnionFind dengan operasi-operasi berikut:
union(a, b)menggabungkan komponen yang mengandungiadanb. Ia mengembalikanTruehanya apabila dua
komponen yang sebelum ini berbeza benar-benar digabungkan.
connected(a, b)melaporkan sama ada kedua-dua nod kini berada dalam komponen yang sama.count()mengembalikan bilangan komponen bersambung semasa.
Versi ini membenarkan n = 0, tetapi sebarang argumen operasi mestilah label yang sah atau mencetuskan IndexError. Masalah asas ini hanya menambah sambungan; ia tidak memadamkan sisi (edges), dan panggilan fungsi datang daripada satu benang (thread). Sebagai contoh, selepas bermula dengan n = 6 dan menggabungkan (0, 1), (1, 2), dan (3, 4), tiga komponen tersebut ialah {0, 1, 2}, {3, 4}, dan {5}. Menggabungkan (2, 4) meninggalkan dua komponen. Panggilan union(0, 3) seterusnya mesti mengembalikan False tanpa mengurangkan kiraan komponen lagi.
Ini ialah soalan temuduga struktur data kejuruteraan perisian umum. Kes penggunaan utamanya adalah aliran sambungan inkremental yang diselangi dengan banyak pertanyaan ketersambungan dan kiraan komponen. Jika semua sisi diberikan sekali gus dan pemanggil hanya memerlukan satu kiraan komponen, DFS atau BFS selalunya lebih langsung; mengenali perbezaan tersebut adalah sebahagian daripada jawapan yang mantap.
Apa yang Dinilai oleh Penemuduga
Isyarat pertama ialah pemilihan keadaan (state). Union-Find tidak mengekalkan keseluruhan graf. Ia mewakili setiap set sebagai pokok penunjuk-induk (parent-pointer tree) yang mana akarnya ialah wakil set dan menunjuk kepada dirinya sendiri. Oleh itu, connected(a, b) boleh membandingkan dua akar dan bukannya merentasi setiap sisi yang disimpan.
Isyarat kedua ialah sama ada operasi union hanya menghubungkan akar. Menulis parent[a] = b secara terus boleh memindahkan satu nod dalaman ke bawah nod lain dan merosakkan perwakilan komponen asalnya. Urutan yang betul mencari root_a dan root_b, mengesahkan bahawa kedua-duanya berbeza, dan menyambungkan akar pokok yang lebih kecil kepada akar pokok yang lebih besar. size hanya bermakna pada akar dan mengawal ketinggian pokok.
Isyarat ketiga ialah penjelasan mengenai pemampatan laluan (path compression) dan bukannya templat yang dihafal semata-mata. Pelaksanaan ini menggunakan path halving: semasa find bergerak ke atas, ia menukar induk nod semasa kepada datuknya (grandparent). Induk baharu itu masih berada dalam pokok yang sama, jadi ketersambungan tidak berubah sementara laluan pada masa hadapan menjadi lebih pendek. Bentuk lelaran (iterative) juga mengelakkan kegagalan had kedalaman rekursi pada laluan yang panjang.
Akhir sekali, penemuduga menyemak batas ketakvarian (invariant) kiraan, istilah kekompleksan, dan pengesahan. components bermula pada n dan hanya berkurang selepas dua akar yang berbeza digabungkan. Union pendua dan self-union tidak boleh mengubahnya. Dengan menggabungkan union by size dan pemampatan laluan bersama-sama, operasi mengambil masa terpelunas (amortized) O(α(n)) sepanjang satu jujukan—bukan kes terburuk mutlak (strict worst-case) O(1) untuk setiap panggilan.
Soalan Penjelasan Sebelum Menjawab
- Adakah sambungan hanya ditambah, atau bolehkah ia juga dipadamkan? Union-Find standard mengendalikan penambahan.
Selepas pemadaman sisi sewenang-wenangnya, hutan induk (parent forest) tidak dapat mendedahkan sama ada sisi lain masih menyambungkan kedua-dua titik hujung; itu memerlukan kaedah luar talian (offline) atau struktur ketersambungan dinamik yang lebih maju.
- Adakah pertanyaan diselangi secara dalam talian (online), atau adakah semua sisi disediakan terlebih dahulu? Pertanyaan union dan
ketersambungan yang berselang-seli memihak kepada Union-Find. Untuk satu kiraan komponen dalam graf statik, DFS/BFS senarai kebirasan (adjacency list) adalah lebih telus dan mengekalkan sisi sebenar.
- Apakah yang patut dikembalikan oleh
union? Di sini ia melaporkan sama ada penggabungan telah berlaku. Nilai boolean tersebut menyokong
pengesanan kitaran secara langsung dan memastikan bilangan komponen berubah tepat sekali.
- Bagaimanakah nod yang tidak sah harus bertindak? Versi ini mencetuskan
IndexError. Penyelesaian pertandingan pengaturcaraan boleh mengetepikan
pengesahan di bawah kontrak input yang dijamin sah, tetapi indeks negatif Python tidak boleh secara senyap merujuk kepada penghujung tatasusunan dalam pelaksanaan awam.
- Adakah API mesti melaporkan saiz komponen atau menyenaraikan ahli?
sizepada akar boleh menjawab saiz dalam masa
terpelunas yang hampir malar. Menyenaraikan ahli masih memerlukan kos sekurang-kurangnya sebesar saiz output, dan struktur asas ini tidak mengekalkan senarai keahlian.
- Bolehkah panggilan dibuat secara serentak (concurrent)? Pelaksanaan asas memutasi
parentdi dalamfind, jadi
pertanyaan ketersambungan sekalipun bukan operasi baca sahaja atau thread-safe. Kekongruenan memerlukan kontrak penguncian atau algoritma Union-Find serentak yang khusus.
Kerangka Jawapan 30 Saat
"Saya akan mengekalkan dua tatasusunan dengan panjang n: parent[x] menunjuk kepada induk, dan size[root] menyimpan saiz pokok bagi akar. Pada mulanya setiap nod ialah induknya sendiri dan kiraan komponen ialah n. find bergerak ke akar dan melakukan path halving dengan menghalakan setiap nod yang dilawati kepada datuknya. union mencari kedua-dua akar; jika ia sama, ia mengembalikan False. Jika tidak, ia menyambungkan akar pokok yang lebih kecil kepada akar pokok yang lebih besar, menambah saiznya, mengurangkan kiraan komponen, dan mengembalikan True. Menyambungkan dua akar tidak boleh mencipta kitaran, dan path halving hanya menukar penunjuk di dalam satu set, jadi ketersambungan kekal betul. Permulaan mengambil masa O(n); operasi seterusnya adalah secara terpelunas O(α(n)) dengan ruang O(n)."
Perbincangan Mendalam Langkah demi Langkah
Perwakilan langsung menetapkan label komponen kepada setiap nod. connected ialah perbandingan label, tetapi menggabungkan dua komponen memerlukan pengimbasan keseluruhan tatasusunan dan menggantikan setiap label lama, menjadikan satu operasi union menelan kos O(n). Satu lagi pendekatan naif menggunakan pokok penunjuk-induk tetapi menyambungkan akar tanpa mengawal saiznya. Susunan union yang buruk boleh mencipta rantaian yang panjang dan menurunkan prestasi find.
Reka bentuk yang disyorkan mengekalkan tiga bahagian keadaan:
parent[x]ialah induk kepadax, dan setiap akar pokok memenuhiparent[root] == root.size[root]ialah kiraan nod komponen bagi akar tersebut; nilai lapuk pada bukan-akar tidak akan dibaca.componentsbersamaan dengan bilangan akar dalam hutan induk.
Pelaksanaannya adalah seperti berikut. _validate adalah pendek dan hanya digunakan oleh kelas ini, jadi ia diletakkan bersebelahan dengan lokasi panggilannya dan bukannya dijadikan modul utiliti berasingan.
class UnionFind:
def __init__(self, n: int) -> None:
if n < 0:
raise ValueError("n must be non-negative")
self.parent = list(range(n))
self.size = [1] * n
self.components = n
def _validate(self, x: int) -> None:
if x < 0 or x >= len(self.parent):
raise IndexError("node out of range")
def find(self, x: int) -> int:
self._validate(x)
while x != self.parent[x]:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self, a: int, b: int) -> bool:
root_a = self.find(a)
root_b = self.find(b)
if root_a == root_b:
return False
if self.size[root_a] < self.size[root_b]:
root_a, root_b = root_b, root_a
self.parent[root_b] = root_a
self.size[root_a] += self.size[root_b]
self.components -= 1
return True
def connected(self, a: int, b: int) -> bool:
return self.find(a) == self.find(b)
def count(self) -> int:
return self.componentsKetepatan algoritma dibuktikan dalam tiga langkah. Pada mulanya, setiap nod adalah satu-satunya akar bagi pokok satu nod, jadi hutan mempunyai n pokok dan ketiga-tiga batas ketakvarian dipenuhi. Path halving menukar induk bagi x kepada induk bagi induk asalnya (datuk). Datuk tersebut kekal pada laluan yang sama ke akar asal, jadi operasi ini tidak boleh merentasi komponen lain atau mengubah akar yang dikembalikan oleh find(x).
Satu operasi union hanya mengubah suai dua akar. Akar yang sama bermakna nod tersebut sudah bersambung, jadi tiada keadaan yang berubah. Untuk akar yang berbeza, menghalakan root_b kepada root_a menggabungkan dua pokok menjadi satu dan tidak boleh mencipta kitaran kerana akar-akar tersebut kepunyaan pokok yang berasingan. Saiz akar baharu ialah hasil tambah saiz pokok lama, dan kiraan akar berkurang tepat satu. Melalui aruhan, dua nod bersambung jika dan hanya jika find mengembalikan akar yang sama, dan count() sentiasa sepadan dengan kiraan komponen sebenar.
Union by size menjamin bahawa setiap kali kedalaman nod bertambah kerana keseluruhan pokoknya disambungkan, saiz komponen baharunya sekurang-kurangnya berganda dua. Walaupun tanpa pemampatan laluan, ketinggian pokok adalah paling banyak O(log n). Digabungkan dengan path halving, satu siri m operasi find dan union selepas permulaan mempunyai had terpelunas O(m α(n)). Fungsi songsang Ackermann α berkembang dengan sangat perlahan. "Masa terpelunas yang hampir malar" ialah istilah temuduga yang tepat; "kes terburuk mutlak O(1)" adalah tidak tepat. Kedua-dua tatasusunan menggunakan ruang O(n), dan find lelaran menggunakan ruang tindanan (stack space) pembantu sebanyak O(1).
Keadaan boleh dijejaki dengan urutan operasi ini:
n = 6 count = 6
union(0, 1) -> True count = 5
union(1, 2) -> True count = 4
union(3, 4) -> True count = 3
connected(0, 2) -> True
connected(0, 4) -> False
union(2, 4) -> True count = 2
union(0, 3) -> False count = 2Pengesahan memerlukan lebih daripada satu contoh. Rangkumi n = 0 tanpa pertanyaan, self-union pada n = 1, union pendua, dua komponen berasingan yang dicantumkan oleh satu jambatan (bridge), nod terpencil, operasi union yang diberikan dalam urutan bertentangan, dan label tidak sah -1 dan n. Bagi graf kecil rawak, kekalkan senarai kebirasan (adjacency list) sebagai rujukan tepat (oracle). Selepas setiap penambahan sisi, kira semula ketersambungan dan bilangan komponen dengan BFS dan bandingkannya langkah demi langkah dengan Union-Find. Ujian pembezaan ini dapat mengesan pepijat kiraan dan akar yang sukar dilihat.
Jika semua m sisi diketahui lebih awal dan pemanggil meminta satu kiraan komponen, DFS/BFS senarai kebirasan menggunakan masa dan ruang O(n + m) serta menyatakan niatnya dengan jelas. Union-Find sangat berguna apabila sisi tiba secara inkremental dan pertanyaan berselang-seli dengan operasi union, atau apabila algoritma Kruskal perlu menguji sama ada sisi tidak berarah akan membentuk kitaran. Model operasi—bukan sekadar kewujudan graf—yang mendorong pilihan tersebut.
Contoh Jawapan Berkualiti Tinggi
"Saya akan mengesahkan terlebih dahulu bahawa hubungan hanya ditambah, pertanyaan diselangi dengan penambahan, dan union mesti melaporkan sama ada penggabungan berlaku. Model operasi tersebut sesuai dengan Union-Find. Jika ini adalah satu kiraan komponen ke atas graf statik, saya akan menggunakan DFS sebagai ganti.
Keadaan saya ialah parent, size pada akar, dan bilangan akar semasa dalam components. Setiap nod pada mulanya menunjuk kepada dirinya sendiri. find berjalan secara lelaran ke arah akar dan menunjuk setiap nod yang dilawati kepada datuknya, memendekkan laluan tanpa rekursi. union mendapatkan kedua-dua akar. Akar yang sama mengembalikan False dan tidak mengubah kiraan. Jika berbeza, akar pokok yang lebih kecil menunjuk kepada akar pokok yang lebih besar, saiznya ditambah, dan kiraan dikurangkan.
Perwakilan ini kekal sebagai hutan. Path halving hanya menunjuk nod kepada leluhur dalam pokok yang sama, dan union menghubungkan akar dua pokok yang berbeza, jadi tiada operasi yang mencipta kitaran induk. Setiap union yang berjaya menukar tepat dua pokok menjadi satu, yang juga membuktikan ketakvarian kiraan.
Membina tatasusunan menelan kos O(n). Dengan union by size dan path halving, operasi find, ketersambungan, dan union adalah terpelunas O(α(n)), dengan ruang O(n). Ujian saya menekankan self-union dan union pendua yang tidak mengubah kiraan, menghubungkan dua komponen besar, nod terpencil, struktur kosong, dan label negatif yang tidak sah. Saya juga akan menguji pembezaan kes-kes kecil rawak terhadap BFS."
Kesilapan Biasa
- Menetapkan
parent[a] = bsecara terus →amungkin bukan akar, jadi pokok asal boleh terpisah atau
berubah menjadi rantai yang tidak terkawal → Cari kedua-dua akar dan hubungkan akar sahaja.
- Sentiasa menyambungkan pokok kedua kepada yang pertama → susunan yang buruk mencipta laluan yang panjang → **Gunakan
size atau rank pada akar untuk memilih arah.**
- Hanya mengembalikan
parent[x]daripadafind→ induk tidak semestinya akar, jadi ketersambungan
tidak langsung tersalah klasifikasi → Ikuti penunjuk sehingga ke akar yang menunjuk kepada dirinya sendiri.
- Mengurangkan
componentsselepas setiap panggilan union → union pendua dan self-union menjadikan
kiraan lebih rendah daripada realiti → Kemas kini ia hanya apabila punca/akar berbeza.
- Mengemas kini saiz akar lama selepas menukar akar → metadata menyimpang daripada pokok sebenar →
Pilih akar induk akhir terlebih dahulu, kemudian hubungkan dan tambah saiz secara konsisten.
- Mendakwa kes terburuk
O(1)bagi setiap operasi → jaminan ini adalah terpelunas sepanjang satu jujukan dan
termasuk fungsi songsang Ackermann → Laporkan terpelunas O(α(n)).
- Mengabaikan indeks negatif Python →
find(-1)mengakses nod terakhir dan bukannya mencetuskan ralat →
Sahkan kedua-dua batas dalam pelaksanaan awam.
- Menggunakan Union-Find asas untuk pemadaman sisi sewenang-wenangnya → penunjuk induk tidak mengekalkan laluan
alternatif selepas pemadaman → Gunakan pemprosesan pemadaman luar talian, rollback Union-Find, atau struktur ketersambungan dinamik.
- Menganggap
connectedsebagai baca sahaja → path halving menulis kepadaparent, mewujudkan keadaan perlumbaan (race condition) di bawah
panggilan serentak → Tentukan penyegerakan sebelum memilih kunci global, pemetakan (partitioning), atau algoritma serentak.
Soalan Susulan dan Cara Mengendalikannya
Soalan Susulan 1: Bagaimanakah Union-Find boleh mengesan kitaran dalam graf tidak berarah?
Proses sisi (u, v) satu demi satu. Jika union(u, v) mengembalikan False, titik hujung tersebut sudah bersambung sebelum sisi baharu ditambah, jadi sisi tersebut menutup satu kitaran. Keputusan True hanya menggabungkan dua komponen yang sebelum ini berasingan. Peraturan ini terpakai secara langsung kepada graf tidak berarah. Pengesanan kitaran berarah memerlukan kaedah seperti DFS tiga warna atau pengisihan topologi (topological sorting).
Soalan Susulan 2: Bagaimana jika pemanggil perlu membatalkan (undo) union yang paling terkini?
Gunakan rollback Union-Find. Kekalkan union by size dan tolak induk lama, saiz akar, dan kiraan komponen bagi setiap perubahan sebenar ke dalam tindanan sejarah (history stack) sebelum mengubah suainya. Operasi undo memulihkan nilai-nilai tersebut. Pemampatan laluan biasanya ditinggalkan kerana satu operasi find memutasi banyak entri, sekali gus membesarkan log rollback dan merumitkan sempadan operasi. Union by size sahaja mengehadkan ketinggian kepada O(log n) dan berfungsi dengan baik bersama pecah-dan-perintah (divide-and-conquer) ke atas garis masa operasi luar talian.
Soalan Susulan 3: Bagaimana jika hubungan boleh dipadamkan secara sewenang-wenangnya?
Union-Find standard tidak boleh menjawab pemadaman dalam talian secara sewenang-wenangnya. Jika urutan operasi penuh diketahui, letakkan selang aktif setiap sisi ke dalam pepohon segmen (segment tree) mengikut masa dan rentasnya dengan rollback Union-Find; pemadaman yang hanya berlaku pada bahagian akhir juga boleh diproses secara terbalik sebagai penambahan. Operasi penambahan, pemadaman, dan pertanyaan yang benar-benar dalam talian dan kerap memerlukan struktur ketersambungan dinamik penuh yang lebih maju. Oleh itu, sama ada operasi adalah luar talian merupakan penjelasan penting yang mentakrifkan masalah.
Soalan Susulan 4: Bagaimanakah anda akan mengembalikan saiz komponen atau semua ahlinya?
Saiz sudah disimpan pada akar, jadi size_of(x) = size[find(x)] mengekalkan had terpelunas yang sama. Penyenaraian ahli tidak boleh diperoleh daripada saiz akar sahaja. Mengimbas semua nod dan membandingkan akar menelan kos O(n α(n)); mengekalkan set ahli menambah kos penggabungan dan memori. Mengimbas biasanya lebih mudah untuk eksport yang jarang dilakukan. Penyenaraian yang kerap mungkin mewajarkan perwakilan struktur data yang berbeza.
Soalan Susulan 5: Bagaimanakah anda mengendalikan panggilan daripada pelbagai benang (thread)?
Perubahan terkecil yang betul ialah satu mutex di sekeliling find, union, dan connected, kerana path halving menulis ke tatasusunan parent. Ia mudah dibuktikan ketepatannya tetapi menyirikan (serializes) semua operasi. Hanya perebutan (contention) yang terukur mewajarkan reka bentuk serentak berasaskan atomic compare-and-swap, penyambungan deterministik, atau pemetakan. Reka bentuk sedemikian mesti membuktikan semula penunjuk induk tak berkitar serta kemas kini saiz dan kiraan komponen secara atomik; menggantikan tatasusunan dengan pembolehubah atomik sahaja tidak mencukupi.