洛谷題單 演算法2-3 分治與倍增

Showball發表於2024-12-02

洛谷題單 演算法2-3 分治與倍增

P2345 [USACO04OPEN] MooFest G 樹狀陣列

題目連結

思路

讓我們求所有兩兩之間 \(max(v_i,v_j)\times |x_i-x_j|\) 的值之和。

經典思路,考慮每個數對答案的貢獻。對於每個數 \(v\),會產生所有比 \(v\) 小的數和它的距離之和倍的

\(v\) 的貢獻。考慮如何求解這個距離之和。為了去掉絕對值,我們可以分開討論,不妨設 \(x_i>x_j\)

那麼貢獻就可以表示為:\(v_i\times(p\times x_i-sum)\)。其中 \(p\) 為所有小於 \(x_i\) 的數的個數,\(sum\) 為所有小於

\(x_i\) 的數之和。這兩部分只需要開兩個樹狀陣列即可維護。對於 \(x_i<x_j\) 的情況,同理。

注意:如果存在兩個值相同的情況,那麼正著反著樹狀陣列維護的時候就會重複計算,因此第二次計算的時候,我們只需要維護 \(v-1\) 即可。

#include<bits/stdc++.h>

using namespace std;

using i64=long long;

const int N = 5e4+10;
void Showball(){
    int n;
    cin>>n;
    vector<int> a(n),b(n),p(n);
    for(int i=0;i<n;i++){
        cin>>a[i]>>b[i];
    }
    iota(p.begin(),p.end(),0);

    sort(p.begin(),p.end(),[&](int x,int y){
        return b[x]<b[y];
    });

    vector<array<i64,2>> tr(N);

    auto add=[&](int op,int x,int v){
        for(;x<N;x+=x&-x) tr[x][op]+=v;
    };

    auto ask=[&](int op,int x){
        i64 ret=0;
        for(;x;x-=x&-x) ret+=tr[x][op];
        return ret;
    };

    i64 ans=0;
    for(int i=0;i<n;i++){
        int v=a[p[i]],id=b[p[i]];
        ans+=1LL*v*(ask(1,v)*id-ask(0,v));
        add(0,v,id);
        add(1,v,1);
    }

    tr.assign(tr.size(),{0,0});
    
    for(int i=n-1;i>=0;i--){
        int v=a[p[i]],id=b[p[i]];
        ans+=1LL*v*(ask(0,v-1)-ask(1,v-1)*id);
        add(0,v,id);
        add(1,v,1);
    }
    cout<<ans<<"\n";
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t=1;
    //cin>>t;

    while(t--){
      Showball();
    }

    return 0;
}

P1966 [NOIP2013 提高組] 火柴排隊 樹狀陣列

題目連結

首先對於最小值的情況。根據 排序不等式 , 兩列都排序後一定是最優解。

那麼我們就可以按照 \(a\) 的順序給 \(b\) 排序即可(反過來也一樣)。因為每次都只能交換

相鄰的火柴(其實就是氣泡排序)。那麼我們知道最少交換次數= 逆序對 的數量。

我們對映一下 \(a\)\(b\) 陣列相對關係,然後求逆序對即可。樹狀陣列求解一下就好。

#include<bits/stdc++.h>

using namespace std;

using i64=long long;

const int mod=1e8-3;
void Showball(){
    int n;
    cin>>n;
    vector<int> a(n+1),b(n+1);

    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) cin>>b[i];

    vector<i64> tr(n+1);
    auto add=[&](int x,int v){
        for(;x<=n;x+=x&-x) tr[x]+=v;
    };

    auto ask=[&](int x){
        i64 ret=0;
        for(;x;x-=x&-x) ret+=tr[x];
        return ret;
    };

    vector<int> p(n+1),q(n+1);
    iota(p.begin(),p.end(),0);
    iota(q.begin(),q.end(),0);

    sort(p.begin()+1,p.end(),[&](int x,int y){
        return a[x]<a[y];
    });

    sort(q.begin()+1,q.end(),[&](int x,int y){
        return b[x]<b[y];
    });    

    vector<int> t(n+1);
    for(int i=1;i<=n;i++){//t[i]表示a中排第i的數,b中排多少?
        t[p[i]]=q[i];
    }

    i64 ans=0;
    for(int i=n;i;i--){
        ans=(ans+ask(t[i]-1))%mod;
        add(t[i],1);
    }
    cout<<ans<<"\n";
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t=1;
    //cin>>t;

    while(t--){
      Showball();
    }

    return 0;
}

