Chương 2·Bài 24

Sqrt Decomposition

Cho một dãy, hỏi tổng một đoạn, và đôi khi sửa một phần tử. Tổng tiền tố không chịu được việc sửa, còn cây phân đoạn (bài 17) thì dài dòng cho những bài lạ.

Ta chia dãy nn phần tử thành các khối dài khoảng n\sqrt n, vậy có khoảng n\sqrt n khối. Mọi đoạn đều gồm vài phần tử lẻ ở hai đầu và một dãy khối nguyên ở giữa: phần tử lẻ xử lý từng cái, khối nguyên dùng thông tin đã gộp sẵn. Mỗi thao tác tốn O(n)O(\sqrt n), chậm hơn cây phân đoạn, nhưng khối giữ được cả những thứ không gộp bằng phép kết hợp: một mảng đã sắp, một bảng đếm, một con trỏ nhảy.

Cùng ý tưởng ấy còn một dạng khác: chọn ngưỡng SS, xử lý phần “nhỏ” bằng bảng tính sẵn và phần “lớn” bằng duyệt thẳng, để cả hai phía đều tốn O(n)O(\sqrt n).

Khối

Chọn độ dài khối BB, đánh số từ 1 và đặt id[i] = (i - 1) / B. Mỗi khối kk giữ một giá trị gộp, chẳng hạn tổng s[k]. Câu hỏi [l,r][l, r] nằm trong một khối thì duyệt thẳng; ngược lại tách thành mảnh trái từ ll tới hết khối của ll, các khối nguyên ở giữa, và mảnh phải từ đầu khối của rr tới rr. Hai mảnh có không quá 2B2B phần tử, giữa có không quá n/Bn / B khối, nên mỗi câu hỏi tốn O(B+n/B)O(B + n / B).

Tổng hai số hạng nhỏ nhất khi B=nB = \sqrt n, còn 2n2\sqrt n. Với n=2⋅105n = 2 \cdot 10^5 là khoảng 900 phép mỗi câu.

Ô cam là hai mảnh biên, ô xanh thuộc khối nguyên. Thử cộng c vào một đoạn: khối nguyên chỉ nhận nhãn.

Gán ap=xa_p = x chỉ làm đổi tổng một khối: s[id[p]] += x - a[p], O(1)O(1). Fenwick làm được việc này nhanh hơn, nhưng khung khối là thứ ta dùng lại ở mọi mục sau.

Hỏi nhanh

Dãy dài n=106n = 10^6, chọn khối dài B=100B = 100. Một câu hỏi tổng đoạn tốn khoảng bao nhiêu phép trong trường hợp xấu nhất?

Ví dụTổng Đoạn Linh Hoạt

Đề. Dãy n≤2⋅105n \le 2 \cdot 10^5 số, q≤2⋅105q \le 2 \cdot 10^5 thao tác: gán ap=xa_p = x, hoặc in tổng al+⋯+ara_l + \dots + a_r.

Lời giải. Chia dãy thành các khối dài B=⌊n⌋B = \lfloor \sqrt n \rfloor và giữ tổng của mỗi khối. Một đoạn bất kỳ gồm tối đa hai mảnh biên lẻ và một dãy khối nằm trọn, mỗi phần tối đa n\sqrt n bước.

Bước 1: chia khối. id[i] = (i - 1) / B là khối của vị trí ii; s[k] là tổng khối kk, cộng dồn ngay khi đọc.

C++
B = max(1LL, (ll)sqrt(n));
for (ll i = 1; i <= n; i++) {
    cin >> a[i];
    id[i] = (i - 1) / B;
    s[id[i]] += a[i];
}

Bước 2: tổng một đoạn. Cùng khối thì cộng thẳng. Khác khối thì ba phần: vòng đầu chạy từ ll tới hết khối của ll, vòng giữa cộng tổng các khối nằm trọn, vòng cuối chạy lùi từ rr tới đầu khối của rr.

