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)\),其中:
先做前缀和得到 \(\{s_n\}\),考虑 \(f_i\) 表示 \([1,i]\) 的答案,有转移:
发现转移几乎可以说只和 \(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\) 处的最大值即可。

浙公网安备 33010602011771号