Rilasciato · in miglioramento
Guida a Divide et impera · 4/6
Per ora questo capitolo è disponibile solo in inglese.
The running time of a divide-and-conquer algorithm is described by a recurrence: the cost of a call equals the cost of its recursive calls plus the cost of dividing and combining. This chapter shows how to solve such recurrences and compares the classic algorithms with their alternatives.
If an algorithm splits an input of size n into a subproblems of size n/b and spends f(n) on dividing and combining, then T(n) = a * T(n/b) + f(n). Merge sort makes two half-size calls and merges in linear time, so T(n) = 2T(n/2) + O(n).
Draw the recursion tree to see the total. Merge sort's tree has log2 n levels and every level does n work in total:
level 0: n -> n
level 1: n/2 n/2 -> n
level 2: n/4 n/4 n/4 n/4 -> n
... (log2 n levels)
total: n * log2 n = O(n log n)For T(n) = a * T(n/b) + f(n) with a >= 1 and b > 1, compare f(n) with n^c, where c = log_b(a) is the critical exponent: the number of leaves of the tree grows like n^c.
f(n) = O(n^d) for some d < c, the leaves dominate: T(n) = Θ(n^c).f(n) = Θ(n^c * (log n)^k) for some k >= 0, every level costs about the same: T(n) = Θ(n^c * (log n)^(k+1)).f(n) = Ω(n^d) for some d > c, and a * f(n/b) <= q * f(n) for some constant q < 1, the root dominates: T(n) = Θ(f(n)).| Algorithm |
|---|
| Recurrence |
|---|
c = log_b a |
|---|
| Case |
|---|
| Result |
|---|
| Binary search | T(n/2) + O(1) | 0 | 2 | O(log n) |
| Fast exponentiation | T(e/2) + O(1) | 0 | 2 | O(log e) multiplications |
| Merge sort, inversions | 2T(n/2) + O(n) | 1 | 2 | O(n log n) |
| Closest pair | 2T(n/2) + O(n) | 1 | 2 | O(n log n) |
| Karatsuba | 3T(n/2) + O(n) | about 1.585 | 1 | O(n^1.585) |
| Naive matrix multiplication | 8T(n/2) + O(n^2) | 3 | 1 | O(n^3) |
| Strassen | 7T(n/2) + O(n^2) | about 2.807 | 1 | O(n^2.807) |
| Quickselect, average | T(n/2) + O(n) | 0 | 3 | O(n) expected |
Quickselect's worst case, T(n) = T(n - 1) + O(n) = O(n^2), does not fit the theorem because the subproblem shrinks by one element instead of a constant factor.
Split two numbers at m digits: x = x1 * 10^m + x0 and y = y1 * 10^m + y0. Then x * y = z2 * 10^(2m) + z1 * 10^m + z0, with z2 = x1 * y1, z0 = x0 * y0 and z1 = x1 * y0 + x0 * y1. Computing z1 directly needs two more products, giving 4T(n/2) and therefore O(n^2), no better than schoolbook multiplication. Karatsuba's trick is z1 = (x1 + x0) * (y1 + y0) - z2 - z0, which needs only one extra product. Three subproblems give O(n^log2(3)), about O(n^1.585).
def karatsuba(x, y):
"""Product of two non-negative integers with three recursive products."""
if x < 10 or y < 10:
return x * y
m = max(len(str(x)), len(str(y))) // 2
x1, x0 = divmod(x, 10 ** m)
y1, y0 = divmod(y, 10 ** m)
z2 = karatsuba(x1, y1)
z0 = karatsuba(x0, y0)
z1 = karatsuba(x1 + x0, y1 + y0) - z2 - z0
return z2 * 10 ** (2 * m) + z1 * 10 ** m + z0
print(karatsuba(1234, 5678) == 1234 * 5678) # TrueTo find the closest pair among n points, sort them by x, split at the median x, and solve both halves to get d, the smaller of the two best distances. A closer pair must cross the dividing line, so both of its points lie in a strip of width 2d around it. Sort the strip by y; within that order each point only needs to be compared with a constant number of following points (at most 7), because more points than that cannot fit in a d by 2d box while staying d apart. The combine step is O(n) if the halves are merged by y as in merge sort, so T(n) = 2T(n/2) + O(n) = O(n log n). Re-sorting the strip at every level instead costs O(n log^2 n).
Counting operations is a quick way to check an analysis:
def count_mults(e):
"""Multiplications done by recursive fast power for exponent e."""
if e == 0:
return 0
return count_mults(e // 2) + (2 if e % 2 else 1)
for e in (10, 1000, 10**6, 10**18):
print(e, "naive:", max(e - 1, 0), "fast:", count_mults(e))For e = 10^18 the naive method needs about 10^18 multiplications; fast power needs fewer than 120.
| Problem | Simple approach | Divide and conquer | Other options |
|---|---|---|---|
| Counting inversions | All pairs, O(n^2) | Merge sort, O(n log n) | Fenwick tree, O(n log n) |
Power b^e | Repeated multiplication, O(e) | Fast power, O(log e) | Built-in pow |
| k-th smallest | Sort, O(n log n) | Quickselect, O(n) expected | Median of medians, O(n) worst case; heap, O(n log k) |
| Big-number product | Schoolbook, O(n^2) | Karatsuba, O(n^1.585) | FFT-based, about O(n log n) |
| Closest pair | All pairs, O(n^2) | Strip method, O(n log n) | Grid hashing, O(n) expected |
Space matters too. Merge sort needs O(n) extra memory for merging, while recursion depth is O(log n) for balanced splits. Quickselect works in place but can recurse O(n) deep with bad pivots, which is why the loop form in the previous chapters is safer in Python.
T(n) = a * T(n/b) + f(n) and compare f(n) with n^log_b(a).log n factor; otherwise the leaves or the root dominate.
0 commenti
Accedi · Accedi per lasciare un commento.
Scrivi tu il primo commento.