C++
ll get(ll l, ll r) {
    ll res = 0;
    if (id[l] == id[r]) {
        for (ll i = l; i <= r; i++) res += a[i];
        return res;
    }
    for (ll i = l; id[i] == id[l]; i++) res += a[i];
    for (ll k = id[l] + 1; k < id[r]; k++) res += s[k];
    for (ll i = r; id[i] == id[r]; i--) res += a[i];
    return res;
}

Bước 3: gán một phần tử. Sửa tổng khối bằng phần chênh y - a[x] rồi mới ghi giá trị mới, O(1)O(1).

C++
s[id[x]] += y - a[x];
a[x] = y;

Tổng có thể tới 2⋅10142 \cdot 10^{14}, phải dùng ll.

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

const ll N = 2e5 + 5;
ll n, q, B, a[N], id[N], s[N];

ll get(ll l, ll r) {
    ll res = 0;
    if (id[l] == id[r]) {
        for (ll i = l; i <= r; i++) res += a[i];
        return res;
    }
    for (ll i = l; id[i] == id[l]; i++) res += a[i];
    for (ll k = id[l] + 1; k < id[r]; k++) res += s[k];
    for (ll i = r; id[i] == id[r]; i--) res += a[i];
    return res;
}

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

    cin >> n >> q;
    B = max(1LL, (ll)sqrt(n));
    for (ll i = 1; i <= n; i++) {
        cin >> a[i];
        id[i] = (i - 1) / B;
        s[id[i]] += a[i];
    }
    while (q--) {
        ll t, x, y;
        cin >> t >> x >> y;
        if (t == 1) {
            s[id[x]] += y - a[x];
            a[x] = y;
        } else cout << get(x, y) << '\n';
    }
}
5 5
2 1 3 4 5
2 2 5
1 3 10
2 1 3
1 5 6
2 4 5

Độ phức tạp: gán O(1)O(1), câu hỏi O(n)O(\sqrt n).

Chọn BB đúng thật sự quan trọng:

200 nghìn câu hỏi tổng đoạn, dãy 200 nghìn phần tử, trung vị 5 lần chạy, i7-10850H, g++ -O2.khối dài 450 (căn n)74 mskhối dài 10611 ms · gấp 8,2 lầnkhối dài 200002080 ms · gấp 28 lần
200 nghìn câu hỏi tổng đoạn trên dãy 200 nghìn phần tử, đo trên i7-10850H, g++ -O2.

Khối quá ngắn chậm gấp khoảng 8 lần, khối quá dài chậm gấp khoảng 27 lần so với B=nB = \sqrt n. Chọn BB cho hai phía bằng nhau.

Nhãn khối

Cộng cc vào cả một đoạn cũng dùng cách tách ấy. Khối nguyên chỉ ghi vào nhãn chung tag[k], nghĩa là “mọi phần tử của khối còn phải cộng thêm tag[k]”. Phần tử ở hai mảnh biên sửa trực tiếp. Giá trị thật của aia_i là a[i] + tag[id[i]], và mỗi cập nhật cũng O(B+n/B)O(B + n / B).

Nhãn có thể là phép cộng, phép gán, hay cặp (nhân, cộng) cho hàm x↦bx+cx \mapsto bx + c. Khi sửa lẻ một phần tử của khối đang mang nhãn không giao hoán với phép sửa, áp nhãn xuống cả khối trước.

Khối giữ dữ liệu đã sắp. Để đếm trên đoạn số phần tử nhỏ hơn xx, mỗi khối giữ thêm một bản sao đã sắp các giá trị của nó. Cộng cả khối không đổi thứ tự trong khối, nên chỉ cần tăng nhãn; khối nguyên trả lời bằng một lần lower_bound với x−tagx - \mathrm{tag}. Mảnh biên bị sửa thì sắp lại khối đó, tốn O(Blog⁡B)O(B \log B). Phần giữa tốn (n/B)log⁡B(n / B) \log B cho các lần chặt nhị phân, nên hai phía vẫn cân ở B=nB = \sqrt n. Nếu phần giữa chỉ tốn n/Bn / B thì nên chọn B=n/log⁡nB = \sqrt{n / \log n}. Ước lượng hai phía rồi chọn BB cho chúng bằng nhau.

