離線演算法
何謂離線
在線 vs. 離線
在線: 讀入一個詢問就馬上回答
離線: 先將所有詢問一次讀完再一起回答
莫隊
使用時機
(通常是)靜態的區間查詢
維護好\([l,r]\)的答案後,可快速得知\([l\pm 1,r]\)
與\([l,r\pm 1]\)的答案
做法
將此序列每\(k\)個分成一塊
將所有詢問歸類到其左界所屬的塊
塊內的詢問照右界排序
複雜度?
假如加入/移除一個東西的時間複雜度是\(O(a)\)
考慮從目前詢問移動到下一個詢問
左界: 只會在塊內跳動
單次詢問\(O(k\times a)\),總共\(O(q\times k\times a)\)
右界: 在同一塊內的詢問會遞增
每一塊\(O(n\times a)\),總共\(O(\frac{n}{k}\times n\times a)\)
總複雜度:\(O(a)\times O(qk+\frac{n^2}{k})\)
當\(k=\frac{n}{\sqrt q}\)有最佳複雜度\(O(a)\times O(n\sqrt q)\)
核心代碼
struct question{int l,r,i;}; // 第i個詢問 [l,r]
int n,q,k; vector<int> arr;
void init(int n){}
void add(int i){}
void rem(int i){}
int get_ans(){}
signed main(){
vector<question> querys;
cin>>n>>q;
arr.resize(n);
querys.resize(q);
k=n/sqrt(q);
for(int i=0;i<n;i++) cin>>arr[i];
for(int i=0;i<q;i++)
cin>>querys[i].l>>querys[i].r,querys[i].i=i;
sort(querys.begin(),querys.end(),
[](question a,question b){
return a.l/k<b.l/k||(a.l/k==b.l/k&&a.r<.r);
});
init(n);
int l=0,r=-1; vector<int> ans(q);
for(int i=0;i<q;i++){
while(r<querys[i].r) add(++r);
while(querys[i].l<l) add(--l);
while(querys[i].r<r) rem(r--);
while(l<querys[i].l) rem(l++);
ans[querys[i].i]=get_ans();
}
return 0;
}
ZJ b417
https://zerojudge.tw/ShowProblem?problemid=b417
給定一個長度為\(N\)的正整數序列\(s\),
對於\(M\)次詢問\([l,r]\),求 \([s_l, s_{l+1},...,s_r]\) 內
"眾數的個數"(出現最多次的人出現幾次)
以及"有幾種數字是眾數"
\(N\leq 10^5, M\leq 10^6\)
ZJ b417
維護"每個數字出現的次數"
以及"每個出現次數出現的次數"
每次操作後,眾數個數最多改變一
要如何快速維護加/刪一個東西的狀態?
練習一下
TIOJ 1699
補充: 更快的莫隊
將"奇數塊"的右界正序,"偶數塊"的右界逆序
常數比較小,據說會快很多
整體二分搜
使用時機
讓"所有操作"一起做二分搜
有點難懂?直接看例題吧
TIOJ 1840
https://tioj.ck.tp.edu.tw/problems/1840
帶修改區間第\(k\)小
TIOJ 1840
做法: 先考慮"只有一筆詢問"的情況
對答案二分搜,假設目前搜尋的值為\(x\)
對原序列維護每個前綴區間\(\leq x\)的數有幾個
如果算出來的值超過詢問的\(k\),則答案會在小於\(x\)的那邊
否則會在大於\(x\)的那邊
TIOJ 1840
再考慮"只有詢問"的情況
讓所有詢問一起二分搜!
你有一堆詢問,目前搜尋的值都是\(x\)
維護每個前綴區間\(\leq x\)的值有幾個
有些詢問要走左邊
有些詢問要走右邊
TIOJ 1840
如果每次詢問都需要重新維護每個前綴區間\(\leq x\)的數有幾個
需要\(O(N^2)\),花太多時間了!
假如二分搜時的範圍為\([l,r]\),目前的答案是\(mid\)
維護介於\([l,mid]\)之間的數有哪些
對於往右邊走的詢問,已經知道有幾個數小於\(mid\)
就可以把原本的\(k\)減去\(mid\),直接往右走!
更棒的是,還能順便維護"序列中每個數字"該往左或右走
TIOJ 1840
目前區間\([l,r]\)
維護每個前綴區間介於\([l,mid]\)的數有幾個
有些詢問要走左邊
介於\([l,mid]\)的數也走左邊
有些詢問要走右邊
介於\([mid,r]\)的數也走右邊
用BIT維護! 複雜度\(O((n+q)log^2n)\)
TIOJ 1840
修改?
把"將a改成b"看成"刪除a與加入b"
讓修改一起二分搜!(其實是搜影響區間)
目前區間\([l,r]\)
維護BIT紀錄區間介於\([l,mid]\)的數有幾個
有些詢問要走左邊
介於\([l,mid]\)的修改也走左邊
有些詢問要走右邊
介於\([mid,r]\)的修改也走右邊
複雜度: \(O((n+q)\cdot logn\cdot log(n+q))\)
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
#define F first
#define S second
const int INF=pow(2,31)-1;
struct act{int tp,l,r,k,i;};
struct BIT{
vector<int> data; vector<pii> rec; int n;
void init(int len){n=len; data.clear(); data.resize(n+1);}
int lowbit(int x){return x&(-x);}
void modify(int pos,int val,bool record=true){
if(record) rec.push_back({pos,val});
while(pos<=n){
data[pos]+=val;
pos+=lowbit(pos);
}
}
int query(int pos){
int val=0;
while(pos>0){
val+=data[pos];
pos-=lowbit(pos);
}
return val;
}
int query(int l,int r){
return query(r)-query(l-1);
}
void undo(){
while(!rec.empty()){
modify(rec.back().F,-rec.back().S,false);
rec.pop_back();
}
}
} bit;
vector<act> action; vector<int> ans,arr,comp;
void total_binary_search(vector<int> &v,int l,int r){
if(l+1==r||v.size()==0){
for(auto &i:v){
if(action[i].tp==1) ans[action[i].i]=l;
}
return void();
}
int mid=(l+r)/2;
vector<int> L,R;
for(auto &i:v){
if(action[i].tp==1){
int k=bit.query(action[i].l,action[i].r);
if(k>=action[i].k){
L.push_back(i);
}
else{
action[i].k-=k;
R.push_back(i);
}
}
else if(action[i].tp==2){
if(action[i].i>0){
if(action[i].k<mid){
bit.modify(action[i].i,1);
L.push_back(i);
}
else{
R.push_back(i);
}
}
else{
if(action[i].k<mid){
bit.modify(-action[i].i,-1);
L.push_back(i);
}
else{
R.push_back(i);
}
}
}
}
bit.undo();
total_binary_search(L,l,mid);
total_binary_search(R,mid,r);
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t;
cin>>t;
while(t--){
int n,q;
cin>>n>>q;
bit.init(n); action.clear(); ans.clear(); arr.clear(); arr.resize(n+1); comp.clear();
for(int i=1;i<=n;i++){
cin>>arr[i];
comp.push_back(arr[i]);
}
for(int i=1;i<=n;i++){
action.push_back(act());
action.back().tp=2;
action.back().i=i;
action.back().k=arr[i];
}
for(int i=0;i<q;i++){
int k;
cin>>k;
if(k==1){
action.push_back(act());
action.back().tp=1;
action.back().i=ans.size();
cin>>action.back().l>>action.back().r>>action.back().k;
ans.push_back(0);
}
else if(k==2){
action.push_back(act());
action.back().tp=2;
cin>>k; action.back().i=-k; action.back().k=arr[k];
action.push_back(act());
action.back().tp=2;
cin>>arr[k]; action.back().i=k; action.back().k=arr[k]; comp.push_back(arr[k]);
}
else if(k==3){cin>>k>>k; ans.push_back(-1);}
}
sort(comp.begin(),comp.end());
comp.erase(unique(comp.begin(),comp.end()),comp.end());
for(act &i:action){
if(i.tp==2) i.k=lower_bound(comp.begin(),comp.end(),i.k)-comp.begin();
}
vector<int> v; for(int i=0;i<action.size();i++) v.push_back(i);
total_binary_search(v,0,comp.size());
for(auto &i:ans){
if(i!=-1) cout<<comp[i]<<endl;
else cout<<7122<<endl;
}
}
return 0;
}
操作分治
分治?
將所有操作依時間分成前半後半
遞迴計算前半與後半的答案
計算前半修改對後半詢問的答案
有點難懂?看一下下面的東西
逆序數對
No Judge
一個二維平面上有\(N\)次操作
\(1,x,y,w\): 將\((x,y)\)加上權重\(w\)
\(2,x,y\): 詢問\((x,y)\)左下角的權重總和
做法
考慮操作分治
將所有操作依時間分成前半後半
遞迴計算前半與後半的答案
計算前半修改對後半詢問的答案
前半修改對後半詢問的答案
因為修改都在前,詢問都在後,時間順序不重要!
將所有操作對\(x\)座標排序
修改: 對\(y\)座標單點修改
詢問: 對\(y\)座標求前綴和
用BIT!
離線演算法
By jass921026
離線演算法
- 811