20230625-线段树专题测试

发布时间 2023-06-27 00:35:13作者: H_W_Y

考点梳理

  1. T1.混凝土粉末-需要转一个弯的线段树
  2. T2.机房惨案-线段树+动态开点+李超线段树(线段树套李超树)
  3. T3.丧钟为谁而鸣-两个线段树+珂朵莉树+动态开点

总结

  1. 线段树区间问题可以从想得到的答案是什么入手,边扫边维护-T1
  2. long long的值在其出现的地方都要开long long-T1+T2
  3. 线段树二分往右走时要减去左子树的值-T1
  4. set自带排序功能,如果用结构体一定要自定义排序-T3
  5. 一些新的知识点:
    1> 动态开点-T2,T3
    2> 线段树上的二分-T1
    3> 李超线段树-T2
    4> 珂朵莉树-T3

T1.混凝土粉末

concrete.cpp

题目大意

有一个长度为\(n\)的序列,\(m\)次操作,要求支持:

  1. 在区间\([l,r]\)中每个位置加入\(h\)个的\(x\)种类的方块
  2. 询问第\(x\)个位置高度为\(y\)的地方是什么方块

\(1 \le n,m \le 10^6,1 \le h \le 10^9,1\le y\le 10^{18}\)

分析

首先加入的\(i\)种类的方块的\(i\)是时间戳
那么我们不妨将所有操作的时间戳表示出来

考虑对于每一个横坐标\(x\)
我们都希望去找到一个在\(x\)上的询问
然后二分答案即可
此时线段树上的\([l,r]\)表示时间戳\([l,r]\)
里面每一个位置的值表示在这段时间里的最高高度
我们二分到高度\(y\)
比较此时的时间戳大小即可
答案的第\(i\)种也正好是所得时间戳的大小

那么我们现在就需要维护每一个位置\(x\)在不同时间戳时的高度
也就是它高度逐渐增加的序列
题目中操作1是在\([l,r]\)区间内每一个位置增加\(h\)
那我们在从左往右沿着\(x\)扫过来是
\(x \in [l,r]\)\(h\)是不变的
这样就有了差分的思想
我们把\(l\)\(r+1\)进行单点修改

将所有的操作离线
再按\(x\)从小到大排序
我们在扫的过程中即可维护当前\(x\)在不同时间戳的高度了

代码

注意有些变量要开long long!!!

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define mid (l+r)/2
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1

const int maxn=2e6+10;
int n,m,op,l,r,tot=0,ans,res[maxn];
bool vis[maxn];
struct node{
  int op,x,tim;
  ll h;
  bool operator <(const node &rhs) const{
    if(x!=rhs.x) return x<rhs.x;
    return op<rhs.op;
  }
}a[maxn];
ll tr[maxn*4],h;

void update(int l,int r,int rt,int x,ll val){//val要开long long ! ! ! 
  tr[rt]+=val;
  if(l==r) return;
  if(x<=mid) update(lson,x,val);
  else update(rson,x,val);
}

int query(int l,int r,int rt,ll val){
  if(tr[rt]<val) return 0;
  if(l==r) return l;
  if(tr[rt<<1]>=val) return query(lson,val);
  else return query(rson,val-tr[rt<<1]);//一定要减去tr[rt<<1] 
}

int main(){
  scanf("%d%d",&n,&m);
  for(int i=1;i<=m;i++){
  	scanf("%d",&op);
  	if(op==1){
  	  scanf("%d%d%lld",&l,&r,&h);
	  a[++tot]=(node){0,l,i,h};
	  a[++tot]=(node){0,r+1,i,-h};	
	}
	else{
	  vis[i]=true;
	  scanf("%d%lld",&l,&h);
	  a[++tot]=(node){1,l,i,h};
	}
  }
  sort(a+1,a+tot+1);
  for(int i=1;i<=tot;i++){
  	if(!a[i].op) update(1,m,1,a[i].tim,a[i].h);
  	else ans=query(1,m,1,a[i].h),res[a[i].tim]=(ans<a[i].tim)?ans:0;
  }
  for(int i=1;i<=m;i++)
     if(vis[i]) printf("%d\n",res[i]);
  return 0;
} 

T2.机房惨案

akioi.cpp

题目大意

对于每一对\(i,j(i \lt j)\)满足\(l_j \le i \le r_j\),则\(i\)可以转移到\(j\)
其中的代价为\((-t_i+ \sum_{k=i+1}^j s_k)^2\)
求从\(1\)转移到\(n\)的最小代价
\(n\le 5 * 10^5\)

分析

很容易想到是一个\(dp\)
\(dp_i\)表示转移到\(i\)的最小代价
$dp_i=min(dp_j+(sum_i-sum_j-t_j)^2) $

\(=min(dp_j+(sum_j+t_j)^2+sum_i^2-2 * sum_i * (sum_j+t_j))\)