Ví dụBao Nhiêu Số Bé Hơn

Đề. Dãy n≤5⋅104n \le 5 \cdot 10^4 số và nn thao tác: cộng cc vào đoạn [l,r][l, r], hoặc đếm trên [l,r][l, r] số phần tử nhỏ hơn hẳn c2c^2.

Lời giải. Mỗi khối giữ nhãn cộng tag[k] và vector v[k] các giá trị gốc của khối đã sắp (chưa cộng nhãn). Cộng vào cả khối không làm đổi thứ tự trong khối, nên chỉ tăng nhãn; chỉ mảnh biên mới làm thứ tự đổi và phải sắp lại. Phần tử của khối nằm trọn nhỏ hơn ww khi giá trị gốc nhỏ hơn w−tagw - \mathrm{tag}, đếm bằng một lần lower_bound. cc tới 2312^{31} nên c2c^2 tới 2622^{62}, vẫn vừa ll.

Bước 1: dựng lại một khối. L[k], R[k] là vị trí đầu, cuối của khối; rebuild chép các giá trị gốc rồi sắp, O(Blog⁡B)O(B \log B).

C++
void rebuild(ll k) {
    v[k].clear();
    for (ll i = L[k]; i <= R[k]; i++) v[k].push_back(a[i]);
    sort(v[k].begin(), v[k].end());
}

Bước 2: mảnh biên. add chỉ được gọi cho một mảnh nằm trong một khối: sửa a[i] rồi dựng lại khối đó. calc đếm thẳng trên mảnh biên, so giá trị thật a[i] + tag với ngưỡng.

C++
void add(ll l, ll r, ll c) {
    for (ll i = l; i <= r; i++) a[i] += c;
    rebuild(id[l]);
}

ll calc(ll l, ll r, ll x) {
    ll res = 0;
    for (ll i = l; i <= r; i++) res += (a[i] + tag[id[i]] < x);
    return res;
}

Bước 3: hai loại thao tác. Cộng: hai mảnh biên qua add, khối giữa tăng tag. Đếm: hai mảnh biên qua calc, khối giữa đếm bằng lower_bound trên v[k] với ngưỡng đã trừ nhãn.

C++
for (ll k = id[x] + 1; k < id[y]; k++)
    res += lower_bound(v[k].begin(), v[k].end(), w - tag[k]) - v[k].begin();
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 5e4 + 5;
ll n, B, a[N], id[N], L[N], R[N], tag[N];
vector<ll> v[N];

void rebuild(ll k) {
    v[k].clear();
    for (ll i = L[k]; i <= R[k]; i++) v[k].push_back(a[i]);
    sort(v[k].begin(), v[k].end());
}

void add(ll l, ll r, ll c) {
    for (ll i = l; i <= r; i++) a[i] += c;
    rebuild(id[l]);
}

ll calc(ll l, ll r, ll x) {
    ll res = 0;
    for (ll i = l; i <= r; i++) res += (a[i] + tag[id[i]] < x);
    return res;
}

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

    cin >> n;
    B = max(1LL, (ll)sqrt(n));
    for (ll i = 1; i <= n; i++) {
        cin >> a[i];
        id[i] = (i - 1) / B;
        R[id[i]] = i;
        if (!L[id[i]]) L[id[i]] = i;
    }
    for (ll k = 0; k <= id[n]; k++) rebuild(k);
    for (ll t = 1; t <= n; t++) {
        ll op, l, r, c;
        cin >> op >> l >> r >> c;
        ll x = l, y = r;
        if (op == 0) {
            if (id[x] == id[y]) add(x, y, c);
            else {
                add(x, R[id[x]], c);
                for (ll k = id[x] + 1; k < id[y]; k++) tag[k] += c;
                add(L[id[y]], y, c);
            }
            continue;
        }
        ll w = c * c, res;
        if (id[x] == id[y]) res = calc(x, y, w);
        else {
            res = calc(x, R[id[x]], w) + calc(L[id[y]], y, w);
            for (ll k = id[x] + 1; k < id[y]; k++)
                res += lower_bound(v[k].begin(), v[k].end(), w - tag[k]) - v[k].begin();
        }
        cout << res << '\n';
    }
}
5
1 4 2 8 3
1 1 5 2
0 2 4 -2
1 1 4 1
1 2 5 3
1 3 3 0

