分塊和離線

這兩堂課應該會講的東西

折半枚舉

有\(n\)個數,\(n\leq 40\),問有多少子集的和為\(x\)

直接枚舉:\(O(2^n)\)燒雞

切一半分別枚舉,把兩邊枚舉出的東西分別排序再用雙指針或二分搜看有多少個:

枚舉:\(O(n\times 2^\frac{n}{2})\)

排序:\(O(2^\frac{n}{2}\times\log2^\frac{n}{2})\)=\(O(n\times 2^\frac{n}{2})\)

找東西:\(O(2^\frac{n}{2}\times\log2^\frac{n}{2})\)=\(O(n\times 2^\frac{n}{2})\)

共\(O(n\times2^\frac{n}{2})\)

code

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll a[25],b[25];
vector<ll> aa,bb;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    ll n,x;
    cin>>n>>x;
    for(int i=0;i<n/2;i++){
        cin>>a[i];
    }
    for(int i=0;i<(n+1)/2;i++){
        cin>>b[i];
    }
    for(ll c=0;c<(1<<(n/2));c++){
        ll ans=0;
        for(int i=0;i<n/2;i++){
            if(c&(1<<i)){
                ans+=a[i];
            }
        }
        aa.push_back(ans);
    }
    for(ll c=0;c<(1<<((n+1)/2));c++){
        ll ans=0;
        for(int i=0;i<(n+1)/2;i++){
            if(c&(1<<i)){
                ans+=b[i];
            }
        }
        bb.push_back(ans);
    }
    sort(aa.begin(),aa.end());
    sort(bb.begin(),bb.end());
    ll ans=0;
    for(auto u:aa){
        ans+=upper_bound(bb.begin(),bb.end(),x-u)-lower_bound(bb.begin(),bb.end(),x-u);
    }
    cout<<ans<<"\n";
    return 0;
}

習題

分塊

分塊:把東西切一切,把同一塊的事情一起做

 

在平面上有\(n\)個格子點\((x_i,y_i),0\leq x_i,y_i\leq 10^6\),\(n\leq 10^6\),找一條哈密頓路徑長度\(\leq 2.5\times 10^9\),兩點距離是他們的曼哈頓距離

把\(y\)每\(k\)個分一塊

每塊照\(x\)排序

\(y\)會動到:\(nk+2000k\)

\(x\)會動到:\(\frac{n}{k}\times n\)

\(y\)

\(x\)

習題

$$\sqrt n維護$$

在這裡的問題通常是把數列切一切,我們會想要切\(\sqrt n\)通常是為了決定\(O(\frac{n}{k}+k)\)的\(k\)

 

先來看長度為\(n\)的序列,有\(q\)筆操作區間改值,區間查最小值的問題

先來做沒有修改的

我們把序列中每\(k\)個數字分成一塊,每一塊紀錄最小值

查詢

  • 完整的塊最多\(O(\frac{n}{k}) \)個
  • 剩下暴力最多有\(O(k)\)個

有修改的

打懶標!

改值

  • 完整的塊:打懶標,最多\(O(\frac{n}{k}) \)個
  • 不完整的塊:推懶標,暴力改值,維護最小值\(O(k)\)

想讓\(\frac{n}{k}+k\)最小

算幾有\(\frac{n}{k}+k\geq 2\sqrt n\),當\(k=\sqrt n\)

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e9;
const int blk=450;
int a[200010],val[450],tag[450];
void push(int pos){
    if(tag[pos]==maxn)return;
    val[pos]=tag[pos];
    for(int i=pos*blk;i<(pos+1)*blk;i++){
        a[i]=tag[pos];
    }
    tag[pos]=maxn;
}
void pull(int pos){
    val[pos]=maxn;
    for(int i=pos*blk;i<(pos+1)*blk;i++){
        val[pos]=min(val[pos],a[i]);
    }
}
void modify(int l,int r,int x){
    int L=l/blk,R=r/blk;
    for(int i=L+1;i<R;i++){
        tag[i]=x;
    }
    push(L);
    push(R);
    if(L==R){
        for(int i=l;i<=r;i++)a[i]=x;
    }else{
        for(int i=l;i<(L+1)*blk;i++)a[i]=x;
        for(int i=R*blk;i<=r;i++)a[i]=x;
    }
    pull(L);
    pull(R);
}
int query(int l,int r){
    int ans=maxn,L=l/blk,R=r/blk;
    for(int i=L+1;i<R;i++){
        if(tag[i]!=maxn)ans=min(ans,tag[i]);
        else ans=min(ans,val[i]);
    }
    if(L==R){
        for(int i=l;i<=r;i++)ans=min(ans,a[i]);
    }else{
        for(int i=l;i<(L+1)*blk;i++)ans=min(ans,a[i]);
        for(int i=R*blk;i<=r;i++)ans=min(ans,a[i]);
    }
    return ans;
}