我们不妨令\(b_j=dp_j+(sum_j+t_j)^2,k_j=-2* (sum_j+t_j)\)
\(sum_i^2\)可以作为初始化
\(dp_i=min(k_j* sum_i + b_j)+sum_i^2\)
这样一来就成了一次函数的形式
再加上\(j\)的取值范围是\(l_i \le j \le r_i\)
所以\(dp_i\)就成了一条线段
这样就想到了李超线段树
\(sum_i\)确定时
我们可以用\(O(log n)\)的时间找到最优的\(j\)

但是发现\(j\)的范围是随着\(i\)的变化而变化的
那我们就需要在外层再开一层线段树
在每一个线段树的节点上维护一棵动态开点的李超树
表示区间\([l,r]\)中的线段即可
线段树套李超树
时间复杂度\(O(n (log_n)^2)\)

我们在加入\(j\)时在其外层线段树所经过的结点的李超树里都加入线段
这样在查找区间时就会有这个线段出现
而动态开点过程中每次最多只会增加一个节点
所以空间复杂度为\(O(n log n)\)

代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define mid (l+r)/2

const int maxn=2e6+10;//数组不要开小了 
const ll inf=1e17;
int n,l[maxn],r[maxn],mn=0x3f3f3f3f,mx=-mn,s[maxn],sum[maxn],t[maxn];
ll dp[maxn];

struct Line{
  ll k,b;
  Line(ll kk=0,ll bb=inf){k=kk;b=bb;}
}L[maxn*4];
ll calc(int id,ll x){return L[id].k*x+L[id].b;}

namespace LCT{
  int ls[maxn*4],rs[maxn*4],tot=0,id[maxn*4];
  inline void update(int &rt,int x,int l=mn,int r=mx){
  	if(!rt){
  	  rt=++tot;
	  id[rt]=x;
	  return ;	
	}
	if(calc(id[rt],mid)>calc(x,mid)) swap(id[rt],x);
	if(calc(id[rt],l)>calc(x,l)) update(ls[rt],x,l,mid);
	else if(calc(id[rt],r)>calc(x,r)) update(rs[rt],x,mid+1,r);
  }
  inline ll query(int rt,ll x,int l=mn,int r=mx){
  	if(!rt) return inf;
  	ll ans=calc(id[rt],x);//首先要在大的区间内算出一个答案 
  	if(l==r) return ans;
  	if(x<=mid) return min(ans,query(ls[rt],x,l,mid));
  	else return min(ans,query(rs[rt],x,mid+1,r));
  }
} 

namespace SGT{
  #define lson l,mid,rt<<1
  #define rson mid+1,r,rt<<1|1
  int tr[maxn*4];
  inline void insert(int x,int l=1,int r=n,int rt=1){
  	LCT::update(tr[rt],x);
  	if(l==r) return;
  	if(x<=mid) insert(x,lson);
  	else insert(x,rson);
  }
  inline ll query(int a,int b,ll x,int l=1,int r=n,int rt=1){
  	if(a<=l&&b>=r) return LCT::query(tr[rt],x);
  	ll ans=inf;
  	if(a<=mid) ans=min(ans,query(a,b,x,lson));
  	if(b>mid) ans=min(ans,query(a,b,x,rson));
  	return ans;
  }
}

void addnode(int x){
  L[x]=Line(-2ll*(sum[x]+t[x]),1ll*(dp[x]+1ll*(sum[x]+t[x])*(sum[x]+t[x])));
  SGT::insert(x);
}

int main(){
  scanf("%d",&n);
  for(int i=1;i<=n;i++){
  	scanf("%d%d",&s[i],&t[i]);
  	sum[i]=sum[i-1]+s[i];
  	mn=min(mn,sum[i]);
  	mx=max(mx,sum[i]);
  }
  for(int i=2;i<=n;i++) scanf("%d%d",&l[i],&r[i]);
  dp[1]=0;
  addnode(1);
  for(int i=2;i<=n;i++){
  	dp[i]=1ll*sum[i]*sum[i]+SGT::query(l[i],r[i],sum[i]);//一定要乘上1ll 
  	addnode(i);
  }
  printf("%lld\n",dp[n]);
  return 0;
}

T3.丧钟为谁而鸣

harmony.cpp

题目大意

一个长度为\(n\)的序列,有\(m\)次操作,要求支持:

  1. 把指定区间内的数全部改为\(t\)
  2. 查询区间内的众数,若有多个,全部输出且满足众数出现的数量$cnt \ge (r-l+1)* p $%

\(n \le 10^5 ,m \le 3 * 10^4, 20 \le p \le 100\)

分析

首先考虑\(p \ge 51\)的情况
那么我们所需要众数的出现次数大于原数列的一半

