Algorytmy Divide and Conquer

Wprowadzenie

Divide and Conquer (dziel i zwyciężaj) to fundamentalny paradygmat projektowania algorytmów, który polega na podziale problemu na mniejsze podproblemy, rozwiązaniu ich rekurencyjnie, a następnie połączeniu wyników w rozwiązanie całości.

Trzy główne etapy: 1. Divide (Dziel) - podziel problem na mniejsze podproblemy tego samego typu 2. Conquer (Zwyciężaj) - rozwiąż podproblemy rekurencyjnie (lub bezpośrednio jeśli są wystarczająco małe) 3. Combine (Łącz) - połącz rozwiązania podproblemów w rozwiązanie całości

Algorytmy Divide and Conquer są wykorzystywane w:

  • Sortowaniu (merge sort, quick sort)
  • Wyszukiwaniu (binary search)
  • Operacjach na macierzach (mnożenie Strassena)
  • Geometrii obliczeniowej (najbliższa para punktów)
  • Analizie danych (znajdowanie mediany)
  • Przetwarzaniu sygnałów (FFT - Fast Fourier Transform)

Schemat ogólny Divide and Conquer

Struktura algorytmu

Wyjście:

=== Struktura algorytmu Divide and Conquer ===

1. DIVIDE (Dziel):
   - Podziel problem na k podproblemów
   - Zazwyczaj k = 2 (podział binarny)
   - Wielkość podproblemu: n/k

2. CONQUER (Zwyciężaj):
   - Rozwiąż każdy podproblem rekurencyjnie
   - Przypadek bazowy: problem wystarczająco mały
   - Rozwiązanie bezpośrednie dla przypadku bazowego

3. COMBINE (Łącz):
   - Połącz rozwiązania podproblemów
   - Złożoność łączenia wpływa na całkowitą złożoność
   - Może być O(1), O(n), O(n log n) itp.

Równanie rekurencyjne:
  T(n) = k·T(n/k) + f(n)

gdzie:
  - k = liczba podproblemów
  - n/k = wielkość każdego podproblemu
  - f(n) = koszt dzielenia i łączenia

Przykład 1: Binary Search (wyszukiwanie binarne)

Implementacja z wizualizacją kroków

Wyjście:

=== Binary Search - przykład Divide and Conquer ===

Tablica: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25]
Szukana wartość: 15

============================================================

Rekurencja głębokość 0:
  Zakres: [0, 12]
  Fragment: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25]
  Środek: index=6, wartość=13
  13 < 15 → szukaj w prawej połowie

Rekurencja głębokość 1:
  Zakres: [7, 12]
  Fragment: [15, 17, 19, 21, 23, 25]
  Środek: index=9, wartość=19
  19 > 15 → szukaj w lewej połowie

Rekurencja głębokość 2:
  Zakres: [7, 8]
  Fragment: [15, 17]
  Środek: index=7, wartość=15
  → ZNALEZIONO na indeksie 7!

============================================================

Wynik: Znaleziono na indeksie 7

============================================================
ANALIZA DIVIDE AND CONQUER:
============================================================

DIVIDE:
  - Dzielimy tablicę na dwie połowy w każdym kroku
  - Punkt podziału: środek tablicy

CONQUER:
  - Wybieramy jedną połowę do dalszego przeszukania
  - Druga połowa jest odrzucana
  - Rekurencyjne wywołanie dla wybranej połowy

COMBINE:
  - Nie ma łączenia! (O(1))
  - Wynik jest zwracany bezpośrednio

Równanie rekurencyjne:
  T(n) = T(n/2) + O(1)
  Rozwiązanie: T(n) = O(log n)

Przykład 2: Merge Sort (sortowanie przez scalanie)

Implementacja z wizualizacją

Wyjście (fragment):

=== Merge Sort - klasyczny przykład D&C ===

Tablica początkowa: [38, 27, 43, 3, 9, 82, 10]

======================================================================

[Poziom 0] Sortowanie: [38, 27, 43, 3, 9, 82, 10]
  DIVIDE na:
    Lewa:  [38, 27, 43]
    Prawa: [3, 9, 82, 10]

[Poziom 1] Sortowanie: [38, 27, 43]
  DIVIDE na:
    Lewa:  [38]
    Prawa: [27, 43]

  [Poziom 2] Sortowanie: [38]
    → Bazowy (1 element): [38]

  [Poziom 2] Sortowanie: [27, 43]
    DIVIDE na:
      Lewa:  [27]
      Prawa: [43]

    [Poziom 3] Sortowanie: [27]
      → Bazowy (1 element): [27]

    [Poziom 3] Sortowanie: [43]
      → Bazowy (1 element): [43]

    COMBINE:
      [27] + [43]
      → [27, 43]

  COMBINE:
    [38] + [27, 43]
    → [27, 38, 43]