Độ phức tạp: mỗi thao tác O(nlog⁡n)O(\sqrt n \log n): hai lần dựng lại khối hoặc n\sqrt n lần chặt nhị phân.

Ngưỡng

Nhiều bài có một tham số kk trong mỗi câu hỏi (bước nhảy, modulo, kích thước tập), và cách giải tốt tùy kk lớn hay nhỏ.

  • k>Sk > S: đối tượng liên quan chỉ có cỡ n/k<n/Sn / k < n / S phần tử, nên duyệt thẳng.
  • k≤Sk \le S: chỉ có SS giá trị khác nhau, nên tính sẵn một bảng cho từng giá trị, hoặc gom các câu hỏi cùng kk lại xử lý một lượt.

Chọn S=nS = \sqrt n thì cả hai phía đều O(n)O(\sqrt n) mỗi thao tác, tổng cộng O(nn)O(n \sqrt n).

Ô cam: x lớn, duyệt thẳng các chỉ số i chia x dư y. Ô xanh: x nhỏ, đáp án đọc thẳng từ bảng tính sẵn.

Hỏi nhanh

Câu hỏi “tổng các aia_i với ii chia xx dư yy” trên dãy dài nn. Vì sao với x>nx > \sqrt n có thể duyệt thẳng?

Ví dụTổng Theo Số Dư

Đề. Mảng 5⋅1055 \cdot 10^5 phần tử ban đầu bằng 0, q≤5⋅105q \le 5 \cdot 10^5 thao tác: cộng yy vào axa_x, hoặc in tổng các aia_i với i mod x=yi \bmod x = y.

Lời giải. Chia câu hỏi theo modulo xx tại ngưỡng S≈5⋅105≈700S \approx \sqrt{5 \cdot 10^5} \approx 700. Modulo nhỏ thì có ít, nên giữ sẵn đáp án cho mọi cặp (d,r)(d, r) với d≤Sd \le S; modulo lớn thì dãy y,y+x,…y, y + x, \dots chỉ có ít số, cộng thẳng.

Bước 1: cộng vào axa_x. Sửa axa_x và SS ô của bảng: với mỗi d≤Sd \le S, phần tử xx thuộc nhóm dư x % d.

C++
a[x] += y;
for (int d = 1; d <= S; d++) sm[d][x % d] += y;

Bước 2: câu hỏi. x≤Sx \le S thì đọc thẳng sm[x][y]; x>Sx > S thì cộng các aia_i với i=y,y+x,…i = y, y + x, \dots, không quá 5⋅105/700≈7005 \cdot 10^5 / 700 \approx 700 số.

C++
else if (x <= S) cout << sm[x][y] << '\n';
else {
    ll res = 0;
    for (ll i = y; i <= M; i += x) res += a[i];
    cout << res << '\n';
}

Biến vòng d, x, y khai báo int để phép chia lấy dư là phép 32 bit. Vòng trong chạy 3,5⋅1083{,}5 \cdot 10^8 lần nên chênh lệch lộ rõ:

Cập nhật bảng dư 500 nghìn lần, mỗi lần 700 phép chia lấy dư, trung vị 5 lần chạy, i7-10850H, g++ -O2.x, d kiểu int1289 msx, d kiểu long long3935 ms · gấp 3,1 lần
Cập nhật bảng dư 500 nghìn lần, đo trên i7-10850H, g++ -O2.

