Chương 1·Bài 11

Divide and Conquer

Sắp xếp nn số bằng cách chèn từng số vào đúng chỗ tốn cỡ n2n^2 phép so sánh; với n=2⋅105n = 2 \cdot 10^5 đó là 4⋅10104 \cdot 10^{10}, quá xa giới hạn một giây. Nhưng nếu hai nửa của dãy đã sắp xong thì ghép chúng chỉ mất nn phép so sánh, và để sắp mỗi nửa ta dùng lại đúng cách ấy trên một nửa nhỏ hơn. Chia để trị (divide and conquer) là khuôn thiết kế đó: đưa bài toán về các bài con cùng dạng nhưng nhỏ hơn, giải từng bài con bằng đệ quy, rồi gộp lời giải lại. Cùng khuôn này cho ra lũy thừa nhanh, đếm nghịch thế, cặp điểm gần nhất, và sẽ gặp lại ở cây phân đoạn (bài 17) cùng chia để trị CDQ (bài 60).

Chia, trị, gộp: lũy thừa nhanh

Một thuật toán chia để trị gồm ba việc.

  • Chia (divide): đưa bài toán cỡ nn về vài bài con cùng dạng, cỡ nhỏ hơn.
  • Trị (conquer): giải từng bài con bằng chính thuật toán đang viết, tức đệ quy. Bài đủ nhỏ thì giải thẳng; đó là điều kiện dừng.
  • Gộp (combine): ghép lời giải các bài con thành lời giải của bài ban đầu.

Ví dụ ngắn nhất là lũy thừa nhanh (fast exponentiation): tính xn mod mx^n \bmod m với nn tới 101810^{18}. Nhân từng thừa số cần n−1n - 1 phép nhân, không thể kịp. Nhưng xn=(xn/2)2x^n = (x^{n/2})^2 khi nn chẵn và xn=x⋅(x(n−1)/2)2x^n = x \cdot (x^{(n-1)/2})^2 khi nn lẻ. Chia là đổi nn thành ⌊n/2⌋\lfloor n/2 \rfloor; trị là tính x⌊n/2⌋x^{\lfloor n/2 \rfloor} bằng đệ quy; gộp là bình phương kết quả và nhân thêm xx nếu nn lẻ; điều kiện dừng là n=0n = 0, khi đó x0=1x^0 = 1.

Chạy tay với x=3x = 3, n=13n = 13. Các lời gọi đi xuống theo 13→6→3→1→013 \to 6 \to 3 \to 1 \to 0, rồi kết quả đi ngược lên: 30=13^0 = 1, 31=33^1 = 3, 33=3⋅32=273^3 = 3 \cdot 3^2 = 27, 36=272=7293^6 = 27^2 = 729, 313=3⋅7292=1 594 3233^{13} = 3 \cdot 729^2 = 1\,594\,323. Chỉ 4 phép bình phương và 3 phép nhân thêm, thay cho 12 phép nhân; tổng quát là khoảng 2log⁡2n2\log_2 n thay vì n−1n - 1. Gọi T(n)T(n) là thời gian chạy với số mũ nn. Mỗi lời gọi làm O(1)O(1) việc rồi gọi một lần với số mũ nửa, nên T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1): có khoảng log⁡2n\log_2 n lời gọi, tức O(log⁡n)O(\log n).

Điểm mấu chốt là kết quả x⌊n/2⌋x^{\lfloor n/2 \rfloor} được tính một lần rồi lưu vào biến: chia để trị chỉ có lợi khi các bài con độc lập và mỗi bài con được giải một lần. Nếu các bài con chồng lên nhau thì đệ quy trần giải đi giải lại cùng một bài; viết F(n)=F(n−1)+F(n−2)F(n) = F(n-1) + F(n-2) thành hai lời gọi cho thời gian tăng theo cấp số nhân, và chỗ của kiểu bài đó là quy hoạch động (bài 12).

Hỏi nhanh

Một bạn viết lũy thừa nhanh như sau: khi nn chẵn thì return power(x, n / 2) * power(x, n / 2) % m, gọi hai lần cho cùng một giá trị. Kết quả vẫn đúng. Thời gian chạy theo nn là bao nhiêu?

Ví dụLũy Thừa Rút Gọn

Đề. Cho ba số nguyên dương A,B,C≤231−1A, B, C \le 2^{31} - 1. In AB mod CA^B \bmod C.

Lời giải. Lũy thừa nhanh đúng như trên, với một lời gọi đệ quy mỗi mức. Ba chi tiết: điều kiện dừng trả 1 % C để C=1C = 1 vẫn cho 0; lấy A % C trước lần gọi đầu để mọi số đem nhân đều nhỏ hơn CC; và vì C<231C < 2^{31} nên tích hai số dư nhỏ hơn 2622^{62}, vừa ll. BB tới 2⋅1092 \cdot 10^9 nên đệ quy chỉ sâu khoảng 31 mức. Với CC tới 101810^{18} thì tích phải tính bằng __int128.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll A, B, C;

ll power(ll x, ll n) {
    if (n == 0) return 1 % C;
    ll h = power(x, n / 2);
    ll res = h * h % C;
    if (n % 2 == 1) res = res * x % C;
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> A >> B >> C;
    cout << power(A % C, B) << '\n';
}
7 13 10

Cây đệ quy và độ phức tạp

Giả sử mỗi lời gọi cỡ nn sinh aa bài con cỡ n/bn/b và phần việc riêng của nó (chia cộng gộp) tốn f(n)f(n). Khi đó thời gian chạy T(n)T(n) thỏa hệ thức truy hồi

T(n)=a T(n/b)+f(n).T(n) = a\,T(n/b) + f(n).

Cách giải thực dụng nhất là vẽ cây đệ quy và cộng chi phí theo từng mức. Mức ii có aia^i nút, mỗi nút cỡ n/bin/b^i nên cả mức tốn aif(n/bi)a^i f(n/b^i); cây dừng ở mức log⁡bn\log_b n, số lần chia nn cho bb tới khi còn 1 (giống log⁡2n\log_2 n ở bài 03, chỉ thay 2 bằng bb). Với sắp xếp trộn, a=b=2a = b = 2 và f(n)=cnf(n) = cn vì gộp hai nửa tốn O(n)O(n): mức ii có 2i2^i nút cỡ n/2in/2^i nên tốn cncn, mọi mức bằng nhau. Có log⁡2n+1\log_2 n + 1 mức, vậy T(n)=O(nlog⁡n)T(n) = O(n \log n).