P7167 [eJOI2020 Day1] Fountain 單調棧+倍增

題目連結

先用單調棧維護出每個圓盤下第一個比它直徑大的圓盤。

然後倍增維護裝水量之和即可。查詢類比 \(LCA\)

#include<bits/stdc++.h>

using namespace std;

using i64=long long;

const i64 inf=1e18;
void Showball(){
    int n,q;
    cin>>n>>q;
    vector<int> d(n+2),c(n+2);
    for(int i=1;i<=n;i++) cin>>d[i]>>c[i];
    
    vector<array<i64,25>> f(n+2),g(n+2);
    for(int i=1;i<=n+1;i++){
        for(int j=0;j<=20;j++){
            g[i][j]=inf;
        }
    }
    //單調棧
    stack<i64> stk;
    for(int i=n;i;i--){
        while(stk.size()&&d[stk.top()]<=d[i]) stk.pop();
        if(stk.size()){
            f[i][0]=stk.top();
            g[i][0]=c[stk.top()];
        }else{
            f[i][0]=0;
        }
        stk.push(i);
    } 

    //倍增
    for(int j=1;(1<<j)<=n;j++){
        for(int i=1;i+(1<<j)<=n;i++){
            f[i][j]=f[f[i][j-1]][j-1];
            g[i][j]=g[i][j-1]+g[f[i][j-1]][j-1];
        }
    }
    
    auto query=[&](int r,int v)->i64{
        if(c[r]>=v) return r;
        v-=c[r];
        for(int i=20;i>=0;i--){
            if(f[r][i]&&g[r][i]<v){
                v-=g[r][i];
                r=f[r][i];
            }
        }
        return f[r][0];
    };

    while(q--){
        int r,v;
        cin>>r>>v;
        cout<<query(r,v)<<"\n";
    }

}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t=1;
    //cin>>t;

    while(t--){
      Showball();
    }

    return 0;
}

P3509 [POI2010] ZAB-Frog 單調佇列+倍增

題目連結

單調佇列維護出每個點下一次跳到的點,然後倍增處理即可。由於跳的次數已經給定了,那麼就可以用類似快速冪的寫法去寫了。

這個單調佇列稍微特殊一點,因為距離該點的距離前 \(k\) 小的點一定位於

該點兩邊,因此我們需要維護區間長度不變,頭和尾需要同時更新。

#include<bits/stdc++.h>

using namespace std;

using i64=long long;

void Showball(){
    i64 n,k,m;
    cin>>n>>k>>m;
    vector<i64> a(n+1); 
    for(int i=1;i<=n;i++) cin>>a[i];

    vector<i64> f(n+1);
    f[1]=k+1;
    i64 l=1,r=k+1;
    for(int i=2;i<=n;i++){
        while(r+1<=n&&a[i]-a[l]>a[r+1]-a[i]) l++,r++;
        if(a[i]-a[l]>=a[r]-a[i]) f[i]=l;
        else f[i]=r;
    }

    vector<i64> ans(n+1);
    iota(ans.begin(),ans.end(),0);

    //倍增
    while(m){
        if(m&1){
            for(int i=1;i<=n;i++) ans[i]=f[ans[i]];
        }
        auto nf=f;
        for(int i=1;i<=n;i++) f[i]=nf[nf[i]];
        m>>=1;
    }

    for(int i=1;i<=n;i++) cout<<ans[i]<<" ";

}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t=1;
    //cin>>t;

    while(t--){
      Showball();
    }

    return 0;
}

P4155 [SCOI2015] 國旗計劃 雙指標+倍增

題目連結

首先,對於環上問題。經典的斷環為鏈,變成線段上的問題。

因為線段不包含,按照左端點排序,右端點也是單調的。

那麼我們先考慮如何求每個 \(i\) 的下一段線段。