Dùng long long chậm gấp khoảng 3 lần. Vòng lặp nóng có phép chia lấy dư thì ưu tiên int.

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

const ll M = 5e5, S = 700;
ll q, a[M + 5], sm[S + 1][S + 1];

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

    cin >> q;
    while (q--) {
        int t, x, y;
        cin >> t >> x >> y;
        if (t == 1) {
            a[x] += y;
            for (int d = 1; d <= S; d++) sm[d][x % d] += y;
        } else if (x <= S) cout << sm[x][y] << '\n';
        else {
            ll res = 0;
            for (ll i = y; i <= M; i += x) res += a[i];
            cout << res << '\n';
        }
    }
}
7
1 3 5
1 8 4
1 10 -2
2 5 3
2 4 0
1 8 -1
2 2 0

Ước lượng phép tính: mỗi thao tác khoảng S≈700S \approx 700 phép.

Nhảy khỏi khối

Mỗi ô ii chỉ sang một ô i+aii + a_i ở bên phải, và aia_i có thể bị sửa. Mô phỏng thẳng một đường nhảy tốn tới nn bước. Chia dãy thành khối và với mỗi ô lưu ba thứ: nxt[i] là ô đầu tiên ngoài khối của ii mà đường đi từ ii tới, cnt[i] là số bước để tới đó, lst[i] là ô cuối cùng trong khối trước khi rời. Chúng tính được từ phải sang trong khối: nếu i+aii + a_i còn trong khối thì kế thừa từ ô đó, ngược lại ii tự rời khối sau một bước.

Một câu hỏi đi theo nxt, mỗi bước qua ít nhất một khối, nên tốn O(n/B)O(n / B). Sửa apa_p chỉ ảnh hưởng các ô cùng khối từ pp về trái, tính lại O(B)O(B).

Vạch đứt chia khối; dưới mỗi ô là ô đầu tiên ngoài khối và số bước để tới đó.

Ví dụNhững Bước Nhảy Thoát Mảng

Đề. n≤105n \le 10^5 ô, ô ii có ai≥1a_i \ge 1, đứng ở ii thì nhảy tới i+aii + a_i, dừng khi vượt nn. m≤105m \le 10^5 thao tác: gán ap=xa_p = x, hoặc bắt đầu ở pp và in ô cuối cùng còn trong mảng cùng tổng số bước nhảy.

Lời giải. Chia ô thành khối dài n\sqrt n. Với mỗi ô ii giữ ba thứ: nxt[i] là ô đầu tiên ngoài khối mà đường nhảy từ ii tới, cnt[i] là số bước để tới đó, lst[i] là ô cuối cùng trong khối trên đường đi. Câu hỏi đi theo nxt, mỗi bước qua ít nhất một khối. Gán apa_p chỉ ảnh hưởng các ô cùng khối và đứng trước pp. Với ví dụ, khối dài 2: từ ô 1 đường đi 1→3→4→6→101 \to 3 \to 4 \to 6 \to 10 qua ba lần nhảy theo nxt.

Bước 1: tính một ô. Nhảy một bước tới j=i+aij = i + a_i. Ra ngoài khối (hoặc ngoài mảng) thì ii rời khối sau 1 bước và ô cuối trong khối là chính ii. Còn trong khối thì kế thừa từ jj, cộng thêm 1 bước.

C++
void calc(ll i) {
    ll j = i + a[i];
    if (j > n || id[j] != id[i]) {
        nxt[i] = j;
        cnt[i] = 1;
        lst[i] = i;
    } else {
        nxt[i] = nxt[j];
        cnt[i] = cnt[j] + 1;
        lst[i] = lst[j];
    }
}

Bước 2: dựng và sửa. Dựng ban đầu duyệt ii từ nn về 1, nên j>ij > i luôn đã tính. Gán apa_p thì tính lại từ pp về đầu khối, O(B)O(B).