Đổi a, b, k: thanh ngang là chi phí của từng mức trong cây đệ quy.

Khi f(n)=c nkf(n) = c\,n^k, chi phí mức ii là c nkqic\,n^k q^i với q=a/bkq = a / b^k, nên chi phí mỗi mức gấp qq lần mức ngay trên nó, và T(n)T(n) là tổng của log⁡bn+1\log_b n + 1 mức như vậy. Có ba khả năng.

  • q<1q < 1: các mức giảm dần, tổng cỡ số hạng đầu, T(n)=O(nk)T(n) = O(n^k). Việc ở gốc quyết định.
  • q=1q = 1: mọi mức bằng nhau, T(n)=O(nklog⁡n)T(n) = O(n^k \log n).
  • q>1q > 1: các mức tăng dần, tổng cỡ mức cuối, tức số lá của cây vì mỗi lá tốn O(1)O(1). Chẳng hạn a=4a = 4, b=2b = 2: mỗi mức số nút gấp 4, có log⁡2n\log_2 n mức nên có 4log⁡2n=n24^{\log_2 n} = n^2 lá và T(n)=O(n2)T(n) = O(n^2). Tổng quát số lá là nlog⁡ban^{\log_b a}. Việc ở các lá quyết định.

Đó là định lý thợ (master theorem) cho dạng hay gặp; bản đầy đủ cho ff tùy ý xem ở phần Đọc thêm. Các hệ thức sẽ gặp trong bài:

Hệ thức qq Thời gian Gặp ở đâu
T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1) 11 O(log⁡n)O(\log n) lũy thừa nhanh, chặt nhị phân
T(n)=2T(n/2)+O(1)T(n) = 2T(n/2) + O(1) 22 O(n)O(n) gọi đôi lũy thừa nhanh; đoạn con lớn nhất với bốn số
T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n) 11 O(nlog⁡n)O(n \log n) sắp xếp trộn, nghịch thế, cặp điểm gần nhất
T(n)=4T(n/2)+O(1)T(n) = 4T(n/2) + O(1) 44 O(n2)O(n^2) chia bảng thành bốn phần tư, nn là cạnh
T(n)=T(n/2)+O(n)T(n) = T(n/2) + O(n) 12\tfrac12 O(n)O(n) quickselect khi pivot luôn chia đôi
T(n)=3T(n/2)+O(n)T(n) = 3T(n/2) + O(n) 32\tfrac32 O(nlog⁡23)≈O(n1,58)O(n^{\log_2 3}) \approx O(n^{1{,}58}) nhân hai số lớn kiểu Karatsuba

Cây chỉ đẹp như vậy khi các bài con co lại theo tỉ lệ. Nếu mỗi lần chỉ bớt một phần tử thì T(n)=T(n−1)+O(n)=O(n2)T(n) = T(n-1) + O(n) = O(n^2) và cây sâu nn mức, có thể tràn ngăn xếp; đó là trường hợp xấu nhất của quicksort. Chia đôi theo chỉ số luôn cân bằng (chỉ sâu log⁡2n≈18\log_2 n \approx 18 mức với n=2⋅105n = 2 \cdot 10^5); chia theo giá trị quanh một pivot thì cân bằng tùy dữ liệu, vì thế mới cần pivot ngẫu nhiên.

Hỏi nhanh

Một chương trình kiểu quicksort luôn lấy phần tử đầu của đoạn làm pivot, chia các phần tử còn lại thành nhóm nhỏ hơn pivot và nhóm không nhỏ hơn pivot, rồi đệ quy vào cả hai nhóm. Dãy vào đã sắp tăng, n=2⋅105n = 2 \cdot 10^5. Số phép so sánh cỡ bao nhiêu?

Ví dụĐộ Sâu Theo Cực Đại

Đề. Cho một hoán vị a1,…,ana_1, \dots, a_n (n≤100n \le 100, tối đa 100 test). Gắn nó với cây nhị phân: phần tử lớn nhất là gốc, phần bên trái nó tạo cây con trái, phần bên phải tạo cây con phải, theo cùng quy tắc. In độ sâu của từng phần tử theo thứ tự trong hoán vị, gốc có độ sâu 0.

Lời giải. Định nghĩa đã là một lời gọi đệ quy: trên đoạn [l,r)[l, r), tìm vị trí pp của số lớn nhất, gán độ sâu hiện tại cho pp, rồi giải [l,p)[l, p) và [p+1,r)[p + 1, r) với độ sâu tăng 1; đoạn rỗng thì dừng. Điểm chia là vị trí số lớn nhất chứ không phải điểm giữa, nên cây không cân bằng: với hoán vị tăng dần, mỗi lần chỉ tách được một phần tử và T(n)=T(n−1)+O(n)=O(n2)T(n) = T(n - 1) + O(n) = O(n^2), đúng trường hợp xấu đã nói. Với n≤100n \le 100 chuyện đó chẳng sao, nhưng với n=105n = 10^5 thì cần cách khác (bài 17).

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll t, n, a[105], d[105];

void go(ll l, ll r, ll dep) {
    if (l >= r) return;
    ll p = l;
    for (ll i = l + 1; i < r; i++)
        if (a[i] > a[p]) p = i;
    d[p] = dep;
    go(l, p, dep + 1);
    go(p + 1, r, dep + 1);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> t;
    while (t--) {
        cin >> n;
        for (ll i = 0; i < n; i++) cin >> a[i];
        go(0, n, 0);
        for (ll i = 0; i < n; i++) cout << d[i] << ' ';
        cout << '\n';
    }
}
1
5
2 5 1 4 3

Sắp xếp trộn

Bài toán: sắp nn số tăng dần bằng sắp xếp trộn (merge sort). Gộp hai dãy đã sắp thành một dãy sắp chỉ cần một lượt duyệt, nên ta giao việc sắp mỗi nửa cho đệ quy.

Quy ước chỉ số của cả bài. Mọi đoạn trong đệ quy là đoạn nửa mở [l,r)[l, r), đánh số từ 0: gồm các vị trí l,l+1,…,r−1l, l+1, \dots, r-1, độ dài r−lr - l. Đặt mid=(l+r)/2\mathrm{mid} = (l + r) / 2 thì hai nửa [l,mid)[l, \mathrm{mid}) và [mid,r)[\mathrm{mid}, r) không giao nhau, hợp lại đúng [l,r)[l, r), không phải cộng trừ 1 ở đâu cả. Đoạn dài 0 hoặc 1 đã sắp sẵn: điều kiện dừng là r - l <= 1.

