Chia dãy phần tử thành các khối dài khoảng thì có khoảng khối. Mọi đoạn đều gồm một ít 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 : chậm hơn cây phân đoạn (bài 17), nhưng khối có thể giữ những thứ không gộp được bằng một phép kết hợp, như một mảng đã sắp, một bảng đếm hay một con trỏ nhảy.
Cùng ý tưởng đó còn có dạng khác: chọn một ngưỡng , xử lý phần “nhỏ” bằng bảng tính sẵn và phần “lớn” bằng duyệt thẳng, sao cho cả hai phía đều tốn .
Khối và hai loại đoạn
Chọn độ dài khối , đánh số từ 1 và đặt id[i] = (i - 1) / B. Mỗi khối giữ một giá trị gộp, chẳng hạn tổng s[k]. Câu hỏi 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ừ tới hết khối của , các khối nguyên ở giữa, và mảnh phải từ đầu khối của tới . Hai mảnh có không quá phần tử, giữa có không quá khối, nên mỗi câu hỏi tốn . Tổng hai số hạng nhỏ nhất khi , còn ; với là khoảng 900 phép mỗi câu.
Gán chỉ làm đổi tổng một khối: s[id[p]] += x - a[p], . Tổng tiền tố không làm được việc này, Fenwick làm được nhanh hơn, nhưng khung khối là thứ dùng lại cho mọi mục sau.
Hỏi nhanh
Dãy dài , chọn khối dài . 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?
Hai mảnh biên tốn tới phép, nhưng giữa có tới khối. Khối quá ngắn làm phần giữa trội lên; chọn thì tổng chỉ khoảng 3000 phép.
Ví dụTổng Đoạn Linh Hoạt
Đề. Dãy số, thao tác: gán , hoặc in tổng .
Lời giải. Chia dãy thành các khối dài 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 bước.
Bước 1: chia khối. id[i] = (i - 1) / B là khối của vị trí ; s[k] là tổng khối , cộng dồn ngay khi đọ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ừ tới hết khối của , 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ừ tới đầu khối của .
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, .
s[id[x]] += y - a[x];
a[x] = y;
Tổng có thể tới , phải dùng ll.
#include <bits/stdc++.h>
using namespace std;
using 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
13
13
10
Gán , câu hỏi .
Nhãn cho cả khối
Cập nhật trên đoạn dùng cùng cách tách. 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 là a[i] + tag[id[i]]. Mỗi cập nhật cũng . Nhãn có thể là phép cộng, phép gán, hay cặp (nhân, cộng) cho hàm bậc nhất ; 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 , 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 . Mảnh biên bị sửa thì sắp lại khối đó, tốn . Lúc này hai phía không còn cân: mảnh biên tốn , phần giữa tốn , vẫn cân ở ; nhưng nếu mảnh biên tốn còn phần giữa chỉ thì nên chọn . Ước lượng hai phía rồi chọn cho chúng bằng nhau.
Ví dụDấu Cộng Trên Đoạn
Đề. Dãy số và thao tác: cộng vào mọi phần tử trên , hoặc in giá trị hiện tại của (hai trường còn lại bị bỏ qua).
Lời giải. Đây là khung tối thiểu của nhãn khối; các bài sau chỉ thay nội dung của nhãn và của dữ liệu trong khối. tag[k] là lượng còn phải cộng cho mọi phần tử của khối , nên giá trị thật của phần tử là a[i] + tag[id[i]].
Bước 1: cộng một đoạn. Mảnh biên sửa thẳng a[i]; khối nằm trọn chỉ tăng tag. Cùng khối thì chỉ có mảnh biên.
if (id[l] == id[r]) {
for (ll i = l; i <= r; i++) a[i] += c;
continue;
}
for (ll i = l; id[i] == id[l]; i++) a[i] += c;
for (ll k = id[l] + 1; k < id[r]; k++) tag[k] += c;
for (ll i = r; id[i] == id[r]; i--) a[i] += c;
Bước 2: hỏi một phần tử. Cộng nhãn của khối chứa nó.
cout << a[r] + tag[id[r]] << '\n';
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 5e4 + 5;
ll n, B, a[N], id[N], tag[N];
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;
}
for (ll t = 1; t <= n; t++) {
ll op, l, r, c;
cin >> op >> l >> r >> c;
if (op == 1) {
cout << a[r] + tag[id[r]] << '\n';
continue;
}
if (id[l] == id[r]) {
for (ll i = l; i <= r; i++) a[i] += c;
continue;
}
for (ll i = l; id[i] == id[l]; i++) a[i] += c;
for (ll k = id[l] + 1; k < id[r]; k++) tag[k] += c;
for (ll i = r; id[i] == id[r]; i--) a[i] += c;
}
}
5
1 2 3 4 5
0 2 4 3
1 99 3 0
0 1 5 -2
1 -7 1 123
1 0 5 0
6
-1
3
Cộng đoạn , hỏi .
Ví dụBao Nhiêu Số Bé Hơn
Đề. Dãy số và thao tác: cộng vào đoạn , hoặc đếm trên số phần tử nhỏ hơn hẳn .
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 khi giá trị gốc nhỏ hơn , đếm bằng một lần lower_bound. tới nên tới , 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, .
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.
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.
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();
#include <bits/stdc++.h>
using namespace std;
using 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
3
1
4
0
Mỗi thao tác : hai lần dựng lại khối hoặc lần chặt nhị phân.
Ngưỡng theo tham số
Nhiều bài có một tham số trong mỗi câu hỏi (bước nhảy, modulo, kích thước tập) mà cách giải tốt phụ thuộc vào lớn hay nhỏ. Với , đối tượng liên quan có ít phần tử, cỡ , nên duyệt thẳng. Với chỉ có 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 lại xử lý một lượt. Chọn thì cả hai phía đều mỗi thao tác, hoặc tổng cộng.
Hỏi nhanh
Câu hỏi “tổng các với chia dư ” trên dãy dài . Vì sao với có thể duyệt thẳng?
Các chỉ số cùng dư theo cách nhau đúng , nên có không quá chỉ số; cho dưới chỉ số. Bộ nhớ chỉ là lý do không tính sẵn cho mọi , còn duyệt được nhanh là nhờ số chỉ số ít.
Ví dụTổng Theo Số Dư
Đề. Mảng phần tử ban đầu bằng 0, thao tác: cộng vào , hoặc in tổng các với .
Lời giải. Chia câu hỏi theo modulo tại ngưỡng . Modulo nhỏ thì có ít, nên giữ sẵn đáp án cho mọi cặp với ; modulo lớn thì dãy chỉ có ít số, cộng thẳng.
Bước 1: cộng vào . Sửa và ô của bảng: với mỗi , phần tử thuộc nhóm dư x % d.
a[x] += y;
for (int d = 1; d <= S; d++) sm[d][x % d] += y;
Bước 2: câu hỏi. thì đọc thẳng sm[x][y]; thì cộng các với , không quá số.
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: chạy lần, phép chia 64 bit chậm hơn hẳn.
#include <bits/stdc++.h>
using namespace std;
using 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
9
4
1
Mỗi thao tác khoảng phép.
Ví dụNhững Bước Nhảy Có Phí
Đề. Mảng cố định dài với . Mỗi câu hỏi : khi thì nhảy ; in số bước nhảy. Có câu hỏi.
Lời giải. Mỗi bước tiến ít nhất , nên thì mô phỏng không quá bước. Với , số bước từ là , bằng 0 khi , tính từ phải sang trong . Lưu bảng cho mọi tốn ô, quá nhiều; thay vào đó sắp câu hỏi theo và chỉ tính lại dp khi đổi.
Bước 1: sắp câu hỏi theo . Giữ chỉ số gốc trong ord để cuối cùng in đáp án theo thứ tự đề.
for (ll i = 1; i <= q; i++) {
cin >> P[i] >> K[i];
ord[i] = i;
}
sort(ord + 1, ord + q + 1, cmp);
Bước 2: nhỏ, dùng chung một bảng. Khi đổi, tính lại dp[p] từ về 1; mỗi ô dựa vào ô nó nhảy tới, nằm bên phải nên đã tính. Có không quá giá trị khác nhau, mỗi lần .
if (k != last) {
for (ll p = n; p >= 1; p--) {
ll j = p + a[p] + k;
dp[p] = 1 + (j <= n ? dp[j] : 0);
}
last = k;
}
ans[i] = dp[P[i]];
Bước 3: lớn, mô phỏng. Mỗi bước tiến hơn , nên không quá bước.
ll p = P[i], cnt = 0;
while (p <= n) {
p += a[p] + k;
cnt++;
}
ans[i] = cnt;
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 1e5 + 5;
ll n, q, S, a[N], dp[N], P[N], K[N], ord[N], ans[N];
bool cmp(ll x, ll y) {
return K[x] < K[y];
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> a[i];
cin >> q;
for (ll i = 1; i <= q; i++) {
cin >> P[i] >> K[i];
ord[i] = i;
}
sort(ord + 1, ord + q + 1, cmp);
S = sqrt(n);
ll last = 0;
for (ll t = 1; t <= q; t++) {
ll i = ord[t], k = K[i];
if (k <= S) {
if (k != last) {
for (ll p = n; p >= 1; p--) {
ll j = p + a[p] + k;
dp[p] = 1 + (j <= n ? dp[j] : 0);
}
last = k;
}
ans[i] = dp[P[i]];
} else {
ll p = P[i], cnt = 0;
while (p <= n) {
p += a[p] + k;
cnt++;
}
ans[i] = cnt;
}
}
for (ll i = 1; i <= q; i++) cout << ans[i] << '\n';
}
5
1 2 1 3 1
4
1 1
2 2
1 3
4 1
3
1
2
1
Với , tổng .
Nhảy ra khỏi khối
Mỗi ô chỉ sang một ô ở bên phải, và có thể bị sửa. Mô phỏng thẳng một đường nhảy tốn tới 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 mà đường đi từ 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 còn trong khối thì kế thừa từ ô đó, ngược lại 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 . Sửa chỉ ảnh hưởng các ô cùng khối từ về trái, tính lại .
Ví dụNhững Bước Nhảy Thoát Mảng
Đề. ô, ô có , đứng ở thì nhảy tới , dừng khi vượt . thao tác: gán , hoặc bắt đầu ở 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 . Với mỗi ô giữ ba thứ: nxt[i] là ô đầu tiên ngoài khối mà đường nhảy từ 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 chỉ ảnh hưởng các ô cùng khối và đứng trước . Với ví dụ, khối dài 2: từ ô 1 đường đi qua ba lần nhảy theo nxt.
Bước 1: tính một ô. Nhảy một bước tới . Ra ngoài khối (hoặc ngoài mảng) thì rời khối sau 1 bước và ô cuối trong khối là chính . Còn trong khối thì kế thừa từ , cộng thêm 1 bướ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 từ về 1, nên luôn đã tính. Gán thì tính lại từ về đầu khối, .
for (ll i = n; i >= 1; i--) calc(i);
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.
ll steps = 0, last = p;
while (p <= n) {
steps += cnt[p];
last = lst[p];
p = nxt[p];
}
#include <bits/stdc++.h>
using namespace std;
using 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
6 4
3 2
6 3
Gán , câu hỏi vì qua tối đa khối.
Chia căn theo bậc trên đồ thị
Mục này dùng vài chữ của chương 3: đồ thị gồm các đỉnh và các cạnh nối hai đỉnh, bậc của một đỉnh là số cạnh nối với nó, và 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à . Vì vậy đồ thị cạnh có không quá đỉnh bậc lớn hơn . 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á ; đỉ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 cả hai phía đều .
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á cạnh đi ra: một đỉnh có cạnh đi ra thì cả hàng xóm đó có bậc ít nhất bằng nó, tức ít nhất , nên .
Ví dụTam Giác Trong Mạng Lưới
Đề. Đồ thị đơn đỉnh, 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á cạnh đi ra: đỉnh có cạnh ra thì mỗi đỉnh nó đi tới có bậc ít nhất , nên .
Bước 1: đếm bậc rồi hướng cạnh.
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 đi tới bằng mark[v] = u; dùng chính làm dấu nên khỏi xóa mảng giữa các lượt. Với mỗi mà đi tới, mỗi mà đi tới và đã được đánh dấu là một tam giá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++;
}
#include <bits/stdc++.h>
using namespace std;
using 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
2
Mỗi cạnh tốn số cạnh ra của , không quá : tổng , khoảng phép đơn giản.
Khi đi thi
- cỡ 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; là thoải mái, với hằng số lớn thì nên thử nhanh trước.
- Viết riêng trường hợp 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 bất kỳ rồi chọn 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 : đoạn bất kỳ là hai mảnh biên dài không quá và không quá khối nguyên; cho .
- 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 : 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 thành bước, cập nhật .
- 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á cạnh ra.
Bài tập
lower_bound, mảnh biên sửa thì sắp lại.upper_bound.deque kèm bảng đếm giá trị; dịch vòng là rút phần tử ở và chèn vào , chuyển một phần tử qua mỗi ranh giới khối.Đọc thêm
- Chia căn và ứng dụngVNOI WikiBài tiếng Việt về chia căn và các ứng dụng.
- Sqrt Decompositioncp-algorithmsKhung khối, cập nhật đoạn và thuật toán Mo.
- 分块思想OI WikiChia khối trên dãy, kèm ví dụ và bài luyện.
- Square Root DecompositionUSACO GuideChia theo khối, theo ngưỡng và theo bậc với bài luyện.