考虑有一个东西叫做
摩尔投票法:
从左到右扫一遍整个序列
维护一个可能的众数以及其“票数”
如果遇到的数与众数相同
则票数 \(+1\)
否则票数 \(-1\)
如果票数变成 \(0\) 了,
就用这个新数替换掉之前的众数

那么最后剩下的数是唯一可能的答案
其他数一定不是答案。

其实也就是相当于把两两不同的数互相消掉
最后剩下的就是大于总个数一半的众数了
最多只有一个

观察题目中 \(20 \le p \le 100\)
那么我们可以同理把这个结论推到 \(\frac{1}{5}\)
注意这里可以等于\(20\),而上面的是不能等于\(50\)

所以我们就可以把每\(6\)个互不相同的数消掉
这样最多会剩下\(5\)个数来
\(5\)个数就是最后的答案
但是这样如果暴力枚举的话
时间复杂度是\(O(n^2)\)的,很明显过不了

考虑用线段树来维护区间
每一个节点维护按照摩尔投票法所剩下的
在合并时,暴力枚举即可
由于会边合并边把不同的消掉
所以会不重不漏地找到所有众数

在考虑如何判断答案的合法性
可以对于每一个\(a_i\)都建立一棵线段树
动态开点来维护它在每一个区间的出现次数

再来考虑每一次修改
都要在对应的\(a_i\)的线段树中修改
需要维护每一段连续的节点种类相同的区间
就需要用到珂朵莉树

总结一下:

  1. 需要用到一棵大的线段树
    维护区间内的众数
  2. 需要对于每一个\(a_i\)有一棵小的线段树
    维护\(a_i\)在每一个区间内出现的次数
    来判断答案的合理性
  3. 需要一棵珂朵莉树
    维护连续的\(a_i\)相同的一段区间
    来实现\(O(log n)\)进行每一次修改操作

代码

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