Giả sử hai nửa đã tăng. Đặt hai con trỏ ii, jj vào phần tử đầu chưa lấy của mỗi nửa. Phần tử nhỏ nhất trong mọi phần tử chưa lấy chắc chắn là một trong hai phần tử đó, nên mỗi bước ta so hai phần tử ấy, ghi cái nhỏ hơn vào mảng phụ rồi dịch con trỏ. Khi một nửa hết, chép nguyên phần còn lại của nửa kia. Ví dụ gộp (2,5)(2, 5) với (1,7)(1, 7): lấy 11, rồi 22, rồi 55, nửa trái hết nên chép 77, ra (1,2,5,7)(1, 2, 5, 7).

Chia đôi xuống tới đoạn dài 1, rồi gộp ngược lên; mỗi mức gộp là một lượt qua n phần tử.

Tính đúng chứng minh bằng quy nạp theo độ dài đoạn. Đoạn dài không quá 1 đúng hiển nhiên. Đoạn dài hơn thì hai lời gọi con sắp đúng hai nửa, và bước gộp giữ bất biến “sau kk bước, kk phần tử đã ghi là kk phần tử nhỏ nhất của cả đoạn, theo thứ tự tăng”, vì phần tử nhỏ nhất chưa lấy luôn là đầu của một trong hai nửa.

Gộp tốn O(r−l)O(r - l) nên T(n)=2T(n/2)+O(n)=O(nlog⁡n)T(n) = 2T(n/2) + O(n) = O(n \log n), cỡ 3,5⋅1063{,}5 \cdot 10^6 phép toán với n=2⋅105n = 2 \cdot 10^5. Mảng phụ khai báo một lần, toàn cục, dùng chung cho mọi lời gọi; nếu mỗi lời gọi tạo một vector mới thì tổng cấp phát lên O(nlog⁡n)O(n \log n) và chậm đi thấy rõ. Lấy bên trái khi hai phần tử bằng nhau giữ nguyên thứ tự của các phần tử bằng nhau (sắp ổn định), và ở ví dụ dưới chính chỗ này quyết định cặp bằng nhau có bị tính hay không. Khi chỉ cần sắp xếp thì cứ gọi sort; ta học sắp xếp trộn vì bước gộp là nơi tính thêm những thứ khác ngay lúc ghép hai nửa.

Hỏi nhanh

Một bạn viết điều kiện dừng if (r <= l) return; thay cho if (r - l <= 1) return; trong hàm sắp đoạn nửa mở [l,r)[l, r) với mid=(l+r)/2\mathrm{mid} = (l + r)/2. Chuyện gì xảy ra khi đoạn có đúng một phần tử?

Ví dụĐếm Cặp Ngược Thứ Tự

Đề. Cho dãy N≤2⋅105N \le 2 \cdot 10^5 số nguyên, ∣ai∣≤109|a_i| \le 10^9. Đếm số cặp i<ji < j với ai>aja_i > a_j (nghịch thế; hai số bằng nhau không tính).

Lời giải. Duyệt mọi cặp tốn 2⋅10102 \cdot 10^{10}. Chia đôi tại mid\mathrm{mid}: mỗi nghịch thế có hai chỉ số cùng ở nửa trái, cùng ở nửa phải, hoặc chéo (i<mid≤ji < \mathrm{mid} \le j). Hai loại đầu giao cho đệ quy. Số nghịch thế chéo chỉ phụ thuộc tập giá trị của mỗi nửa chứ không phụ thuộc thứ tự bên trong nửa, nên ta được quyền sắp mỗi nửa trước, và sắp xếp trộn làm sẵn việc đó. Trong bước gộp, mỗi lần lấy phần tử phải aja_j vì phần tử trái đang trỏ ai>aja_i > a_j thì, do nửa trái tăng, mọi phần tử trái từ ii tới mid−1\mathrm{mid} - 1 đều lớn hơn aja_j: cộng ngay mid−i\mathrm{mid} - i.

Mỗi cặp chéo u>vu > v được cộng đúng một lần, lúc vv được lấy: khi đó uu chưa được lấy, vì nếu uu được lấy trước thì phần tử đầu nửa phải lúc đó là v′≤vv' \le v và điều kiện lấy trái cho u≤v′≤vu \le v' \le v, mâu thuẫn. Bằng nhau thì lấy trái trước, nên cặp bằng nhau không bị cộng. Chạy tay trên (3,1,2,1)(3, 1, 2, 1): hai nửa có 1 nghịch thế mỗi bên và thành (1,3)(1, 3), (1,2)(1, 2); gộp thì 11 với 11 lấy trái, 33 với 11 lấy phải cộng 1, 33 với 22 lấy phải cộng 1. Tổng 1+1+2=41 + 1 + 2 = 4. Đáp án tới 2⋅10102 \cdot 10^{10} nên dùng ll.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, a[N], t[N];

ll calc(ll l, ll r) {
    if (r - l <= 1) return 0;
    ll mid = (l + r) >> 1;
    ll res = calc(l, mid) + calc(mid, r);
    ll i = l, j = mid, k = l;
    while (i < mid || j < r) {
        if (j == r || (i < mid && a[i] <= a[j])) t[k++] = a[i++];
        else {
            res += mid - i;
            t[k++] = a[j++];
        }
    }
    for (ll q = l; q < r; q++) a[q] = t[q];
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 0; i < n; i++) cin >> a[i];
    cout << calc(0, n) << '\n';
}
4
3 1 2 1

Hỏi nhanh

Trong bước gộp trên, một bạn đổi điều kiện lấy bên trái từ a[i] <= a[j] thành a[i] < a[j], mọi thứ khác giữ nguyên. Với dãy (2,2)(2, 2), chương trình in gì?

Đếm nghịch thế cũng làm được bằng cây Fenwick (bài 16); cách chia để trị này ngắn hơn và là điểm khởi đầu của CDQ (bài 60).

Phần cắt ngang: việc của bước gộp

Khuôn “trái, phải, chéo” của đếm nghịch thế dùng được cho nhiều bài về đoạn hay cặp. Bài thứ nhất: cho dãy số nguyên, có thể âm, tìm tổng lớn nhất của một đoạn con liên tiếp không rỗng. Bài này còn có lời giải tuyến tính khác; ta làm bằng chia để trị vì nó dạy đúng khuôn.

