離線演算法

何謂離線

在線 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 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;
}

操作分治

分治?

將所有操作依時間分成前半後半

遞迴計算前半與後半的答案

計算前半修改對後半詢問的答案

有點難懂?看一下下面的東西

逆序數對

https://tioj.ck.tp.edu.tw/problems/1080

merge sort

將所有數字分成前後兩半

遞迴計算兩半的答案

計算橫跨兩半的答案

看起來很像(?

No Judge

一個二維平面上有\(N\)次操作

\(1,x,y,w\): 將\((x,y)\)加上權重\(w\)

\(2,x,y\): 詢問\((x,y)\)左下角的權重總和

做法

考慮操作分治

將所有操作依時間分成前半後半

遞迴計算前半與後半的答案

計算前半修改對後半詢問的答案

前半修改對後半詢問的答案

因為修改都在前,詢問都在後,時間順序不重要!

將所有操作對\(x\)座標排序

修改: 對\(y\)座標單點修改

詢問: 對\(y\)座標求前綴和

用BIT!

Made with Slides.com