lxl 数据结构(一)(1)

lxl 数据结构(一)

2023.11.23-2023.12

讲课人:lxl


前言

这篇学习笔记也是写了半年之久了吧,虽然总是断断续续。

这是(一),很快在七月份也有二推出。

这些日子里面,我在英才集训时也分享过一次扫描线与根号数据结构,而到如今仍有一些题没有补完。

或许我应该好好补补了。

……

这半年来,我的码风、精神状态其实都有好一些变化,在写博客挂链接上面也大有区别。


Day 1 线段树+平衡树-颜色段均摊

lxl 讲得让我听懂了!/bx

还惊现了两道我之前讲平衡树时涉及到的题目。

思路都挺巧妙的,但是代码难度想都不敢想。


Keynote

在线段树题目的时候考虑区间合并的维护,

首先画出线段树和分治中心,把我们想维护的列出来。

不断往下找,构成一些依赖关系,使得最后得到了闭包就可以了。

这样在实现的时候会好写很多。

下面的图是 P7706 的分析过程,也是这个技巧的很好的方法。

image


关于均摊问题的时间复杂度分析,

首先对于均摊问题我们考虑构造一些简单的方法去找是不是有一种情况使得经过若干次操作之后回到了初始局面,

那么这样的操作是不好的,所以在实现的时候我们考虑直接打上区间的标记即可。

(eg:P5066,PKUSC2021D1T2)


前置题目 P4198 楼房重建

下面有一道题会用到,但是我不会。。。

有一排楼,每次把一个位置的楼的高度修改为 \(x\),每次输出可以从最左边看到的楼个数
形式化来说,给一个序列 \(a\),每次修改一个位置的值,查询有多少个位置 \(i\) 满足 \([1,i-1]\) 里的所有 \(j\),都有 \(a[j] \lt a[i]\)

\(1 \le n \le 10^5\)

首先可以发现去维护每一个斜率,

那么题目就转化成了去询问有多少个全局的前缀最大值。

那么难点就在于 pushup 部分,

合并两个区间的时候,左子树是一定不会变的,

而对于右边的,我们考虑递归去处理,找到第一个 \(\ge mx\) 的值即可,其中 \(mx\) 是这里左区间的最大值。

这样就可以得到一个 \(O(n \log^2 n)\) 的做法。

Code
#include <bits/stdc++.h>
using namespace std;
#define db double

const int N=1e5+5;
int n,m;
db a[N];
struct sgt{
  int cnt;
  db mx;
}tr[N<<2];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

int find(int l,int r,int p,db x){
  if(tr[p].mx<=x) return 0;	
  if(a[l]>x) return tr[p].cnt;
  if(l==r) return tr[p].mx>x;
  if(tr[lc].mx>x) return find(lson,x)+tr[p].cnt-tr[lc].cnt;
  return find(rson,x);
}

void pu(int l,int r,int p){
  tr[p].mx=max(tr[lc].mx,tr[rc].mx);
  tr[p].cnt=find(rson,tr[lc].mx)+tr[lc].cnt;
}

void upd(int l,int r,int p,int x,int y){
  if(l==r){
  	tr[p].cnt=1;
  	tr[p].mx=1.0*y/x;
  	return;
  }
  if(x<=mid) upd(lson,x,y);
  else upd(rson,x,y);pu(l,r,p);
}

int main(){
  /*2023.11.23 H_W_Y P4198 楼房重建 SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1,x,y;i<=m;i++){
  	cin>>x>>y;a[x]=1.0*y/x;
	  upd(1,n,1,x,y);
  	cout<<tr[1].cnt<<'\n';
  } 
  return 0;
}

P7706 复杂信息合并 - SGT

P7706 「Wdsr-2.7」文文的摄影布置

尽管图片非常多,但幸运的是,文文已经将它们排成了一列,从左到右分别编号为 \(1 \sim n\),文文选取的三张图片,应该是一个长度为 \(\bf 3\) 的子序列。(不妨设选取的照片的序号为 \(i,j,k\) ,则必须要有 \(i<j<k\) )。

此外,文文给每张照片定了一个吸引度 \(A_i\)大小 \(B_i\)

因为报纸版面太大会降低读者的兴趣,于是选定两张照片 \(i,k\) 后,规定必须选择最小的 \(B_j\)

形式化地说,规定 \(\psi(i,k) = A_i + A_k - \min(B_j)\),其中需要满足 \(i < j < k\)

摸清了照片价值的计算,文文会告诉你共 \(m\) 个操作,可以分为以下三种:

  • 1 l r:照片的吸引度发生变化。文文要将 \(A_x\) 修改为 \(y\)

  • 2 l r:照片的大小发生变化。文文要将 \(B_x\) 修改为 \(y\)

  • 3 l r :文文打算利用素材库的第 \(l\) 到第 \(r\) 张中的图片,你要告诉她 \(\psi(x,y)\)最大值\(l\le x\le x+1<y \le r\) )。

\(1 \le n,m \le 5 \times 10^5\)


首先我们把问题转化成在区间中选三个数 \(i,j,k\),我们需要 \(i \le j \le k,A_i-B_j+A_k\) 最大,

由于修改是单点的,所以我们只用考虑区间合并的时候。

这个时候我们用到上面讲的小技巧,

这里的答案可以是左区间的,右区间的和两个区间合并在一起,

于是我们就需要维护更多的信息,就构成了下图的依赖关系。(没画完,另外一种情况本质相同)

image

于是关系画出来就很好做了,这种方法能帮我们解决很多问题。


一发过哩~

Code
#include <bits/stdc++.h>
using namespace std;

const int N=5e5+5,inf=2e9;
int n,m,a[N],b[N];
struct sgt{
  int a,b,pre,suf,ans;
}tr[N<<2];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1

sgt merge(sgt l,sgt r){
  sgt res;
  res.a=max(l.a,r.a);
  res.b=min(l.b,r.b);
  res.pre=max(max(l.pre,r.pre),l.a-r.b);
  res.suf=max(max(l.suf,r.suf),r.a-l.b);
  res.ans=max(max(l.ans,r.ans),max(l.a+r.suf,l.pre+r.a));	
  return res;
}

void pu(int p){tr[p]=merge(tr[lc],tr[rc]);}

void upd(int l,int r,int p,int x,int op,int val){
  if(l==r){
  	if(op==0) tr[p].a=val;
  	else tr[p].b=val;
  	return;
  }
  if(x<=mid) upd(lson,x,op,val);
  else upd(rson,x,op,val);pu(p);
}

void build(int l,int r,int p){
  tr[p].a=tr[p].ans=tr[p].pre=tr[p].suf=-inf;tr[p].b=inf;
  if(l==r){tr[p].a=a[l];tr[p].b=b[l];return;}
  build(lson);build(rson);pu(p);
}

sgt qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p];
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return merge(qry(lson,x,y),qry(rson,x,y));
}

int main(){
  /*2023.11.23 H_W_Y P7706 「Wdsr-2.7」文文的摄影布置 SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1;i<=n;i++) cin>>b[i];
  build(1,n,1);
  for(int i=1,op,x,y;i<=m;i++){
  	cin>>op>>x>>y;
  	if(op==1) upd(1,n,1,x,0,y);
  	if(op==2) upd(1,n,1,x,1,y);
  	if(op==3) cout<<qry(1,n,1,x,y).ans<<'\n';
  } 
  return 0;
}

P3215 复杂标记 - Balance Tree

P3215 [HNOI2011] 括号修复 / [JSOI2011]括号序列

这么和我讲的题撞了呢?还没人补题。(我之前还写过题解,直接贺过来,和 lxl 讲的本质相同)

现在给你一个长度为 \(n\) 的由()组成的字符串,位置标号从 \(1\)\(n\)。对这个字符串有下列四种操作:

  • Replace a b c:将 \([a,b]\) 之间的所有括号改成 \(c\)。假设原来的字符串为:))())())(,那么执行操作 Replace 2 7 ( 后原来的字符串变为:)(((((()(

  • Swap a b:将 \([a,b]\) 之间的字符串翻转。假设原来的字符串为:))())())(,那么执行操作 Swap 3 5 后原来的字符串变为:))))(())(

  • Invert a b:将 \([a,b]\) 之间的 ( 变成 )) 变成 (。假设原来的字符串为:))())())(,那么执行操作 Invert 4 8 后原来的字符串变为:))((()(((

  • Query a b:询问 \([a,b]\) 之间的字符串至少要改变多少位才能变成合法的括号序列。改变某位是指将该位的 ( 变成 )) 变成 (。注意执行操作 Query 并不改变当前的括号序列。假设原来的字符串为:))())())(,那么执行操作 Query 3 6 的结果为 \(2\),因为要将位置 \(5\))变成(并将位置 \(6\)(变成)

对于 \(100\%\) 的数据,\(1\le n,q \le 10^5\)

问题主要在于 query 时的查询,

我们钦定 ( 记作 -1) 记作 1 ,于是我们只需要维护前缀最大值 \(prmx\) 和后缀最小值 \(sfmn\) ,那么一个区间的答案就是:

\[\lceil prmx/2 \rceil+\lceil sfmx/2 \rceil \]

画画图就很容易证明。


于是现在考虑具体操作:

  1. 每个节点需要记录那些信息:区间和,前缀最大最小,后缀最大最小
  2. 需要哪些标记:翻转标记,覆盖标记,取反标记
  3. 如何下传标记:翻转(直接像文艺平衡树),覆盖(把区间维护的信息都改变),取反(前后缀最大最小交换,\(prmx\to-prmn,prmn\to -prmax\)
  4. 区间整体修改?把前驱后继分别转到根和根的右儿子。
  5. 合并区间?枚举是否有左右儿子即可。

为什么有那么长的代码???

注意前面有一个哨兵,所以转的时候是转 \(l,r+2\)

Code
#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5;
int n,m,cnt=0,rt=0,a[N];
struct Splay{
  int fa,siz,sum,val,prmx,prmn,sfmx,sfmn,s[2],inv,rev,cov;
}tr[N];

#define lc(p) tr[p].s[0]
#define rc(p) tr[p].s[1]

void pu(int p){
  tr[p].siz=tr[lc(p)].siz+tr[rc(p)].siz+1;
  tr[p].sum=tr[lc(p)].sum+tr[rc(p)].sum+tr[p].val;
  tr[p].prmx=max(tr[lc(p)].prmx,tr[rc(p)].prmx+tr[lc(p)].sum+tr[p].val);
  tr[p].prmn=min(tr[lc(p)].prmn,tr[rc(p)].prmn+tr[lc(p)].sum+tr[p].val);
  tr[p].sfmx=max(tr[rc(p)].sfmx,tr[rc(p)].sum+tr[p].val+tr[lc(p)].sfmx);
  tr[p].sfmn=min(tr[rc(p)].sfmn,tr[rc(p)].sum+tr[p].val+tr[lc(p)].sfmn);
} 

void cov(int p,int val){
  if(!p) return;
  tr[p].cov=val;
  if(val==1){
  	tr[p].sum=tr[p].siz;
  	tr[p].prmn=tr[p].sfmn=0;	
	tr[p].prmx=tr[p].sfmx=tr[p].sum;
  }
  else{
  	tr[p].sum=~tr[p].siz+1;
  	tr[p].prmx=tr[p].sfmx=0;
  	tr[p].prmn=tr[p].sfmn=tr[p].sum;
  }
  tr[p].val=val;
  tr[p].inv=0;
}

void rev(int p){
  if(!p) return;
  tr[p].rev^=1;
  swap(lc(p),rc(p));
  swap(tr[p].prmx,tr[p].sfmx);
  swap(tr[p].prmn,tr[p].sfmn);
}

void inv(int p){
  if(!p) return;
  if(tr[p].cov!=0){
  	if(tr[p].cov==1) cov(p,-1);
  	else cov(p,1);
  	return;
  }
  tr[p].inv^=1;
  int t=tr[p].prmx;
  tr[p].prmx=~tr[p].prmn+1;
  tr[p].prmn=~t+1;
  t=tr[p].sfmx;
  tr[p].sfmx=~tr[p].sfmn+1;
  tr[p].sfmn=~t+1;
  tr[p].sum=~tr[p].sum+1;
  tr[p].val=~tr[p].val+1;
}

void pd(int p){
  if(tr[p].cov!=0){
  	cov(lc(p),tr[p].cov);
  	cov(rc(p),tr[p].cov);
  	tr[p].cov=0;
  }
  if(tr[p].inv!=0) inv(lc(p)),inv(rc(p)),tr[p].inv=0;
  if(tr[p].rev!=0) rev(lc(p)),rev(rc(p)),tr[p].rev=0;
}

void rotate(int x){
  int y=tr[x].fa,z=tr[y].fa;
  int k=(rc(y)==x);
  pd(y);pd(x);
  tr[y].s[k]=tr[x].s[k^1];
  tr[tr[x].s[k^1]].fa=y;
  tr[x].s[k^1]=y;
  tr[y].fa=x;
  tr[z].s[(rc(z)==y)]=x;
  tr[x].fa=z;
  pu(y);pu(x); 
}

void splay(int x,int k){
  while(tr[x].fa!=k){
  	int y=tr[x].fa,z=tr[y].fa;
  	if(z!=k) ((rc(y)==x)^(rc(z)==y))?rotate(x):rotate(y);
  	rotate(x);
  }
  if(k==0) rt=x;
}

int find(int k){
  int p=rt;
  while(k){
  	pd(p);
  	if(k<=tr[lc(p)].siz) p=lc(p);
  	else if(k>tr[lc(p)].siz+1) k-=tr[lc(p)].siz+1,p=rc(p);
  	else break;
  }
  splay(p,0);
  return p;
}

void invert(int l,int r){
  int x=find(l),y=find(r+2);
  splay(x,0);splay(y,x);
  inv(lc(y));
  pu(y);pu(x);
}

void reverse(int l,int r){
  int x=find(l),y=find(r+2);
  splay(x,0);splay(y,x);
  rev(lc(y));
  pu(y);pu(x);
}

void replace(int l,int r,int val){
  int x=find(l),y=find(r+2);
  splay(x,0);splay(y,x);
  cov(lc(y),val);
  pu(y);pu(x);
}

int query(int l,int r){
  int x=find(l),y=find(r+2);
  splay(x,0);splay(y,x);
  x=lc(y);
  return ((tr[x].prmx+1)/2-(tr[x].sfmn-1)/2);
}

#define mid ((l+r)>>1)

int build(int l,int r,int fa){
  if(l>r) return 0;
  int p=++cnt;
  tr[p].val=tr[p].sum=a[mid];
  tr[p].siz=1;tr[p].fa=fa;
  tr[p].s[0]=build(l,mid-1,p);
  tr[p].s[1]=build(mid+1,r,p);
  pu(p);return p; 
} 

void dfs(int p){
  if(!p) return;
  pd(p);
  dfs(lc(p));
  printf("%d:%d %d->%d %d\n",p,lc(p),rc(p),tr[p].val,tr[p].siz);
  printf("%d %d %d %d %d\n",tr[p].sum,tr[p].prmx,tr[p].prmn,tr[p].sfmx,tr[p].sfmn);
  dfs(rc(p));
}

int main(){
  /*2023.10.23 H_W_Y P3215 [HNOI2011] 括号修复 / [JSOI2011]括号序列 Splay*/
  scanf("%d%d",&n,&m);
  for(int i=1;i<=n;i++){
  	char s=getchar();
  	while(s!='('&&s!=')') s=getchar();
  	if(s=='(') a[i]=-1;
  	else a[i]=1;
  }
  rt=build(0,n+1,0);
  for(int i=1;i<=m;i++){
  	char s[15];int l,r;
    scanf("%s",s);
    scanf("%d%d",&l,&r);
    if(s[0]=='R'){
      char ch;ch=getchar();
      while(ch!='('&&ch!=')') ch=getchar();
      replace(l,r,(ch=='(')?-1:1);
	}
	if(s[0]=='I') invert(l,r);
	if(s[0]=='S') reverse(l,r);
	if(s[0]=='Q') printf("%d\n",query(l,r));
  }
  return 0;
}