Chia dãy tại mid\mathrm{mid}. Mỗi đoạn [i,j][i, j] nằm hẳn trong nửa trái, nằm hẳn trong nửa phải, hoặc cắt ngang đường chia (i<mid≤ji < \mathrm{mid} \le j). Hai loại đầu giao cho đệ quy; bước gộp chỉ lo loại thứ ba. Đoạn cắt ngang là một hậu tố của nửa trái nối với một tiền tố của nửa phải, cả hai không rỗng. Hai phần chọn độc lập, nên ta chọn riêng hậu tố tốt nhất và tiền tố tốt nhất, mỗi thứ bằng một lượt quét từ mid\mathrm{mid} ra một phía.

Có thể bỏ cả lượt quét bằng cách để mỗi lời gọi trả về bốn số mô tả đoạn của nó: tổng cả đoạn, tiền tố tốt nhất, hậu tố tốt nhất và đoạn con tốt nhất. Tiền tố tốt nhất của đoạn gộp nằm trong nửa trái, hoặc là cả nửa trái nối tiền tố tốt nhất của nửa phải; hậu tố đối xứng; đoạn tốt nhất là của nửa trái, của nửa phải, hoặc hậu tố trái nối tiền tố phải:

C++
struct Info {
    ll sum, pre, suf, best;
};

Info combine(Info a, Info b) {
    return {a.sum + b.sum, max(a.pre, a.sum + b.pre), max(b.suf, b.sum + a.suf),
            max({a.best, b.best, a.suf + b.pre})};
}

Gộp tốn O(1)O(1) nên T(n)=2T(n/2)+O(1)=O(n)T(n) = 2T(n/2) + O(1) = O(n). Ý “tóm tắt mỗi đoạn bằng vài con số mà hai bản tóm tắt gộp được trong O(1)O(1)” chính là thứ mỗi nút của cây phân đoạn (bài 17) lưu.

Ví dụĐoạn Liên Tiếp Tốt Nhất

Đề. Cho dãy N≤2⋅105N \le 2 \cdot 10^5 số nguyên, ∣ai∣≤109|a_i| \le 10^9. In tổng lớn nhất của một đoạn con liên tiếp không rỗng.

Lời giải. Cài bản quét từ giữa ra. Với a=(−4,6,−2,5,−7,4,3)a = (-4, 6, -2, 5, -7, 4, 3) và mid=3\mathrm{mid} = 3: nửa trái có đáp án riêng 66, nửa phải có đáp án riêng 77 (đoạn 4,34, 3). Hậu tố của nửa trái có tổng −2,4,0-2, 4, 0, tốt nhất 44; tiền tố của nửa phải có tổng 5,−2,2,55, -2, 2, 5, tốt nhất 55. Đoạn cắt ngang tốt nhất 4+5=94 + 5 = 9, đáp án max⁡(6,7,9)=9\max(6, 7, 9) = 9. Hai biến hậu tố, tiền tố tốt nhất khởi tạo bằng −1018-10^{18} chứ không phải 0: khởi tạo 0 là cho phép phần rỗng, và với dãy toàn số âm chương trình sẽ in 0, tổng của một đoạn không tồn tại. Tổng tới 2⋅10142 \cdot 10^{14} nên dùng ll; thời gian O(nlog⁡n)O(n \log n).

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, a[N];

ll solve(ll l, ll r) {
    if (r - l == 1) return a[l];
    ll mid = (l + r) >> 1;
    ll res = max(solve(l, mid), solve(mid, r));
    ll s = 0, bl = -1e18, br = -1e18;
    for (ll i = mid - 1; i >= l; i--) {
        s += a[i];
        bl = max(bl, s);
    }
    s = 0;
    for (ll j = mid; j < r; j++) {
        s += a[j];
        br = max(br, s);
    }
    return max(res, bl + br);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 0; i < n; i++) cin >> a[i];
    cout << solve(0, n) << '\n';
}
7
-4 6 -2 5 -7 4 3

Ví dụBình Phương Khoảng Cách Nhỏ Nhất

Đề. Cho N≤105N \le 10^5 điểm nguyên, ∣xi∣,∣yi∣≤104|x_i|, |y_i| \le 10^4, hai điểm khác chỉ số có thể trùng tọa độ. In giá trị nhỏ nhất của (xi−xj)2+(yi−yj)2(x_i - x_j)^2 + (y_i - y_j)^2 trên mọi cặp i<ji < j: bài cặp điểm gần nhất (closest pair of points).

Lời giải. Sắp các điểm theo xx một lần, rồi chia theo chỉ số tại mid\mathrm{mid}; đường chia là x=xmidx = x_{\mathrm{mid}}. Giải hai nửa bằng đệ quy, gọi DD là bình phương khoảng cách nhỏ hơn trong hai kết quả và d=Dd = \sqrt D. Còn lại các cặp chéo.

Cặp chéo muốn tốt hơn DD thì hai điểm cách nhau theo hoành độ chưa tới dd, mà đường chia nằm giữa chúng, nên cả hai thuộc dải ∣x−xmid∣<d|x - x_{\mathrm{mid}}| < d. Sắp các điểm trong dải theo yy; với điểm pp, chỉ so với các điểm qq đứng trước có py−qy<dp_y - q_y < d. Những điểm đó cùng pp nằm trong hình chữ nhật 2d×d2d \times d. Nửa trái của hình chữ nhật là hình vuông d×dd \times d mà các điểm bên trong đôi một cách nhau ít nhất dd; chia nó thành bốn ô d2×d2\tfrac d2 \times \tfrac d2, đường chéo mỗi ô là d/2<dd/\sqrt2 < d nên mỗi ô chứa không quá một điểm. Vậy hình chữ nhật chứa không quá 8 điểm kể cả pp, và vòng so của mỗi điểm chạy tối đa 7 bước.

Dải xanh rộng 2d quanh đường chia; khung cam là hình chữ nhật 2d × d của điểm đang xét.

Mọi so sánh dùng bình phương bằng số nguyên, không có căn: điều kiện dải là (x−xmid)2<D(x - x_{\mathrm{mid}})^2 < D. Code gồm ba bước trong hàm solve(l, r), trả về đáp án của đoạn [l,r)[l, r) và để lại đoạn đó đã sắp theo yy.

