建國中學 賴昭勳
處理有多筆詢問的問題的時候
先將詢問依照左界分組
在每一組裡面,右界會遞增,左界則會在大小為\(k\)的範圍內亂跳。
也就是說,假設有\(x\)個東西在同一組,則總移動量為
\(O(kx + n)\)
那這樣的複雜度分析是多少?
分塊的時間是\(O(q)\)
排序的總時間是\(O(qlogq)\)
每塊處理移動\(O((kx + n)*m), \sum x = q\)
兩塊之間的移動\(O(nm)\)
總共有\(O(n / k)\)塊
所以總複雜度為 \(O(qlogq + (n^2/ k + qk)m)\)
故當\(n^2/k = qk \rightarrow k \approx \sqrt{n}\)時
\(n^2/k + qk\)的最小值為\(O((n + q)\sqrt{n})\)
所以最佳總複雜度為 \(O(qlogq + (n + q)\sqrt{n}\cdot m)\)
註:在這裡我們假設\(n \approx q\)
「假設我已知\([l, r]\)的答案,能夠快速求出\([l, r + 1], [l, r - 1], [l + 1, r], [l - 1, r]\)的答案的時候」
也就是說,要對一個題目使用莫隊,必須先找出方法快速移動一個區間的答案
給你一個長度為\(n\)的序列,有\(q\)筆詢問\([l, r]\),求\([l, r]\)區間眾數出現的次數
\(n, q \leq 10^5, 0 <a_i \leq 10^5\)
Hint: 眾數->出現最多次的數字,你必須知道每個數字出現幾次,還有?
Ans: 紀錄兩個陣列,一個為每數字在目前的區間中出現幾次,另一個為每個出現次數有幾個數字
設 \(a[i]\)代表 數字 \(i\)出現的次數,
\(b[i]\)代表有幾種數字在目前的區間出現\(i\)次。
則:
a[0] | a[1] | a[2] |
---|---|---|
1 | 2 | 2 |
b[0] | b[1] | b[2] |
---|---|---|
0 | 1 | 2 |
移除一個 \(1\)的時候:
另外紀錄一個變數代表「目前最大的\(x\),使\(b[x] > 0\)」
因為每次新增/減少一個東西時,\(x\)至多改變一,所以可在\(O(1)\)之內進行莫隊的「移動」
由前面的複雜度分析可得知,時間複雜度為
\(O(qlogq + (n + q)\sqrt{n})\)
附上code: https://pastebin.com/T0V5vvTf
Hint:按我獲取提示
搭配資料結構,考慮新增一個數字時會多形成多少對逆序數對
區間詢問:如果\(a_i\)出現\(k\)次,則會獲得價值\(a_i \times k^2\),求每個區間的價值和
Hint:
當\(a_i\)的個數增加一的時候,價值會如何增加?
給你一個數列\(a_i\)和數字\(k\),每次詢問一個區間有多少個子區間 xor 起來是 \(k\)
\(n, q \leq 10^5, 0 \leq a_i, k \leq 10^6\)
Hint:
子區間xor -> 前綴xor
增加一個\(a_i\)時,只有一種數字和他xor起來是\(k\)
正常二分搜的時候,可以把拿來二分搜的東西想成一個區間,每次查詢答案是在區間的左半還是右半,然後更新區間走下去。
將區間切成兩半
原因:可以改變詢問順序,一次處理多筆詢問
1. 查詢 \([l, r]\)第 \(k\)小的值
2. 改變一個元素的值
3. 輸出 7122
\(n \leq 50000, q \leq 10000\)
看看有沒有方法用二分搜在 \(O(n\log C)\)之內找到答案
這裡要特別注意二分搜的做法,因為有一個小小優化,之後處理多筆詢問時將大大的改變複雜度
當前有個答案的可能區間\([l, r)\),每次檢查\(mid = (l + r) / 2\),看有幾個數字小於等於他
如果個數大於\(k\),則答案在\([l, mid)\),否則在\([mid, r)\)
這裡遞迴下去的時候,注意到每個數字也能被分到左邊右邊
(如果答案大於\(mid\),只需算\(mid \leq a_i < r\)的數字)
一筆詢問時,這樣複雜度不變:\(O(n\log C)\)
但是多筆詢問呢?
目前有一個序列,每筆詢問有\(l, r, k\),現在要對每筆詢問看他的答案是否小於\(mid\)
對位置維護一個 BIT,如果\(a_i \leq mid\)就把位置 \(i\)增加一,這樣即可在\(O(\log n)\)的時間內查詢在一個區間\( \leq mid\)的個數
這樣就可以將每個詢問分成兩邊,且依照每個\(a_i\)也分成兩邊
把修改也當成一種詢問!
處理詢問時維持原本的詢問順序。把「將\(x\)改為\(y\)」看成「刪掉一個\(x\),加上一個\(y\)」,同樣能用BIT好好做。
而且這樣的話一個修改操作也可以被分到不同的兩塊!
每一層的遞迴中,所有詢問,所有元素皆只出現一次,而每次處理需要\(O((n + q) \log n)\)的時間,至多有\(O(\log C)\)層
總複雜度 \(O((n + q) \log n \log C)\)
#pragma GCC optimize("Ofast")
#pragma loop_opt(on)
#include <iostream>
#include <algorithm>
#include <utility>
#include <vector>
#define maxn 100005
#define ll long long
using namespace std;
int cnt = 0;
struct query {
int type = 0, ind = 0;
int l = 0, r = 0, k = 0;
query(int t, int a, int b, int c) {
type = t, l = a, r = b, k = c;
ind = cnt++;
}
};
int ans[maxn];
struct BIT {
int arr[maxn];
void modify(int ind, int n, int val) {
for (;ind <= n;ind += ind & (-ind)) arr[ind] += val;
}
int query(int ind) {
int ret = 0;
for (;ind;ind -= ind & (-ind)) ret += arr[ind];
return ret;
}
}bit;
int a[maxn];
int n;
void solve(vector<query> que, vector<int> ind, ll low, ll up) { //[low, up)
//cout << low << " " << up << " " << mid << endl;
if (up <= low || que.size() == 0) return;
if (up - low == 1) {
for (query i:que) {
ans[i.ind] = low;
}
return;
}
ll mid = (low + up) / 2;
//cout << low << " " << up << " " << mid << " " << endl;
vector<int> lind, rind, bm;
for (int i:ind) {
if (a[i] < mid) {
bit.modify(i, n, 1), bm.push_back(i);
lind.push_back(i);
} else {
rind.push_back(i);
}
}
/*
for (int i:lind) cout << i << " ";
cout << endl;
for (int i:rind) cout << i << " ";
cout << endl;
*/
vector<query> left, right;
for (auto q:que) {
//cout << q.ind << " ";
int tol = 0;
if (q.type == 1) {
//cout << " " << q.ind << " " << bit.query(q.r) - (q.l - 1 ? bit.query(q.l - 1) : 0) << " " << q.k<< endl;
int val = bit.query(q.r) - (q.l - 1 ? bit.query(q.l - 1) : 0);
if (val >= q.k) tol = 1;
else q.k -= val;
} else if (q.type == 2) {
if (q.l < 0) {
if (q.k < mid) {
//cout << " " << -q.l << ' ' << q.k << endl;
bit.modify(-q.l, n, -1), bm.push_back(q.l);
//cout << " " << bit.query(1) << endl;
tol = 1;
}
} else {
if (q.k < mid) {
//cout << " " << q.l << ' ' << q.k << endl;
bit.modify(q.l, n, 1), bm.push_back(q.l);
tol = 1;
}
}
}
if (tol) left.push_back(q);
else right.push_back(q);
}
//cout << endl;
for (int i:bm) {
if (i > 0) bit.modify(i, n, -1);
else bit.modify(-i, n, 1);
}
solve(left, lind, low, mid);
solve(right, rind, mid, up);
}
int main() {
ios_base::sync_with_stdio(0);cin.tie(0);
int t;
cin >> t;
while (t--) {
int q;
cin >> n >> q;
int cop[n];
vector<int> index;
for (int i = 1;i <= n;i++) cin >> a[i], cop[i] = a[i], index.push_back(i);
vector<query > que;
for (int i = 0;i < q;i++) {
int type;
cin >> type;
if (type == 1) {
int l, r,k;
cin >> l >> r >> k;
que.push_back(query(type, l, r, k));
} else if (type == 2) {
int ind, v;
cin >> ind >> v;
que.push_back(query(type, -ind, 0, cop[ind]));
//cout << -ind << " " << cop[ind] << endl;
cop[ind] = v;
que.push_back(query(type, ind, 0, v));
//cout << ind << " " << v << endl;
} else {
int x, v;
cin >> x >> v;
que.push_back(query(type, 0, 0, 7122));
}
}
solve(que, index, -(1LL<<31), 1LL<<31);
for (query q:que) {
if (q.type == 3) {
cout << "7122" << "\n";
} else if (q.type == 1) {
cout << ans[q.ind] << "\n";
}
}
}
}
大家可以先想想第三個Subtask (\(a_i\)相異)怎麼做
Hint:
對天數(答案)整體二分
對每筆詢問紀錄他還距離目標還有多遠
假設我們有一系列的操作,可能是修改/詢問目前的東西
那我們可以把操作看成是按照「時間」順序排!
處理詢問的時候必須考慮「時間」在他前面的所有修改操作
操作分治,就是對這個「時間」的序列/維度進行分治
假設處理好(遞迴)前半操作對前半詢問的影響,
後半操作對後半詢問的影響,
那麼我們只剩下前半操作對後半詢問的影響了!
紅對紅,綠對綠都已做完
只剩下紅對綠
把一個數字\(a_i\)看成:
此時,我們能在\(O(n)\)內找到左半數字對右半查詢的影響
每次給一個\(x\),把所有為\(x\)的數字都刪除。
請輸出每次操作後的逆序數對數。
每個數字有三個屬性:\(i, a_i, t_i\),其中\(t_i\)代表這個元素會被刪除的時間
問題變成,對於每一個\(i\),有多少\(j\)符合
\((i < j, a_i > a_j \ or \ i > j, a_i < a_j), t_i > t_j\)
當我們套分治的時候,目的是希望把一個維度做掉,這時候可以把哪個維度去除呢?
考慮把操作倒回來做,每次增加一些元素,看這些元素加進來時會多多少逆序數對
對\(index\),也就是\(i < j\)的維度做分治。
每次把當前的序列切成左右兩半,遞迴處理好各自的答案後,只需要處理左邊和右邊之間的點對就好。
左邊
\(i\)
\(a_i\)
\(t_i\)
右邊
\(j\)
\(a_j\)
\(t_j\)
左邊
\(j\)
\(a_j\)
\(t_j\)
右邊
\(i\)
\(a_i\)
\(t_i\)
或是
對時間開一個BIT
用merge sort 的方式,對於一個\(a_i\),所有小於他的\(a_j\)全部都被用\(t_j\)丟到BIT裡面,再查詢當前比\(t_i\)小的元素有幾個,就可以去更新第\(i\)項的答案
這樣的總複雜度就是\(O(n {log}^2 n)\)
左邊
\(i\)
\(a_i\)
\(t_i\)
右邊
\(j\)
\(a_j\)
\(t_j\)
\(q \leq 10^5, 0 \leq x_i, y_i \leq 10^9\)
Hint:
只考慮前半對後半的影響的話
你就可以把詢問和修改的一個維度(\(x_i\)或\(y_i\))排序然後用merge sort做
離線真的很難
這裡面的內容我也是最近才弄懂
大家加油><