dsfz2026 DS

DS 专题

CF264E Roadside Trees*

又看漏题了。需要注意到 \(1 \leq x_i,h_i \leq 10\)

首先将 \(h_i \mapsto h_i-t_i\)\(t_i\) 是插入时间。这样就可以忽略掉生长。

然后我们考虑如何做 LIS,经典的方法是 DP,设 \(f_i\)\([1,i]\) 的 LIS 或者是 LIS=i 的最小前缀。

考虑到删除时前面少后面多的性质(\(x_i \leq 10\)),我们不应该直接使用传统定义,可以略做修改变成 \(f_i\) 表示 \([i,n]\) 的 LIS。这样删除时就只需要修改 \(10\) 个位置,可以暴力做。

还要考虑怎么插入。插入有 \(h_i \leq 10\) 的性质,也就是说一个点在插入部分最多被影响 \(10\) 次。

所以我们发现我们每次暴力更新可能被更新的位置就是对的。

于是就可以做了。我们可以维护序列 \(f_i\)\(g_i\)\(g_i\) 表示值为 \(i\) 的下标 \(p\) 对应的 \(f_p\),可以线段树维护 \(f\),平衡树维护 \(g\)。但是由于任意时刻不会有高度完全相同的两棵树,所以 \(g\) 也可以线段树。

代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5,M=800040;
int n,m,h[100005];
namespace DS{
    int f[M+5],g[M+5];
    void ins(int pos,int vl,int *v,int x=1,int l=1,int r=2e5+10){
        if(l==r)return v[x]=vl,void(0);
        int mid=(l+r)>>1;
        if(pos<=mid)ins(pos,vl,v,x<<1,l,mid);
        else ins(pos,vl,v,x<<1|1,mid+1,r);
        v[x]=max(v[x<<1],v[x<<1|1]);
    }
    int que(int L,int R,int *v,int x=1,int l=1,int r=2e5+10){
        if(L>R)return 0;
        if(L<=l&&r<=R)return v[x];
        int mid=(l+r)>>1,res=0;
        if(L<=mid)res=max(res,que(L,R,v,x<<1,l,mid));
        if(mid<R)res=max(res,que(L,R,v,x<<1|1,mid+1,r));
        return res;
    }
    set<int> ps;
    int col[200015];

    void Ins(int p,int H){
        ps.insert(p);col[h[p]=H]=p;
        vector<int> cl;
        for(int i=H;i>=max(1,H-10);i--)if(col[i])cl.push_back(col[i]);
        for(auto ed:cl)ins(ed,0,f),ins(h[ed],0,g);
        for(auto ed:cl){
            int fp=que(ed+1,n,f)+1;
            ins(ed,fp,f),ins(h[ed],fp,g);
        }
    }
    void Del(int x){
        auto it=ps.begin();vector<int> cl;
        for(int i=1;i<x;i++,it++)cl.push_back(*it),ins(*it,0,f),ins(h[*it],0,g);
        reverse(cl.begin(),cl.end());
        ins(*it,0,f),ins(h[*it],0,g);col[h[*it]]=0;ps.erase(it);
        for(auto ed:cl){
            int fp=que(h[ed]+1,2e5+10,g)+1;
            ins(ed,fp,f),ins(h[ed],fp,g);
        }
    }
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    for(int t=1;t<=m;t++){
        int op,p,h;
        cin>>op>>p;
        if(op==1){
            cin>>h;
            DS::Ins(p,h-t+2e5);
        }
        else{
            DS::Del(p);
        }
        cout<<DS::que(1,n,DS::f)<<'\n';
    }
}

CF1051G Distinctification*