Bước 1: chia và giải hai nửa. Đoạn dưới hai điểm thì chưa có cặp nào, trả về vô cùng. Hoành độ đường chia mx phải lưu trước khi đệ quy, vì sau đó đoạn đã bị sắp lại theo yy.

C++
if (r - l <= 1) return 1e18;
ll mid = (l + r) >> 1, mx = p[mid].first;
ll d = min(solve(l, mid), solve(mid, r));

Bước 2: trộn hai nửa theo yy. Hai nửa đã sắp theo yy, trộn như sắp xếp trộn trong O(r−l)O(r - l). Nhờ vậy không phải gọi sort theo yy ở mỗi lời gọi, việc sẽ nhân độ phức tạp thêm một thừa số log⁡N\log N.

C++
ll i = l, j = mid, k = l;
while (i < mid || j < r) {
    if (j == r || (i < mid && p[i].second <= p[j].second)) t[k++] = p[i++];
    else t[k++] = p[j++];
}
for (ll q = l; q < r; q++) p[q] = t[q];

Bước 3: quét dải. Đi qua đoạn theo thứ tự yy, giữ các điểm của dải trong t[l..l + m). Điểm nằm ngoài dải thì bỏ. Điểm trong dải so với các điểm dải đứng trước, lùi dần khi hiệu tung độ còn nhỏ hơn dd, rồi tự nhập vào dải. Theo lập luận ở trên, vòng lùi chạy tối đa 7 bước.

C++
ll m = 0;
for (ll q = l; q < r; q++) {
    if (sq(p[q].first - mx) >= d) continue;
    for (ll s = m - 1; s >= 0 && sq(p[q].second - t[l + s].second) < d; s--)
        d = min(d, sq(p[q].first - t[l + s].first) + sq(p[q].second - t[l + s].second));
    t[l + m++] = p[q];
}

Điểm trùng nhau cho D=0D = 0, khi đó dải rỗng và chương trình vẫn đúng. Với test ví dụ, nửa trái {(0,0),(1,1)}\{(0,0), (1,1)\} cho D=2D = 2 và không cặp chéo nào tốt hơn.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5;
ll n;
pair<ll, ll> p[N], t[N];

ll sq(ll v) {
    return v * v;
}

ll solve(ll l, ll r) {
    if (r - l <= 1) return 1e18;
    ll mid = (l + r) >> 1, mx = p[mid].first;
    ll d = min(solve(l, mid), solve(mid, r));
    ll i = l, j = mid, k = l;
    while (i < mid || j < r) {
        if (j == r || (i < mid && p[i].second <= p[j].second)) t[k++] = p[i++];
        else t[k++] = p[j++];
    }
    for (ll q = l; q < r; q++) p[q] = t[q];
    ll m = 0;
    for (ll q = l; q < r; q++) {
        if (sq(p[q].first - mx) >= d) continue;
        for (ll s = m - 1; s >= 0 && sq(p[q].second - t[l + s].second) < d; s--)
            d = min(d, sq(p[q].first - t[l + s].first) + sq(p[q].second - t[l + s].second));
        t[l + m++] = p[q];
    }
    return d;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 0; i < n; i++) cin >> p[i].first >> p[i].second;
    sort(p, p + n);
    cout << solve(0, n) << '\n';
}
5
0 0
3 4
1 1
9 9
1 4

T(N)=2T(N/2)+O(N)T(N) = 2T(N/2) + O(N), tức O(Nlog⁡N)O(N \log N) cộng một lần sắp ban đầu.

Chỉ đi vào một nhánh

Có khi bước gộp chẳng còn việc gì vì lời giải nằm trọn trong một bài con; ta chỉ cần biết nên đi vào bài con nào. Cây đệ quy khi đó rút thành một đường đi. Có hai dạng hay gặp.

Phần tử thứ kk (quickselect). Tìm số đứng thứ kk (đánh số từ 0) khi sắp tăng, không sắp cả dãy. Chọn một pivot xx ngẫu nhiên, chia các số thành ba nhóm: nhỏ hơn xx (pp số), bằng xx (qq số), lớn hơn xx. Số cần tìm nằm trong nhóm nhỏ nếu k<pk < p; bằng xx nếu p≤k<p+qp \le k < p + q; ngược lại nằm ở vị trí k−p−qk - p - q của nhóm lớn. Chỉ một nhóm cần đi tiếp.

C++
mt19937 rng(712367);

ll kth(vector<ll> a, ll k) {
    while (true) {
        ll x = a[rng() % a.size()];
        vector<ll> lo, eq, hi;
        for (ll v : a) {
            if (v < x) lo.push_back(v);
            else if (v == x) eq.push_back(v);
            else hi.push_back(v);
        }
        if (k < (ll)lo.size()) a = lo;
        else if (k < (ll)(lo.size() + eq.size())) return x;
        else {
            k -= lo.size() + eq.size();
            a = hi;
        }
    }
}

Xấu nhất là O(n2)O(n^2) nếu pivot luôn rơi vào đầu mút, nhưng với pivot ngẫu nhiên trung bình chỉ O(n)O(n). Gọi pivot là tốt nếu nó nằm trong nửa giữa của thứ tự sắp, xác suất ít nhất 1/21/2; khi đó mỗi nhóm có không quá 3n/43n/4 phần tử. Trung bình chờ 2 vòng là gặp pivot tốt, mỗi vòng tốn tuyến tính theo cỡ hiện tại, nên trung bình tổng công việc không quá 2n(1+34+(34)2+… )2n\left(1 + \tfrac34 + \left(\tfrac34\right)^2 + \dots\right); tổng trong ngoặc bằng 4, nên cả bài trung bình không quá 8n8n. Thư viện có nth_element làm đúng việc này tại chỗ.

Hỏi nhanh

Một bạn cài quickselect với hai nhóm: “nhỏ hơn pivot” và “lớn hơn hoặc bằng pivot”, bỏ riêng pivot ra ngoài. Cho nn số giống hệt nhau và k=n/2k = n/2. Số phép so sánh cỡ bao nhiêu?