const int maxn=1e5+10,inf=0x3f3f3f3f;
int n,m,a[maxn],p,d;//d表示超过p%的最多众数个数
struct node{
  int len,a[5],c[5];
  node(){len=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;
} 

namespace SGT{//大线段树,维护众数 
  #define mid (l+r)/2
  #define lc rt<<1
  #define rc rt<<1|1
  #define lson l,mid,lc
  #define rson mid+1,r,rc
  const int M=maxn<<2;
  node tr[M];
  int tag[M];
  node operator + (const node &x,const node &y){ //重载+,完成暴力合并 
  	node ret=x;
  	for(int i=0;i<y.len;i++){
  	  bool flag=false;
	  for(int j=0;j<ret.len;j++) 
	    if(ret.a[j]==y.a[i]){ //找到相同的,直接合并 
	      ret.c[j]+=y.c[i];
	      flag=true;break;
		}
	  if(flag) continue;
	  if(ret.len<d){ //ret没有装满直接加入 
	  	int cnt=ret.len++;
	  	ret.c[cnt]=y.c[i];
	  	ret.a[cnt]=y.a[i];
	  	continue;
	  }	
	  int pos=0,tmp=y.c[i];
	  for(int j=0;j<ret.len;j++) //找到一个ret中cnt最小的 
	    if(ret.c[j]<ret.c[pos]) pos=j;
	  if(ret.c[pos]<y.c[i]){
	  	tmp=ret.c[pos];
	  	ret.c[pos]=y.c[i];ret.a[pos]=y.a[i];
	  } 
	  for(int j=0;j<ret.len;j++) ret.c[j]-=tmp;
	}
	return ret;
  }
  inline void pushup(int rt){tr[rt]=tr[lc]+tr[rc];}
  inline void cov(int l,int r,int rt,int val){
  	tag[rt]=val;
  	tr[rt].len=1;
  	tr[rt].a[0]=val;
  	tr[rt].c[0]=r-l+1;
  	return;
  } 
  inline void pushdown(int l,int r,int rt){
  	if(tag[rt]==0) return ;
  	cov(lson,tag[rt]);
	cov(rson,tag[rt]);
	tag[rt]=0;
  }
  inline void update(int l,int r,int rt,int a,int b,int val){
  	if(a<=l&&b>=r){
  	  cov(l,r,rt,val);
	  return;	
	}
	pushdown(l,r,rt);
	if(a<=mid) update(lson,a,b,val);
	if(b>mid) update(rson,a,b,val);
	pushup(rt); 
  }
  inline node query(int l,int r,int rt,int a,int b){
  	if(a<=l&&b>=r) return tr[rt];
  	pushdown(l,r,rt);
  	if(b<=mid) return query(lson,a,b);
  	if(a>mid) return query(rson,a,b);
  	return query(lson,a,b)+query(rson,a,b);
  }
  inline void build(int l=1,int r=n,int rt=1){
  	if(l==r){
  	  tr[rt].len=1;
  	  tr[rt].a[0]=a[l];
  	  tr[rt].c[0]=1;
	  return;	
	}
	build(lson);
	build(rson);
	pushup(rt);
  }
}

int rt[10000005];
namespace sgt{//动态开点的小线段树 
  const int N=maxn<<6;
  int siz[N],tag[N],ls[N],rs[N],tot=0;
  inline void pushup(int rt){siz[rt]=siz[ls[rt]]+siz[rs[rt]];}
  inline void cov(int l,int r,int rt,int val){
  	tag[rt]=val;
  	siz[rt]=val*(r-l+1);
  }
  inline void pushdown(int l,int r,int rt){
  	if(tag[rt]==-1) return;
  	if(!ls[rt]) ls[rt]=++tot,tag[ls[rt]]=-1;
  	if(!rs[rt]) rs[rt]=++tot,tag[rs[rt]]=-1;
  	cov(l,mid,ls[rt],tag[rt]);cov(mid+1,r,rs[rt],tag[rt]);
  	tag[rt]=-1;
  }
  inline void update(int l,int r,int &rt,int a,int b,int val){
  	if(!rt) rt=++tot,tag[rt]=-1;
  	if(a<=l&&b>=r){
  	  cov(l,r,rt,val);
	  return;	
	}
	pushdown(l,r,rt);
	if(a<=mid) update(l,mid,ls[rt],a,b,val);
	if(b>mid) update(mid+1,r,rs[rt],a,b,val);
	pushup(rt);
  }
  inline int query(int l,int r,int rt,int a,int b){
  	if(!rt) return 0;
  	if(a<=l&&b>=r) return siz[rt];
  	pushdown(l,r,rt);
  	int res=0;
  	if(a<=mid) res+=query(l,mid,ls[rt],a,b);
  	if(b>mid) res+=query(mid+1,r,rs[rt],a,b);
  	return res;
  }
}

struct seg{
  int l,r,val;//珂朵莉树,区间[l,r]的值为val
  seg(int ll,int rr,int vv){l=ll;r=rr,val=vv;} 
  bool operator <(const seg &rhs) const{//用了set就必须加自定义排序 
    if(l!=rhs.l) return l<rhs.l;
    return r<rhs.r;
  }
};

set<seg> s;
inline void split(int x){ //珂朵莉树核心,从x处分裂 
  if(x==n+1) return ;
  seg it=*prev(s.upper_bound(seg(x,inf,0)));//prev(it,n),n为正整数,返回距离"it"n个单位的迭代器,不写n则默认n=1
  if(it.l==x) return ;//本来就是分开的
  s.erase(it);
  s.insert(seg(it.l,x-1,it.val));
  s.insert(seg(x,it.r,it.val)); 
} 
inline void cover(int l,int r,int val){ //区间修改 
  SGT::update(1,n,1,l,r,val);
  split(l);split(r+1);
  auto ql=s.lower_bound(seg(l,0,0)),qr=s.lower_bound(seg(r+1,0,0));
  vector<seg> ret;
  while(ql!=qr){
  	ret.push_back(*ql);
  	ql++;
  }
  for(auto q:ret){
  	s.erase(q);
  	sgt::update(1,n,rt[q.val],q.l,q.r,0);
  }
  s.insert(seg(l,r,val));
  sgt::update(1,n,rt[val],l,r,1);
}

int main(){
  n=read();m=read();p=read();
  d=100/p;
  for(int i=1;i<=n;i++) a[i]=read();
  SGT::build();
  s.insert(seg(0,0,0));
  s.insert(seg(n+1,n+1,0));
  for(int l=1,r=0;l<=n;l=r+1){
  	r=l;
  	while(r<n&&a[r+1]==a[l]) r++;
  	s.insert(seg(l,r,a[l]));
  	sgt::update(1,n,rt[a[l]],l,r,1);
  }
  for(int i=1,op,l,r,val;i<=m;i++){
  	op=read();
  	if(op==1){
  	  l=read();r=read();val=read();
	  cover(l,r,val);	
	}
	else {
	  l=read();r=read();
	  node ans=SGT::query(1,n,1,l,r);
	  vector<pair<int,int> >ret;
	  for(int j=0;j<ans.len;j++){
	  	int tmp=sgt::query(1,n,rt[ans.a[j]],l,r);
	  	if(tmp*100>=p*(r-l+1)) ret.push_back(make_pair(tmp,ans.a[j]));  
	  }
	  if(!ret.size()){printf("0\n");continue;}
	  sort(ret.begin(),ret.end());
	  int pos=0,sum=0,res=ret.back().first;
	  for(int j=ret.size()-1;j>=0;j--) 
	    if(ret[j].first==res) sum++,pos=j; 
	  printf("%d ",sum);
	  for(int j=pos;j<ret.size();j++) printf("%d ",ret[j].second);
	  printf("\n");
	}
  } 
  return 0;
}