離線演算法
建國中學 賴昭勳
離線是什麼
OFFLINE?
處理有多筆詢問的問題的時候
-
在線:對每個詢問分別處理,詢問之間沒有任何關聯
-
離線:先讀入所有詢問,可以透過排序詢問等方法在不同詢問之間取得共同的資訊
莫隊演算法
有一種問題長這樣...
給你一個長度為n的整數序列,有Q筆詢問,每次詢問區間[l,r]的某種東西(數量/最大最小值...)
- 看到n沒有到超級無敵大的時候(≤105)
- 想要用奇怪資料結構做卻沒有辦法/不會的時候...
- 想要唬爛的時候
莫隊(Mo's Algorithm)是什麼
假設我已知[l,r]的答案,能夠快速求出[l,r+1],[l,r−1],[l+1,r],[l−1,r]的答案的時候
我們的目標
找到一種處理詢問的順序,使得各詢問之間的總移動量最少。
做法:對詢問分塊!
分塊想法
假設每塊大小為 k
-
先將詢問依照左界分組
- 把每塊內的詢問按照右界排序
- 沒了
具體會發生什麼事
在每一組裡面,右界會遞增,左界則會在大小為k的範圍內亂跳。
也就是說,假設有x個東西在同一組,則總移動量為
O(kx+n)
那這樣的複雜度分析是多少?
複雜度分析
假設移動一次需要O(m)的時間,則
分塊的時間是O(q)
排序的總時間是O(qlogq)
每塊處理移動O((kx+n)∗m),∑x=q
兩塊之間的移動O(nm)
總共有O(n/k)塊
所以總複雜度為 O(qlogq+(n2/k+qk)m)
算幾又來啦
由算幾不等式可得n2/k+qk≥2n2q
故當n2/k=qk→k≈n時
n2/k+qk的最小值為O((n+q)n)
所以最佳總複雜度為 O(qlogq+(n+q)n⋅m)
註:在這裡我們假設n≈q
這樣好像就能好好做了?
回到莫隊的使用時機...
「假設我已知[l,r]的答案,能夠快速求出[l,r+1],[l,r−1],[l+1,r],[l−1,r]的答案的時候」
也就是說,要對一個題目使用莫隊,必須先找出方法快速移動一個區間的答案
來看看例題
對於一個區間,我要存下什麼資訊?
當我移動一格的時候,這個資訊又會怎麼改變?
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的時候:
- b[2]−−
- a[1]−−
- b[1]++
最後紀錄答案
另外紀錄一個變數代表「目前最大的x,使b[x]>0」
因為每次新增/減少一個東西時,x至多改變一,所以可在O(1)之內進行莫隊的「移動」
由前面的複雜度分析可得知,時間複雜度為
O(qlogq+(n+q)n)
實作時間!
實作這題的時候要注意一些細節:
- 詢問的排序:要在每筆詢問額外紀錄他的id,最後按照順序輸出
- 移動的順序:在移動時必須保證左界≤右界,所以應該先讓左界往左/右界往右,再讓左界往右/右界往左
附上code: https://pastebin.com/T0V5vvTf
其他例題
區間逆序數對數,n≤23000,q≤2×105
Hint:按我獲取提示
搭配資料結構,考慮新增一個數字時會多形成多少對逆序數對
區間詢問:如果ai出現k次,則會獲得價值ai×k2,求每個區間的價值和
Hint:
當ai的個數增加一的時候,價值會如何增加?
XOR and Favorite Number
給你一個數列ai和數字k,每次詢問一個區間有多少個子區間 xor 起來是 k
n,q≤105,0≤ai,k≤106
Hint:
子區間xor -> 前綴xor
增加一個ai時,只有一種數字和他xor起來是k
整體二分搜
大家有聽過二分搜吧?
正常二分搜的時候,可以把拿來二分搜的東西想成一個區間,每次查詢答案是在區間的左半還是右半,然後更新區間走下去。
那假設我要一次搜很多的詢問呢?
整體二分搜:
-
將區間切成兩半
- 對每個詢問+元素看要去左半還是右半
- 遞迴下去做
原因:可以改變詢問順序,一次處理多筆詢問
直接看例題:Coding Days
給你一個序列,支援三種操作:
1. 查詢 [l,r]第 k小的值
2. 改變一個元素的值
3. 輸出 7122
n≤50000,q≤10000
這是什麼鬼
先考慮靜態(沒有第二種操作)的版本
看看有沒有方法用二分搜在 O(nlogC)之內找到答案
這裡要特別注意二分搜的做法,因為有一個小小優化,之後處理多筆詢問時將大大的改變複雜度
對答案二分搜吧
當前有個答案的可能區間[l,r),每次檢查mid=(l+r)/2,看有幾個數字小於等於他
如果個數大於k,則答案在[l,mid),否則在[mid,r)
這裡遞迴下去的時候,注意到每個數字也能被分到左邊右邊
(如果答案大於mid,只需算mid≤ai<r的數字)
一筆詢問時,這樣複雜度不變:O(nlogC)
但是多筆詢問呢?
全部一起二分搜
目前有一個序列,每筆詢問有l,r,k,現在要對每筆詢問看他的答案是否小於mid
對位置維護一個 BIT,如果ai≤mid就把位置 i增加一,這樣即可在O(logn)的時間內查詢在一個區間≤mid的個數
這樣就可以將每個詢問分成兩邊,且依照每個ai也分成兩邊
那修改怎麼辦啊www
把修改也當成一種詢問!
處理詢問時維持原本的詢問順序。把「將x改為y」看成「刪掉一個x,加上一個y」,同樣能用BIT好好做。
而且這樣的話一個修改操作也可以被分到不同的兩塊!
時間複雜度是多少?
每一層的遞迴中,所有詢問,所有元素皆只出現一次,而每次處理需要O((n+q)logn)的時間,至多有O(logC)層
總複雜度 O((n+q)lognlogC)
澂逝嘜
#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 (ai相異)怎麼做
Hint:
對天數(答案)整體二分
對每筆詢問紀錄他還距離目標還有多遠
CDQ 分治
還記得分治嗎?
merge sort 的時候,把區間切成兩半解決之後合併結果。
這個想法套用在離線的框架上可以做什麼事情?
操作分治!
假設我們有一系列的操作,可能是修改/詢問目前的東西
那我們可以把操作看成是按照「時間」順序排!
處理詢問的時候必須考慮「時間」在他前面的所有修改操作
操作分治,就是對這個「時間」的序列/維度進行分治
回顧分治
假設處理好(遞迴)前半操作對前半詢問的影響,
後半操作對後半詢問的影響,
那麼我們只剩下前半操作對後半詢問的影響了!
紅對紅,綠對綠都已做完
只剩下紅對綠
用修改與查詢看逆序數對
把一個數字ai看成:
- 詢問比ai小的數字有幾個
- 新增一個ai
此時,我們能在O(n)內找到左半數字對右半查詢的影響
中國競程圈流傳著一個名言...
CDQ 分治可以解決高維偏序問題
給你一個數列,有m次操作。
每次給一個x,把所有為x的數字都刪除。
請輸出每次操作後的逆序數對數。
先將問題抽象化
每個數字有三個屬性:i,ai,ti,其中ti代表這個元素會被刪除的時間
問題變成,對於每一個i,有多少j符合
(i<j,ai>aj or i>j,ai<aj),ti>tj
當我們套分治的時候,目的是希望把一個維度做掉,這時候可以把哪個維度去除呢?
考慮把操作倒回來做,每次增加一些元素,看這些元素加進來時會多多少逆序數對
套上CDQ分治
對index,也就是i<j的維度做分治。
每次把當前的序列切成左右兩半,遞迴處理好各自的答案後,只需要處理左邊和右邊之間的點對就好。
左邊
i
ai
ti
右邊
j
aj
tj
左邊
j
aj
tj
右邊
i
ai
ti
或是
套上CDQ分治
對時間開一個BIT
用merge sort 的方式,對於一個ai,所有小於他的aj全部都被用tj丟到BIT裡面,再查詢當前比ti小的元素有幾個,就可以去更新第i項的答案
這樣的總複雜度就是O(nlog2n)
左邊
i
ai
ti
右邊
j
aj
tj
例題:
No Judge: 給你一個二維平面,處理兩種操作
- 在xi,yi的點上加入權重wi
- 詢問以0,0為左下角,xi,yi為右上角的矩形所覆蓋的權重和
q≤105,0≤xi,yi≤109
Hint:
只考慮前半對後半的影響的話
你就可以把詢問和修改的一個維度(xi或yi)排序然後用merge sort做
謝謝大家
離線真的很難
這裡面的內容我也是最近才弄懂
大家加油><
離線演算法 (資讀)
By justinlai2003
離線演算法 (資讀)
- 1,168