P7739 复杂标记 - Balance Tree

P7739 [NOI2021] 密码箱

Yelekastee 是 U 国著名的考古学家。在最近的一次考古行动中,他发掘出了一个远古时期的密码箱。经过周密而严谨的考证,Yelekastee 得知密码箱的密码和某一个数列 \(\{ a_n \}\) 相关。数列 \(\{ a_n \}\) 可以用如下方式构造出来:

  1. 初始时数列长度为 \(2\) 且有 \(a_0 = 0, a_1 = 1\)
  2. 对数列依次进行若干次操作,其中每次操作是以下两种类型之一:
  • W 类型:给数列的最后一项\(1\)
  • E 类型:若数列的最后一项\(1\),则给倒数第二项加 \(1\);否则先给数列的最后一项\(1\),接着在数列尾再加两项,两项的值都是 \(1\)

受到技术限制,密码箱并没有办法完整检查整个数列,因此密码箱的密码设定为数列 \(\{ a_n \}\) 经过函数 \(f\) 作用后的值,其中 \(f\) 的定义如下:

\[f(a_0, \ldots , a_{k - 1}, a_k) = \begin{cases} a_0, & k = 0 \\ f \! \left( a_0, a_1, \ldots , a_{k - 2}, a_{k - 1} + \frac{1}{a_k} \right) \! , & k \ge 1 \end{cases} \]

Yelekastee 并不擅长运算,因此他找到了你,希望你能根据他提供的操作序列计算出密码箱的密码。不幸的是,他的记性并不是很好,因此他会随时对提供的操作序列做出一些修改,这些修改包括以下三种:

  • APPEND c,在现有操作序列后追加一次 c 类型操作,其中 c 为字符 WE
  • FLIP l r,反转现有操作序列中第 \(l\) 个至第 \(r\) 个(下标从 \(1\) 开始,修改包含端点 \(l\)\(r\),下同)操作,即所有 W 变为 E,所有 E 变为 W
  • REVERSE l r,翻转现有操作序列中第 \(l\) 个至第 \(r\) 个操作,也就是将这个区间中的操作逆序。

对于所有测试点:\(1 \le n \le {10}^5\)\(1 \le q \le {10}^5\)

lxl:其实大赛出的题也是很板的。

看到这里的 reverse 和 filp 就和上一道题基本一样,直接用平衡树维护即可。


其实也没有想象那么简单——毕竟是 NOI D2T2,我们需要推一推矩阵。


直接平衡树 split 没有 pu,平衡树一定不要写假了!

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
mt19937 rd(time(0));

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void print(ll x,char ch){
  int p[25],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x) p[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
  putchar(ch);
}

const int N=1e6+5;
const ll mod=998244353;
int n,m,idx=0,rt,x,y,z;
struct mat{
  ll a00,a01,a10,a11;
  mat operator *(const mat &b) const{
  	mat res;
  	res.a00=(a00*b.a00%mod+a01*b.a10%mod+mod)%mod;
    res.a01=(a00*b.a01%mod+a01*b.a11%mod+mod)%mod;
    res.a10=(a10*b.a00%mod+a11*b.a10%mod+mod)%mod;
    res.a11=(a10*b.a01%mod+a11*b.a11%mod+mod)%mod;
    return res;
  }
}I=(mat){1,0,0,1},mt[2]={(mat){1,1,0,1},(mat){0,-1,1,2}},ans;
struct node{mat v,inv;};
struct fhq{
  node val,frt,bck;
  int siz,key,s[2],rev,flp;
}tr[N];

#define lc(p) tr[p].s[0]
#define rc(p) tr[p].s[1]

int nwnode(int op){
  int p=++idx;
  tr[p].siz=1;tr[p].key=rd();tr[p].s[0]=tr[p].s[1]=tr[p].rev=tr[p].flp=0;
  tr[p].val=tr[p].frt=tr[p].bck=(node){mt[op],mt[op^1]};
  return p;
}

void pu(int p){
  if(!p) return;
  tr[p].siz=tr[lc(p)].siz+tr[rc(p)].siz+1;
  
  tr[p].frt.v=(tr[lc(p)].frt.v*tr[p].val.v)*tr[rc(p)].frt.v;
  tr[p].frt.inv=(tr[lc(p)].frt.inv*tr[p].val.inv)*tr[rc(p)].frt.inv;
  
  tr[p].bck.v=(tr[rc(p)].bck.v*tr[p].val.v)*tr[lc(p)].bck.v;
  tr[p].bck.inv=(tr[rc(p)].bck.inv*tr[p].val.inv)*tr[lc(p)].bck.inv;
}

void pushrev(int p){
  if(!p) return;
  swap(tr[p].bck,tr[p].frt);
  swap(tr[p].s[0],tr[p].s[1]);
  tr[p].rev^=1;
}
void pushflp(int p){
  if(!p) return;
  swap(tr[p].frt.v,tr[p].frt.inv);
  swap(tr[p].bck.v,tr[p].bck.inv);
  swap(tr[p].val.v,tr[p].val.inv);
  tr[p].flp^=1;
}

void pd(int p){
  if(tr[p].flp) pushflp(lc(p)),pushflp(rc(p)),tr[p].flp=0;
  if(tr[p].rev) pushrev(lc(p)),pushrev(rc(p)),tr[p].rev=0;
}

void split(int p,int v,int &x,int &y){
  if(!p){x=y=0;return;}pd(p);
  if(tr[lc(p)].siz+1<=v) x=p,split(rc(p),v-tr[lc(p)].siz-1,rc(p),y);
  else y=p,split(lc(p),v,x,lc(p));pu(p);
}

int merge(int x,int y){
  if(!x||!y) return x+y;
  pd(x);pd(y);
  if(tr[x].key<tr[y].key){
  	tr[x].s[1]=merge(tr[x].s[1],y);
  	pu(x);return x;
  }
  else{
  	tr[y].s[0]=merge(x,tr[y].s[0]);
  	pu(y);return y;
  }
}

void flip(int l,int r){
  x=y=z=0;
  split(rt,r,y,z);split(y,l-1,x,y);
  pushflp(y);rt=merge(x,merge(y,z));
}

void reverse(int l,int r){
  x=y=z=0;
  split(rt,r,y,z);split(y,l-1,x,y);
  pushrev(y);rt=merge(x,merge(y,z));
}
char s[100];
int main(){
  /*2023.11.29 H_W_Y P7739 [NOI2021] 密码箱 Balance Tree*/
  n=read();m=read();rt=0;tr[0].frt=tr[0].bck=tr[0].val=(node){I,I};
  for(int i=1;i<=n;i++){
  	char ch=getchar();
  	while(ch!='W'&&ch!='E') ch=getchar();
  	rt=merge(rt,nwnode(ch=='E'));
  }
  ans=mt[0]*tr[rt].frt.v;
  print(ans.a11,' ');print(ans.a01,'\n');
  for(int i=1,l,r;i<=m;i++){
  	scanf("%s",s);
  	if(s[0]=='A'){
  	  char ch=getchar();
  	  while(ch!='W'&&ch!='E') ch=getchar();
  	  rt=merge(rt,nwnode(ch=='E'));
  	}
  	if(s[0]=='F') l=read(),r=read(),flip(l,r);
  	if(s[0]=='R') l=read(),r=read(),reverse(l,r);
  	ans=mt[0]*tr[rt].frt.v;
  	print(ans.a11,' ');print(ans.a01,'\n');
  }
  return 0;
}

P6617 Search pre小技巧 - SGT

P6617 查找 Search

给定 \(n\) 个垃圾桶,你需要维护一个数据结构,支持以下操作:

  • 1 pos val 表示将 第 \(pos\) 个垃圾桶里的垃圾的编号换成 \(val\)

  • 2 l r 询问在 \([l\oplus cnt, r\oplus cnt]\) 内是否存在垃圾编号和为 \(w\)两个 垃圾桶。

其中 \(\oplus\) 表示异或运算,\(cnt\) 表示在 此次询问之前,答案为 Yes 的个数。

对于每个操作 2,若存在请输出 Yes,不存在请输出 No

值得注意的是,对于所有询问, \(w\)同一个数

\(1 \le n,m \le 10^5\)

直接维护是很难完成区间合并的,因为我们需要维护的权值是不独立的。

那么现在我们考虑用 前驱pre 把它转化成独立的询问。


现在我们维护每一个节点前面第一个和它匹配得到 \(w\) 的数,

于是用线段树维护,区间查询的时候相当于查询这段区间的前驱最大值,

判断它是否 \(\gt l\) 即可。(这和数颜色的思想非常类似)


而对于每一次修改操作,我们现在考虑如何维护每一个颜色。

很明显可以用 set,于是每一次其实就是对 set 的中前面和后面的数改变,

但是我们需要知道连到这个点的是哪些点。


但是出现了这种情况:假设 \(w=3\)

\(1,2,2,2,2, \dots\) 的情况,发现我们每一次修改 \(1\) 的时候,会修改 \(O(n)\) 个节点,这样的时间又退回去了。


那怎么办呢?

这时我们去发现一个有用的性质:

对于两个数 \(i,j\) 的四个位置 \(i_1,i_2,j_1,j_2\),假设我们配对成了 \([i_1,j_1]\)\([i_2,j_2]\)

如果前者的区间是包含了后者的区间——

我们发现前面的是没有意义的,因为能包括前面的区间一定能包括后面的。


转化到上面的情况就是我们只需要维护第一个 \(2\) 的 pre,

这样就可以变成每次修改 \(O(1)\) 个点,于是就做完了。


代码写的是关于 nxt 的,这里用到了实现的小技巧,

我们可以在最后加入一个数 \(n+1\) 避免掉判 \(0\)

Code
#include <bits/stdc++.h>
using namespace std;

const int N=5e5+5;
int n,m,w,a[N],tr[N<<2],nxt[N],cnt=0;
set<int> g[N];

#define mid ((l+r)>>1)
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1
#define lc p<<1
#define rc p<<1|1

void pu(int p){tr[p]=min(tr[lc],tr[rc]);}

void upd(int l,int r,int p,int x){
  if(l==r) return tr[p]=nxt[l],void();
  if(x<=mid) upd(lson,x);
  else upd(rson,x);pu(p);
}

int qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p];
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return min(qry(lson,x,y),qry(rson,x,y)); 
}

void build(int l,int r,int p){
  if(l==r) return tr[p]=nxt[l],void();
  build(lson);build(rson);pu(p); 
}

