Cho số nguyên, tìm hiệu nhỏ nhất giữa hai số. Thử mọi cặp cần khoảng phép trừ; với là , quá chậm. Nhưng nếu các số đã xếp từ bé tới lớn, hai số gần nhau nhất chắc chắn đứng cạnh nhau, và chỉ còn cặp kề phải xét.
Sắp xếp làm được nhiều việc như thế: một lần sắp biến cả lớp bài thành một lượt quét, từ đếm giá trị khác nhau, tìm trung vị, tới các lời giải tham lam duyệt theo thứ tự. Bài này đi từ std::sort và hàm so sánh, qua sắp xếp trộn và sắp xếp đếm, tới cách tự chọn một thứ tự đúng.
Sắp rồi quét cặp kề
Bài toán: cho dãy , tìm với . Gọi là dãy sau khi sắp tăng dần. Hiệu nhỏ nhất luôn đạt ở một cặp kề , vì với bất kỳ,
là tổng các hiệu kề không âm, nên . Với , dãy đã sắp là , các hiệu kề là , và đáp án ứng với cặp .
sort(a + 1, a + n + 1);
ll ans = 1e18;
for (ll i = 2; i <= n; i++) ans = min(ans, a[i] - a[i - 1]);
sort tốn , với là , số lần chia đôi cho tới khi còn 1 (bài B10). Với con số này chỉ khoảng 18, nên sắp xếp tốn cỡ phép so sánh, nhanh hơn thử mọi cặp hàng nghìn lần.
Dãy đã sắp còn cho thêm vài thứ gần như miễn phí. Các giá trị bằng nhau thành một khối liền, nên số giá trị phân biệt là số vị trí có cộng một. Phần tử nhỏ thứ là , một trung vị là phần tử đứng giữa, a[(n + 1) / 2]. Chỉ cần một lần giá trị nhỏ nhất hay lớn nhất thì không cần sắp, duyệt một lượt là đủ. Sắp xếp đáng giá khi bài cần nhiều thông tin thứ tự cùng lúc, và thường đi kèm hai con trỏ (bài 06) hoặc chặt nhị phân (bài 05).
Ví dụBộ Xếp Hình Đồng Đều
Đề. Có bộ xếp hình, bộ có mảnh. Chọn đúng bộ khác nhau để số mảnh lớn nhất trừ nhỏ nhất là nhỏ nhất.
Lời giải. Sắp tăng dần. Một cách chọn tốt nhất luôn là bộ liên tiếp trong dãy đã sắp: nếu cách chọn có min là và max là , thay nó bằng bộ không làm max tăng, vì . Vậy chỉ cần thử mọi cửa sổ và lấy nhỏ nhất. Cả bài .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, m, f[55];
signed main() {
cin >> n >> m;
for (ll i = 1; i <= m; i++) cin >> f[i];
sort(f + 1, f + m + 1);
ll ans = 1e18;
for (ll l = 1; l + n - 1 <= m; l++) ans = min(ans, f[l + n - 1] - f[l]);
cout << ans << '\n';
}
5 9
14 4 8 9 10 15 6 7 20
4
Hàm so sánh
Trong thi đấu ta không tự viết lại sắp xếp thông thường: std::sort đúng và nhanh, chuẩn C++ bảo đảm phép so sánh kể cả trường hợp xấu nhất.
sort(a + 1, a + n + 1);
sort(a + 1, a + n + 1, greater<ll>());
pair, tuple, string có sẵn thứ tự từ điển: so thành phần đầu, bằng nhau thì so tiếp. Với thứ tự khác, ta đưa thêm hàm cmp(x, y), trả về true khi phải đứng trước một cách chặt chẽ. Hàm này phải là thứ tự yếu chặt (strict weak ordering):
cmp(x, x)luôn sai;- nếu
cmp(x, y)đúng thìcmp(y, x)sai; - nếu
cmp(x, y)vàcmp(y, z)đúng thìcmp(x, z)đúng; - quan hệ “ngang hàng” (cả hai chiều đều sai) có tính bắc cầu.
Cách dễ nhất để thỏa cả bốn là gán mỗi phần tử một khóa và chỉ so khóa bằng < hoặc > chặt. Lỗi hay gặp nhất là viết <= hay >= với ý “bằng nhau cũng được”: một phần tử bị coi là đứng trước chính nó có thể khiến vòng quét bên trong sort chạy ra ngoài mảng, nên chương trình đúng ở test nhỏ rồi sập khi dữ liệu có nhiều giá trị bằng nhau.
Hỏi nhanh
Một bạn sắp mảng giảm dần bằng hàm bool cmp(ll x, ll y) { return x >= y; }. Test mẫu chạy đúng, nhưng ở test có rất nhiều số bằng nhau thì chương trình báo Runtime Error. Nguyên nhân là gì?
Hàm so sánh trả lời “ phải đứng trước một cách chặt chẽ”. Với câu trả lời đúng là không, mà >= lại nói có; điều đó vi phạm cmp(x, x) phải sai, nên các vòng quét trong sort có thể không dừng đúng chỗ. Chương trình vẫn biên dịch và chạy qua dữ liệu nhỏ, nên lỗi hay lọt qua test mẫu. Sửa thành x > y.
std::sort không ổn định (stable): thứ tự các phần tử bằng nhau là không xác định. Khi đề cần thứ tự duy nhất, thêm một khóa phân định (chỉ số gốc, tên); khi muốn giữ thứ tự nhập, dùng stable_sort. Khuôn của hàm nhiều khóa: khóa này khác nhau thì trả lời ngay theo nó, bằng thì sang khóa sau, khóa cuối là một phép so sánh chặt. Khi cần in số hiệu thay vì giá trị, ta sắp một mảng chỉ số, hoặc gói chỉ số vào bản ghi như ví dụ dưới.
Ví dụSắp Xếp Hồ Sơ Địa Điểm
Đề. hồ sơ, hồ sơ có tên thành phố và điểm (đôi một khác nhau). Sắp theo tên tăng dần; cùng thành phố thì điểm cao đứng trước. In chỉ số ban đầu theo thứ tự mới.
Lời giải. Mỗi hồ sơ là một bản ghi (tên, điểm, chỉ số). Hai khóa ngược chiều nhau nên viết hàm so sánh: tên khác nhau thì so tên tăng dần, cùng tên thì so điểm giảm dần. Điểm đôi một khác nhau nên không còn hòa, không cần khóa thứ ba.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
struct Rec {
string city;
ll score, id;
};
bool cmp(Rec x, Rec y) {
if (x.city != y.city) return x.city < y.city;
return x.score > y.score;
}
ll n;
Rec a[105];
signed main() {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> a[i].city >> a[i].score;
a[i].id = i;
}
sort(a + 1, a + n + 1, cmp);
for (ll i = 1; i <= n; i++) cout << a[i].id << '\n';
}
5
delta 70
alpha 30
alpha 90
delta 50
beta 60
3
2
5
1
4
Sắp xếp trộn
Chọn, chèn, nổi bọt đều có hai vòng lặp lồng nhau và tốn . Với nổi bọt và chèn, lý do nằm ở nghịch thế. Nghịch thế (inversion) là cặp mà ; dãy giảm dần có nghịch thế. Đổi chỗ hai phần tử kề nhau đang sai thứ tự xóa đúng một nghịch thế, nên thuật toán chỉ đổi chỗ phần tử kề nhau phải đổi ít nhất bằng số nghịch thế. Muốn nhanh hơn phải cho phần tử nhảy xa.
Sắp xếp trộn (merge sort) làm điều đó bằng cách chia đôi. Để sắp đoạn : nếu chỉ có một phần tử thì xong; ngược lại đặt mid = (l + r) / 2 (chia nguyên), sắp đệ quy và , rồi trộn hai nửa bằng hai con trỏ, mỗi bước lấy ra số nhỏ hơn trong hai số đang trỏ: trộn với cho . Cách này đúng vì phần tử nhỏ nhất còn lại luôn nằm ở đầu một trong hai nửa.
Độ dài đoạn giảm một nửa sau mỗi tầng nên có khoảng tầng; ở mỗi tầng các đoạn rời nhau và phủ cả mảng, trộn tốn thời gian tuyến tính, nên cả thuật toán tốn . Với có 18 tầng.
Trong lúc trộn ta còn đếm được nghịch thế. Một nghịch thế nằm trọn trong nửa trái, trọn trong nửa phải, hoặc bắc qua hai nửa; hai loại đầu do hai lời gọi đệ quy đếm. Loại bắc qua đếm ngay lúc trộn: khi lấy của nửa phải ra, các phần tử nửa trái chưa lấy đều lớn hơn và đứng trước nó, nên có đúng mid - i + 1 nghịch thế mới, với i là con trỏ nửa trái. Hai số bằng nhau thì lấy bên trái trước, nên chúng không bị tính.
Hỏi nhanh
Khi trộn, nửa trái là a[l..mid], nửa phải là a[mid+1..r], con trỏ nửa trái đang ở i. Ta vừa lấy a[j] của nửa phải ra trong khi nửa trái chưa hết. Cần cộng bao nhiêu vào số nghịch thế vì a[j]?
Các phần tử nửa trái chưa lấy là a[i], ..., a[mid], tức mid - i + 1 phần tử. Chúng đứng trước a[j] và đều lớn hơn nó, mỗi phần tử tạo một nghịch thế. r - j + 1 đếm phần tử nửa phải còn lại, j - mid đếm phần tử nửa phải đã lấy cộng một; cả hai không liên quan. mid - i thiếu đúng phần tử a[i] đang trỏ.
Còn câu hỏi cuối: có thuật toán nào nhanh hơn không? Với các thuật toán chỉ dựa vào so sánh thì không. Dãy phần tử khác nhau có thứ tự ban đầu, và mỗi thứ tự cần một cách sắp riêng. Mỗi phép so sánh chỉ trả lời “có” hoặc “không”, nên nhiều nhất chia đôi số khả năng còn lại; muốn phân biệt hết khả năng thì cần khoảng phép so sánh, mà con số này cỡ . Muốn nhanh hơn phải thôi so sánh, tức dùng thêm thông tin về giá trị.
Ví dụĐếm Cặp Ngược Thứ Tự
Đề. Dãy số nguyên, . Đếm số cặp với (bằng nhau không tính).
Lời giải. Sắp xếp trộn và cộng mid - i + 1 mỗi lần lấy phần tử nửa phải ra trước. Điều kiện lấy bên trái là a[i] <= a[j]: hai số bằng nhau lấy bên trái trước nên không bị đếm, đúng với dấu so sánh chặt của đề. Số nghịch thế tới , cần long long.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, a[N], tmp[N];
ll solve(ll l, ll r) {
if (l == r) return 0;
ll mid = (l + r) >> 1;
ll res = solve(l, mid) + solve(mid + 1, r);
ll i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) tmp[k++] = a[i++];
else {
res += mid - i + 1;
tmp[k++] = a[j++];
}
}
while (i <= mid) tmp[k++] = a[i++];
while (j <= r) tmp[k++] = a[j++];
for (ll x = l; x <= r; x++) a[x] = tmp[x];
return res;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> a[i];
cout << solve(1, n) << '\n';
}
4
3 1 2 1
4
Sắp xếp đếm
Có số nguyên trong . std::sort cần cỡ phép so sánh, trong khi chỉ có giá trị khác nhau. Ta chỉ cần biết mỗi giá trị xuất hiện mấy lần: bảng đếm cnt[v], rồi viết ra đúng cnt[v] lần theo thứ tự tăng của . Đó là sắp xếp đếm (counting sort), tốn khi giá trị nằm trong , không so sánh phần tử nào nên không vướng cận .
Sắp xếp đếm chỉ đi qua mảng hai lượt, mỗi lượt là phép cộng vào ô nhớ; std::sort phải so sánh và đổi chỗ cỡ lần, chậm hơn khoảng 30 lần.
Khi mỗi phần tử còn mang dữ liệu khác (tên, chỉ số) và ta sắp theo một khóa nguyên nhỏ, cần bản ổn định. Lấy tổng tiền tố của bảng đếm (bài 02) để biết khối của mỗi khóa bắt đầu ở đâu, rồi duyệt đầu vào từ trái sang phải, đặt mỗi phần tử vào chỗ trống đầu tiên của khối khóa của nó. Phần tử đến trước nhận vị trí nhỏ hơn nên thứ tự giữa các phần tử cùng khóa được giữ nguyên.
for (ll i = 1; i <= n; i++) st[key[i] + 1]++;
st[0] = 1;
for (ll v = 1; v <= V + 1; v++) st[v] += st[v - 1];
for (ll i = 1; i <= n; i++) ord[st[key[i]]++] = i;
Sau đoạn trên ord[1..n] là các chỉ số theo khóa tăng dần, cùng khóa thì giữ thứ tự nhập. Tính ổn định còn cho phép sắp nhiều khóa bằng nhiều lượt: sắp theo khóa phụ trước, rồi sắp ổn định theo khóa chính; áp dụng với các “chữ số” từ thấp lên cao ta được sắp xếp theo cơ số (radix sort).
Sắp xếp đếm đáng dùng khi cỡ trở xuống. Với giá trị tới , bảng đếm cần ô; khi đó dùng std::sort, hoặc nén tọa độ ở mục cuối nếu chỉ cần thứ tự.
Hỏi nhanh
Có số nguyên, mỗi số trong . Một bạn nói: “Sắp xếp đếm tốn , tuyến tính, nên nhanh hơn std::sort.” Nhận định này sai ở đâu?
chỉ là “tuyến tính” khi cỡ . Với ta phải khởi tạo và quét ô, trong khi . Ba đáp án kia sai: bản duyệt xuôi ổn định; số 0 là chỉ số hợp lệ; số lần trùng không ảnh hưởng thời gian.
Ví dụDãy Số Theo Thứ Tự Tăng
Đề. số nguyên dương , mỗi số một dòng. In dãy sau khi sắp không giảm, mỗi số một dòng.
Lời giải. Giá trị nhỏ còn rất lớn: đếm cnt[v] rồi in đúng cnt[v] lần, . Phần tính toán chỉ là vài dòng; phần chậm nhất lại là đọc và ghi, vì đầu vào cỡ 60 MB và đầu ra cũng vậy. Ngay cả cin đã tắt đồng bộ cũng mất cỡ một giây cho số, nên ta tự làm việc đọc ghi theo từng khối lớn. Lời giải gồm bốn phần.
Bước 1: đọc theo khối. fread chép một lần byte từ đầu vào vào mảng ibuf; gc() trả về từng ký tự trong khối đó và chỉ gọi fread lại khi đã dùng hết. Hết dữ liệu thì fread trả về 0 và gc() trả về ký tự 0.
char gc() {
if (ip == il) {
il = fread(ibuf, 1, sizeof ibuf, stdin);
ip = 0;
if (il == 0) return 0;
}
return ibuf[ip++];
}
Bước 2: tách số. read() bỏ qua mọi ký tự đứng trước '0' trong bảng mã, tức dấu cách và xuống dòng, rồi ghép các chữ số thành một số như ở bài B1 (nhân 10 rồi cộng chữ số mới). Đề chỉ có số dương nên không cần lo dấu trừ.
ll read() {
char c = gc();
while (c < '0') c = gc();
ll x = 0;
while (c >= '0') {
x = x * 10 + c - '0';
c = gc();
}
return x;
}
Bước 3: ghi qua bộ đệm. put(c) thêm một ký tự vào obuf; đầy thì đẩy cả khối ra bằng fwrite. Cuối main còn một lần fwrite cho phần dư.
void put(char c) {
if (op == sizeof obuf) {
fwrite(obuf, 1, op, stdout);
op = 0;
}
obuf[op++] = c;
}
Bước 4: đếm rồi in. Đọc số vào bảng đếm. Duyệt từ 1 tới , đổi thành xâu một lần bằng to_string, rồi ghi xâu đó cnt[v] lần. Thứ tự tăng của cho thứ tự không giảm của dãy.
n = read();
for (ll i = 1; i <= n; i++) cnt[read()]++;
for (ll v = 1; v <= 10000; v++) {
string s = to_string(v);
for (ll k = 1; k <= cnt[v]; k++) {
for (char c : s) put(c);
put('\n');
}
}
fwrite(obuf, 1, op, stdout);
Ghép bốn phần lại:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, cnt[10005];
char ibuf[1 << 16], obuf[1 << 16];
ll ip, il, op;
char gc() {
if (ip == il) {
il = fread(ibuf, 1, sizeof ibuf, stdin);
ip = 0;
if (il == 0) return 0;
}
return ibuf[ip++];
}
ll read() {
char c = gc();
while (c < '0') c = gc();
ll x = 0;
while (c >= '0') {
x = x * 10 + c - '0';
c = gc();
}
return x;
}
void put(char c) {
if (op == sizeof obuf) {
fwrite(obuf, 1, op, stdout);
op = 0;
}
obuf[op++] = c;
}
signed main() {
n = read();
for (ll i = 1; i <= n; i++) cnt[read()]++;
for (ll v = 1; v <= 10000; v++) {
string s = to_string(v);
for (ll k = 1; k <= cnt[v]; k++) {
for (char c : s) put(c);
put('\n');
}
}
fwrite(obuf, 1, op, stdout);
}
8
4
1
4
2
10000
2
1
7
1
1
2
2
4
4
7
10000
Mỗi số được đọc một lần và in một lần, cộng bước duyệt bảng đếm: thời gian, bộ nhớ ngoài hai bộ đệm.
Tự chọn thứ tự
Đề hiếm khi nói “hãy sắp”; ta phải tự chọn thứ tự, bằng hai cách.
Quy về khóa. Gán mỗi phần tử một giá trị dễ sắp sao cho sắp theo khóa đúng là thứ tự cần. Muốn sắp tên theo một bảng chữ cái khác, thay mỗi chữ bằng hạng của nó trong bảng mới. Muốn nhóm các chuỗi là hoán vị chữ cái của nhau, sắp chữ cái trong mỗi chuỗi: chúng cho ra cùng một chuỗi chuẩn.
So sánh cặp. Viết thẳng quy tắc “ đứng trước khi …”. Ví dụ ưu tiên tỉ lệ lớn: x.p * y.t > y.p * x.t. Nhân chéo thay cho phép chia nên không có số thực, vì so bằng double có thể coi hai tỉ lệ bằng nhau là khác nhau chút ít và phá tính bắc cầu. Nhớ ước lượng tích lớn nhất: cho tích tới , còn vừa long long.
Để chắc quy tắc so sánh cặp hợp lệ, hãy tìm hàm khóa sao cho cmp(x, y) đúng khi và chỉ khi (hoặc ). có thể là số thực ta không bao giờ tính ra; có nó thì bốn điều kiện tự đúng.
Còn làm sao biết thứ tự nào là đúng? Phương pháp quen thuộc là đổi chỗ hai phần tử kề nhau: lấy một phương án bất kỳ, xét hai phần tử đứng cạnh nhau và hỏi đổi chỗ thì tốt lên hay tệ đi. Ví dụ: việc, việc cần phút, làm lần lượt; chọn thứ tự để tổng thời điểm hoàn thành nhỏ nhất. Nếu hai việc kề nhau rồi có và cùng bắt đầu lúc , tổng thời điểm xong của chúng là ; đổi chỗ thì là , nhỏ hơn đúng , các việc khác không đổi. Vậy phương án tối ưu không có cặp kề nào nghịch thế: tăng dần. Khuôn lập luận này quay lại ở bài 04.
Ví dụGhép Thành Số Lớn Nhất
Đề. thẻ, mỗi thẻ ghi số nguyên dương . Xếp thành một hàng rồi đọc liền các số để được số lớn nhất, in số đó.
Lời giải. Mọi cách xếp cho chuỗi cùng độ dài, nên so hai kết quả là so hai chuỗi. Ký hiệu là số thu được khi viết rồi . Với hai thẻ kề nhau, phần đứng trước và sau chúng không đổi khi hoán đổi, nên đổi chỗ có lợi khi và chỉ khi . Sắp theo quy tắc “ đứng trước khi ”.
Quy tắc này hợp lệ vì có hàm khóa. Gọi , là số chữ số của , ; khi đó và , nên
Vậy quy tắc chính là “khóa lớn hơn thì đứng trước”, với là số gồm chữ số 9. Thứ tự đó cho kết quả lớn nhất: một cách xếp tối ưu không thể có cặp kề nào đổi chỗ được lợi, nên khóa không tăng từ trái sang phải.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n;
string s[10005];
bool cmp(string x, string y) {
return x + y > y + x;
}
signed main() {
cin >> n;
for (ll i = 1; i <= n; i++) cin >> s[i];
sort(s + 1, s + n + 1, cmp);
for (ll i = 1; i <= n; i++) cout << s[i];
cout << '\n';
}
5
9 91 34 3 30
99134330
Nén tọa độ
Nhiều bài cho giá trị cỡ nhưng chỉ cần biết cái nào nhỏ hơn, cái nào bằng nhau, trong khi ta lại muốn dùng giá trị làm chỉ số của mảng đếm hay cây Fenwick (bài 16). Khi đó thay mỗi giá trị bằng thứ hạng của nó trong tập giá trị phân biệt: nén tọa độ (coordinate compression). Phép thay giữ nguyên mọi quan hệ và , nên bài chỉ dựa trên so sánh có cùng đáp án.
Ba bước: chép dãy ra, sắp, bỏ trùng bằng unique; thứ hạng của là vị trí của nó trong dãy đã bỏ trùng, tìm bằng lower_bound trong (bài 05).
for (ll i = 1; i <= n; i++) v[i] = a[i];
sort(v + 1, v + n + 1);
ll m = unique(v + 1, v + n + 1) - v - 1;
for (ll i = 1; i <= n; i++) a[i] = lower_bound(v + 1, v + m + 1, a[i]) - v;
Sau đoạn trên các giá trị của nằm trong . Với , các giá trị phân biệt nhận hạng và thành .
Hỏi nhanh
Mảng v = {5, 3, 5, 3} được gọi unique mà không sắp trước. Bốn phần tử đầu của v sau đó thế nào?
unique chỉ bỏ các phần tử bằng nhau mà đứng kề nhau: nó so mỗi phần tử với phần tử ngay trước. Ở đây không có hai phần tử kề nào bằng nhau nên dãy giữ nguyên. Muốn bỏ mọi giá trị trùng thì phải sắp trước, để các giá trị bằng nhau tụ thành khối. unique cũng không bao giờ đổi thứ tự phần tử.
Khi đi thi
- Trước khi viết hai vòng lặp trên mọi cặp, hãy hỏi: nếu dãy đã sắp theo một khóa nào đó thì đáp án có nằm ở hai phần tử kề nhau, ở một cửa sổ liên tiếp hay ở ranh giới giữa hai khối không? Nếu có thì sắp rồi quét một lượt.
- Hàm so sánh dùng
<hoặc>chặt, so từng khóa, thêm chỉ số gốc ở cuối khi đề cần thứ tự duy nhất. Runtime Error chỉ ở test có nhiều giá trị bằng nhau thì nghĩ tới<=trước tiên. So tỉ lệ bằng nhân chéo trênlong long. - Đọc miền giá trị trước khi chọn công cụ: miền nhỏ thì mảng đếm, miền lớn mà chỉ cần thứ tự thì nén tọa độ. Số nghịch thế của phần tử tới nên dùng
long long.
Tóm tắt
- Sắp xếp tốn và biến nhiều bài thành một lượt quét: cặp kề, cửa sổ liên tiếp, khối giá trị bằng nhau.
- Hàm so sánh của
std::sortphải là thứ tự yếu chặt:cmp(x, x)sai, so khóa bằng<chặt, thêm chỉ số để phân định,stable_sortkhi cần giữ thứ tự nhập. - Sắp xếp trộn tốn và đếm nghịch thế bằng
mid - i + 1mỗi lần lấy phần tử nửa phải; mọi thuật toán chỉ so sánh cần ít nhất lần so sánh. - Sắp xếp đếm tốn khi giá trị trong ; giá trị lớn mà chỉ cần thứ tự thì nén tọa độ bằng
sort,unique,lower_bound. - Một thứ tự đúng được chứng minh bằng đổi chỗ hai phần tử kề nhau, và quan hệ “đứng trước” phải xuất phát từ một hàm khóa thì mới hợp lệ.
Bài tập
Đọc thêm
- Thuật toán sắp xếpVNOI Wiki
- Competitive Programmer’s Handbook, chương 3
- Custom Comparators and Coordinate CompressionUSACO Guide
- std::sortcppreference
- 归并排序OI Wiki