注意到答案为 \(\sum(a'-a)b=\sum a'b-\sum ab\),我们只需要求最小的 \(\sum a'b\) 即可。

考虑到修改条件是 \(a_i=a_j\)\(a_i=a_j+1\),启发我们把一堆 \(a\) 连续段提出来看。首先是不能突破下限,因为没有 \(a_j=\min{a}-1\)。然后可以一直往上摊(变大),可以知道最后一定是 \([\min a,\min a + cnt - 1]\)

然后我们发现可以交换 \(a\) 相邻的 \(b\),先把大的减下来再把小的升上去。于是可以知道一段内的 \(b\) 可以随意排列,肯定是大的 \(b\) 配小的 \(a\)

考虑一段的最小 \(a\)\(w=\min{a}\),这一段的贡献是 \(\sum (w+rk-1) b=(w-1)\sum b+\sum rkb\)

然后考虑怎么插入。插入一定可以视为以下两种行为:

  • 给某一段插入一个 \(a+cnt\)(或者新建一个段 \(a\)
  • 合并两个不交的连续段。

为了给一个连续段插入一个新的 \(b\),可以用线段树维护 \(cnt,\sum b,\sum rkb\),然后就可以很简单的维护出答案。然后第二个行为也可以用线段树合并做,由于我们的信息相当于只来源于最底层,只需要合并最底层,上面节点的信息可以用 pushup 更新。由于不存在相同的 \(b\),底层的信息合并也很简单。

于是就做完了。

代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5;
int n,ans1,ans2;
namespace Seg{
    namespace sgt{
        const int M=N*20;
        int v[M+5],iv[M+5],sz[M+5],ch[M+5][2];
        int cnt;
        vector<int> bin;
        int newnode(){
            int x;
            if(!bin.empty())x=bin.back(),bin.pop_back();
            else x=++cnt;
            v[x]=iv[x]=sz[x]=ch[x][0]=ch[x][1]=0;
            return x;
        }
        void pushup(int x){
            v[x]=v[ch[x][0]]+v[ch[x][1]];
            iv[x]=iv[ch[x][0]]+iv[ch[x][1]]+sz[ch[x][1]]*v[ch[x][0]];
            sz[x]=sz[ch[x][0]]+sz[ch[x][1]];
        }
        void ins(int pos,int &x,int l,int r){
            if(!x)x=newnode();
            if(l==r)return v[x]=l,iv[x]=l,sz[x]=1,void(0);
            int mid=(l+r)>>1;
            if(pos<=mid)ins(pos,ch[x][0],l,mid);
            else ins(pos,ch[x][1],mid+1,r);
            pushup(x);
        }
        void merge(int &x,int y){
            if(!x||!y)return x=x|y,void(0);
            merge(ch[x][0],ch[y][0]);
            merge(ch[x][1],ch[y][1]);
            pushup(x);bin.push_back(y);
        }
    }
    int a[N+5],len[N+5],b[N+5],tot;
    struct met{int id,a;};
    bool operator<(met x,met y){
        if(x.a!=y.a)return x.a<y.a;
        return x.id<y.id;
    }
    set<met> seg;
    void ins(int ai,int bi){
        met nw;
        auto ed=seg.upper_bound({(int)1e9,ai});
        if(ed!=seg.begin()){
            met lst=*prev(ed);seg.erase(lst);
            if(lst.a+len[lst.id]<ai){
                seg.insert(lst);
                nw={++tot,ai};a[tot]=ai,len[tot]=1;
                sgt::ins(bi,b[tot],1,n);
            }
            else{
                ans1-=(lst.a-1)*sgt::v[b[lst.id]]+sgt::iv[b[lst.id]];
                nw=lst;len[nw.id]++;sgt::ins(bi,b[nw.id],1,n);
            }
        }
        else{
            nw={++tot,ai};a[tot]=ai,len[tot]=1;
            sgt::ins(bi,b[tot],1,n);
        }
        ed=seg.upper_bound(nw);
        if(ed!=seg.end()){
            met nxt=*ed;seg.erase(nxt);
            if(nw.a+len[nw.id]==nxt.a){
                ans1-=(nxt.a-1)*sgt::v[b[nxt.id]]+sgt::iv[b[nxt.id]];
                sgt::merge(b[nw.id],b[nxt.id]);
                len[nw.id]+=len[nxt.id];
            }
            else seg.insert(nxt);
        }
        seg.insert(nw);
        ans1+=(nw.a-1)*sgt::v[b[nw.id]]+sgt::iv[b[nw.id]];
    }
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++){
        int a,b;
        cin>>a>>b;
        Seg::ins(a,b);ans2+=a*b;
        cout<<ans1-ans2<<'\n';
    }
}

CF555C Case of Chocolate

随便写吧。

CF803G Periodic RMQ Problem

每个块维护左右两端推平,区间最值。随便写吧。

CF1004F Sonya and Bitwise OR

bitwise OR 有单调性吧。如果对于每个 \(l\) 维护出最小 \(r\) 那么回答就可以在 \(O(\log{n})\) 完成(先二分出最大的 \(l\) 满足 \(r \leq R\),然后就是形如 \(kR-\sum l\) 的询问了)。

然后考虑单点修改之后怎么更新 \(r_l\)。要更新的都是 \(l \leq i \leq r_l\) 的,更新完之后也一定有 \(l \leq i \leq r_l\)。可以按 \(a_l \operatorname{or} \cdots \operatorname{or} a_i=k\) 分组,只有不超过 \(O(\log{V})\) 个组,因为 bitwise OR 有包含性。组内的结果都是一样的,\(O(\log{n})\) 更新即可。

CF1555E Boring Segments

当我们确定答案 \(\delta\) 和最小值 \(d\) 之后,可以知道是无脑将 \([d,d+\delta]\) 加入。

我们考虑对于 \(d\) 答案是 \(\delta_d\),那么 \(\delta_d \leq \delta_{d+1}\)

这是一个明显的 two-pointer,用线段树区间加维护单点被几个线段覆盖,维护区间最小值看看是否存在不被覆盖的位置即可。

注意不能有一半满足 \(r \leq i\) 一半满足 \(i+1 \leq l\) 的情况,所以应该维护边 \((i,i+1)\) 是否被覆盖。

CF1784C Monsters (hard version)

假设当前怪物血量的集合(要去重)为 \(\{1,2,\cdots,k,\geq k+2\}\),释放 2 之后就只剩下 \(\geq k+2\),且每个变成了 \(x-k-1\)

要尽可能的让血是被 2 扣掉的。那我们将怪物血量排序,若当前值 \(x \leq k+1\) 就不管,否则就把当前值改成 \(k+1\)。然后就对完了。

但是这又是一个前缀在线(插入)问题。我们直接对于每个值记录把这个值加入完之后对应的 \(k\) 是多少。

如果 \(a\) 是第一次加入的那么 \(a\) 肯定有位置,直接设为 \(k_{prev(a)}+1\)。考虑对更大值的影响,有一段 \(k_x<x\) 的直接加一,后面的都不变了。

如果 \(a\) 不是第一次加入,那么就要考虑 \(k_a=a\) 就没有任何变化。否则 \(k_a \mapsto k_a+1\),剩下的和上面一样了。

然后考虑代价的变化。\(a\) 产生 \(a-k\) 的新代价,那些 \(k+1\) 的位置(除了 \(a\))获得 \(-cnt\) 的减免。

于是我们就线段树维护 \(cnt\)\(a-k\) 的值,维护区间 \(a-k\) 最小值和区间 \(cnt\) 和。

CF1667B Optimal Partition

考虑划分完之后对于和为 \(s\) 的区间,一个点的贡献是 \(L(s)\),其中:

\[L(s)=\begin{cases}(s<0) & -1\\(s=0) & 0\\(s>0) & 1\end{cases} \]

先做前缀和得到 \(\{s_n\}\),考虑 \(f_i\) 表示 \([1,i]\) 的答案,有转移:

\[f_i = \max_{j<i}f_j+L(s_i-s_j)(i-j) \]

发现转移几乎可以说只和 \(L(s_i-s_j)\) 有关。于是我们开两颗线段树根据 \(s_j\) 的值分别维护 \(a_k=\max_{s_j=k} f_j-j\)\(b_k=\max_{s_j=k} f_j+j\) 的最大值即可。

然后其实还有从所有 \(s_j=s_i\) 的地方进行 \(f_i=\max f_j\) 的转移。记录一下 \(s_i=k\) 处的最大值即可。

posted @ 2026-01-19 15:43  Aysct  阅读(0)  评论(0)    收藏  举报