C++
for (ll i = n; i >= 1; i--) calc(i);
C++
cin >> a[p];
for (ll i = p; i >= L[id[p]]; i--) calc(i);

Bước 3: câu hỏi. Cộng cnt, nhớ lst của bước cuối để in ô cuối cùng còn trong mảng.

C++
ll steps = 0, last = p;
while (p <= n) {
    steps += cnt[p];
    last = lst[p];
    p = nxt[p];
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1e5 + 5;
ll n, m, B, a[N], id[N], L[N], nxt[N], cnt[N], lst[N];

void calc(ll i) {
    ll j = i + a[i];
    if (j > n || id[j] != id[i]) {
        nxt[i] = j;
        cnt[i] = 1;
        lst[i] = i;
    } else {
        nxt[i] = nxt[j];
        cnt[i] = cnt[j] + 1;
        lst[i] = lst[j];
    }
}

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

    cin >> n >> m;
    B = max(1LL, (ll)sqrt(n));
    for (ll i = 1; i <= n; i++) {
        cin >> a[i];
        id[i] = (i - 1) / B;
        if (!L[id[i]]) L[id[i]] = i;
    }
    for (ll i = n; i >= 1; i--) calc(i);
    while (m--) {
        ll t, p;
        cin >> t >> p;
        if (t == 0) {
            cin >> a[p];
            for (ll i = p; i >= L[id[p]]; i--) calc(i);
            continue;
        }
        ll steps = 0, last = p;
        while (p <= n) {
            steps += cnt[p];
            last = lst[p];
            p = nxt[p];
        }
        cout << last << ' ' << steps << '\n';
    }
}
6 5
2 3 1 2 1 4
1 1
0 3 4
1 1
1 2
0 6 1

Độ phức tạp: gán O(n)O(\sqrt n), câu hỏi O(n)O(\sqrt n) vì qua tối đa n\sqrt n khối.

Đỉnh nặng, đỉnh nhẹ

Mục này dùng vài chữ của chương 3: bậc của một đỉnh là số cạnh nối với nó. Mỗi cạnh góp 1 vào bậc của cả hai đầu, nên tổng mọi bậc là 2m2m. Vì vậy đồ thị mm cạnh có không quá 2m/S2m / S đỉnh bậc lớn hơn SS.

Gọi chúng là đỉnh nặng, còn lại là đỉnh nhẹ. Duyệt hàng xóm của một đỉnh nhẹ tốn không quá SS. Đỉnh nặng thì ít, có thể giữ cho mỗi đỉnh nặng một bảng hay một bộ đếm riêng. Với S=mS = \sqrt m cả hai phía đều O(m)O(\sqrt m).

Một cách dùng gọn: hướng mỗi cạnh từ đầu có bậc nhỏ hơn sang đầu có bậc lớn hơn (bằng nhau thì theo chỉ số). Đồ thị thành không có chu trình, và mỗi đỉnh có không quá 2m\sqrt{2m} cạnh đi ra: một đỉnh có dd cạnh đi ra thì cả dd hàng xóm đó có bậc ít nhất bằng nó, tức ít nhất dd, nên d⋅d≤2md \cdot d \le 2m.

Ví dụTam Giác Trong Mạng Lưới

Đề. Đồ thị đơn n≤105n \le 10^5 đỉnh, m≤2⋅105m \le 2 \cdot 10^5 cạnh. Đếm số tam giác.

Lời giải. Hướng mỗi cạnh từ đầu có bậc nhỏ hơn sang đầu bậc lớn hơn (bằng nhau thì theo chỉ số); đồ thị hướng này không có chu trình. Mỗi tam giác có đúng một đỉnh mà cả hai cạnh của tam giác đều đi ra từ nó, nên được đếm đúng một lần ở đỉnh đó. Mỗi đỉnh có không quá 2m\sqrt{2m} cạnh đi ra: đỉnh có dd cạnh ra thì mỗi đỉnh nó đi tới có bậc ít nhất dd, nên d⋅d≤2md \cdot d \le 2m.