[... dalsze kroki ...]

======================================================================

Tablica posortowana: [3, 9, 10, 27, 38, 43, 82]

Przykład 3: Quick Sort

Implementacja z wyborem pivota

Wyjście:

=== Quick Sort - D&C z partycjonowaniem ===

Tablica początkowa: [10, 7, 8, 9, 1, 5]

======================================================================

[Poziom 0] Zakres [0:5]: [10, 7, 8, 9, 1, 5]
  Pivot: 5 (index 1)
  Po partycjonowaniu: [1, 5, 8, 9, 10, 7]
    Lewa:  [1]
    Prawa: [8, 9, 10, 7]

  [Poziom 1] Zakres [2:5]: [8, 9, 10, 7]
    Pivot: 7 (index 2)
    Po partycjonowaniu: [7, 9, 10, 8]
      Lewa:  []
      Prawa: [9, 10, 8]

    [Poziom 2] Zakres [3:5]: [9, 10, 8]
      Pivot: 8 (index 3)
      Po partycjonowaniu: [8, 10, 9]
        Lewa:  []
        Prawa: [10, 9]

      [Poziom 3] Zakres [4:5]: [10, 9]
        Pivot: 9 (index 4)
        Po partycjonowaniu: [9, 10]
          Lewa:  []
          Prawa: [10]

======================================================================

Tablica posortowana: [1, 5, 7, 8, 9, 10]

======================================================================
MERGE SORT vs QUICK SORT:
======================================================================

Aspekt                 Merge Sort                 Quick Sort
---------------------- -------------------------- --------------------------
DIVIDE                 Połowa array (O(1))        Partycjonowanie (O(n))
CONQUER                2 podproblemy n/2          2 podproblemy n/2
COMBINE                Scalanie (O(n))            Brak! (O(1))
Złożoność średnia      O(n log n)                 O(n log n)
Złożoność najgorsza    O(n log n)                 O(n²)
Pamięć dodatkowa       O(n)                       O(log n)
Stabilny?              TAK                        NIE
In-place?              NIE                        TAK

Problem maksymalnej podtablicy (Maximum Subarray)

Algorytm Kadane'a i podejście D&C

Wyjście:

=== Problem maksymalnej podtablicy ===

Tablica: [-2, 1, -3, 4, -1, 2, 1, -5, 4]

Rozwiązanie Divide and Conquer:
  Maksymalna suma: 6
  Podtablica: [4, -1, 2, 1]
  Indeksy: [3, 6]
  Złożoność: O(n log n)

Algorytm Kadane'a:
  Maksymalna suma: 6
  Podtablica: [4, -1, 2, 1]
  Indeksy: [3, 6]
  Złożoność: O(n)

======================================================================
ANALIZA D&C dla maksymalnej podtablicy:
======================================================================

DIVIDE:
  - Podziel tablicę na dwie połowy w środku

CONQUER:
  - Rekurencyjnie znajdź max w lewej połowie
  - Rekurencyjnie znajdź max w prawej połowie

COMBINE:
  - Znajdź max podtablicę przechodzącą przez środek
  - Wybierz najlepszą z trzech opcji:
    1. Max w lewej połowie
    2. Max w prawej połowie
    3. Max przechodząca przez środek

Równanie rekurencyjne:
  T(n) = 2·T(n/2) + O(n)
  Rozwiązanie: T(n) = O(n log n)

Uwaga: Algorytm Kadane'a jest lepszy (O(n)),
       ale D&C pokazuje elegancki podział problemu!

Mnożenie macierzy Strassena

Klasyczne vs Strassen

Wyjście:

=== Mnożenie macierzy Strassena ===

Macierz A:
  [1, 2, 3, 4]
  [5, 6, 7, 8]
  [9, 10, 11, 12]
  [13, 14, 15, 16]

Macierz B:
  [1, 0, 0, 1]
  [0, 1, 0, 1]
  [0, 0, 1, 1]
  [1, 1, 1, 0]

A × B (klasyczne):
  [5, 6, 7, 6]
  [13, 14, 15, 22]
  [21, 22, 23, 38]
  [29, 30, 31, 54]

A × B (Strassen):
  [5, 6, 7, 6]
  [13, 14, 15, 22]
  [21, 22, 23, 38]
  [29, 30, 31, 54]

Wyniki identyczne? True

======================================================================
ALGORYTM STRASSENA:
======================================================================

KLASYCZNE MNOŻENIE:
  C = A × B wymaga 8 mnożeń podmacierzy:
  C11 = A11×B11 + A12×B21
  C12 = A11×B12 + A12×B22
  C21 = A21×B11 + A22×B21
  C22 = A21×B12 + A22×B22
  Złożoność: T(n) = 8·T(n/2) + O(n²) = O(n³)

