Bỏ qua điều hướng, tới nội dung chính
Học C
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.
  1. 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.

  2. 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ở.

  3. 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ánSố bài toán conCỡ mỗi conChi phí chia và gộp
Tìm nhị phân1n/2O(1)
Merge sort2n/2O(n) để trộn
Quick sort2n/2 trung bìnhO(n) để phân hoạch
Lũy thừa nhanh1n/2O(1)
Nhân ma trận Strassen7n/2O(n bình phương)
Cặp điểm gần nhất2n/2O(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ánCông thứcc = log_b(a)Trường hợpKết quả
Tìm nhị phânT(n) = T(n/2) + O(1)log2(1) = 02O(log n)
Merge sortT(n) = 2T(n/2) + O(n)log2(2) = 12O(n log n)
Nhân ma trận thườngT(n) = 8T(n/2) + O(n²)log2(8) = 31O(n³)
StrassenT(n) = 7T(n/2) + O(n²)log2(7) ≈ 2.8071O(n^2.807)
Duyệt câyT(n) = 2T(n/2) + O(1)log2(2) = 11O(n)
Chia nhưng gộp rất đắtT(n) = 2T(n/2) + O(n²)13O(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ạpn = 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ếnO(n) thời gian, O(1) bộ nhớKhoảng 1 giây
Lũy thừa ma trậnO(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ệuVí dụ
Bài toán chia được thành các phần độc lập cùng dạngSắ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ếpTrộ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 đượcCặp điểm gần nhất, biến đổi Fourier nhanh
Cần song song hóaCác bài toán con độc lập nên chạy song song được

Tự làm thử

  1. Á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.
  2. Cài luy_thua và kiểm với 3 mũ 13, rồi đo số vòng lặp với n bằng một tỷ.
  3. Cài Fibonacci bằng lũy thừa ma trận và so kết quả với bản lặp cho n từ 0 tới 90.
  4. Cài công thức Binet bằng double và tìm giá trị n nhỏ nhất mà nó cho kết quả sai.
  5. Cài cặp điểm gần nhất bằng cả hai cách và đo với n bằ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 so f(n) với n mũ 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ừ n bằ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.