int main(){
  /*2023.11.23 H_W_Y P6617 查找 Search SGT+set*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>w;
  memset(nxt,0x3f,sizeof(nxt));
  memset(tr,0x3f,sizeof(tr));a[n+1]=-w;
  for(int i=1;i<=n;i++) cin>>a[i],g[min(a[i],w-a[i])].insert(i);
  for(int i=1;i<=w/2;i++) g[i].insert(n+1);
  for(int i=1;i<=n;i++){
  	int v=min(a[i],w-a[i]);
  	auto it=g[v].upper_bound(i);
  	nxt[i]=(a[*it]+a[i]==w)?(*it):(n+1); 
  }
  
  build(1,n,1);
  for(int i=1,op,x,y;i<=m;i++){
  	cin>>op>>x>>y;
  	if(op==1){
  	  int vx=min(a[x],w-a[x]),vy=min(y,w-y);
      g[vx].erase(x);
	  auto it=g[vx].lower_bound(x);
	  if(it!=g[vx].begin()){
	  	int tmp=*(it--);
	  	nxt[*it]=(a[tmp]+a[*it]==w)?tmp:n+1;
	  	upd(1,n,1,*it);
	  }	  
	  a[x]=y;
	  g[vy].insert(x);
	  it=g[vy].upper_bound(x);
	  nxt[x]=(a[*it]+a[x]==w)?(*it):(n+1);
	  upd(1,n,1,x);
	  it=g[vy].lower_bound(x);
	  if(it!=g[vy].begin()){
	  	int tmp=*(it--);
	  	nxt[*it]=(a[tmp]+a[*it]==w)?tmp:(n+1);
	  	upd(1,n,1,*it); 
	  }
	}
	else{
	  x^=cnt,y^=cnt;
	  if(qry(1,n,1,x,y)<=y) puts("Yes"),cnt++;
	  else puts("No");
	}
  }
  return 0;
}

CF444C 颜色段均摊(板)

CF444C DZY Loves Colors

  • 有一个 \(n\) 个元素组成的序列,每个元素有两个属性:颜色 \(c_i\) 和权值 \(w_i\)\(c_i\) 初始为 \(i\)\(w_i\) 初始为 \(0\)\(m\) 次操作,操作有两种:
    1. 1 l r x:对 \(i\in [l,r]\) 的所有 \(i\) 进行如下操作:设第 \(i\) 个元素 原来 的颜色为 \(y\),您要把第 \(i\) 个元素的颜色改为 \(x\),权值 增加 \(|y-x|\)
    2. 2 l r:求 \(\displaystyle\sum_{i=l}^r w_i\)
  • \(1\le n,m\le 10^5\)\(1\le x\le 10^8\)

我们考虑每一段颜色相同的,把他们看作一个颜色段,

那么每一次修改的时候就相当于把这些颜色段合并,而每一个颜色段的贡献是很容易算出来的,

所以这个是好做的,画一个图:

image

在这个图中,每一段红色的就是一个颜色段,

我们的修改操作是绿色的一个区间。

那么中间完全覆盖的区间是可以直接删除的,两边没有覆盖完的区间需要重新插入一下,

而最后再插入一个大的绿色的区间,而贡献是直接可以在枚举的过程中用线段树维护。


这就是颜色段均摊的板子。

具体实现的时候,我们维护一个线段树和一个 set,

每次加入的时候在 set 上面 lower_bound 一下,跟着迭代器往后面跳即可。


分析一下时间复杂度。

假设一次操作我们合并了 \(t\) 个区间,那么我们额外的操作有三种:

对于最左边区间,最右边区间和新插入的绿色区间的修改。

所以颜色段均摊在这道题中是自带了 \(3\) 倍常数的,于是这道题就做完了。


其实这道题并不用 set,直接线段树做就可以了。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e5+5;
int n,m;
struct sgt{
  ll tag,s,col,vis;
}tr[N<<2];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

void pu(int p){
  tr[p].s=tr[lc].s+tr[rc].s;
  if(tr[lc].vis&&tr[rc].vis&&tr[lc].col==tr[rc].col) tr[p].vis=true,tr[p].col=tr[lc].col;
  else tr[p].vis=false,tr[p].col=0;
}

void pd(int l,int r,int p){
  tr[lc].tag+=tr[p].tag;
  tr[rc].tag+=tr[p].tag;
  tr[lc].s+=1ll*(mid-l+1)*tr[p].tag;
  tr[rc].s+=1ll*(r-mid)*tr[p].tag;
  if(tr[p].vis) tr[lc].vis=tr[rc].vis=1,tr[lc].col=tr[rc].col=tr[p].col;
  tr[p].tag=0;
}

void upd(int l,int r,int p,int x,int y,int col){
  if(x<=l&&y>=r&&tr[p].vis){
  	tr[p].tag+=1ll*abs(tr[p].col-col);
  	tr[p].s+=1ll*(r-l+1)*abs(tr[p].col-col);
  	tr[p].col=col;
  	return;
  }
  pd(l,r,p);
  if(x<=mid) upd(lson,x,y,col);
  if(y>mid) upd(rson,x,y,col);
  pu(p);
}

ll qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].s;
  pd(l,r,p);ll res=0;
  if(x<=mid) res+=qry(lson,x,y);
  if(y>mid) res+=qry(rson,x,y);
  return res;
}

void build(int l,int r,int p){
  if(l==r){
  	tr[p].s=tr[p].tag=0;
  	tr[p].vis=true;tr[p].col=l;
  	return ;
  }
  build(lson);build(rson);pu(p);
}

int main(){
  /*2023.11.23 H_W_Y CF444C DZY Loves Colors 颜色段均摊*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;build(1,n,1);
  for(int i=1,op,l,r,x;i<=m;i++){
  	cin>>op>>l>>r;
  	if(op==1) cin>>x,upd(1,n,1,l,r,x);
	else cout<<qry(1,n,1,l,r)<<'\n';
  }
  return 0;
}

CF453E 颜色段均摊 - 二维数点

CF453E Little Pony and Lord Tirek

给一个序列。
每个位置有初值 \(a_i\),最大值 \(m_i\),这个值每秒会增大 \(r_i\),直到 \(m_i\)
\(m\) 个发生时间依此增大的询问,每次询问区间和并且将区间的所有数字变成 \(0\)

\(1 \le n,m \le 10^5\)

感觉很复杂的样子。


发现每一次是区间查询并赋值,不难想到可以用颜色段均摊去维护。

假设上一次的操作时间是 \(x\),这一次是 \(y\),考虑如何维护区间信息?


这时可以分两种情况讨论,这段时间达到上界和没有达到上界的,而每一个数达到上界的时间是可以预处理出来的。

设每个点要加 \(k_i\) 次才加满,那么对于 \(k_i \gt y-x\) 的我们直接算上他们的 \(r_i\) 的和即可,

而对于 \(k_i \le y-x\) 的,我们需要加上他们的 \(m_i\) 的和。


发现这个问题就很像二维数点问题了,于是直接用二维数点维护即可。


具体实现的时候我们用主席树维护权值线段树,再用 set 做颜色段均摊就可以了。

注意权值是从 \(0\) 开始的!

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define lb lower_bound
#define ub upper_bound

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
  return x*f;
}

void print(ll x){
  int p[25],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x) p[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;--i) putchar(p[i]+'0');
  putchar('\n');
}

const int N=1e5+5,mx=1e5;
int n,m,rt[N],rt1[N];
struct lxs{int s,m,r;}a[N];
struct node{
  int l,r,tim;
  bool vis;
  bool operator <(const node &rhs) const{return l<rhs.l;}
};
set<node> s;

struct SGT{
  int idx=0;
  struct sgt{
  	int s[2];ll val;
  }tr[N*20];
  
  #define lc(p) tr[p].s[0]
  #define rc(p) tr[p].s[1]
  #define mid ((l+r)>>1)
  
  void pu(int p){tr[p].val=tr[lc(p)].val+tr[rc(p)].val;}
  int build(int l=1,int r=mx){
  	int p=++idx;
  	if(l==r) return p;
  	lc(p)=build(l,mid);
  	rc(p)=build(mid+1,r);
  	return p;
  }
  int upd(int x,int val,int p,int l=0,int r=mx){
  	int nw=++idx;tr[nw]=tr[p];
  	if(l==r) return tr[nw].val+=1ll*val,nw;
  	if(x<=mid) lc(nw)=upd(x,val,lc(p),l,mid);
  	else rc(nw)=upd(x,val,rc(p),mid+1,r);pu(nw);
  	return nw;
  }
  ll qry(int pre,int p,int x,int y,int l=0,int r=mx){
  	if(!p&&!pre) return 0;
  	if(!p) return -1ll*tr[pre].val;
  	if(!pre) return tr[p].val;
  	if(x<=l&&y>=r) return tr[p].val-tr[pre].val;
  	if(y<=mid) return qry(lc(pre),lc(p),x,y,l,mid);
  	if(x>mid) return qry(rc(pre),rc(p),x,y,mid+1,r);
  	return qry(lc(pre),lc(p),x,y,l,mid)+qry(rc(pre),rc(p),x,y,mid+1,r);
  }
}tr,trm;

ll bf(const node &it,int t){
  ll res=0;
  for(int i=it.l;i<=it.r;++i) res+=min(1ll*a[i].s+1ll*a[i].r*(t-it.tim),1ll*a[i].m),a[i].s=0;
  return res;
}

ll qry(const node &it,int t){
  if(it.vis) return bf(it,t);t-=it.tim;
  return trm.qry(rt1[it.l-1],rt1[it.r],0,t-1)+1ll*t*tr.qry(rt[it.l-1],rt[it.r],t,mx);
}

int main(){
  /*2023.11.28 H_W_Y CF453E Little Pony and Lord Tirek SGT*/
  n=read();
  rt[0]=tr.build();rt1[0]=trm.build();
  for(int i=1;i<=n;++i){
  	a[i].s=read(),a[i].m=read(),a[i].r=read();
  	if(a[i].r==0) rt[i]=tr.upd(0,0,rt[i-1]),rt1[i]=trm.upd(0,0,rt1[i-1]);
    else rt[i]=tr.upd(a[i].m/a[i].r,a[i].r,rt[i-1]),rt1[i]=trm.upd(a[i].m/a[i].r,a[i].m,rt1[i-1]);
  }
  s.insert((node){1,n,0,1});m=read();
  for(int i=1,t,l,r;i<=m;++i){
  	t=read();l=read();r=read();ll ans=0;
  	auto itl=--s.ub((node){l,0,0,0}),itr=--s.ub((node){r,0,0,0});
  	if(itl==itr){
  	  ans=qry((node){l,r,itl->tim,itl->vis},t);
  	  node nw=(*itl);
  	  s.erase(itl);
  	  if(l!=nw.l) s.insert((node){nw.l,l-1,nw.tim,nw.vis});
  	  s.insert((node){l,r,t,0});
  	  if(r!=nw.r) s.insert((node){r+1,nw.r,nw.tim,nw.vis});
  	}
  	else{
  	  node nwl=*itl,nwr=*itr;
  	  ans=qry((node){l,itl->r,itl->tim,itl->vis},t);
  	  ans+=qry((node){itr->l,r,itr->tim,itr->vis},t);
  	  for(auto it=s.erase(itl);it!=s.end()&&it!=itr;it=s.erase(it)) ans+=qry(*it,t);
  	  s.erase(itr);
  	  if(nwl.l<l) s.insert((node){nwl.l,l-1,nwl.tim,nwl.vis});
  	  if(nwr.r>r) s.insert((node){r+1,nwr.r,nwr.tim,nwr.vis});
  	  s.insert((node){l,r,t,0});
  	}
  	print(ans);
  }
  return 0;
}

P5066 Ynoi 颜色段均摊打标记 - 按位操作 Balance Tree

P5066 [Ynoi2014] 人人本着正义之名

你需要帮珂朵莉维护一个长为 \(n\)01序列 \(a\),有 \(m\) 个操作:

  • 1 l r:把区间 \([l,r]\) 的数变成 \(0\)
  • 2 l r:把区间 \([l,r]\) 的数变成 \(1\)
  • 3 l r\([l,r-1]\) 内所有数 \(a_i\),变为 \(a_i\)\(a_{i+1}\) 按位或的值,这些数同时进行这个操作。
  • 4 l r\([l+1,r]\) 内所有数 \(a_i\),变为 \(a_i\)\(a_{i-1}\) 按位或的值,这些数同时进行这个操作。
  • 5 l r\([l,r-1]\) 内所有数 \(a_i\),变为 \(a_i\)\(a_{i+1}\) 按位与的值,这些数同时进行这个操作。
  • 6 l r\([l+1,r]\) 内所有数 \(a_i\),变为 \(a_i\)\(a_{i-1}\) 按位与的值,这些数同时进行这个操作。
  • 7 l r:查询区间 \([l,r]\) 的和。

本题强制在线,每次的 \(l,r\) 需要与上次答案做 \(\operatorname{xor}\) 运算,如果之前没有询问,则上次答案为 \(0\)

对于 \(100\%\) 的数据,\(1\leq n,m\leq 3 \times 10^6\)\(0\leq a_i\leq 1\)

首先考虑用平衡树维护每一个相同颜色的区间,把每一个相同颜色段合并起来。

而每一次的操作其实就是把有些段的长度 \(+1/-1\),分类讨论一下就可以发现,于是感觉可以用颜色段均摊直接做了。


但是我们发现一个问题,

如果第一次和右边的或,第二次和左边的与。

这样我们就成功变回了原序列,这样的操作是没有意义的。

那我们怎么办呢?


考虑我们的颜色只有两种,而每一次区间的操作,我们可以在平衡树上面维护两个标记,

\(tag0,tag1\) 分别表示为 \(0\) 的段和 \(1\) 的段的长度变化量,

这样就可以很好避免掉多余的时间。


而对于那些直接消失的段,发现是不可逆的。

于是每一次修改之后我们要去把那些已经消失的段直接删掉,合并它两边的段即可。

这些操作都是很好去用平衡树维护的,主要就是颜色段均摊打标记。


最终的时间复杂度是 \(O((n+m)\log n)\)


不知道写了多久,最后才意识到每一次 pushdown 的时候维护的区间两个端点是不一定正确的,

于是我们是只能每一次去找前驱和后继。——一直没有 pushdown 我真的。。。


还要用上 gyy 的超级快读才能过。。。我平衡树的 split 是不是写假了。

Code
#include <bits/stdc++.h>
using namespace std;
mt19937 rd(time(0));

namespace Fastio {
    #define USE_FASTIO 1
    #define IN_LEN 45000
    #define OUT_LEN 45000
    char ch, c; int len;
	short f, top, s;
    inline char Getchar() {
        static char buf[IN_LEN], *l = buf, *r = buf;
        if (l == r) r = (l = buf) + fread(buf, 1, IN_LEN, stdin);
        return (l == r) ? EOF : *l++;
    }
    char obuf[OUT_LEN], *ooh = obuf;
    inline void Putchar(char c) {
        if (ooh == obuf + OUT_LEN) fwrite(obuf, 1, OUT_LEN, stdout), ooh = obuf;
        *ooh++ = c;
    }
    inline void flush() { fwrite(obuf, 1, ooh - obuf, stdout); }

    #undef IN_LEN
    #undef OUT_LEN
    struct Reader {
        template <typename T> Reader& operator >> (T &x) {
            x = 0, f = 1, c = Getchar();
            while (!isdigit(c)) { if (c == '-') f *= -1; c = Getchar(); }
            while ( isdigit(c)) x = (x << 3) + (x << 1) + (c ^ 48), c = Getchar();
            x *= f;
            return *this;
        }
        
        Reader() {}
    } cin;
    const char endl = '\n';
    struct Writer {
        typedef long long mxdouble;
        template <typename T> Writer& operator << (T x) {
            if (x == 0) { Putchar('0'); return *this; }
            if (x < 0) Putchar('-'), x = -x;
            static short sta[40];
            top = 0;
            while (x > 0) sta[++top] = x % 10, x /= 10;
            while (top > 0) Putchar(sta[top] + '0'), top--;
            return *this;
        }
        Writer& operator << (const char *str) {
            int cur = 0;
            while (str[cur]) Putchar(str[cur++]);
            return *this;
        }
        inline Writer& operator << (char c) {Putchar(c); return *this;}
        Writer() {}
        ~ Writer () {flush();}
    } cout;
	#define cin Fastio::cin
	#define cout Fastio::cout
	#define endl Fastio::endl
}

const int N=3e6+5,inf=1e9;
int n,m,idx=0,rt=0,lst=0,pre;

struct lxs{
  int l0,l1,r0,r1;
  void init(){l0=l1=r0=r1=0;}
};
struct node{int l,r;bool col;};
struct treap{
  int s[2],mn[2],siz,sum,key,ct;
  node val;lxs tag;
  void init(){tag.init();siz=sum=s[0]=s[1]=key=ct=0;mn[0]=mn[1]=inf;}
}tr[N<<1];
const lxs cg[4]={(lxs){0,-1,-1,0},(lxs){1,0,0,1},(lxs){-1,0,0,-1},(lxs){0,1,1,0}};
const int f[4]={0,1,1,0},b[4]={1,0,0,1};

#define lc(p) tr[p].s[0]
#define rc(p) tr[p].s[1]
#define c(p) tr[p].val.col

inline void pu(int p){
  tr[p].siz=tr[lc(p)].siz+tr[rc(p)].siz+1;
  tr[p].sum=tr[lc(p)].sum+tr[rc(p)].sum+tr[p].val.col*(tr[p].val.r-tr[p].val.l+1);
  tr[p].ct=tr[lc(p)].ct+tr[rc(p)].ct+(c(p)==1);
  tr[p].mn[0]=min(tr[lc(p)].mn[0],tr[rc(p)].mn[0]);
  tr[p].mn[1]=min(tr[lc(p)].mn[1],tr[rc(p)].mn[1]);
  tr[p].mn[c(p)]=min(tr[p].mn[c(p)],tr[p].val.r-tr[p].val.l+1);
}

inline int nwnode(int l,int r,bool col){
  if(l>r) return idx;int nw=++idx;
  tr[nw].init();
  tr[nw].val=(node){l,r,col};
  tr[nw].sum=col*(r-l+1);
  tr[nw].siz=1;tr[nw].key=rd()%1919810;
  tr[nw].mn[col]=r-l+1;tr[nw].ct=(col==1);
  return nw;
}

inline void change(int p,lxs tag){
  int l0=tag.l0,l1=tag.l1,r0=tag.r0,r1=tag.r1;
  tr[p].tag.l0+=l0;tr[p].tag.l1+=l1;
  tr[p].tag.r0+=r0;tr[p].tag.r1+=r1;
  tr[p].mn[0]+=r0-l0;
  tr[p].mn[1]+=r1-l1;
  if(c(p)==0) tr[p].val.l+=l0,tr[p].val.r+=r0;
  else tr[p].val.l+=l1,tr[p].val.r+=r1;
  tr[p].sum+=tr[p].ct*(r1-l1);
}

inline void pd(int p){
  if(!tr[p].tag.l0&&!tr[p].tag.l1&&!tr[p].tag.r0&&!tr[p].tag.r1) return;
  if(lc(p)) change(lc(p),tr[p].tag);
  if(rc(p)) change(rc(p),tr[p].tag);
  tr[p].tag.init();
}

void splitl(int p,int l,int &x,int &y){
  if(!p){x=y=0;return;}pd(p);
  if(tr[p].val.l<=l) x=p,splitl(rc(p),l,rc(p),y);
  else y=p,splitl(lc(p),l,x,lc(p));pu(p);
}

void splitr(int p,int r,int &x,int &y){
  if(!p){x=y=0;return;}pd(p);
  if(tr[p].val.r<=r) x=p,splitr(rc(p),r,rc(p),y);
  else y=p,splitr(lc(p),r,x,lc(p));pu(p);
}

int merge(int x,int y){
  if(!x||!y) return x+y;
  pd(x);pd(y);
  if(tr[x].key<tr[y].key){
  	tr[x].s[1]=merge(tr[x].s[1],y);
  	pu(x);return x;
  }
  else{
  	tr[y].s[0]=merge(x,tr[y].s[0]);
  	pu(y);return y;
  }
}