ALGORYTM STRASSENA:
  Używa tylko 7 mnożeń (zamiast 8):
  M1 = (A11 + A22) × (B11 + B22)
  M2 = (A21 + A22) × B11
  M3 = A11 × (B12 - B22)
  M4 = A22 × (B21 - B11)
  M5 = (A11 + A12) × B22
  M6 = (A21 - A11) × (B11 + B12)
  M7 = (A12 - A22) × (B21 + B22)

  Następnie:
  C11 = M1 + M4 - M5 + M7
  C12 = M3 + M5
  C21 = M2 + M4
  C22 = M1 + M3 - M2 + M6

  Złożoność: T(n) = 7·T(n/2) + O(n²) = O(n^2.807)

  Poprawa: O(n³) → O(n^2.807) ✓

Problem najbliższej pary punktów

Geometria obliczeniowa z D&C

Wyjście:

=== Problem najbliższej pary punktów ===

Punkty (10):
  P0: (2, 3)
  P1: (12, 30)
  P2: (40, 50)
  P3: (5, 1)
  P4: (12, 10)
  P5: (3, 4)
  P6: (20, 25)
  P7: (30, 15)
  P8: (25, 20)
  P9: (8, 12)

======================================================================

Brute Force (O(n²)):
  Najbliższa para: (2, 3) - (3, 4)
  Odległość: 1.4142

Divide and Conquer (O(n log n)):
  Najbliższa para: (2, 3) - (3, 4)
  Odległość: 1.4142

======================================================================
ANALIZA ALGORYTMU D&C:
======================================================================

DIVIDE:
  - Sortuj punkty po współrzędnej x
  - Podziel na dwie równe połowy pionową linią

CONQUER:
  - Rekurencyjnie znajdź najbliższą parę w lewej połowie
  - Rekurencyjnie znajdź najbliższą parę w prawej połowie
  - Niech δ = min(δ_left, δ_right)

COMBINE:
  - Sprawdź pary punktów przechodzące przez linię podziału
  - Wystarczy sprawdzić pasmo szerokości 2δ wokół linii
  - Dla każdego punktu sprawdź tylko 7 najbliższych sąsiadów!
  - (dowód matematyczny: w prostokącie δ×2δ max 8 punktów)

Równanie rekurencyjne:
  T(n) = 2·T(n/2) + O(n)
  Rozwiązanie: T(n) = O(n log n)

Master Theorem (Twierdzenie Master)

Analiza złożoności rekurencyjnych algorytmów D&C

Wyjście:

=== Master Theorem dla algorytmów D&C ===

RÓWNANIE REKURENCYJNE:
  T(n) = a·T(n/b) + f(n)

gdzie:
  - a = liczba podproblemów
  - n/b = wielkość każdego podproblemu
  - f(n) = koszt dzielenia i łączenia

======================================================================
TRZY PRZYPADKI MASTER THEOREM:
======================================================================

Niech c = log_b(a)  (wykładnik krytyczny)

PRZYPADEK 1: f(n) = O(n^d) gdzie d < c
  → Praca jest zdominowana przez liście drzewa rekurencji
  → T(n) = Θ(n^c) = Θ(n^(log_b a))
  Przykład: T(n) = 8·T(n/2) + O(n²)
            a=8, b=2, c=log₂(8)=3, f(n)=O(n²), d=2
            d < c → T(n) = Θ(n³)

PRZYPADEK 2: f(n) = Θ(n^c · log^k n) gdzie k ≥ 0
  → Praca jest równomiernie rozłożona na wszystkich poziomach
  → T(n) = Θ(n^c · log^(k+1) n)
  Przykład: T(n) = 2·T(n/2) + O(n)
            a=2, b=2, c=log₂(2)=1, f(n)=Θ(n), d=1
            d = c → T(n) = Θ(n log n)

PRZYPADEK 3: f(n) = Ω(n^d) gdzie d > c
  → Praca jest zdominowana przez korzeń (łączenie)
  → T(n) = Θ(f(n))
  Warunek regularności: a·f(n/b) ≤ k·f(n) dla k < 1
  Przykład: T(n) = 2·T(n/2) + O(n²)
            a=2, b=2, c=log₂(2)=1, f(n)=Θ(n²), d=2
            d > c → T(n) = Θ(n²)

======================================================================
PRZYKŁADY ALGORYTMÓW:
======================================================================

Algorytm:                 Binary Search
Rekurencja:               T(n) = 1·T(n/2) + O(1)
Parametry:                a=1, b=2, f(n)=O(1)
Wykładnik krytyczny:      c = log_2(1) = 0.000
Master Theorem:           Przypadek 2
Złożoność:                Θ(log n)

Algorytm:                 Merge Sort
Rekurencja:               T(n) = 2·T(n/2) + O(n)
Parametry:                a=2, b=2, f(n)=O(n)
Wykładnik krytyczny:      c = log_2(2) = 1.000
Master Theorem:           Przypadek 2
Złożoność:                Θ(n log n)

