PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Union-Find (Disjoint Set) – implementacja PHP, wykrywanie cykli, deduplicacja

by Henryk Tews / czwartek, 13 sierpnia 2026 / Opublikowano w Algorytmy

Union-Find (Disjoint Set Union) to struktura danych do zarządzania zbiorami rozłącznymi. Umożliwia dwie operacje w czasie niemal O(1): sprawdzenie czy dwa elementy należą do tego samego zbioru (Find) i połączenie dwóch zbiorów (Union). Algorytm pojawia się w problemach grafowych, wykrywaniu cykli i grupowaniu elementów. W Magento 2 przydaje się przy łączeniu zduplikowanych klientów i produktów.

Idea – las drzew

Każdy element wskazuje na swojego rodzica. Korzeń drzewa wskazuje na siebie. Elementy w tym samym drzewie należą do tego samego zbioru. Find śledzi wskaźniki do korzenia. Union łączy dwa drzewa przez podpięcie jednego korzenia pod drugi.

Zbiory: {1,4,7} {2,5} {3,6,8}

parent: [_, 1, 2, 3, 1, 2, 3, 1, 3]
         0  1  2  3  4  5  6  7  8

Find(7): 7->1->1 (korzeń) = 1
Find(4): 4->1->1 (korzeń) = 1
Find(7) == Find(4) => ten sam zbiór ✓

Union(1,2): podepnij korzeń 2 pod korzeń 1
parent[2] = 1
Teraz: {1,2,4,5,7} {3,6,8}

Implementacja w PHP

<?php

declare(strict_types=1);

class UnionFind
{
    private array $parent;
    private array $rank;   // przybliżona głębokość drzewa
    private int   $count;  // liczba zbiorów

    public function __construct(int $n)
    {
        $this->count = $n;
        // Każdy element jest własnym rodzicem (osobny zbiór)
        $this->parent = range(0, $n - 1);
        $this->rank   = array_fill(0, $n, 0);
    }

    // Find z path compression - O(α(n)) amortyzowane, praktycznie O(1)
    public function find(int $x): int
    {
        if ($this->parent[$x] !== $x) {
            // Path compression: skróć ścieżkę do korzenia
            $this->parent[$x] = $this->find($this->parent[$x]);
        }
        return $this->parent[$x];
    }

    // Union by rank - O(α(n)) amortyzowane
    public function union(int $x, int $y): bool
    {
        $rootX = $this->find($x);
        $rootY = $this->find($y);

        if ($rootX === $rootY) return false; // już w tym samym zbiorze

        // Podepnij mniejsze drzewo pod większe (by rank)
        if ($this->rank[$rootX] < $this->rank[$rootY]) {
            $this->parent[$rootX] = $rootY;
        } elseif ($this->rank[$rootX] > $this->rank[$rootY]) {
            $this->parent[$rootY] = $rootX;
        } else {
            $this->parent[$rootY] = $rootX;
            $this->rank[$rootX]++;
        }

        $this->count--;
        return true; // połączono
    }

    public function connected(int $x, int $y): bool
    {
        return $this->find($x) === $this->find($y);
    }

    public function getCount(): int { return $this->count; }
}

Zastosowanie 1 – wykrywanie cykli w grafie

<?php

// Sprawdź czy graf zawiera cykl (np. w dependency graph modułów)
function hasCycle(int $vertices, array $edges): bool
{
    $uf = new UnionFind($vertices);

    foreach ($edges as [$u, $v]) {
        if ($uf->connected($u, $v)) {
            return true; // krawędź łączy już połączone wierzchołki = cykl
        }
        $uf->union($u, $v);
    }

    return false;
}

// Sprawdź circular dependencies między modułami Magento
$moduleGraph = [
    [0, 1], // ModuleA zależy od ModuleB
    [1, 2], // ModuleB zależy od ModuleC
    [2, 3], // ModuleC zależy od ModuleD
    // [3, 0] // ModuleD zależy od ModuleA - to byłby cykl!
];

echo hasCycle(4, $moduleGraph) ? "Cykl wykryty!" : "Graf acykliczny";

Zastosowanie 2 – grupowanie zduplikowanych klientów

<?php

declare(strict_types=1);

// Zduplikowani klienci: ten sam email lub ten sam telefon = ta sama osoba
class CustomerDeduplicator
{
    public function findDuplicateGroups(array $customers): array
    {
        $n  = count($customers);
        $uf = new UnionFind($n);

        // Indeksy po emailu i telefonie
        $emailIndex = [];
        $phoneIndex = [];

        foreach ($customers as $i => $customer) {
            $email = strtolower($customer['email']);
            $phone = preg_replace('/[^0-9]/', '', $customer['phone'] ?? '');

            // Jeśli email już widziany - połącz klientów
            if (isset($emailIndex[$email])) {
                $uf->union($i, $emailIndex[$email]);
            }
            $emailIndex[$email] = $i;

            // Jeśli telefon już widziany - połącz klientów
            if ($phone && isset($phoneIndex[$phone])) {
                $uf->union($i, $phoneIndex[$phone]);
            }
            if ($phone) $phoneIndex[$phone] = $i;
        }

        // Zgrupuj według korzenia zbioru
        $groups = [];
        foreach ($customers as $i => $customer) {
            $root = $uf->find($i);
            $groups[$root][] = $customer;
        }

        // Zwróć tylko grupy z duplikatami
        return array_filter($groups, fn($g) => count($g) > 1);
    }
}

$customers = [
    ['id' => 1, 'email' => 'jan@example.com', 'phone' => '600100200', 'name' => 'Jan K.'],
    ['id' => 2, 'email' => 'jan@example.com', 'phone' => '700200300', 'name' => 'Jan Kowalski'],
    ['id' => 3, 'email' => 'anna@example.com','phone' => '600100200', 'name' => 'Anna K.'],
    ['id' => 4, 'email' => 'piotr@example.com','phone' => '500400300', 'name' => 'Piotr N.'],
];

$deduplicator = new CustomerDeduplicator();
$groups       = $deduplicator->findDuplicateGroups($customers);

// Wynik: klienci 1, 2, 3 są w jednej grupie
// (1 i 2 mają ten sam email, 1 i 3 mają ten sam telefon)
// Klient 4 jest osobny

Złożoność

Operacja Bez optymalizacji Z path compression + union by rank
Find O(n) O(α(n)) – praktycznie O(1)
Union O(n) O(α(n)) – praktycznie O(1)
n operacji O(n²) O(n · α(n)) – prawie liniowe

α(n) to odwrotna funkcja Ackermanna – rośnie tak wolno że dla wszystkich praktycznych wartości n wynosi co najwyżej 4.

Podsumowanie

Union-Find to prosta w implementacji struktura o niemal liniowej złożoności dla dynamicznego zarządzania zbiorami. Kluczowe optymalizacje: path compression w Find i union by rank eliminują liniowe ścieżki. Klasyczne zastosowania: wykrywanie cykli w grafach, algorytm Kruskala (MST), komponenty spójne grafu i deduplicacja danych.

About Henryk Tews

Co możesz przeczytać następne

Lista dwukierunkowa – implementacja od zera, SplDoublyLinkedList, tabela porównawcza
Bloom Filter – probabilistyczna struktura, blacklisty tokenów, negative cache
Trie – drzewo prefiksowe, autouzupełnianie, filtr spamu, benchmark vs array
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}