即找出 \(j\) 滿足 \(j=max(x|a_x.l\le a_i.r)\) 。那麼這部分就可以用

雙指標維護。

接著下來倍增即可。查詢時倍增至 \(a_j.r<a_i.l+m\) 即可。

最後答案要加上 \(2\) ,即最後一段線段和第 \(i\) 條線段。

#include<bits/stdc++.h>

using namespace std;

using i64=long long;

struct node{
    int id,l,r;
    bool operator<(const node &u)const{
        return l<u.l;
    }
};
void Showball(){
    int n,m;
    cin>>n>>m;
    vector<node> a(n<<1|1);
    for(int i=1;i<=n;i++){
        int l,r;
        cin>>l>>r;
        if(l>r) r+=m;
        a[i]=node{i,l,r};
    }   

    sort(a.begin()+1,a.begin()+n+1);
    for(int i=1;i<=n;i++){
        a[i+n].l=a[i].l+m;
        a[i+n].r=a[i].r+m;
    }

    //雙指標
    vector<array<int,25>> f(n<<1|1); 
    for(int i=1,j=1;i<=2*n;i++){
        while(j<=2*n&&a[j].l<=a[i].r) j++;
        f[i][0]=j-1;
    }

    //倍增
    for(int j=1;(1<<j)<=2*n;j++){
        for(int i=1;i+(1<<j)<=2*n;i++){
            f[i][j]=f[f[i][j-1]][j-1];
        }
    }

    auto query=[&](int id){
        i64 ret=2;
        int lim=a[id].l+m;
        for(int i=20;i>=0;i--){
            if(f[id][i]&&a[f[id][i]].r<lim){
                ret+=(1<<i);
                id=f[id][i];
            }
        }
        return ret;
    };

    vector<int> ans(n+1);
    for(int i=1;i<=n;i++){
        ans[a[i].id]=query(i);
    }

    for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t=1;
    //cin>>t;

    while(t--){
      Showball();
    }

    return 0;
}

P6648 [CCC2019] Triangle: The Data Structure 單調佇列+倍增

題目連結

本質是一個二維的RMQ問題。

\(f_{i,j,k}\) 表示從 \((i,j)\) 開始大小為 \(2^k\) 的三角形內的最大值。

那麼就可以去倍增維護。

空間會超,所以我們需要滾動最佳化一下。

發現會TLE一部分資料。因為時間複雜度暴了。

觀察到中間,我們維護的是固定區間長度的最大值。

那麼就可以單調佇列進行最佳化。

#include<bits/stdc++.h>

using namespace std;

using i64=long long;

const int N=3010;

int f[N][N][2];
int a[N][N],q[N];
void Showball(){
    int n,k;
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            cin>>a[i][j];
            f[i][j][0]=a[i][j];
        }
    }

    for(int l=1;l<=log2(k);l++){
        for(int i=1;i+(1<<l-1)<=n;i++){
            int t=i+(1<<l-1);
            int len=(1<<l-1)+1;
            int hh=1,tt=0;
            for(int j=1;j<=n;j++){
                f[i][j][l%2]=f[i][j][(l-1)%2];
                while(hh<=tt&&j-q[hh]+1>len) hh++;
                while(hh<=tt&&f[t][j][(l-1)%2]>f[t][q[tt]][(l-1)%2]) tt--;
                q[++tt]=j;
                if(j>=len) f[i][j-len+1][l%2]=max(f[i][j-len+1][l%2],f[t][q[hh]][(l-1)%2]);
            }
        }
    }

    i64 ans=0;
    for(int i=1;i+k-1<=n;i++){
        for(int j=1;j<=i;j++){
            int len=log2(k);
            int maxn=f[i][j][len%2];
            int t=i+k-(1<<len);
            for(int u=j;u<=j+k-(1<<len);u++){
                maxn=max(maxn,f[t][u][len%2]);
            }
            ans+=maxn;
        }
    }

    cout<<ans<<"\n";
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int ttt=1;
    //cin>>t;

    while(ttt--){
      Showball();
    }

    return 0;
}

P7562 [JOISC 2021 Day4] イベント巡り 2 (Event Hopping 2) 思維+倍增