int gfir(int p){
  if(!p) return 0;pd(p);
  while(lc(p)) p=lc(p),pd(p);return p;
}

int glst(int p){
  if(!p) return 0;pd(p);
  while(rc(p)) p=rc(p),pd(p);return p;
}

void sol(int p){
  if(!p) return ;	
  pd(p);int x=0,y=0,z=0;
  if(tr[p].val.l>tr[p].val.r){
  	if(tr[p].val.l==1){splitr(rt,1,x,rt);return;}
  	if(tr[p].val.r==n){splitl(rt,n,rt,x);return;}
  	splitr(rt,tr[p].val.r-1,x,y);splitl(y,tr[p].val.l,y,z);
  	int lp=gfir(y),rp=glst(y);
  	rt=merge(x,merge(nwnode(tr[lp].val.l,tr[rp].val.r,tr[lp].val.col),z));
  	return;
  }
  if(lc(p)&&(tr[lc(p)].mn[0]==0||tr[lc(p)].mn[1]==0)) return sol(lc(p));
  if(rc(p)&&(tr[rc(p)].mn[0]==0||tr[rc(p)].mn[1]==0)) return sol(rc(p));
}

inline void cov(int l,int r,int col){
  int x=0,y=0,z=0,id=0;
  splitr(rt,l-1,x,y);splitl(y,r,y,z);
  int lp=gfir(y),rp=glst(y);
  int L=tr[lp].val.l,R=tr[rp].val.r,col1=tr[lp].val.col,col2=tr[rp].val.col;
  if(L<l&&col1==col) l=L;
  else if(L<l) x=merge(x,nwnode(L,l-1,col1));
  else if((id=glst(x))&&tr[id].val.col==col){splitr(x,L-2,x,y);l=tr[y].val.l;}
  if(R>r&&col2==col) r=R;
  else if(R>r) z=merge(nwnode(r+1,R,col2),z);
  else if((id=gfir(z))&&tr[id].val.col==col){splitl(z,R+1,y,z);r=tr[y].val.r;}
  rt=merge(x,merge(nwnode(l,r,col),z));
}

inline void wrk(int l,int r,int op){
  int x=0,y=0,z=0,xx=0;
  splitr(rt,l-1,x,y);splitl(y,r,y,z);
  if(!y) return rt=merge(x,z),void();
  int id=gfir(y);
  if(tr[id].val.col!=f[op]){
  	splitl(y,tr[id].val.l,xx,y),x=merge(x,xx),xx=0;
  	if(!y) return rt=merge(x,z),void();
  }
  id=glst(y);
  if(tr[id].val.col!=b[op]){
  	splitr(y,tr[id].val.r-1,y,xx),z=merge(xx,z);
  	if(!y) return rt=merge(x,z),void();
  }
  change(y,cg[op]);rt=merge(x,merge(y,z));
}

inline int qry(int l,int r){
  int x=0,y=0,z=0;
  splitr(rt,l-1,x,y);splitl(y,r,y,z);
  int lp=gfir(y),rp=glst(y);
  int res=tr[y].sum-(l-tr[lp].val.l)*tr[lp].val.col-(tr[rp].val.r-r)*tr[rp].val.col;
  rt=merge(merge(x,y),z);
  return res;
}

int main(){
  //ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  /*2023.11.28 H_W_Y P5066 [Ynoi2014] fhq-Treap*/
  cin>>n>>m;rt=lst=0;pre=-1;tr[0].init();
  for(int i=1;i<=n;i++){
  	bool x;cin>>x;
  	if(x==pre) continue;
  	if(pre==-1){pre=x,lst=i;continue;}
  	rt=merge(rt,nwnode(lst,i-1,pre));pre=x,lst=i;
  }
  rt=merge(rt,nwnode(lst,n,pre));lst=0;
  for(int i=1,op,l,r;i<=m;i++){
  	cin>>op>>l>>r;l^=lst,r^=lst; 
  	if(op<=2) cov(l,r,op-1);
    if(op>=3&&op<=6) wrk(l,r,op-3);
  	if(op==7) cout<<(lst=qry(l,r))<<endl;
  	while(tr[rt].mn[0]==0||tr[rt].mn[1]==0) sol(rt);
  }
  return 0;
}

UOJ228 颜色段均摊打标记 - 有关根号 SGT

基础数据结构练习题

sylvia 是一个热爱学习的女孩子,今天她想要学习数据结构技巧。

在看了一些博客学了一些姿势后,她想要找一些数据结构题来练练手。于是她的好朋友九条可怜酱给她出了一道题。

给出一个长度为 \(n\) 的数列 \(A\),接下来有 \(m\) 次操作,操作有三种:

  1. 对于所有的 \(i \in [l,r]\),将 \(A_i\) 变成 \(A_i+x\)
  2. 对于所有的 \(i \in [l,r]\),将 \(A_i\) 变成 \(\lfloor \sqrt {A_i} \rfloor\)
  3. 对于所有的 \(i \in [l,r]\),询问 \(A_i\) 的和。

作为一个不怎么熟练的初学者,sylvia 想了好久都没做出来。而可怜酱又外出旅游去了,一时间联系不上。于是她决定向你寻求帮助:你能帮她解决这个问题吗。

感觉和上一道题目很类似,因为我们的开根操作可以让这个区间的每一个数的差距迅速减小。

从而感觉可以用颜色段均摊去完成。


但是发现会被奇怪的数据卡掉:

\(3,4,3,4,3,4\)

我们先进行一次开根变成 \(1,2,1,2,1,2\)

于是再 \(+2\) 操作就变回去了。


这样的是没有意义的,那怎么去避免呢。

发现我们可以去维护一个区间的极差,如果开根过后的极差没有变化,

那么就相当于一个区间减的操作,我们就不必要往下递归了,直接打上标记即可。


这就是颜色段均摊打标记的操作,

具体实现的时候,每一次开根操作,

我们都直接在线段树上面去找,利用极差去判断是否有必要走下去,

走到极差不变的节点位置就打上标记返回即可。


为什么这样的时间复杂度是对的呢?

分析一下极差的式子,每一次一定是会 \(/2\) 的,所以也只需要 \(\log\) 次操作就可以做到区间推平。

这样做的时间复杂度是 \(O((n+m)\log n\log \log v)\) 的。


当你做了两道 Ynoi 之后发现这是一道超级好写的题,真的很好写啊。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void print(ll x){
  int p[25],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x) p[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
  putchar('\n');
}

const int N=1e5+5;
int n,m;
struct sgt{
  ll mx,mn,sum,tag;
}tr[N<<2];

#define mid ((l+r)>>1)
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1
#define lc p<<1
#define rc p<<1|1

void pu(int p){
  tr[p].mx=max(tr[lc].mx,tr[rc].mx);
  tr[p].mn=min(tr[lc].mn,tr[rc].mn);
  tr[p].sum=tr[lc].sum+tr[rc].sum;
}

void change(int l,int r,int p,ll v){
  tr[p].tag+=v;
  tr[p].mx+=v;tr[p].mn+=v;
  tr[p].sum+=1ll*(r-l+1)*v;
}

void pd(int l,int r,int p){
  if(tr[p].tag==0) return ;
  change(lson,tr[p].tag);change(rson,tr[p].tag);
  tr[p].tag=0;
}

void upd(int l,int r,int p,int x,int y,ll v){
  if(x<=l&&y>=r) return change(l,r,p,v),void();pd(l,r,p);
  if(x<=mid) upd(lson,x,y,v);
  if(y>mid) upd(rson,x,y,v);pu(p);
}

void mof(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r&&tr[p].mx-tr[p].mn==(ll)floor(sqrt(tr[p].mx))-(ll)floor(sqrt(tr[p].mn))){
  	ll v=tr[p].mx-(ll)floor(sqrt(tr[p].mx));
    change(l,r,p,-v);return;
  }pd(l,r,p);
  if(x<=mid) mof(lson,x,y);
  if(y>mid) mof(rson,x,y);pu(p); 
}

ll qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].sum;pd(l,r,p);
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return qry(lson,x,y)+qry(rson,x,y);
}

void build(int l,int r,int p){
  if(l==r){
  	tr[p].mx=tr[p].mn=tr[p].sum=1ll*read();
    tr[p].tag=0;return;
  }
  build(lson);build(rson);pu(p);
}

int main(){
  /*2023.11.29 UOJ228 SGT*/
  n=read();m=read();build(1,n,1);
  for(int i=1,op,l,r,v;i<=m;i++){
    op=read();l=read();r=read();
    if(op==1){v=read();upd(1,n,1,l,r,v);}
    if(op==2) mof(1,n,1,l,r);
    if(op==3) print(qry(1,n,1,l,r));
  }
  return 0;
}

LOJ6029 颜色段均摊打标记 - 有关整除 SGT

#6029. 「雅礼集训 2017 Day1」市场

从前有一个贸易市场,在一位执政官到来之前都是非常繁荣的,自从他来了之后,发布了一系列奇怪的政令,导致贸易市场的衰落。

\(n\) 个商贩,从 \(0 \sim n - 1\) 编号,每个商贩的商品有一个价格 \(a_i\),有两种政令;同时,有一个外乡的旅客想要了解贸易市场的信息,有两种询问方式:

  1. (政令)\(l, r, c\),对于 \(i \in [l, r], a_i \leftarrow a_i + c\)
  2. (政令)\(l, r, d\),对于 \(i \in [l, r], a_i \leftarrow \lfloor {a_i}/{d} \rfloor\)
  3. (询问)给定 \(l, r\),求 \(\min_{i \in [l, r]} a_i\)
  4. (询问)给定 \(l, r\),求 \(\sum_{i \in [l,r]} a_i\)

\(1 \le n,q \le 10^5\)

和上一道题非常类似,我们也可以利用相同的方法去处理。

同样维护极差,每一次极差要变化就一定会 \(/2\) ,时间复杂度是正确的。


而由于每一次还会被 \(3,4,3,4,3,4\) 的数据卡掉,

于是我们还是同样去区间修改即可。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void print(ll x){
  int p[25],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x) p[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
  putchar('\n');
}

const int N=1e5+5;
int n,m;
struct node{
  ll mx,mn,sum,tag;
}tr[N<<2];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1

void pu(int p){
  tr[p].sum=tr[lc].sum+tr[rc].sum;
  tr[p].mx=max(tr[lc].mx,tr[rc].mx);
  tr[p].mn=min(tr[lc].mn,tr[rc].mn);
}

void change(int l,int r,int p,ll v){
  tr[p].sum+=1ll*(r-l+1)*v;
  tr[p].mx+=v;tr[p].mn+=v;tr[p].tag+=v;
}

void pd(int l,int r,int p){
  if(!tr[p].tag) return;
  change(lson,tr[p].tag);change(rson,tr[p].tag);
  tr[p].tag=0;
}

void upd(int l,int r,int p,int x,int y,ll v){
  if(x<=l&&y>=r) return change(l,r,p,v),void();pd(l,r,p);
  if(x<=mid) upd(lson,x,y,v);
  if(y>mid) upd(rson,x,y,v);pu(p);
}

ll dv(ll x,ll d){return (x<0)?(((x+1ll)/d)-1ll):(x/d);}

void mof(int l,int r,int p,int x,int y,ll d){
  if(x<=l&&y>=r&&tr[p].mx-tr[p].mn==dv(tr[p].mx,d)-dv(tr[p].mn,d)){
  	ll v=tr[p].mx-dv(tr[p].mx,d);
  	change(l,r,p,-v);return;
  }pd(l,r,p);
  if(x<=mid) mof(lson,x,y,d);
  if(y>mid) mof(rson,x,y,d);pu(p);
}

ll qry_s(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].sum;pd(l,r,p);
  if(y<=mid) return qry_s(lson,x,y);
  if(x>mid) return qry_s(rson,x,y);
  return qry_s(lson,x,y)+qry_s(rson,x,y);
}

ll qry_min(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].mn;pd(l,r,p);
  if(y<=mid) return qry_min(lson,x,y);
  if(x>mid) return qry_min(rson,x,y);
  return min(qry_min(lson,x,y),qry_min(rson,x,y));
}

void build(int l,int r,int p){
  if(l==r){
  	tr[p].sum=tr[p].mn=tr[p].mx=1ll*read();
  	tr[p].tag=0;return;
  }
  build(lson);build(rson);pu(p);
}

int main(){
  /*2023.11.29 H_W_Y #6029. 「雅礼集训 2017 Day1」市场 SGT*/
  n=read();m=read();build(1,n,1);
  for(int i=1,op,l,r,v;i<=m;i++){
    op=read();l=read()+1;r=read()+1;
    if(op==1) v=read(),upd(1,n,1,l,r,1ll*v);
    if(op==2) v=read(),mof(1,n,1,l,r,1ll*v);
    if(op==3) print(qry_min(1,n,1,l,r));
    if(op==4) print(qry_s(1,n,1,l,r));
  }
  return 0;
}

PKUSC2021D1T2 颜色段均摊打标记 询问楼房重建

PKUSC2021D1T2 逛街