Algorytm:                 Klasyczne mnożenie macierzy (D&C)
Rekurencja:               T(n) = 8·T(n/2) + O(n²)
Parametry:                a=8, b=2, f(n)=O(n²)
Wykładnik krytyczny:      c = log_2(8) = 3.000
Master Theorem:           Przypadek 1
Złożoność:                Θ(n³)

Algorytm:                 Strassen (mnożenie macierzy)
Rekurencja:               T(n) = 7·T(n/2) + O(n²)
Parametry:                a=7, b=2, f(n)=O(n²)
Wykładnik krytyczny:      c = log_2(7) = 2.807
Master Theorem:           Przypadek 1
Złożoność:                Θ(n^2.807)

Algorytm:                 Karatsuba (mnożenie liczb)
Rekurencja:               T(n) = 3·T(n/2) + O(n)
Parametry:                a=3, b=2, f(n)=O(n)
Wykładnik krytyczny:      c = log_2(3) = 1.585
Master Theorem:           Przypadek 1
Złożoność:                Θ(n^1.585)

Złożoność obliczeniowa

Porównanie algorytmów D&C

Algorytm Rekurencja Master Theorem Złożoność
Binary Search T(n) = T(n/2) + O(1) Przypadek 2 O(log n)
Merge Sort T(n) = 2T(n/2) + O(n) Przypadek 2 O(n log n)
Quick Sort T(n) = 2T(n/2) + O(n) Przypadek 2 O(n log n) średnio
Max Subarray (D&C) T(n) = 2T(n/2) + O(n) Przypadek 2 O(n log n)
Closest Pair T(n) = 2T(n/2) + O(n) Przypadek 2 O(n log n)
Strassen T(n) = 7T(n/2) + O(n²) Przypadek 1 O(n^2.807)
Karatsuba T(n) = 3T(n/2) + O(n) Przypadek 1 O(n^1.585)

Podsumowanie

Kiedy używać Divide and Conquer?

Używaj D&C gdy: - Problem można podzielić na niezależne podproblemy - Podproblemy są tego samego typu co oryginalny problem - Rozwiązania podproblemów można efektywnie połączyć - Rekurencja prowadzi do lepszej złożoności niż iteracja

NIE używaj D&C gdy: - Podproblemy nie są niezależne (użyj programowania dynamicznego) - Koszt łączenia jest zbyt wysoki - Problem ma prostsze rozwiązanie iteracyjne

Kluczowe koncepcje

Pojęcie Opis
Divide Podział problemu na mniejsze podproblemy
Conquer Rozwiązanie podproblemów rekurencyjnie
Combine Połączenie rozwiązań w rozwiązanie całości
Master Theorem Narzędzie do analizy złożoności
Przypadek bazowy Warunek zatrzymania rekurencji

Zalety i wady

Zalety: - Eleganckie i intuicyjne rozwiązania - Często optymalna złożoność - Łatwe do zrównoleglenia - Naturalna implementacja rekurencyjna

Wady: - Narzut wywołań rekurencyjnych - Może być trudne do debugowania - Czasem wymaga dodatkowej pamięci - Nie zawsze najszybsze w praktyce


Co dalej?

Po opanowaniu Divide and Conquer, następne tematy to:

  1. Lekcja 78: Backtracking - Problem N-Hetmanów
  2. Strategia backtrackingu
  3. N-Queens problem
  4. Pruning i optymalizacje

  5. Zaawansowane D&C:

  6. Fast Fourier Transform (FFT)
  7. Algorytm Karatsuba (mnożenie wielkich liczb)
  8. Quickselect (k-ty najmniejszy element)

  9. Programowanie dynamiczne:

  10. Różnice między D&C a DP
  11. Memoizacja vs tabulacja
  12. Optymalna podstruktura

Zadania praktyczne

Zadanie 1: Potęgowanie binarne

Zaimplementuj algorytm szybkiego potęgowania używając D&C. Oblicz a^n w czasie O(log n).

Zadanie 2: Median of Medians

Zaimplementuj algorytm Median of Medians do znajdowania k-tego najmniejszego elementu w O(n).

Zadanie 3: Convex Hull

Rozwiąż problem otoczki wypukłej (Convex Hull) używając D&C w czasie O(n log n).

Zadanie 4: Skyline Problem

Rozwiąż problem linii horyzontu (Skyline Problem) dla n budynków używając D&C.

Zadanie 5: Porównanie implementacji

Porównaj wydajność merge sort i quick sort dla różnych rozmiarów danych i różnych rozkładów (losowe, prawie posortowane, odwrotnie posortowane).


Następna lekcja: Backtracking - Problem N-Hetmanów