有一個長度為\(n\)的序列,有\(q\)筆操作:

  1. 區間加值
  2. 給一個數\(y\),問序列中最遠的兩個\(y\)差多遠

\(n\leq 5\times 10^5,q\leq 5\times 10^4\)

把\(k\)個切一塊,需要找最左邊和最右邊的\(y\),用multiset維護每塊有的東西

加值:過整塊的打懶標,剩下暴力維護\(O(\frac{n}{k}+k\log k)\)

詢問:每塊看有沒有\(y\),然後暴力戳\(y\)在哪         \( O( \frac{n}{k}\log k+ k) \)

取\(k=\sqrt n\)然後亂壓有\(O(n\log\sqrt n+q\sqrt n \log\sqrt n)\)

切一切!

code

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll maxn=2e9;
const ll blk=710;
ll a[500010],tag[710],n,q;
multiset<ll> val[710];
void modify(int l,int r,ll x){
    int L=l/blk,R=r/blk;
    for(int i=L+1;i<R;i++){
        tag[i]+=x;
    }
    if(L==R){
        for(int i=l;i<=r;i++){
            val[L].erase(val[L].find(a[i]));
            a[i]+=x;
            val[L].insert(a[i]);
        }
    }else{
        for(int i=l;i<(L+1)*blk;i++){
            val[L].erase(val[L].find(a[i]));
            a[i]+=x;
            val[L].insert(a[i]);
        }
        for(int i=R*blk;i<=r;i++){
            val[R].erase(val[R].find(a[i]));
            a[i]+=x;
            val[R].insert(a[i]);
        }
    }
}
int query(ll y){
    ll mn=maxn,mx=0;
    for(int i=0;i<=n/blk;i++){
        if(val[i].find(y-tag[i])!=val[i].end()){
            for(int j=i*blk;j<min(n,(i+1)*blk);j++){
                if(a[j]==y-tag[i]){
                    mn=j;
                    break;
                }
            }
            break;
        }
    }
    for(int i=n/blk;i>=0;i--){
        if(val[i].find(y-tag[i])!=val[i].end()){
            for(int j=min(n-1,(i+1)*blk-1);j>=i*blk;j--){
                if(a[j]==y-tag[i]){
                    mx=j;
                    break;
                }
            }
            break;
        }
    }
    return mn<=mx?mx-mn:-1;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>q;
    for(int i=0;i<n;i++){
        cin>>a[i];
        val[i/blk].insert(a[i]);
    }
    for(int i=0;i<q;i++){
        int x;
        cin>>x;
        if(x==1){
            ll l,r,v;
            cin>>l>>r>>v;
            modify(l-1,r-1,v);
        }else{
            ll y;
            cin>>y;
            cout<<query(y)<<"\n";
        }
    }
    return 0;
}

13E

 

習題

操作分塊

有一顆\(n\)點的樹,點被編號從\(1\)到\(n\)

一開始\(1\)是紅色,其他藍色

有\(q\)筆操作會是

1.戳一個藍點把它塗成紅色

2.戳一個點問離它最近的紅點有多近

\(n\leq 10^5,q\leq 10^5\)

 

把\(k\)個操作分一塊

那每個詢問只要看到塊內的紅點和到塊外的紅點取min

塊內:做lca,可以預處理\(O(1)\)

塊外:每次把\(k\)中改到的點跑多點源bfs

 

\(O(q\times k+n\times \frac{q}{k})\)

習題

莫隊算法

把區間詢問離線+分塊

通常莫隊算法處理的問題是一些可離線的區間詢問,