给定长度为 \(n\) 的序列 \(a_i\)\(Q\) 次操作。

  • 1 l r\(\forall l \le i \lt r,a_i =\max(a_i',a_{i+1}')\),其中 \(a_i'\) 为未修改前 \(a_i\) 的权值。
  • 2 l r:求出 \(\sum_{x \in S} a_x\),其中 \(S=\{x|l \le x\le r ,\forall l \le i \lt x,a_i \lt a_x\}\)

\(1 \le n,Q \le 2 \times 10^5,1 \le a_i \le 10^9,a_i\) 互不相同。

首先看到修改操作,和之前那道 \(01\) 是很像的。

于是我们一样是用平衡树去维护颜色段,

在修改的时候同样分情况讨论,共有四种情况,分别取决于这个段左右两边段的权值与它的大小关系,这里不列出来了。

同样要注意看一下是不是有消失的区间。


而现在来考虑查询,发现和楼房重建的查询是一样的,

于是我们直接维护楼房重建需要维护的信息进行查询即可。


P9061 Ynoi 颜色段均摊 - 二维数点

P9061 [Ynoi2002] Optimal Ordered Problem Solver

给定 \(n\) 个点 \((x_i,y_i)_{i=1}^n\),你需要按顺序处理 \(m\) 次操作。每次操作给出 \(o,x,y,X,Y\)

  • 首先进行修改:
    • \(o=1\) 则将满足 \(x_i\le x,\;y_i\le y\) 的点的 \(y_i\) 修改为 \(y\)
    • \(o=2\) 则将满足 \(x_i\le x,\;y_i\le y\) 的点的 \(x_i\) 修改为 \(x\)
  • 然后进行查询,询问满足 \(x_i\le X,\;y_i\le Y\) 的点数。

对于所有数据,\(1 \le n,m \le 10^6\)\(1\le x_i,y_i,x,y,X,Y\le n\)

感觉是非常复杂的题目,确实也是这样。


发现每一次操作其实是把一些区间推平了,

那么最终得到的图形也是一个阶梯型的,而这个阶梯的下面是没有点的,就像这样:

image

黑色就是之前的轮廓线。

现在考虑新加入一个操作,也就是绿色的轮廓线。(假设是推成横着的)

发现这个操作就很像颜色段均摊,我们用两棵线段树分别维护横着的和竖着的轮廓线上面每个位置的点数,

而在转移的时候,横着的是不会变的,

而对于竖着的,我们直接枚举这里的每一个线的位置加到横着的线段树上面即可。

具体实现的时候还会用到 set。


再来考虑中间那些绿色点的数量,我们也许要把它们推平。

考虑再用一棵线段树维护这些没有使用的点,那么其实我们每次只想去询问一个 \(x\) 的区间的 \(y\) 最小值,

取出这个值把它改到线上面去,不断重复知道所有点都在这个轮廓线的上面即可。


那点数如何处理呢?

于是可以考虑二维数点,数出红色区域的点数进行简单容斥即可。

感觉比较抽象,可能写得时候还要再梳理一下思路。


理一下思路:

  1. 离线下来,预处理每一个之后可能用到的点的二维数点(每一次询问的 \(X,Y\) 每一次推平的 \(x,y\),都是数这些点的右上角点数)
  2. 两棵线段树分别维护 \(x\) 轴区间在 \(l\sim r\) 中的 \(y\) 最小值,\(y\) 轴区间 \(l \sim r\) 中的 \(x\) 最小值,每一次推平会用到,不断取出最小的。
  3. 两个 priority_queue 分别维护横纵坐标在某个点的编号,另一坐标最小值——每次找最小值用的,最开始都加入一个 \((n+1)\) 的元素,还要用两棵树状数组维护一维数点。
  4. 两棵线段树分别维护颜色段均摊,就是维护在轮廓线上面点的分布,每个点在两棵树上面都会被统计,每次操作相当于是清空一个区间。

看着都常数巨大,zfy 和 Nit 写的平衡树,现在还在卡常中。(2023.11.24 15:50 - 17:22 [只有一种推平没写了,先下班])


现在是 2023.11.27 22:34,今天一天除了听了一会儿讲其他都在调——一共该是交了 \(65\) 发了,不愧是我。

一直在想那些卡常的小优化,而我这种情况应该是直接用一个 \(\log\) 的优化才可以过,最后才想到 set 只用维护一个。

用一种全新的做法 AC 了,极限数据是 \(3.66s\),稳得很。。。

一点一点得优化上去终究是过了——不断自己去发觉优化的过程还是很有意思的。

放上我的代码,去写一篇题解~[第一篇题解 P9061](P9061 sol - hewanying 的博客 - 洛谷博客 (luogu.com.cn))

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second
#define lb lower_bound
#define ub upper_bound
#define reg register

inline int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

inline void print(int x){
  int pr[15],tmp=0;
  if(x<=0) putchar('0');
  while(x>0) pr[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;--i) putchar(pr[i]+'0');
  putchar('\n');
}

const int N=1e6+5,inf=2e9;
int n,m,ct=0,bit,dep=0,num=0;
struct Point{int x,y;}a[N];
struct node{int op,x,y,X,Y,c;}q[N];
vector<int> G[N];
vector<pii> f[N],g[N];
bool vis[N];

struct BIT{
  int tr[N];
  inline int lowbit(int i){return i&(-i);}
  inline void upd(int x,int val){for(reg int i=x;i<=n;i+=lowbit(i)) tr[i]+=val;}
  inline int qry(int x){int res=0;for(reg int i=x;i>=1;i-=lowbit(i)) res+=tr[i];return res;}
}sd,ty;

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

inline auto max(auto x,auto y){return (x>y)?x:y;}
inline auto min(auto x,auto y){return (x>y)?y:x;}

struct SGT2{
  int tr[N<<2],d[N<<2];
  inline void pu(int p){tr[p]=tr[lc]+tr[rc];}
  inline void pd(int p){if(d[p]) tr[lc]=tr[rc]=tr[p],d[p]=0,d[lc]=d[rc]=1;}
  inline void upd(int p,int val){
  	p+=bit;for(reg int i=dep;i>0;--i) pd(p>>i);
  	tr[p]+=val;for(p>>=1;p;p>>=1) pu(p);
  }
  inline void covx(int p,int val){
  	if(p==0) return;
  	p+=bit;for(reg int i=dep;i>0;--i) pd(p>>i);
  	tr[p]=val;for(p>>=1;p;p>>=1) pu(p);
  }    
  inline void cov(int x,int y,int delta,int cnt){
  	if(x>y||(x==0&&y==0)){upd(y+1,cnt);covx(x-1,delta);return ;}
  	x=x+bit-1;y=y+bit+1;
  	for(reg int i=dep;i>0;--i) pd(x>>i),pd(y>>i);
  	tr[x]=delta;tr[y]+=cnt;
  	while(x^y^1){
  	  if(~x&1) tr[x^1]=0,d[x^1]=1;
  	  if(y&1) tr[y^1]=0,d[y^1]=1;
	  x>>=1;y>>=1;pu(x),pu(y); 
	}
	for(x>>=1;x;x>>=1) pu(x);
  }
  inline int qry(int x,int y){
  	if(x>y||(x==0&&y==0)) return 0;
  	x=x+bit-1,y=y+bit+1;int res=0;
	for(reg int i=dep;i>0;--i) pd(x>>i),pd(y>>i);
  	for(;x^y^1;x>>=1,y>>=1){
  	  if(~x&1) res+=tr[x^1];
  	  if(y&1) res+=tr[y^1];
	}return res;
  }
}sx,sy;

struct SGT1{
  int id[N<<2],c[N<<2];
  pii tr[N<<2];
  inline void pu(int p){tr[p]=min(tr[lc],tr[rc]);c[p]=c[lc]+c[rc];}
  inline void build(){
  	for(reg int p=bit+1;p<=bit+n;++p) id[p]=0,tr[p]=f[p-bit][0],c[p]=(int)f[p-bit].size()-1;
  	for(reg int p=bit-1;p;--p) pu(p);
  } 
  inline void del(int p){
  	p+=bit;id[p]++;tr[p]=f[p-bit][id[p]];--c[p];
  	for(p>>=1;p;p>>=1) pu(p);
  }
  inline pii qry(int x,int y){
    pii res={n+1,n+1};
	for(x=x+bit-1,y=y+bit+1;x^y^1;x>>=1,y>>=1){
      if(~x&1) res=min(res,tr[x^1]);
      if(y&1) res=min(res,tr[y^1]);
	}return res;
  }
  inline int qryc(int x){
  	int res=0,l=1,r=x;
  	for(l=l+bit-1,r=r+bit+1;l^r^1;l>>=1,r>>=1){
  	  if(~l&1) res+=c[l^1];
	  if(r&1) res+=c[r^1];	
	}return res;
  }
}t;

set<pii> c;

inline void init(){
  n=read();m=read();
  for(int i=1;i<=n;++i) a[i].x=read(),a[i].y=read(),G[a[i].x].pb(a[i].y),f[a[i].x].pb({a[i].y,i}),ty.upd(a[i].y,1);
  for(bit=1;bit<=n+1;bit<<=1) dep++;
  a[0]=(Point){inf,inf};
  for(int i=1;i<=m;++i){
  	q[i].op=read(),q[i].x=read(),q[i].y=read(),q[i].X=read(),q[i].Y=read();
	g[q[i].X].pb({q[i].Y,i});
  }
  num=0;
  for(reg int i=n;i>=1;--i){
  	sort(f[i].begin(),f[i].end());
  	f[i].pb({n+1,0});
  	for(auto j:g[i]) q[j.se].c=num-sd.qry(j.fi);
  	for(auto j:G[i]) num++,sd.upd(j,1);
  }  
  t.build();c.insert({0,0});c.insert({n+1,0});
}

inline int qry(node nw){
  auto it=c.lb({nw.X,n+1});
  if((*it).se>nw.Y) return 0;
  return sx.qry(1,nw.X)-sy.qry(nw.Y+1,n)+t.qryc(nw.X)+ty.qry(nw.Y)+nw.c-n+ct;
}

inline void upd1(node nw){
  int x=nw.x,y=nw.y,id,l;
  auto it=c.lb({x,n+1});
  l=(*it).se;bool vis=false;
  if(l>=y) return;
  if(it!=c.begin()){
  	--it;
    while((*it).se<=y){
  	  if(it==c.begin()){c.erase(it);vis=true;break;}
  	  else c.erase(it--);
    }  
    if(vis||(!vis&&(*it).fi!=x)) c.insert({x,y});	
  }
  else c.insert({x,y}); 
  int delta=sx.qry(x+1,n)-sy.qry(1,l-1),cnt=sy.qry(l,y-1)-delta;
  if(cnt>0) sy.cov(l+1,y-1,delta,cnt);
  num=0;
  while(a[id=t.qry(1,x).se].y<=y){
    t.del(a[id].x);ty.upd(a[id].y,-1);
    sx.upd(a[id].x,1);++ct;++num;
  }
  if(num) sy.upd(y,num);
}

inline void upd2(node nw){
  int x=nw.x,y=nw.y,id,l=0;
  auto it=c.lb({x,n+1});
  if((*it).se>y) return;
  bool vis=false;
  if(it!=c.begin()){
  	--it;
    while((*it).se<=y){
  	  if(it==c.begin()){c.erase(it);vis=true;break;}
      else c.erase(it--);
    }
    if(vis||(!vis&&(*it).fi!=x)) c.insert({x,y}); 
    l=vis?0:(*it).fi;
  }
  else if((*it).se!=y) c.insert({x,y});
  int delta=sy.qry(y+1,n)-sx.qry(1,l-1),cnt=sx.qry(l,x-1)-delta;
  if(cnt>0) sx.cov(l+1,x-1,delta,cnt);
  num=0;
  while(a[id=t.qry(1,x).se].y<=y){
  	t.del(a[id].x);ty.upd(a[id].y,-1);
  	sy.upd(a[id].y,1);++ct;++num;
  }
  if(num) sx.upd(x,num);
} 

inline void sol(int i){
  (q[i].op==1)?upd1(q[i]):upd2(q[i]);
  print(qry(q[i]));
}

inline void wrk(){
  init();
  for(reg int i=1;i<=m;++i) sol(i);
}

/*2023.11.24 H_W_Y P9061 [Ynoi2002] Optimal Ordered Problem Solver SGT*/
int main(){wrk();return 0;}

CF679E 颜色段均摊 - 42,SGT

CF679E Bear and Bad Powers of 42

定义一个正整数是坏的,当且仅当它是 \(42\) 的次幂,否则它是好的。

给定一个长度为 \(n\) 的序列 \(a_i\),保证初始时所有数都是好的。

\(q\) 次操作,每次操作有三种可能:

  • 1 i 查询 \(a_i\)
  • 2 l r x\(a_{l\dots r}\) 赋值为一个好的数 \(x\)
  • 3 l r x\(a_{l \dots r}\) 都加上 \(x\),重复这一过程直到所有数都变好。

\(n,q \le 10^5\)\(a_i,x \le 10^9\)

看到区间的赋值就不难想到用颜色段均摊,

直接用平衡树维护即可。


发现维护 \(a_i\) 的值是没有什么意义的,

而我们需要的是去维护 \(a_i\) 到它上面的那个 \(42\) 的幂次方的距离。

每一次就相当于把这段区间减去 \(x\) 再判断是否有 \(0\) 即可。


但是会发现有直接减成负的情况,这个时候我们就需要对它进行更新。

直接暴力枚举就可以了,每一次修改一个节点的时间是 \(O(\log n)\) 的。

那为什么这样做一定是对的呢?


这里用到了颜色段均摊的思想,发现其实每一个数最多重新赋值 \(\log v\) 次,是完全可以接受的,

因为越大两者之间的距离就越远,而我们加的 \(x\) 是较小的。

于是直接用平衡树维护颜色段均摊做就可以了。


平衡树对于这道题还比较复杂,我写的线段树,注意 pushup 的时候一定要想清楚!

判断两边相等是所有都相等。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e5+5;
ll D[11],a[N];
int n,m;
struct sgt{
  bool vis;
  ll val,tag;
  int id;
}tr[N<<2];
bool vis=false;

void init(){
  D[0]=1ll;
  for(int i=1;i<=10;i++) D[i]=D[i-1]*42ll;
}

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1

void pu(int p){tr[p].val=min(tr[lc].val,tr[rc].val);tr[p].vis=false;}

void build(int l,int r,int p){
  if(l==r){
    for(int i=0;i<=10;i++)
      if(D[i]<a[l]&&D[i+1]>=a[l]){
      	tr[p].id=i+1;tr[p].val=D[i+1]-1ll*a[l];
      	break;
      }
  	tr[p].vis=true;tr[p].tag=0;
  	return;
  }
  build(lson);build(rson);pu(p);
}

void pd(int p){
  if(tr[p].vis){
  	tr[lc].vis=tr[rc].vis=true,tr[lc].id=tr[rc].id=tr[p].id;
  	tr[lc].val=tr[rc].val=tr[p].val;
  	tr[lc].tag=tr[rc].tag=tr[p].tag=0;
  	return;
  }
  tr[lc].tag+=tr[p].tag;tr[rc].tag+=tr[p].tag;
  tr[lc].val-=tr[p].tag;tr[rc].val-=tr[p].tag;
  tr[p].tag=0;
}

void upd(int l,int r,int p,int x,int y,int val){
  if(x<=l&&y>=r&&tr[p].val>=val){
  	tr[p].tag+=val,tr[p].val-=1ll*val;
  	return;
  }
  else if(x<=l&&y>=r&&tr[p].vis){
  	tr[p].tag+=val;tr[p].val-=val;
  	while(tr[p].val<0) tr[p].val+=D[tr[p].id+1]-D[tr[p].id],tr[p].id++;
  	return;
  }
  pd(p);
  if(x<=mid) upd(lson,x,y,val);
  if(y>mid) upd(rson,x,y,val);
  pu(p);
}

ll qry(int l,int r,int p,int x){
  if(l==r||tr[p].vis) return D[tr[p].id]-tr[p].val;
  pd(p);
  if(x<=mid) return qry(lson,x);
  return qry(rson,x);
}

void cov(int l,int r,int p,int x,int y,int val){
  if(x<=l&&y>=r){
  	tr[p].vis=true;
    for(int i=0;i<=10;i++)
      if(D[i]<val&&D[i+1]>=val){
      	tr[p].id=i+1;tr[p].val=D[i+1]-1ll*val;
      	break;
      }
    return;
  }
  pd(p);
  if(x<=mid) cov(lson,x,y,val);
  if(y>mid) cov(rson,x,y,val);
  pu(p);
}

int main(){
  /*2023.11.23 H_W_Y CF679E Bear and Bad Powers of 42 SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;init();
  for(int i=1;i<=n;i++) cin>>a[i];
  build(1,n,1);
  for(int i=1,op,x,y,v;i<=m;i++){
  	cin>>op;
  	if(op==1) cin>>x,cout<<qry(1,n,1,x)<<'\n';
  	else if(op==2) cin>>x>>y>>v,cov(1,n,1,x,y,v);
  	else{
  	  cin>>x>>y>>v;
  	  do upd(1,n,1,x,y,v);
  	  while(tr[1].val==0);
  	}
  }
  return 0;
}

CF702F T-Shirts 均摊杂题 - Balance Tree

CF702F T-Shirts

怎么有和我讲的平衡树撞了呢?也没人补题,我同样写过题解。。。

\(n\) 种 T 恤,每种有价格 \(c_i\) 和品质 \(q_i\)

\(m\) 个人要买 T 恤,第 \(i\) 个人有 \(v_i\) 元,每人每次都会买一件能买得起的 \(q_i\) 最大的 T 恤。一个人只能买一种 T 恤一件,所有人之间都是独立的。

问最后每个人买了多少件 T 恤?如果有多个 \(q_i\) 最大的 T 恤,会从价格低的开始买。

\(1 \le n,m \le 2 \times 10^5\) ,时限 \(4s\)

首先不难想到对于 \(n\) 种 T 恤,我们按照 \(q_i\) 从大到小排序,

如果 \(q_i\) 相同则按照 \(c_i\) 从小到大排序。

对于每一个人,我们可以从前往后遍历一遍,能买就买了。


现在考虑是否可以对于每一种 T恤,我们对整个序列进行操作,

具体来说也就是把 \(\ge c_i\) 的数全部减去 \(c_i\),并且 \(ans+1\)

发现这样并不能保证序列的单调性,

中间在 \(c_i \le val \lt 2 c_i\) 的数有可能产生冲突,

那怎么办呢?


由于 \(c_i \le val \lt 2c_i\) ,那么 \(c_i \gt \frac{val}{2}\)

所以对于这样的 \(val\) ,我们直接进行暴力的删除和插入,

因为这样的操作最多进行 \(\log val\) 次,每次都会减小到原来的一半。

时限是 \(4s\) ,足以通过,用平衡树维护即可。

时间复杂度 \(O((n+\sum \log_2q_i)\log_2 n)\)


注意标记要清空!!!

Code
#include <bits/stdc++.h>
using namespace std;

const int N=2e5+5;
int n,idx=0,rt,m,ans[N],val[N],x,y,z;
struct node{
  int c,q;
  bool operator <(const node &rhs) const{
    if(q!=rhs.q) return q>rhs.q;
    return c<rhs.c;
  }
}a[N];
struct Treap{
  int siz,val,id,s[2],key,tag,add;
}tr[N];
mt19937 rd(time(0));
queue<int> q; 

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void print(int x){
  int p[15],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x){
    p[++tmp]=x%10;
    x/=10;
  } 
  for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
  putchar(' ');
}

#define lc(p) tr[p].s[0]
#define rc(p) tr[p].s[1]

int nwnode(int val,int id){
  int it;
  if(!q.empty()) it=q.front(),q.pop();
  else it=++idx;
  tr[it].val=val;
  tr[it].id=id;
  tr[it].key=rd();
  tr[it].siz=1;
  tr[it].s[0]=tr[it].s[1]=tr[it].tag=tr[it].add=0;
  return it;
}

void pu(int p){tr[p].siz=tr[lc(p)].siz+tr[rc(p)].siz+1;}

void change(int p,int tag,int add){
  if(!p) return ;
  tr[p].tag+=tag;
  tr[p].add+=add;
  ans[tr[p].id]+=add;
  tr[p].val+=tag;
}

void pd(int p){
  if(tr[p].add==0) return;
  if(lc(p)) change(lc(p),tr[p].tag,tr[p].add);
  if(rc(p)) change(rc(p),tr[p].tag,tr[p].add);
  tr[p].tag=0;tr[p].add=0;
}

void split(int p,int k,int &x,int &y){
  if(!p){x=y=0;return;}
  pd(p);
  if(tr[p].val<=k) x=p,split(rc(p),k,rc(p),y);
  else y=p,split(lc(p),k,x,lc(p));
  pu(p); 
}

int merge(int x,int y){
  if(!x||!y) return (x|y);
  pd(x);pd(y);
  if(tr[x].key<tr[y].key){
  	tr[x].s[1]=merge(tr[x].s[1],y);
  	pu(x);return x;
  }
  else{
  	tr[y].s[0]=merge(x,tr[y].s[0]);
  	pu(y);return y;
  }
}

void ins(int &p,int val,int id){
  int xx=0,yy=0;
  split(p,val,xx,yy);
  p=merge(merge(xx,nwnode(val,id)),yy);
}

void dfs(int p,int c){
  if(!p) return ;
  pd(p); 
  if(lc(p)) dfs(lc(p),c);
  if(rc(p)) dfs(rc(p),c);
  q.push(p);ans[tr[p].id]++;
  if(tr[p].val-c>0) ins(x,tr[p].val-c,tr[p].id);
}

void update(int c){
  split(rt,c-1,x,y);
  split(y,2*c,y,z);
  if(z) change(z,-c,1);
  dfs(y,c);
  rt=merge(x,z);
}

void getans(int p){
  pd(p);
  if(lc(p)) getans(lc(p));
  if(rc(p)) getans(rc(p));
}

int main(){
  /*2023.10.25 H_W_Y CF702F T-Shirts FHQ-Treap*/
  n=read();
  for(int i=1;i<=n;i++) a[i].c=read(),a[i].q=read();
  sort(a+1,a+n+1);
  m=read();
  for(int i=1;i<=m;i++) val[i]=read(),ins(rt,val[i],i);
  for(int i=1;i<=n;i++) update(a[i].c);
  getans(rt);
  for(int i=1;i<=m;i++) print(ans[i]);
  return 0;
}

Conclusion

很多颜色段均摊的问题都是没有什么套路,都很灵活于是就要靠人类智慧。平衡树不要写假了!

  1. set 维护的时候可以在最后加入一个数以防止去判是结尾的情况,实现的时候有很多技巧要想好啊。(P6617 查找 Search)
  2. 判断两点相等是里面所有变量都相等,而不是只是其中一些,写之前一定想清楚!(CF679E Bear and Bad Powers of 42)
  3. 注意权值线段树的边界一般都是从 \(0\) 开始,查询的时候不要漏了(CF453E Little Pony and Lord Tirek)
  4. 平衡树实现的时候一定要记得 pushdown 啊!(P5066 [Ynoi2014] 人人本着正义之名)

2023.11.29 18:01 补完 Day1 的题目(PKUSC 的那道没写,没找到交的地方),赶在下一次讲课前写完了。

感觉 DS 写着挺好玩的,自己用前无古人的做法没有参考任何资料过了 P9061 之后感觉一切题目都变得简单了——后面做的题目就挺顺的了。

看来该经历的还是要经历的。[第一篇题解 P9061](P9061 sol - hewanying 的博客 - 洛谷博客 (luogu.com.cn))


Day 2 扫描线模型

2023.11.30

时隔一周终于再上了一次课。

Keynote

扫描线,一个再 DS 中经常会用到的技巧,它的做法也是比较套路的。

关于定义:我们分为两种东西,维度自由度,自由度就是操作中可以动的量。

扫描线 解决的问题一般都是先操作再查询的 静态 问题,扫描线就是将问题减少一维而将 静态 问题变成 动态 问题的方法。

对于扫描线的题目,我们主要分为三种方法。

  1. 反演:对于有关每一个值的,我们可以把它转化成求 每一个元素对区间的贡献,具体来说将区间的询问变成二维平面上面的每一个点,将每一个元素贡献的区间变成有贡献的矩形,最后跑一遍扫描线求值即可。

  2. 区间子区间问题:(lxl:这是一个批量出题的好方法)

    我们将 区间子区间 问题表示到二维的平面上面,同样用一个点表示一个区间,

    那么区间子区间问题就是求一个二维矩阵中的答案个数,这样我们从左往右扫过来的过程中我们需要维护区间的历史信息和就可以了。

    这样相当于会在线段树上面维护两个标记,需要分析一下两个标记交错时如何处理。

  3. 换维扫描线:在做 DS 问题的时候我们可以采用枚举算法的方法,而在扫描线的时候我们也需要枚举从哪里扫,

    简单来说,扫描线一般都是按照时间的先后顺序依次扫过去的,

    而在有些题目中,这是非常不好做的,所以我们考虑 换维

    具体来说假设现在构成的二维平面又两维,时间和序列,我们可以尝试按照序列依次扫过去,把区间的修改变成单点的修改和查询。

这是这节课最主要所讲的,而还有一些挺有用的思维方式。

  1. 对于森林计数,我们考虑用 点 - 边。(还是挺好理解的)

  2. 数颜色 相关的题目,我们常常考虑容斥,去计算没有这些颜色没出现的区间(出现 \(0\) 次总会比你去讨论出现 \(1,2,\dots\) 次简单)

  3. 最大值 所影响的区间的两种方法:

    • 单调栈维护(简单易懂)

    • 最大值分治:

      当最大值在 \(i\) 位置时,那么他所影响的区间是左端点在 \([1,i]\) 中而右端点在 \([i,n]\) 的区间。

      于是我们进行分治,将序列分成两个部分 \([1,i-1]\)\([i+1,n]\) 再分别递归下去找最大值。

  4. 由于区间的 \(l \lt r\),所以有一些 4-side 问题可以直接转化成 2-side 问题。

感觉题也不是很多,也没有 Day 1 写着那么恶心。

扫描线的问题主要难在思路部分,但总是还是有许多套路的。


CF1000F 区间查出现一次的个数 - 反演

CF1000F One Occurrence

给定长为 \(n\) 的序列,\(m\) 次查询区间中有多少值只出现一次。

\(1 \le n,m \le 5 \times 10^5\)

一上来没什么感觉直接不会做了。


发现把每一个数出现的位置画出来,容易发现只出现一次的区间范围是很容易得到的。

图中左端点在绿色区间,右端点在红色区间就是一个例子,同理我们可以以此类推下去。

image

而对于这样的 \(l,r\) 区间我们可以转化成二维平面上面的区间,

横纵坐标分别表示 \(l,r\),于是可以离线下来和询问一起跑一次扫描线即可。

这样会有 \(n\) 个矩形和 \(m\) 个询问的点,时间是完全满足的。


实现的时候我写的类似 Hanghang 的项链的方法,10 分钟完成。

Code
#include <bits/stdc++.h>
using namespace std;

const int N=1e6+5;
int n,m,c[N],ans[N];
struct node{
  int pos,val;
  bool operator<(const node &rhs) const{return pos<rhs.pos;}
}tr[N<<2],a[N];
struct D{
  int l,r,id;
  bool operator <(const D &rhs) const{return r<rhs.r;}
}q[N];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

void pu(int p){tr[p]=min(tr[lc],tr[rc]);}
void upd(int l,int r,int p,int x,node val){
  if(l==r) return tr[p]=val,void();
  if(x<=mid) upd(lson,x,val);
  else upd(rson,x,val);pu(p);
}
node qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p];
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return min(qry(lson,x,y),qry(rson,x,y));
}