Vị trí trong dãy sinh theo quy tắc. Nhiều đề cho một dãy sinh bằng quy tắc đệ quy, dài tới 101810^{18}, rồi hỏi phần tử thứ pp. Không dựng được cả dãy, nhưng cấu trúc của nó đã là một lời gọi đệ quy sẵn. Ví dụ S0=(0)S_0 = (0) và Sk=Sk−1,k,Sk−1S_k = S_{k-1}, k, S_{k-1}: hai bản sao của Sk−1S_{k-1} kẹp số kk ở giữa. Vậy S2=(0,1,0,2,0,1,0)S_2 = (0, 1, 0, 2, 0, 1, 0) và ∣Sk∣=2k+1−1|S_k| = 2^{k+1} - 1. Phần tử thứ pp (từ 1) của SkS_k chỉ ở ba chỗ: bản sao trái nếu p≤∣Sk−1∣p \le |S_{k-1}|; phần tử giữa, bằng kk, nếu p=∣Sk−1∣+1p = |S_{k-1}| + 1; còn lại là bản sao phải, nơi nó là phần tử thứ p−∣Sk−1∣−1p - |S_{k-1}| - 1 của Sk−1S_{k-1}. Ta chỉ lưu độ dài từng cấp rồi đi xuống một nhánh.

C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll q, len[61];

ll at(ll k, ll p) {
    if (k == 0) return 0;
    if (p <= len[k - 1]) return at(k - 1, p);
    if (p == len[k - 1] + 1) return k;
    return at(k - 1, p - len[k - 1] - 1);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    len[0] = 1;
    for (ll k = 1; k <= 60; k++) len[k] = 2 * len[k - 1] + 1;
    cin >> q;
    while (q--) {
        ll k, p;
        cin >> k >> p;
        cout << at(k, p) << '\n';
    }
}
3
3 6
3 8
60 1000000000000000000

Mỗi truy vấn qua tối đa k+1≤61k + 1 \le 61 lời gọi. Nhìn kết quả thấy phần tử thứ pp của SkS_k chính là số mũ của 2 trong pp (vì 1018=218⋅51810^{18} = 2^{18} \cdot 5^{18} nên ra 18), một công thức đóng; viết vài phần tử đầu ra giấy luôn đáng làm. Cùng khuôn này giải các bài về dãy bit tự đảo, xâu mở rộng hay thứ tự loại trong vòng tròn.

Ví dụĐếm Một Trong Dãy Gấp Đôi

Đề. Với n≥0n \ge 0, dãy F(n)F(n) là (n)(n) nếu n≤1n \le 1, và là F(⌊n/2⌋)F(\lfloor n/2 \rfloor), rồi n mod 2n \bmod 2, rồi lại F(⌊n/2⌋)F(\lfloor n/2 \rfloor) nếu n>1n > 1. Cho n≤1018n \le 10^{18} và l≤rl \le r, đếm số phần tử bằng 1 ở các vị trí từ ll tới rr (đánh số từ 1).

Lời giải. Đếm trên đoạn bằng hiệu hai tiền tố: gọi pre(n,k)\mathrm{pre}(n, k) là số số 1 trong kk phần tử đầu của F(n)F(n), đáp án là pre(n,r)−pre(n,l−1)\mathrm{pre}(n, r) - \mathrm{pre}(n, l - 1). Đặt h=∣F(⌊n/2⌋)∣h = |F(\lfloor n/2 \rfloor)|. Nếu k≤hk \le h, cả kk phần tử nằm trong bản sao trái: đi vào nó. Nếu k>hk > h thì kk phần tử đầu gồm trọn bản sao trái, phần tử giữa n mod 2n \bmod 2, và k−h−1k - h - 1 phần tử đầu của bản sao phải.

Đi vào bản sao phải là một nhánh, nhưng “trọn bản sao trái” lại là thêm một nhánh nữa, và hai nhánh mỗi mức là O(n)O(n) lời gọi. Mẹo là F(m)F(m) có đúng mm số 1: số số 1 thỏa c(m)=2c(⌊m/2⌋)+m mod 2c(m) = 2c(\lfloor m/2 \rfloor) + m \bmod 2, đúng hệ thức của chính m=2⌊m/2⌋+m mod 2m = 2\lfloor m/2 \rfloor + m \bmod 2, và hai bên bằng nhau ở m≤1m \le 1. Vậy trọn bản sao trái đóng góp ⌊n/2⌋\lfloor n/2 \rfloor không cần gọi gì, và mỗi mức chỉ còn một lời gọi. Độ sâu là log⁡2n≈60\log_2 n \approx 60 mức, mỗi mức tính hh thêm 60 bước.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, l, r;

ll len(ll m) {
    return m <= 1 ? 1 : 2 * len(m / 2) + 1;
}

ll pre(ll m, ll k) {
    if (k <= 0) return 0;
    if (m <= 1) return m;
    ll h = len(m / 2);
    if (k <= h) return pre(m / 2, k);
    return m / 2 + m % 2 + pre(m / 2, k - h - 1);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> l >> r;
    cout << pre(n, r) - pre(n, l - 1) << '\n';
}
5 2 6

Chia thành bốn phần: lát bảng bằng mảnh chữ L

Trên bảng vuông ta chia thành bốn phần tư. Ví dụ kinh điển: bảng 2k×2k2^k \times 2^k khuyết đúng một ô; lát kín 4k−14^k - 1 ô còn lại bằng các mảnh chữ L, mỗi mảnh phủ 3 ô (chia hết vì 4k4^k chia 3 luôn dư 1). Chia thành bốn phần tư thì chỉ phần tư chứa ô khuyết là bài con cùng dạng, ba phần tư kia thì không. Mẹo là tạo ra ô khuyết: đặt một mảnh chữ L vào giữa bảng, phủ ba ô sát tâm thuộc ba phần tư không chứa ô khuyết. Giờ mỗi phần tư thiếu đúng một ô và là bài con cùng dạng; bảng 1×11 \times 1 là điều kiện dừng. Tính đúng suy ra từ quy nạp theo kk.

Ô đen là ô khuyết, bấm ô khác để đổi; khung cam là hình vuông đang chia.

Chương trình dưới đọc cạnh nn (lũy thừa của 2) và ô khuyết (hr,hc)(h_r, h_c), tính từ 0, rồi in số hiệu mảnh phủ từng ô; ô khuyết giữ số 0. Hàm tile(r0, c0, s, hr, hc) lát hình vuông cạnh ss, góc trên trái (r0,c0)(r_0, c_0), thiếu ô (hr,hc)(h_r, h_c). Hai mảng mr, mc là hai hàng và hai cột sát tâm.

C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, hr, hc, id, b[130][130];

