Bài 28.126 phút đọc
Chia để trị
Sau bài này bạn sẽ làm được
- Nhận ra bài toán giải được bằng chia để trị
- Áp dụng định lý thợ cho ba trường hợp
- Cài lũy thừa nhanh trong O(log n)
- Ước lượng độ phức tạp từ công thức truy hồi
Chia để trị là khuôn của merge sort và quick sort ở Chương 27, và của tìm nhị phân ở Bài 26.2. Bài này rút ra khuôn chung, đưa ra công cụ phân tích, và áp dụng cho ba bài toán không liên quan gì tới sắp xếp.
#Ba bước
Chia để trị
Chia bài toán cỡ
n thành a bài toán con cỡ n/b, giải chúng bằng đệ quy, rồi gộp kết quả. Điều kiện: các bài toán con phải độc lập với nhau. Nếu chúng chồng lấn thì đó là quy hoạch động ở Bài 28.3.Chia
Tách bài toán thành các bài toán con nhỏ hơn cùng dạng. Merge sort chia ở giữa, quick sort chia theo chốt.
Trị
Giải từng bài toán con, thường bằng đệ quy. Khi đủ nhỏ thì giải trực tiếp, đó là trường hợp cơ sở.
Gộp
Kết hợp lời giải của các bài toán con thành lời giải của bài toán gốc. Merge sort trộn, quick sort không cần gộp gì.
| Thuật toán | Số bài toán con | Cỡ mỗi con | Chi phí chia và gộp |
|---|---|---|---|
| Tìm nhị phân | 1 | n/2 | O(1) |
| Merge sort | 2 | n/2 | O(n) để trộn |
| Quick sort | 2 | n/2 trung bình | O(n) để phân hoạch |
| Lũy thừa nhanh | 1 | n/2 | O(1) |
| Nhân ma trận Strassen | 7 | n/2 | O(n bình phương) |
| Cặp điểm gần nhất | 2 | n/2 | O(n) sau khi đã sắp |
#Định lý thợ
Định lý thợ
Công cụ giải nhanh công thức truy hồi dạng
T(n) = a T(n/b) + f(n), với a >= 1 và b > 1. So f(n) với n mũ log_b(a) để biết bên nào chi phối./* Dat c = log_b(a). So sanh f(n) voi n^c:
Truong hop 1: f(n) = O(n^(c-e)) voi e > 0
Cac bai toan con chi phoi.
T(n) = Theta(n^c)
Truong hop 2: f(n) = Theta(n^c)
Hai ben ngang nhau.
T(n) = Theta(n^c log n)
Truong hop 3: f(n) = Omega(n^(c+e)) voi e > 0, va thoa dieu kien deu
Buoc chia va gop chi phoi.
T(n) = Theta(f(n)) */| Thuật toán | Công thức | c = log_b(a) | Trường hợp | Kết quả |
|---|---|---|---|---|
| Tìm nhị phân | T(n) = T(n/2) + O(1) | log2(1) = 0 | 2 | O(log n) |
| Merge sort | T(n) = 2T(n/2) + O(n) | log2(2) = 1 | 2 | O(n log n) |
| Nhân ma trận thường | T(n) = 8T(n/2) + O(n²) | log2(8) = 3 | 1 | O(n³) |
| Strassen | T(n) = 7T(n/2) + O(n²) | log2(7) ≈ 2.807 | 1 | O(n^2.807) |
| Duyệt cây | T(n) = 2T(n/2) + O(1) | log2(2) = 1 | 1 | O(n) |
| Chia nhưng gộp rất đắt | T(n) = 2T(n/2) + O(n²) | 1 | 3 | O(n²) |
#Lũy thừa nhanh
luy-thua.c
#include <stdint.h>
/* Tính (a mũ n) chia dư mod, trong O(log n).
Ý tưởng: a^n = (a^(n/2))^2 nếu n chẵn
a^n = a * (a^(n/2))^2 nếu n lẻ
Bản lặp đọc n theo bit từ thấp lên cao. */
uint64_t luy_thua(uint64_t a, uint64_t n, uint64_t mod)
{
uint64_t kq = 1 % mod; /* xử lý đúng cả khi mod bằng 1 */
a %= mod;
while (n > 0) {
if (n & 1) kq = kq * a % mod; /* bit hiện tại là 1 */
a = a * a % mod; /* bình phương cho bit kế */
n >>= 1;
}
return kq;
}/* Vet voi a = 3, n = 13, mod = 1000000007
13 trong nhi phan la 1101, tuc 13 = 8 + 4 + 1
nen 3^13 = 3^8 * 3^4 * 3^1
buoc n bit a hien tai kq
0 13 1 3 3
1 6 0 9 3
2 3 1 81 243
3 1 1 6561 1594323
4 0 - - ket qua 1594323
Kiem: 3^13 = 1594323. Dung. Chi 4 vong lap thay vi 13. */terminal
gcc -std=c17 -Wall -Wextra luy-thua.c main.c -o t && ./t
3^13 mod 1000000007 = 1594323 2^62 mod 1000000007 = 132727571 7^1000000000 mod 1000000007 = 356962000 so vong lap cho n = 1000000000: 30 so vong lap neu nhan tung buoc: 1000000000
#Nhân ma trận và dãy Fibonacci
fib-ma-tran.c
#include <stdint.h>
/* Quan sat:
[ F(n+1) ] [ 1 1 ]^n [ F(1) ]
[ F(n) ] = [ 1 0 ] * [ F(0) ]
Nen tinh F(n) bang cach nang ma tran 2x2 len luy thua n,
dung luy thua nhanh -> O(log n) phep nhan ma tran. */
typedef struct { uint64_t m[2][2]; } M2;
static M2 nhan(M2 x, M2 y, uint64_t mod)
{
M2 z;
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j) {
__uint128_t s = 0;
for (int k = 0; k < 2; ++k)
s += (__uint128_t)x.m[i][k] * y.m[k][j];
z.m[i][j] = (uint64_t)(s % mod);
}
return z;
}
uint64_t fib_ma_tran(uint64_t n, uint64_t mod)
{
M2 kq = { { { 1, 0 }, { 0, 1 } } }; /* ma trận đơn vị */
M2 nen = { { { 1, 1 }, { 1, 0 } } };
while (n > 0) {
if (n & 1) kq = nhan(kq, nen, mod);
nen = nhan(nen, nen, mod);
n >>= 1;
}
return kq.m[0][1]; /* F(n) */
}terminal
gcc -std=c17 -Wall -Wextra fib-ma-tran.c main.c -o t && ./t
F(10) = 55 F(50) = 12586269025 F(1000000000) mod 1e9+7 = 21597462 so phep nhan ma tran cho n = 1000000000: 60 so vong lap neu tinh tuyen tinh: 1000000000
| Cách tính F(n) | Độ phức tạp | n = 10^9 tốn |
|---|---|---|
| Đệ quy ngây thơ | O(1.618 mũ n) | Không tưởng |
| Đệ quy có nhớ | O(n) thời gian, O(n) bộ nhớ | 4 GB bộ nhớ |
| Lặp hai biến | O(n) thời gian, O(1) bộ nhớ | Khoảng 1 giây |
| Lũy thừa ma trận | O(log n) | Vài micro giây |
Strassen: nhân ma trận nhanh hơn
/* Nhan hai ma tran n x n theo dinh nghia: O(n^3).
Strassen chia moi ma tran thanh bon khoi n/2 x n/2,
roi dung BAY phep nhan thay vi TAM, bang cach cong tru kheo leo.
T(n) = 7 T(n/2) + O(n^2) -> O(n^log2(7)) = O(n^2.807)
Voi n = 1024:
n^3 = 1073741824
n^2.807 = 454875234 <- it hon 2.4 lan
Nhung hang so an lon hon nhieu, va no khong on dinh ve so hoc,
nen chi dang dung tu khoang n = 1000 tro len. */terminal
./do-strassen
n thuong strassen ben nao nhanh hon 128 0.004 s 0.012 s thuong 256 0.032 s 0.058 s thuong 512 0.284 s 0.312 s thuong 1024 2.410 s 1.980 s strassen 2048 19.400 s 13.100 s strassen
#Cặp điểm gần nhất
Cho n điểm trên mặt phẳng, tìm cặp có khoảng cách nhỏ nhất. Cách vét cạn tốn O(n bình phương). Chia để trị cho O(n log n).
cap-gan-nhat.c
#include <math.h>
#include <stdlib.h>
typedef struct { double x, y; } Diem;
static double kc(Diem a, Diem b)
{
double dx = a.x - b.x, dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
/* px đã sắp theo x, py đã sắp theo y, cùng chứa n điểm. */
static double gan_nhat(Diem *px, Diem *py, size_t n, Diem *tam)
{
if (n <= 3) { /* trường hợp cơ sở, vét cạn */
double d = 1e308;
for (size_t i = 0; i < n; ++i)
for (size_t j = i + 1; j < n; ++j) {
double t = kc(px[i], px[j]);
if (t < d) d = t;
}
return d;
}
size_t giua = n / 2;
double x_mid = px[giua].x;
/* Tách py thành hai nửa theo x_mid, giữ nguyên thứ tự theo y */
Diem *py_t = tam;
Diem *py_p = tam + giua;
size_t nt = 0, np = 0;
for (size_t i = 0; i < n; ++i) {
if (nt < giua && py[i].x <= x_mid) py_t[nt++] = py[i];
else py_p[np++] = py[i];
}
double dt = gan_nhat(px, py_t, giua, tam + n);
double dp = gan_nhat(px + giua, py_p, n - giua, tam + n);
double d = (dt < dp) ? dt : dp;
/* Dải giữa: chỉ những điểm cách đường chia dưới d mới đáng xét */
Diem *dai = tam + n;
size_t nd = 0;
for (size_t i = 0; i < n; ++i)
if (fabs(py[i].x - x_mid) < d)
dai[nd++] = py[i];
/* Điểm mấu chốt: với mỗi điểm, chỉ cần so với NHIỀU NHẤT 7 điểm
kế tiếp trong dải, vì trong hình chữ nhật d nhân 2d không thể
có quá 8 điểm cách nhau ít nhất d. */
for (size_t i = 0; i < nd; ++i)
for (size_t j = i + 1; j < nd && py[j].y - py[i].y < d; ++j) {
double t = kc(dai[i], dai[j]);
if (t < d) d = t;
}
return d;
}terminal
./do-cap-gan-nhat
n vet can chia de tri nhanh hon
1000 0.004 s 0.001 s 4.0 lan
10000 0.412 s 0.012 s 34.3 lan
100000 41.200 s 0.148 s 278.4 lan
1000000 khong doi noi 1.810 s ---#Khi nào chia để trị thắng
| Dấu hiệu | Ví dụ |
|---|---|
| Bài toán chia được thành các phần độc lập cùng dạng | Sắp xếp, tìm kiếm, nhân ma trận |
| Gộp hai lời giải rẻ hơn giải trực tiếp | Trộn hai dãy đã sắp tốn O(n), sắp cả dãy tốn O(n log n) |
| Có tính chất hình học hoặc đại số khai thác được | Cặp điểm gần nhất, biến đổi Fourier nhanh |
| Cần song song hóa | Các bài toán con độc lập nên chạy song song được |
Tự làm thử
- Áp dụng định lý thợ cho sáu công thức trong bảng và kiểm lại kết quả bằng cách vẽ cây đệ quy.
- Cài
luy_thuavà kiểm với3 mũ 13, rồi đo số vòng lặp vớinbằng một tỷ. - Cài Fibonacci bằng lũy thừa ma trận và so kết quả với bản lặp cho
ntừ 0 tới 90. - Cài công thức Binet bằng
doublevà tìm giá trịnnhỏ nhất mà nó cho kết quả sai. - Cài cặp điểm gần nhất bằng cả hai cách và đo với
nbằng một nghìn, mười nghìn và một trăm nghìn.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Chia để trị gồm ba bước chia, trị, gộp, và nó chỉ đúng khi các bài toán con độc lập với nhau.
- Định lý thợ giải nhanh công thức
T(n) = a T(n/b) + f(n)bằng cách sof(n)vớinmũlog_b(a). - Lũy thừa nhanh áp dụng cho mọi phép toán có tính kết hợp, không chỉ cho số nguyên.
- Lũy thừa ma trận cho Fibonacci trong O(log n), và công thức đóng Binet sai từ
nbằng 71 trở đi vì sai số số thực. - Bài toán con chồng lấn thì chia để trị thành mũ. Đó là dấu hiệu phải chuyển sang quy hoạch động.