int main(){
  /*2023.11.30 H_W_Y CF1000F One Occurrence SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;
  for(int i=1,x;i<=n;i++) cin>>x,a[i]=(node){c[x],x},c[x]=i;
  cin>>m;
  for(int i=1;i<=m;i++) cin>>q[i].l>>q[i].r,q[i].id=i;
  sort(q+1,q+m+1);int it=0;
  for(int i=1;i<=n;i++){
  	if(a[i].pos) upd(1,n,1,a[i].pos,(node){n+1,0});
  	upd(1,n,1,i,a[i]);
  	while(it<m&&q[it+1].r==i){
  	  ++it;node res=qry(1,n,1,q[it].l,q[it].r);
  	  if(res.pos<q[it].l) ans[q[it].id]=res.val;
  	  else ans[q[it].id]=0;
  	}
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

UOJ637 数颜色 - 反演

【美团杯2021】A. 数据结构

给一个长为 \(n\) 的序列,\(m\) 次查询:

如果将区间 \([l,r]\) 中所有数都 \(+1\),那么整个序列有多少个不同的数?

询问间独立,也就是说每次查询后这个修改都会被撤销。

\(1 \le n,m \le 10^6\)

由于每一次我们询问的是整个序列的,

所以现在考虑离线下来反演,也就是枚举每一个元素。


发现每个颜色的出现个数是不好去枚举的,于是我们考虑枚举在那些区间操作时没有出现数 \(x\)

这也是好分析的,和上一道题有类似的方式,

我们在求出这些区间之后还要去与 \(x-1\) 的区间取交即可。

这都是比较好做的,于是这道题就做完了。


代码是简单的,要简单分情况讨论一下(还有一些存留的注释语句)。

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second

const int N=1e6+5;
int n,m,cnt=0,ans[N],tr[N<<2];
vector<int> pos[N];
struct node{int l,r,v,ts;};
vector<node> g[N];
vector<pii> q[N];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

void pd(int p){tr[lc]+=tr[p],tr[rc]+=tr[p],tr[p]=0;}
void upd(int l,int r,int p,int x,int y,int val){
  if(x<=l&&y>=r) return tr[p]+=val,void();pd(p);
  if(x<=mid) upd(lson,x,y,val);
  if(y>mid) upd(rson,x,y,val);
}
int qry(int l,int r,int p,int x){
  if(l==r) return tr[p];pd(p);
  if(x<=mid) return qry(lson,x);
  return qry(rson,x);
}

int main(){
  /*2023.11.30 H_W_Y #637. 【美团杯2021】A. 数据结构 SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1,x;i<=n;i++) cin>>x,pos[x].pb(i);
  for(int i=1,l,r;i<=m;i++) cin>>l>>r,q[l].pb({r,i});
  for(int i=1,l,r;i<=n+1;i++){
  	if(!pos[i].size()&&!pos[i-1].size()){g[1].pb((node){1,n,1,i});continue;}
  	if(!pos[i].size()){
  	  for(int j=1;j<pos[i-1].size();j++){
  	  	g[pos[i-1][j-1]+1].pb({pos[i-1][j-1]+1,pos[i-1][j]-1,1,i});
  	  	g[pos[i-1][j]].pb({pos[i-1][j-1]+1,pos[i-1][j]-1,-1,i});
  	  }
  	  if(pos[i-1][0]>1) g[1].pb({1,pos[i-1][0]-1,1,i}),g[pos[i-1][0]].pb({1,pos[i-1][0]-1,-1,i});
  	  if(pos[i-1].back()<n) g[pos[i-1].back()+1].pb({pos[i-1].back()+1,n,1,i});
  	  continue;
  	}
  	if(!pos[i-1].size()){
  	  g[1].pb({pos[i].back(),n,1,i}),g[pos[i][0]+1].pb({pos[i].back(),n,-1,i});
  	  continue;
  	}
  	l=pos[i][0],r=pos[i].back();
  	//cout<<i<<"->(l,r)= ("<<l<<","<<r<<")"<<endl;
  	for(int j=1;j<pos[i-1].size();j++){
  	  if(pos[i-1][j-1]+1<=l&&pos[i-1][j]-1>=r){
  	  	g[pos[i-1][j-1]+1].pb({r,pos[i-1][j]-1,1,i});
  	  	g[l+1].pb({r,pos[i-1][j]-1,-1,i});break;
  	  }
  	}
  	if(pos[i-1][0]-1>=r) g[1].pb({r,pos[i-1][0]-1,1,i}),g[l+1].pb({r,pos[i-1][0]-1,-1,i});
  	if(pos[i-1].back()+1<=l) g[pos[i-1].back()+1].pb({r,n,1,i}),g[l+1].pb({r,n,-1,i});
  }
  for(int i=1;i<=n;i++){
  	for(auto j:g[i]) upd(1,n,1,j.l,j.r,j.v);//cout<<i<<"= ("<<j.l<<","<<j.r<<","<<j.v<<") "<<j.ts<<endl;
  	for(auto j:q[i]) ans[j.se]=qry(1,n,1,j.fi);
  }
  for(int i=1;i<=m;i++) cout<<(n+1-ans[i])<<'\n';
  return 0;
}

Hdu5603 点与区间 - 反演

Hdu5603 the soldier of love

\(n\) 个区间。

\(m\) 次询问,每次询问给出一些点(点是一维的),求对于这些点而言,有多少个初始给定的区间包含了至少这些点中的一个点。

\(n,m\),总点数 \(\le 3\times 10^5\)

课上想出来了。


也是比较套路的,我们直接考虑那些区间没有出现这些点,

他一定是一个阶梯状的,而每一个矩形是不交的,于是我们最后跑一遍扫描线就可以了。


这样用扫描线思维是不差的,但是实现很差!

还不如用 Hanghang 的项链的方法,用上区间数颜色的经典操作—— 维护颜色上一次出现的位置 即可。


Code
#include <bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back

const int N=1e6+5,mx=1e6;
int n,m,ans[N],tr[N],l[N];
vector<int> g[N];
vector<pii> q[N];

int lowbit(int i){return i&(-i);}
void upd(int x,int val){for(int i=x;i<=mx;i+=lowbit(i)) tr[i]+=val;}
int qry(int x){int res=0;for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];return res;}

int main(){
  /*2023.11.30 H_W_Y hdu5603 the soldier of love BIT*/
  while(scanf("%d%d",&n,&m)!=EOF){
  	for(int i=0;i<=mx;i++) ans[i]=tr[i]=l[i]=0,g[i].resize(0),q[i].resize(0);
    for(int i=1,x,y;i<=n;++i) scanf("%d%d",&x,&y),g[y].pb(x);
    for(int i=1,c;i<=m;++i){
      scanf("%d",&c);int lst=0;
      for(int j=1,x;j<=c;++j) scanf("%d",&x),q[x].pb({lst,i}),lst=x;
      l[i]=lst;
    }
    for(int i=1;i<=mx;++i){
  	  for(auto j:q[i]) ans[j.se]+=qry(i)-qry(j.fi);
  	  for(auto j:g[i]) upd(j,1);
    }
    for(int i=1;i<=m;++i) ans[i]+=n-qry(l[i]);
    for(int i=1;i<=m;++i) printf("%d\n",n-ans[i]);
  }
  return 0;
}