void tile(ll r0, ll c0, ll s, ll hr, ll hc) {
    if (s == 1) return;
    ll h = s / 2, cur = ++id;
    ll mr[2] = {r0 + h - 1, r0 + h}, mc[2] = {c0 + h - 1, c0 + h};
    for (ll i = 0; i < 2; i++)
        for (ll j = 0; j < 2; j++) {
            ll x = r0 + i * h, y = c0 + j * h;
            if (x <= hr && hr < x + h && y <= hc && hc < y + h) tile(x, y, h, hr, hc);
            else {
                b[mr[i]][mc[j]] = cur;
                tile(x, y, h, mr[i], mc[j]);
            }
        }
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> hr >> hc;
    tile(0, 0, n, hr, hc);
    for (ll i = 0; i < n; i++) {
        for (ll j = 0; j < n; j++) cout << b[i][j] << ' ';
        cout << '\n';
    }
}
4 1 2

Mảnh số 1 là mảnh giữa bảng, phủ các ô (1,1)(1, 1), (2,1)(2, 1), (2,2)(2, 2) của ba phần tư không chứa ô khuyết (1,2)(1, 2). Mỗi lời gọi tốn O(1)O(1) ngoài đệ quy và sinh 4 bài con cạnh một nửa: T(n)=4T(n/2)+O(1)T(n) = 4T(n/2) + O(1) với nn là cạnh bảng. Ở đây q=4>1q = 4 > 1 nên các lá quyết định, O(nlog⁡24)=O(n2)O(n^{\log_2 4}) = O(n^2), đúng bằng số ô phải ghi.

Ví dụNhững Mảnh Giấy Hai Màu

Đề. Tờ giấy N×NN \times N (N≤128N \le 128, lũy thừa của 2), mỗi ô trắng (0) hoặc xanh (1). Một vùng vuông cùng màu thì giữ nguyên thành một mảnh; có cả hai màu thì chia thành bốn hình vuông bằng nhau và làm lại cho từng phần. In số mảnh trắng, rồi số mảnh xanh.

Lời giải. Đề đã là lời gọi chia bốn. Việc còn lại là hỏi nhanh “vùng này có cùng màu không”: đếm số ô xanh trong vùng bằng tổng tiền tố hai chiều (bài 02) trong O(1)O(1); bằng 0 là mảnh trắng, bằng k2k^2 là mảnh xanh, còn lại thì chia bốn.

Bước 1: tổng tiền tố khi đọc. s[i][j] là số ô xanh trong hình chữ nhật từ (1,1)(1, 1) tới (i,j)(i, j), tính ngay khi đọc từng ô bằng công thức bao hàm loại trừ.

C++
for (ll i = 1; i <= n; i++)
    for (ll j = 1; j <= n; j++) {
        ll v;
        cin >> v;
        s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + v;
    }

Bước 2: đếm ô xanh của một vùng. Vùng vuông cạnh kk có góc trên trái là ô (x+1,y+1)(x + 1, y + 1) (vì s đánh số từ 1) chứa get(x, y, k) ô xanh.

C++
ll get(ll x, ll y, ll k) {
    return s[x + k][y + k] - s[x][y + k] - s[x + k][y] + s[x][y];
}

Bước 3: chia bốn. Vùng toàn trắng hay toàn xanh thì đếm một mảnh và dừng; ngược lại gọi đệ quy cho bốn hình vuông cạnh k/2k / 2.

C++
void go(ll x, ll y, ll k) {
    ll c = get(x, y, k);
    if (c == 0) {
        w++;
        return;
    }
    if (c == k * k) {
        g++;
        return;
    }
    ll h = k / 2;
    go(x, y, h);
    go(x, y + h, h);
    go(x + h, y, h);
    go(x + h, y + h, h);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, w, g, s[130][130];

ll get(ll x, ll y, ll k) {
    return s[x + k][y + k] - s[x][y + k] - s[x + k][y] + s[x][y];
}

void go(ll x, ll y, ll k) {
    ll c = get(x, y, k);
    if (c == 0) {
        w++;
        return;
    }
    if (c == k * k) {
        g++;
        return;
    }
    ll h = k / 2;
    go(x, y, h);
    go(x, y + h, h);
    go(x + h, y, h);
    go(x + h, y + h, h);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++)
        for (ll j = 1; j <= n; j++) {
            ll v;
            cin >> v;
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + v;
        }
    go(0, 0, n);
    cout << w << '\n' << g << '\n';
}
4
0 0 1 1
0 0 1 1
1 1 0 0
1 1 0 1

Số vùng được xét không quá 1+4+16+⋯<43N21 + 4 + 16 + \dots < \tfrac43 N^2, mỗi vùng O(1)O(1), nên cả bài O(N2)O(N^2). Thiếu tổng tiền tố thì mỗi vùng tốn k2k^2 để kiểm, thành O(N2log⁡N)O(N^2 \log N), vẫn qua với N=128N = 128 nhưng không còn đẹp.

Khi đi thi

  • Bài đếm hoặc tối ưu trên các cặp (i,j)(i, j) cần quan hệ giữa aia_i và aja_j mà O(n2)O(n^2) chậm: nghĩ tới chia đôi chỉ số, giao hai nửa cho đệ quy và xử lý cặp chéo lúc gộp hai nửa đã sắp; nếu còn có cập nhật xen giữa thì chuyển sang cây Fenwick (bài 16) hoặc CDQ (bài 60).
  • Trước khi cài, viết hệ thức T(n)T(n) và đọc độ phức tạp từ cây đệ quy: bài con phải co lại theo tỉ lệ và mỗi bài chỉ giải một lần. Cài với đoạn nửa mở [l,r)[l, r), điều kiện dừng r - l <= 1 và một mảng phụ toàn cục dùng chung.
  • Dãy hoặc xâu sinh theo quy tắc đệ quy dài tới 101810^{18} thì đừng dựng nó: lưu độ dài từng cấp rồi đi một nhánh theo vị trí, và so với bản vét cạn trên cỡ nhỏ trước khi nộp.