而且在知道\([L,R]\)後可以很快(通常\(O(1),O(log\ n)\) )知道\([L+1,R],[L-1,R],[L,R+1],[L,R-1]\)

把詢問畫到平面上

 

\(L\)

\(R\)

在很多時候你可能會想讓\(l\leq r\)恆成立

假設現在在\((l,r)\)要到\((l',r')\)

如果可能有\(l<r<l'<r'\)就要讓\(r++\)先於\(l++\)

如果可能有\(l'<r'<l<r\)就要讓\(l--\)先於\(r--\)

 

如果你不想管這麼多的話就先讓區間變大再變小

關於區間要怎麼動

有一個長度為\(n\)的序列\(a\),\(q\)筆詢問\(l_i,r_i\)問

有多少三元組\((i,j,k)\)滿足

  • \(l_i\leq i<j<k\leq r_i​\)
  • \(a_i​=a_j​=a_k​\)

 

\(n\leq 2\times 10^5,q\leq 2\times 10^5,1\leq a_i\leq 2\times 10^5\)

code

#include<bits/stdc++.h>
#define ll long long
#define F first 
#define S second 
#define piii pair<pair<int,int>,int>
using namespace std;
const ll blk=450;
ll n,q,nowans,nowl,nowr,ans[200010],cnt[200010],a[200010];
vector<piii> query;
bool cmp(piii a,piii b){
    return a.F.F/blk==b.F.F/blk?a.F.S<b.F.S:a.F.F<b.F.F;
}
void add(int pos){
    nowans+=cnt[a[pos]]*(cnt[a[pos]]-1)/2;
    cnt[a[pos]]++;
}
void sub(int pos){
    cnt[a[pos]]--;
    nowans-=cnt[a[pos]]*(cnt[a[pos]]-1)/2;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>q;
    for(int i=0;i<n;i++){
        cin>>a[i];
    }
    for(int i=0;i<q;i++){
        int l,r;
        cin>>l>>r;
        query.push_back({{l-1,r-1},i});
    }
    sort(query.begin(),query.end(),cmp);
    int nowl=0,nowr=-1;
    
    for(auto u:query){
        auto l=u.F.F,r=u.F.S,id=u.S;
        while(nowr<r)add(++nowr);
        while(nowl>l)add(--nowl);
        while(nowr>r)sub(nowr--);
        while(nowl<l)sub(nowl++);
        ans[id]=nowans;
    }
    for(int i=0;i<q;i++){
        cout<<ans[i]<<"\n";
    }
    return 0;
}

習題

補充

帶修改莫隊

把修改也變成一個維度,然後做和普通莫隊差不多的事,把\(L\)和\(R\)每\(n^{\frac{2}{3}}\)(用算幾壓一壓)切一塊,複雜度會是\(O(n^\frac{5}{3})\)

回滾莫隊

斷了手的莫隊

通常在某些涉及min/max的問題,我們容易從\([L,R]\)推到\([L-1,R],[L,R+1]\)(推一格)但很難推到

\([L+1,R],[L,R-1]\)(縮一格),把詢問再畫到平面上就是只能往兩個方向走,我們試著做和莫隊一樣的事。

當要戳下一個詢問時會有:

1.\(L,R\)在同一塊:讓他自己額外算

2.\(L\)在同一塊:讓\(R\)向右動,然後\(L\)額外算

3.\(L\)在不同塊:重新來過

分case

習題

整體二分搜

多筆詢問全域查詢第\(k\)小的數

二分搜戳\(\leq m\)的數

分別做會\(O(QN\log C)\)

把詢問一起做

把答案區間在\([L,R]\)之間的人戳\(mid\)然後丟到\([L,mid]\)\([mid,R]\)

把二分搜一起做

多筆詢問區間查詢第\(k\)小的數

用BIT維護

 

其實差不多

改值->刪值再加值

帶修改?

CDQ分治

CDQ是一種思想,我們把\([L,R]\)切成\([L,mid]\)\([mid,R]\)然後想辦法在\([L,mid]\)轉移到\([mid,R]\)時利用一些酷酷的優超性

把點照\(x\)排序,合併時用\(y\)排再用BIT維護\(z\)

三維偏序

分塊離線

By owoovo

分塊離線

  • 132