CF526F 复杂度矩形加减 - 反演

CF526F Pudding Monsters

给定一个 \(n \times n\) 的棋盘,其中有 \(n\) 个棋子,每行每列恰好有一个棋子。

对于所有的 \(1 \leq k \leq n\),求有多少个 \(k \times k\) 的子棋盘中恰好有 \(k\) 个棋子,输出所有 \(k\) 对应的答案之和。

\(n \le 3 \times 10^5\)


容易发现这是一个排列,我们把二维的问题转化到一维上面。

一个区间 \([l,r]\) 满足条件当且仅当这个区间里面的 \(max-min\)\(r-l\) 相等,这是好理解的。

因为只有这样我们才能做到中间出现了 \(k\) 个数。


于是现在再来考虑如何统计。

同样用一个二维的平面来表示每一个区间,即点 \((i,j)\) 表示 \([i,j]\) 的区间。

这个区间可以被统计是当且仅当 \(max-min-(r-l)=0\) 的,而我们就希望取维护这个值。

首先对于 \(-j+i\) 是好维护的,就直接用 \(2n\) 个矩形的加减。

而对于这里的 \(max\)\(min\),我们可以用 Keynote 中提到的方法算出每一个值所影响的区间,

同样变成 \(2n\) 个矩形的加减就可以了。


这样我们就可以用 \(4n\) 个矩形的加减算出我们想要的值,

那么现在的问题就在于如何去维护 \(0\) 的个数。

容易发现 \(0\) 一定是这个值的最小值,所以我们直接维护最小值和最小值出现次数即可。


最后跑一次扫描线就做完了。(直接一发过~)

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

const int N=2e6+5;
int n,a[N],st[N],tp=0;
ll ans=0;
struct node{int l,r,v;};
vector<node> g[N];

struct sgt{
  int val,cnt,tag;
}tr[N<<2];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

void pu(int p){
  if(tr[lc].val==tr[rc].val) tr[p].val=tr[lc].val,tr[p].cnt=tr[lc].cnt+tr[rc].cnt;
  else if(tr[lc].val<tr[rc].val) tr[p].val=tr[lc].val,tr[p].cnt=tr[lc].cnt;
  else tr[p].val=tr[rc].val,tr[p].cnt=tr[rc].cnt;
}

void pd(int p){
  if(!tr[p].tag) return ;
  tr[lc].tag+=tr[p].tag;
  tr[rc].tag+=tr[p].tag;
  tr[lc].val+=tr[p].tag;
  tr[rc].val+=tr[p].tag;
  tr[p].tag=0;
}

void upd(int l,int r,int p,int x,int y,int val){
  if(x<=l&&y>=r){tr[p].tag+=val;tr[p].val+=val;return;}pd(p);
  if(x<=mid) upd(lson,x,y,val);
  if(y>mid) upd(rson,x,y,val);pu(p);
}

void build(int l,int r,int p){
  if(l==r){tr[p].cnt=1,tr[p].val=tr[p].tag=0;return;}
  build(lson);build(rson);pu(p);
}

int main(){
  /*2023.11.30 H_W_Y CF526F Pudding Monsters SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;tp=0;build(1,n,1);
  for(int i=1,x,y;i<=n;i++){
  	cin>>x>>y,a[x]=y;
  	g[i].pb({1,n,i});g[i+1].pb({1,n,-i});g[1].pb({i,i,-i});
  }a[n+1]=n+1;
  for(int i=1;i<=n+1;i++){
  	while(tp>0&&a[st[tp]]<a[i]){
  	  g[st[tp-1]+1].pb({st[tp],i-1,a[st[tp]]});
  	  g[st[tp]+1].pb({st[tp],i-1,-a[st[tp]]});
  	  tp--;
  	}
  	st[++tp]=i;
  }a[n+1]=0;tp=0;
  for(int i=1;i<=n+1;i++){
  	while(tp>0&&a[st[tp]]>a[i]){
  	  g[st[tp-1]+1].pb({st[tp],i-1,-a[st[tp]]});
  	  g[st[tp]+1].pb({st[tp],i-1,a[st[tp]]});
  	  tp--; 
  	}
  	st[++tp]=i;
  }
  for(int i=1;i<=n;i++){
  	for(auto j:g[i]) upd(1,n,1,j.l,j.r,j.v);
  	if(tr[1].val==0) ans+=1ll*tr[1].cnt;
  }
  cout<<ans<<'\n';
  return 0;
}

P8868 [NOIP2022] 比赛 - 区间子区间

P8868 [NOIP2022] 比赛

给定 \(A_{1,2,\dots,n},B_{1,2,\dots,n}\),以及 \(m\) 个询问,每次询问给出 \(l,r\),求

\[\sum_{p=l}^r \sum_{q=p}^r (\max_{i=p}^q A_i)(\max_{i=p}^q B_i) \]

\(1 \le n,m \le 2.5 \times 10^5\)

区间子区间问题典。


区间子区间问题的常规处理方法已经在 Keynote 中讲的很清楚了,

这道题的 \(\max\) 我们同样可以用单调栈维护出来,分析一下去加信息累加时的标记。


其实题解部分已经讲得很清楚了

主要的思想就是先将同类的标记合并之后,我们对于交错的标记再进行一个讨论就可以了,

也就是分组,累加时分类讨论。具体看代码吧。


代码中我们钦定一个标记组是先历史值累加再赋值。

Code
#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back

const int N=1e6+5;
int n,m,a[N],b[N],st[N],tp=0,fa[N],fb[N];
ull ans[N];
vector<pii> g[N];

struct tag{ull cx,cy,xy,x,y,c;}tg[N<<2];
struct sgt{
  ull s,xy,x,y;
  sgt operator +(const sgt &t)const{return (sgt){s+t.s,xy+t.xy,x+t.x,y+t.y};}
}tr[N<<2];

#define mid ((l+r)>>1)
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1
#define lc p<<1
#define rc p<<1|1

void pu(int p){tr[p]=tr[lc]+tr[rc];}

/*先历史值累计再更新*/
void change(int l,int r,int p,tag t){
  ull &cx=tg[p].cx,&cy=tg[p].cy,&xy=tg[p].xy,&x=tg[p].x,&y=tg[p].y,&c=tg[p].c;
  int len=r-l+1;
  
  if(cx&&cy) c+=t.xy*cx*cy+t.x*cx+t.y*cy+t.c;
  else if(cx) y+=t.y+cx*t.xy,c+=cx*t.x+t.c;
  else if(cy) x+=t.x+cy*t.xy,c+=cy*t.y+t.c;
  else x+=t.x,y+=t.y,xy+=t.xy,c+=t.c;
  if(t.cx) cx=t.cx;
  if(t.cy) cy=t.cy;
  
  ull &s=tr[p].s,&sxy=tr[p].xy,&sx=tr[p].x,&sy=tr[p].y; 
  s+=t.xy*sxy+t.x*sx+t.y*sy+1ull*t.c*len;
  if(t.cx&&t.cy) sxy=t.cx*t.cy*len,sx=t.cx*len,sy=t.cy*len;
  else if(t.cx) sxy=t.cx*sy,sx=t.cx*len;
  else if(t.cy) sxy=t.cy*sx,sy=t.cy*len;
}

void pd(int l,int r,int p){
  if(tg[p].cx||tg[p].cy||tg[p].x||tg[p].xy||tg[p].y||tg[p].c) 
    change(lson,tg[p]),change(rson,tg[p]),tg[p]=(tag){0,0,0,0,0,0};
}

void upd(int l,int r,int p,int x,int y,tag t){
  if(x<=l&&y>=r) return change(l,r,p,t);pd(l,r,p);
  if(x<=mid) upd(lson,x,y,t);
  if(y>mid) upd(rson,x,y,t);pu(p);
}

ull qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].s;pd(l,r,p);
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return qry(lson,x,y)+qry(rson,x,y);
}