題目連結

\(N\) 條線段,選 \(K\) 條,它們互不相交(但可以在端點相連)。輸出字典序最小的選擇方案。

因為要輸出字典序最小的方案。考慮遍歷每個區間,如果該區間能選就一定要選。

因此問題的關鍵為 如何判斷一個區間能不能選

1.判斷一個區間與之前已經選擇的區間是否有衝突(交叉或者包含)

2.判斷選擇一個區間之後,剩下的區間可選數量能確保一共夠 \(k\)

對於第一個問題:

我們可以用 \(set\) 來維護空閒的區間,定義結構體,並過載運算子。

struct node{
    int l,r;
    bool operator<(const node &u)const{
        return r<u.l;
    }
};

這樣我們就可以用 find 函式查到與當前區間 [x,y]相交的區間,然後再判斷是否完全包含即可。包含的區間為 [l,r] 。如果包含,那麼我們就在 set 中刪除 [l,r] ,並且新增 [l,x][y,r] 這兩個區間。

對於第二個問題:

我們用 calc(l,r) 表示在 [l,r] 區間中可選區間的最大數量。令 cur 表示當前可選區間數量。

那麼每次我們需要去維護 curcur=cur-calc(l,r)+calc(l,x)+calc(y,r)

然後判斷剩餘可選區間數量 + 已選區間數量是否 \(\geq k\) 即可。

那麼如何去求 \(calc(l,r)\) 呢?倍增即可,具體參考程式碼。

#include<bits/stdc++.h>

using namespace std;

using i64=long long;

const int  N=1e5+10;
struct node{
    int l,r;
    bool operator<(const node &u)const{
        return r<u.l;
    }
};
int f[N<<1][25];
void Showball(){
    int n,k;
    cin>>n>>k;
    vector<int> l(n+1),r(n+1),rk;
    for(int i=1;i<=n;i++){
        cin>>l[i]>>r[i];
        rk.push_back(l[i]);
        rk.push_back(r[i]);
    }
    
    //離散化
    map<int,int> pos;
    sort(rk.begin(),rk.end());
    rk.erase(unique(rk.begin(),rk.end()),rk.end());
    int m=rk.size();
    for(int i=0;i<m;i++) pos[rk[i]]=i+1;
    for(int i=1;i<=n;i++){
        l[i]=pos[l[i]];
        r[i]=pos[r[i]];
    }

    //倍增
    memset(f,0x3f,sizeof f);
    for(int i=1;i<=n;i++){
        f[l[i]][0]=min(f[l[i]][0],r[i]);
    }

    for(int i=m;i>=1;i--){
        f[i][0]=min(f[i][0],f[i+1][0]);
    }
    
    for(int j=1;(1<<j)<=n;j++){
        for(int i=1;i<=m;i++){
            if(f[i][j-1]<=m)
            f[i][j]=f[f[i][j-1]][j-1];
        }
    }

    auto calc=[&](int l,int r){
        int u=l,ret=0;
        for(int i=20;i>=0;i--){
            if(f[u][i]<=r){
                ret+=(1<<i);
                u=f[u][i];
            }
        }
        return ret;
    };

    set<node> st;
    st.insert(node{1,m});
    int cur=calc(1,m);

    vector<int> ans;
    for(int i=1;i<=n;i++){
        int x=l[i],y=r[i];
        auto it=st.find(node{x,y});
        if(it==st.end()) continue;
        auto [L,R]=*it;
        //區間不包含或者選了i之後剩餘的區間不夠選足k個
        if(L<=x&&y<=R){
            if(cur-calc(L,R)+calc(L,x)+calc(y,R)>=k-ans.size()-1){
                cur=cur-calc(L,R)+calc(L,x)+calc(y,R);
                ans.push_back(i);
                st.erase(it);
                st.insert(node{L,x});
                st.insert(node{y,R});
            }
        }
        if(ans.size()==k){
            for(auto v:ans) cout<<v<<"\n";
            return;
        }
    }
    cout<<"-1";
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t=1;
    //cin>>t;

    while(t--){
      Showball();
    }

    return 0;
}

相關文章