Bước 1: đếm bậc rồi hướng cạnh.

C++
for (ll i = 1; i <= m; i++) {
    ll u = eu[i], v = ev[i];
    if (deg[u] > deg[v] || (deg[u] == deg[v] && u > v)) swap(u, v);
    g[u].push_back(v);
}

Bước 2: đếm tam giác ở đỉnh có hai cạnh ra. Đánh dấu các đỉnh uu đi tới bằng mark[v] = u; dùng chính uu làm dấu nên khỏi xóa mảng giữa các lượt. Với mỗi vv mà uu đi tới, mỗi ww mà vv đi tới và đã được đánh dấu là một tam giác (u,v,w)(u, v, w).

C++
for (ll u = 1; u <= n; u++) {
    for (ll v : g[u]) mark[v] = u;
    for (ll v : g[u])
        for (ll w : g[v])
            if (mark[w] == u) ans++;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1e5 + 5, M = 2e5 + 5;
ll n, m, deg[N], eu[M], ev[M], mark[N];
vector<ll> g[N];

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

    cin >> n >> m;
    for (ll i = 1; i <= m; i++) {
        cin >> eu[i] >> ev[i];
        deg[eu[i]]++;
        deg[ev[i]]++;
    }
    for (ll i = 1; i <= m; i++) {
        ll u = eu[i], v = ev[i];
        if (deg[u] > deg[v] || (deg[u] == deg[v] && u > v)) swap(u, v);
        g[u].push_back(v);
    }
    ll ans = 0;
    for (ll u = 1; u <= n; u++) {
        for (ll v : g[u]) mark[v] = u;
        for (ll v : g[u])
            for (ll w : g[v])
                if (mark[w] == u) ans++;
    }
    cout << ans << '\n';
}
5 6
1 2
2 3
1 3
3 4
4 5
3 5

Độ phức tạp: mỗi cạnh u→vu \to v tốn số cạnh ra của vv, không quá 2m\sqrt{2m}: tổng O(mm)O(m \sqrt m), khoảng 10810^8 phép đơn giản.

Khi đi thi

  • n,qn, q cỡ 10510^5 và khối cần giữ thứ không gộp được bằng một phép kết hợp (mảng đã sắp, bảng đếm, con trỏ nhảy): chia căn; nn≈3⋅107n \sqrt n \approx 3 \cdot 10^7 là thoải mái, 2⋅1052 \cdot 10^5 với hằng số lớn thì nên thử nhanh trước.
  • Viết riêng trường hợp l,rl, r cùng khối, và giữ một quy ước chỉ số duy nhất cho id, đầu khối, cuối khối.
  • Đếm chi phí hai phía với BB bất kỳ rồi chọn BB cho chúng bằng nhau; hằng số thực tế khác nhau nên thử vài giá trị quanh mức cân bằng nếu sát giới hạn thời gian.

Tóm tắt

  • Khối dài BB: đoạn bất kỳ là hai mảnh biên dài không quá BB và không quá n/Bn / B khối nguyên; B=nB = \sqrt n cho O(n)O(\sqrt n).
  • Nhãn khối cho cập nhật đoạn; giá trị thật là giá trị lưu cộng nhãn.
  • Khối giữ dữ liệu đã sắp để đếm theo ngưỡng; sửa mảnh biên thì dựng lại khối.
  • Ngưỡng SS: tham số nhỏ dùng bảng hay gom câu hỏi, tham số lớn duyệt thẳng.
  • Con trỏ nhảy ra khỏi khối biến đường đi dài nn thành n/Bn / B bước, cập nhật O(B)O(B).
  • Trên đồ thị: ít đỉnh nặng, đỉnh nhẹ có ít hàng xóm; hướng cạnh theo bậc cho mỗi đỉnh không quá 2m\sqrt{2m} cạnh ra.

Bài tập

Đọc thêm