int main(){
  /*2023.11.30 H_W_Y P8868 [NOIP2022] 比赛 SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;cin>>n;
  for(int i=1;i<=n;i++){
  	cin>>a[i];
  	while(tp>0&&a[st[tp]]<a[i]) tp--;
  	fa[i]=st[tp]+1;st[++tp]=i;
  }tp=0;
  for(int i=1;i<=n;i++){
  	cin>>b[i];
  	while(tp>0&&b[st[tp]]<b[i]) tp--;
  	fb[i]=st[tp]+1;st[++tp]=i;
  }
  cin>>m;
  for(int i=1,l,r;i<=m;i++) cin>>l>>r,g[r].pb({l,i});
  for(int r=1;r<=n;r++){
  	upd(1,n,1,fa[r],r,(tag){1ull*a[r],0,0,0,0,0});
  	upd(1,n,1,fb[r],r,(tag){0,1ull*b[r],0,0,0,0});
  	upd(1,n,1,1,r,(tag){0,0,1,0,0,0});
  	for(auto j:g[r]) ans[j.se]=qry(1,n,1,j.fi,r);
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

CF997E 526F加强版 - 区间子区间

CF997E Good Subsegments

就是 CF526F 的全局统计变成在 \([l,r]\) 区间内的统计。

\(1 \le n,q \le 1.2 \times 10^5\)

区间子区间问题,扫描线维护区间历史值的累加即可。


很多时候这种历史值累加的标记都只是争对最小值来做的,所以直接维护最小值的历史累加记录就可以了。

一般的操作就是去判断一下左右区间是否存在最小值。


感觉多少有点绕,但是需要分析一下什么时候需要下传标记,还是比较好理解。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back

const int N=1e6+5;
int n,m,a[N],st[N],lst[N],tp=0;
ll ans[N];
struct D{int l,r,v;};
struct node{int l,r,id,op;};
vector<node> q[N];
vector<D> g[N];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1

struct sgt{
  ll mn,add,s;
  int cnt,tag;
}tr[N<<2];

void pu(int p){
  tr[p].mn=min(tr[lc].mn,tr[rc].mn);
  tr[p].s=tr[lc].s+tr[rc].s;tr[p].cnt=0;
  if(tr[lc].mn==tr[p].mn) tr[p].cnt+=tr[lc].cnt;
  if(tr[rc].mn==tr[p].mn) tr[p].cnt+=tr[rc].cnt;
}

void cadd(int p,ll val){tr[p].mn+=val,tr[p].add+=val;}
void ctag(int p,int val){tr[p].s+=1ll*tr[p].cnt*val,tr[p].tag+=val;}

void pd(int p){
  if(tr[p].add) cadd(lc,tr[p].add),cadd(rc,tr[p].add),tr[p].add=0;
  if(tr[p].tag){
  	if(tr[lc].mn==tr[p].mn) ctag(lc,tr[p].tag);
  	if(tr[rc].mn==tr[p].mn) ctag(rc,tr[p].tag);
  	tr[p].tag=0;
  }
}

void build(int l,int r,int p){
  if(l==r){
  	tr[p].mn=tr[p].tag=tr[p].s=tr[p].add=0;
  	tr[p].cnt=1;return;
  }
  build(lson);build(rson);pu(p);
}

void upd(int l,int r,int p,int x,int y,ll val){
  if(x<=l&&y>=r) return cadd(p,val),void();pd(p);
  if(x<=mid) upd(lson,x,y,val);
  if(y>mid) upd(rson,x,y,val);pu(p);
}

ll qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].s;pd(p);
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return qry(lson,x,y)+qry(rson,x,y);
}

int main(){
  /*2023.12.2 H_W_Y CF997E Good Subsegments SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;build(1,n,1);
  for(int i=1;i<=n;i++) cin>>a[i],g[1].pb({i,i,-i}),g[i].pb({1,n,i}),g[i+1].pb({1,n,-i});
  a[n+1]=n+1;
  for(int i=1;i<=n+1;i++){
    while(tp>0&&a[st[tp]]<a[i]){
      g[st[tp-1]+1].pb({st[tp],i-1,a[st[tp]]});
      g[st[tp]+1].pb({st[tp],i-1,-a[st[tp]]});
      tp--;
    }
    st[++tp]=i;
  }
  tp=0;a[n+1]=0;
  for(int i=1;i<=n+1;i++){
  	while(tp>0&&a[st[tp]]>a[i]){
  	  g[st[tp-1]+1].pb((D){st[tp],i-1,-a[st[tp]]});
  	  g[st[tp]+1].pb((D){st[tp],i-1,a[st[tp]]});
  	  tp--;
  	}
  	st[++tp]=i;
  }
  cin>>m;
  for(int i=1,l,r;i<=m;i++) cin>>l>>r,q[l-1].pb((node){l,r,i,-1}),q[r].pb((node){l,r,i,1});
  for(int i=1;i<=n;i++){
  	for(auto j:g[i]) upd(1,n,1,j.l,j.r,1ll*j.v);
  	ctag(1,1);
  	for(auto j:q[i]) ans[j.id]+=1ll*j.op*qry(1,n,1,j.l,j.r);
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

EC final2020 G 关于异或 - 区间子区间

Codeforces GYM 103069G EC final2020 G

给定一个序列,求区间有多少子区间,其内部出现过的颜色数为奇数。

\(1 \le n,m \le 10^5\)


同样是用扫描线维护,而每一次操作就是区间的异或操作。

具体来说就是维护每一个 \(l\) 表示左端点是 \(l\) 的答案,我们从左往右扫描 \(r\) 即可。

我们去维护区间内 \(0,1\) 的个数分别时多少,每一次再进行一次历史值累加即可。


区间子区间问题。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back
#define pii pair<int,int> 
#define fi first
#define se second

const int N=1e6+5;
int n,m,a[N],lst[N],c[N];
ll ans[N];
vector<pii> g[N];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1

struct sgt{int c[2];ll s;}tr[N<<2];
struct tag{int t[2],add;}tg[N<<2];

void pu(int p){
  tr[p].s=tr[lc].s+tr[rc].s;
  tr[p].c[0]=tr[lc].c[0]+tr[rc].c[0];
  tr[p].c[1]=tr[lc].c[1]+tr[rc].c[1];
}

void change(int p,tag nw){
  if(tg[p].add==0) tg[p].t[0]+=nw.t[0],tg[p].t[1]+=nw.t[1];
  else tg[p].t[0]+=nw.t[1],tg[p].t[1]+=nw.t[0];
  tg[p].add^=nw.add;
  
  tr[p].s+=1ll*nw.t[0]*tr[p].c[0]+1ll*nw.t[1]*tr[p].c[1];
  if(nw.add) swap(tr[p].c[0],tr[p].c[1]);
}

void pd(int p){
  if(!tg[p].t[0]&&!tg[p].t[1]&&!tg[p].add) return;
  change(lc,tg[p]);change(rc,tg[p]);tg[p]=(tag){{0,0},0};
}

void build(int l,int r,int p){
  if(l==r){
  	tr[p].c[0]=1,tr[p].c[1]=tr[p].s=0;
  	tg[p]=(tag){{0,0},0};return;
  }
  build(lson);build(rson);pu(p);
}

void upd(int l,int r,int p,int x,int y,tag val){
  if(x<=l&&y>=r) return change(p,val);pd(p);
  if(x<=mid) upd(lson,x,y,val);
  if(y>mid) upd(rson,x,y,val);pu(p);
}

ll qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].s;pd(p);
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return qry(lson,x,y)+qry(rson,x,y);
}

int main(){
  /*2023.12.2 H_W_Y EC final 2020 G. Prof. Pang's sequence SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;build(1,n,1);
  for(int i=1;i<=n;i++) cin>>a[i],lst[i]=c[a[i]]+1,c[a[i]]=i;
  cin>>m;
  for(int i=1,l,r;i<=m;i++) cin>>l>>r,g[r].pb({l,i});
  for(int i=1;i<=n;i++){
  	upd(1,n,1,lst[i],i,(tag){{0,0},1});
  	upd(1,n,1,1,i,(tag){{0,1},0});
  	for(auto j:g[i]) ans[j.se]=qry(1,n,1,j.fi,i);
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

LOJ3489 JOISC2021 T3 - 换维扫描线

LOJ3489 JOISC2021 饮食区

有一个长为 \(n\) 的序列,序列每个位置有个队列。

\(m\) 个操作。

  1. 每个操作形如 \([l,r]\) 的每个队列中进来了 \(k\)\(type=c\) 的人。

  2. 或者 \([l,r]\) 的每个队列中出去了 \(k\) 个人(不足 \(k\) 个则全部出去)

  3. 还有查询某个队列中第 \(k\) 个人的 \(type\)(不足 \(k\) 个输出 \(0\)

\(1 \le n,m,q \le 2.5 \times 10^5\)


首先容易想到扫描线,但是直接做是困难的。

所以我们考虑 换维扫描线,从左往右扫描每一个序列,

于是每一个区间操作就变成了单点修改,而操作二是相当于清空一段前缀。


具体建出来平面直角坐标系就是这样的(lxl 的图):

image

这里的黑色线段表示着每一次修改,红色线是一次关于前缀的查询。


前面是好理解的,但是发现后面删除前面的 \(k\) 个人和找第 \(k\) 个人并不好处理。

我们用了线段树去维护时间轴,而对于每一次的修改操作看成一次单点的修改,

每一次的清空操作我们看作是前缀减去 \(k\)


那么每一次查询的时候,我们就只需要求到一个上一次清空到的位置再整体二分就可以了。

如何找到上一次清空到的位置?


发现前面被清空的前缀和一定是 \(\le 0\) 的,如果我们现在维护一个后缀和,

那么后缀和最大的位置就一定是上一次清空的位置,这是好理解的。


于是我们就可以轻松找到这个位置,于是再二分就可以了。

这道题的重点在于换维扫描线,感觉讲起来比较抽象。


实现部分代码中比较清楚。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define pll pair<int,ll>
#define fi first
#define se second
#define pb push_back

const int N=5e5+5;
int n,m,q,ans[N],col[N];
vector<pii> add[N],del[N];
vector<pll> f[N];

struct SGT{
  struct sgt{ll mx,mn,tag;}tr[N<<2];
  #define mid ((l+r)>>1)
  #define lc p<<1
  #define rc p<<1|1
  #define lson l,mid,lc
  #define rson mid+1,r,rc
  
  void pu(int p){
  	tr[p].mx=max(tr[lc].mx,tr[rc].mx);
  	tr[p].mn=min(tr[lc].mn,tr[rc].mn);
  }
  
  void change(int p,ll v){tr[p].mx+=v,tr[p].mn+=v,tr[p].tag+=v;}
  
  void pd(int p){
  	if(!tr[p].tag) return ;
  	change(lc,tr[p].tag);change(rc,tr[p].tag);tr[p].tag=0;
  }
  
  void upd(int l,int r,int p,int x,int y,ll v){
    if(x<=l&&y>=r) return change(p,v),void();pd(p);
    if(x<=mid) upd(lson,x,y,v);
    if(y>mid) upd(rson,x,y,v);pu(p);
  }
  
  ll qry(int l,int r,int p,int x){
  	if(l==r) return tr[p].mn;pd(p);
  	if(x<=mid) return qry(lson,x);
  	if(x>mid) return qry(rson,x);
  	return -1;
  }
  
  ll qry_mn(int l,int r,int p,int x,int y){
    if(x<=l&&y>=r) return tr[p].mn;pd(p);
    if(y<=mid) return qry_mn(lson,x,y);
    if(x>mid) return qry_mn(rson,x,y);
    return min(qry_mn(lson,x,y),qry_mn(rson,x,y));
  }
  
  int find(int l,int r,int p,int x,int y,ll v){
  	if(tr[p].mx<v) return 0;
  	if(l==r) return col[l];pd(p);
  	if(x<=l&&y>=r){
  	  if(tr[rc].mx>=v) return find(rson,x,y,v);
  	  return find(lson,x,y,v); 
  	}else if(y>mid){
  	  int nw=find(rson,x,y,v);
  	  if(nw) return nw;
  	}
  	return find(lson,x,y,v);
  }
}T1,T2;

int main(){
  /*2023.12.2 H_W_Y JOISC2021 C - フードコート(Foodcourt) SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>q;
  for(int i=1,op,l,r,c,k;i<=q;i++){
  	cin>>op;
  	if(op==1){
  	  cin>>l>>r>>c>>k;col[i]=c;
  	  add[l].pb({i,k});add[r+1].pb({i,-k});
  	}else if(op==2){
  	  cin>>l>>r>>k;
  	  del[l].pb({i,-k});del[r+1].pb({i,k});
  	}else{
  	  ll x;cin>>l>>x;
  	  f[l].pb({i,x});
  	}
  }
  memset(ans,-1,sizeof(ans));
  for(int i=1;i<=n;i++){
    for(auto j:add[i]) T1.upd(1,q,1,j.fi,q,1ll*j.se),T2.upd(1,q,1,1,j.fi,1ll*j.se);
    for(auto j:del[i]) T1.upd(1,q,1,j.fi,q,1ll*j.se);
    for(auto j:f[i]){
      ll x=T1.qry(1,q,1,j.fi)-min(0ll,T1.qry_mn(1,q,1,1,j.fi)),y=j.se;
      if(y>x) ans[j.fi]=0;
      else{
      	x=x-y+1;
      	ans[j.fi]=T2.find(1,q,1,1,j.fi,x+T2.qry(1,q,1,j.fi));
      }
    }
  }
  for(int i=1;i<=q;i++) if(ans[i]!=-1) cout<<ans[i]<<'\n';
  return 0;
}

P3863 序列 - 换维扫描线

P3863 序列

给定一个长度为 \(n\) 的序列,给出 \(q\) 个操作,形如:

\(1~l~r~x\) 表示将序列下标介于 \([l,r]\) 的元素加上 \(x\) (请注意,\(x\) 可能为负)

\(2~p~y\) 表示查询 \(a_p\) 在过去的多少秒时间内不小于 \(y\) (不包括这一秒,细节请参照样例)

开始时为第 \(0\) 秒,第 \(i\) 个操作发生在第 \(i\) 秒。

\(1 \le n,q \le 10^5\)

Mea orz


看到题目就很像是去用换维扫描线维护时间轴,

但是似乎并不是想象那么好做。


发现我们不知道如何去维护权值大小,这是非常难做的。

所以我们考虑一种根号数据结构——分块。


在每一个块中,我们将元素按照大小排序,查询和修改都是简单的。

于是时间复杂度是 \(\mathcal O(n \sqrt n \log n)\)\(2s\) 的时限是可以通过的。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

const int N=1e5+5;
int n,m,bl[N],L[N],R[N],cnt,ans[N];
ll a[N],t[N];
struct node{int op,pos;ll v;};
struct D{
  int pos;ll v;
  bool operator <(const D &rhs) const{return (v==rhs.v)?(pos<rhs.pos):(v<rhs.v);}
}p[N];
vector<node> g[N];

void upd(int l,ll v){
  for(int i=L[bl[l]];i<=R[bl[l]];i++) if(p[i].pos>=l) p[i].v+=v;
  sort(p+L[bl[l]],p+R[bl[l]]+1);
  for(int i=bl[l]+1;i<=bl[m];i++) t[i]+=v; 
}

int qry(int r,ll v){
  int res=0;
  for(int i=L[bl[r]];i<=R[bl[r]];i++) if(p[i].pos<r&&p[i].v+t[bl[r]]>=v) res++;
  for(int i=1;i<bl[r];i++) res+=cnt-(lower_bound(p+L[i],p+R[i]+1,(D){0,v-t[i]})-p-L[i]+1)+1;
  if(v<=0) res++;
  return res;
}

int main(){
  /*2023.12.2 H_W_Y P3863 序列 FK*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1,op,l,r;i<=m;i++){
  	cin>>op;ll v;
  	if(op==1) cin>>l>>r>>v,g[l].pb((node){op,i,v}),g[r+1].pb((node){op,i,-v});
    else cin>>l>>v,g[l].pb((node){op,i,v});
  }
  cnt=(int)floor(sqrt(n));
  for(int i=1;i<=m;i++){
  	bl[i]=(i-1)/cnt+1;p[i]={i,0};
  	if(!L[bl[i]]) L[bl[i]]=i,R[bl[i]-1]=i-1;
  }
  R[bl[m]]=m;
  memset(ans,-1,sizeof(ans));
  for(int i=1;i<=n;i++)
  	for(auto j:g[i]){
  	  if(j.op==1) upd(j.pos,j.v);
  	  else ans[j.pos]=qry(j.pos,j.v-a[i]);
	}
  for(int i=1;i<=m;i++) if(~ans[i]) cout<<ans[i]<<'\n';
  return 0;
}


P8518 [IOI2021] 分糖果 - 换维扫描线

P8518 [IOI2021] 分糖果

Khong 阿姨正在给附近一所学校的学生准备 \(n\) 盒糖果。盒子的编号分别为 \(0\)\(n - 1\),开始时盒子都为空。第 \(i\) 个盒子 \((0 \leq i \leq n - 1)\) 至多可以容纳 \(c[i]\) 块糖果(容量为 \(c[i]\))。

Khong 阿姨花了 \(q\) 天时间准备糖果盒。在第 \(j\)\((0 \leq j \leq q - 1)\),她根据三个整数 \(l[j]\)\(r[j]\)\(v[j]\) 执行操作,其中 \(0 \leq l[j] \leq r[j] \leq n - 1\)\(v[j] \neq 0\)。对于每个编号满足 \(l[j] \leq k \leq r[j]\) 的盒子 \(k\)

  • 如果 \(v[j] > 0\),Khong 阿姨将糖果一块接一块地放入第 \(k\) 个盒子,直到她正好放了 \(v[j]\) 块糖果或者该盒子已满。也就是说,如果该盒子在这次操作之前已有 \(p\) 块糖果,那么在这次操作之后盒子将有 \(\min(c[k], p + v[j])\) 块糖果。

  • 如果 \(v[j] < 0\),Khong 阿姨将糖果一块接一块地从第 \(k\) 个盒子取出,直到她正好从盒子中取出 \(-v[j]\) 块糖果或者该盒子已空。也就是说,如果该盒子在这次操作之前已有 \(p\) 块糖果,那么在这次操作之后盒子将有 \(\max(0, p + v[j])\) 块糖果。

你的任务是求出 \(q\) 天之后每个盒子中糖果的数量。

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


上周一次联考考到了,考场上想出来了,但是代码死活调不出来,

于是后面就没有心情去写了,知道现在。


发现直接做是几乎不能做的,

所以我们考虑 换维扫描线,枚举每一个位置,用线段树维护时间轴上的值。


发现对于最后的答案,我们其实只关心最后一次到达上界/下界的时刻,

而答案就是后面的时间所造成的区间和。


那么我们如何去找这个值呢?

(联考的时候没想清楚搞了很久)

发现其实我们去维护一个区间的前缀最小值和最大值,

对于序列的某一个后缀而言,如果它的前缀最小最大的差值 \(\gt c_i\) 就说明它撞了两次界,

这个求出来之后就可以轻松的求到最终的值了——分类讨论一下最后一次撞的是哪一个界。

而找到这个最靠后的点我们可以直接用线段树二分实现,于是这道题就做完了。


难点还是在于换维扫描线。

Code
#include <bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define ll long long
#define fi first
#define se second
#define pb push_back

const int N=2e5+5;
int n,m;
ll a[N];
vector<pii> g[N];
vector<int> ans;

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

struct sgt{
  ll s,mx,mn;
  sgt operator +(const sgt &nw){return (sgt){s+nw.s,max(mx,s+nw.mx),min(mn,s+nw.mn)};}
  void init(ll x){s=x;mx=max(0ll,x);mn=min(0ll,x);}
}tr[N<<2];

void pu(int p){tr[p]=tr[lc]+tr[rc];}

void upd(int l,int r,int p,int x,ll v){
  if(l==r) return tr[p].init(tr[p].s+v),void();
  if(x<=mid) upd(lson,x,v);
  else upd(rson,x,v);pu(p);
}

ll qry(int l,int r,int p,ll v,sgt nw){
  if(l==r){
  	if((tr[p].s<0&&nw.mx>v)||(tr[p].s>0&&nw.mn>=-v)) return v+nw.s-nw.mx;
  	else return nw.s-nw.mn;
  }
  sgt cur=tr[rc]+nw;
  if(cur.mx-cur.mn<=v) return qry(lson,v,cur);
  return qry(rson,v,nw);
}

/*2023.12.2 H_W_Y P8518 [IOI2021] 分糖果 SGT*/
std::vector<int> distribute_candies(std::vector<int> c, std::vector<int> l,std::vector<int> r, std::vector<int> v){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  n=c.size();m=l.size();
  for(int i=0;i<n;i++) a[i+1]=c[i],g[i].resize(0);
  for(int i=0;i<m;i++) g[l[i]+1].pb({i+1,v[i]}),g[r[i]+2].pb({i+1,-v[i]});
  ans.resize(0);
  for(int i=1;i<=n;++i){
  	for(auto j:g[i]) upd(0,m,1,j.fi,1ll*j.se);
  	ans.pb(qry(0,m,1,a[i],(sgt){0,0,0}));
  }
  return ans;
}

Conclusion

第二天的考试直接考了个 换维扫描线+历史值累加,就挺抽象的,但是我场上过了。

  1. 对于区间子区间的复杂标记下传问题一定要想好顺序和具体合并时的方法,我们把它变成一个标记组一个标记组去合并。
  2. 区间子区间问题很多只是争对最小值的累加,所以处理的时候直接维护最小值的累加次数,下传时注意。
  3. 分块 可以对值域和下标进行同时的处理,将每一个块排序即可。(P3863 序列)

2023.12.2 lxl DS 再次补完。(感觉 PPT 其他没讲的内容也挺有意思的)

posted @ 2024-06-24 15:45  H_W_Y  阅读(116)  评论(1编辑  收藏  举报