Tóm tắt

  • Chia để trị gồm chia, trị (đệ quy, có điều kiện dừng) và gộp; các bài con phải cùng dạng, độc lập và co lại theo tỉ lệ. Dùng đoạn nửa mở [l,r)[l, r) với mid=(l+r)/2\mathrm{mid} = (l + r)/2.
  • Hệ thức T(n)=a T(n/b)+f(n)T(n) = a\,T(n/b) + f(n) đọc bằng cây đệ quy: cộng chi phí theo mức rồi xem mức đầu, mọi mức hay mức cuối trội hơn. Nhớ 2T(n/2)+O(n)=O(nlog⁡n)2T(n/2) + O(n) = O(n \log n), T(n/2)+O(1)=O(log⁡n)T(n/2) + O(1) = O(\log n), 4T(n/2)+O(1)=O(n2)4T(n/2) + O(1) = O(n^2).
  • Sắp xếp trộn gộp hai nửa đã sắp bằng hai con trỏ trong O(n)O(n); lấy bên trái khi bằng nhau để giữ ổn định.
  • Phần khó là cặp chéo, việc của bước gộp: cộng mid−i\mathrm{mid} - i khi gộp (nghịch thế), quét từ giữa ra (đoạn con lớn nhất), lọc dải rồi so với tối đa 7 điểm kế tiếp (cặp điểm gần nhất).
  • Khi lời giải nằm trọn trong một bài con thì chỉ đi một nhánh: quickselect với phân hoạch ba phía trung bình O(n)O(n), vị trí trong dãy sinh theo quy tắc cần O(log⁡)O(\log) lời gọi.

Bài tập

1
Tách Đống Theo Tỉ Lệcanuc80k1107Đống xx chỉ tách được thành x/3x/3 và 2x/32x/3 khi xx chia hết cho 3; đệ quy cả hai nhánh, dừng khi x=mx = m hoặc x<mx < m.
LQDOJ
2
Ô Trên Đường Zcanuc80k1104Mỗi cấp, ô rơi vào một trong bốn phần tư; cộng số ô của các phần tư đứng trước rồi đi vào đúng phần tư đó.
LQDOJ
3
Bit Tự Đảo Thứ Kcanuc80k1106Trong khối dài 2h2h, nửa sau là nửa đầu đảo bit: đưa KK về K−hK - h và đảo kết quả.
LQDOJ
4
Gộp Dãy Và Đếm Lượt Chọncanuc80k1120Cài đúng sắp xếp trộn theo quy tắc của đề; mỗi bước gộp đoạn [l,r)[l, r) đóng góp đúng r−lr - l lượt chọn.
LQDOJ
5
Xâu Tốt Theo Cấpcanuc80k1117Có hai cách: nửa trái toàn một chữ hoặc nửa phải toàn một chữ; đếm chữ khác ở nửa đó, cộng với đệ quy nửa kia rồi lấy nhỏ hơn.
LQDOJ
6
Ký Tự Trong Dãy Mở Rộngcanuc80k1114Lưu độ dài từng cấp, rồi xét vị trí rơi vào bản sao trái, đoạn giữa hay bản sao phải của cấp đang xét.
LQDOJ
7
Đảo Hai Nửa Câycanuc80k1115Đoạn của mỗi nút phải chứa một khoảng giá trị liền nhau; nửa trái chứa khoảng lớn hơn thì đổi chỗ, tốn một thao tác, còn không liền thì in −1-1.
LQDOJ
8
Giá Trị Ở Hạng Lớn Thứ Kcanuc80k1124Quickselect với phân hoạch ba phía; phần tử lớn thứ KK có chỉ số N−KN - K khi đánh số từ 0 theo thứ tự tăng.
LQDOJ
9
Sơn Hàng Rào Ít Nét Nhấtcanuc80k1130Với mỗi khối, so hai cách: sơn dọc từng tấm, hoặc sơn ngang tới độ cao nhỏ nhất rồi đệ quy vào các khối cao hơn.
LQDOJ
10
Những Đoạn Nối Đảo Thứ Tựcanuc80k1126Sắp các đoạn nối theo đầu trái; hai đoạn giao nhau đúng khi đầu phải bị đảo thứ tự, nên đáp án là số nghịch thế nghiêm ngặt.
LQDOJ
11
Cặp Lệch Gấp Đôicanuc80k1127Đếm cặp chéo ai>2aja_i > 2a_j trước khi gộp: hai nửa đã sắp nên con trỏ đếm số phần tử thỏa mãn không bao giờ lùi.
LQDOJ
12
Hình Chữ Nhật Dưới Đường Cộtcanuc80k1132Hình chữ nhật tối ưu nằm trong một nửa hoặc cắt qua giữa; với loại cắt qua, mở rộng từ hai cột giữa về phía cột cao hơn.
LQDOJ
13
Đếm Đoạn Có Tổng Nhỏ Hơn Ngưỡngcanuc80k1137Với tổng tiền tố PP, đếm cặp i<ji < j có Pi>Pj−tP_i > P_j - t bằng chia để trị trên chỉ số và gộp theo giá trị.
LQDOJ
14
Hợp Đường Chân Trờicanuc80k1141Dựng đường chân trời của từng nửa rồi gộp hai hàm bậc thang bằng hai con trỏ, chỉ ghi điểm khi chiều cao hợp thay đổi.
LQDOJ
15
Dãy Có Phần Tử Đơn Nhất Trong Mọi Đoạncanuc80k1139Giá trị là đơn nhất trong đoạn khi lần xuất hiện trước và sau nó đều nằm ngoài đoạn; chọn điểm tách bằng cách quét từ hai đầu vào giữa để tổng thời gian là O(nlog⁡n)O(n \log n).
LQDOJ

Đọc thêm

  • Chia để trịVNOI WikiBài tiếng Việt về ba bước chia, trị, gộp và cách tính độ phức tạp bằng định lý thợ.
  • Chia để trị và một số bài toán thú vịVibloBa ví dụ tiếng Việt về đi một nhánh: tìm kiếm nhị phân, xâu Fibonacci, dãy gấp đôi.
  • 递归 & 分治OI WikiPhân biệt đệ quy với chia để trị, và khi nào các bài con chồng nhau thì phải chuyển sang quy hoạch động.
  • 归并排序OI WikiSắp xếp trộn từ trên xuống và từ dưới lên, tính ổn định, đếm nghịch thế ngay trong bước gộp.
  • Finding the nearest pair of pointscp-algorithmsChứng minh chặn số điểm trong hình chữ nhật 2d×d2d \times d và cách gộp theo yy.
  • Master theoremWikipediaPhát biểu đầy đủ ba trường hợp với điều kiện chính quy, và những hệ thức mà định lý thợ không áp dụng được.