20230625-线段树专题测试
考点梳理
- T1.混凝土粉末-需要转一个弯的线段树
- T2.机房惨案-线段树+动态开点+李超线段树(线段树套李超树)
- T3.丧钟为谁而鸣-两个线段树+珂朵莉树+动态开点
总结
- 线段树区间问题可以从想得到的答案是什么入手,边扫边维护-T1
- long long的值在其出现的地方都要开long long-T1+T2
- 线段树二分往右走时要减去左子树的值-T1
- set自带排序功能,如果用结构体一定要自定义排序-T3
- 一些新的知识点:
1> 动态开点-T2,T3
2> 线段树上的二分-T1
3> 李超线段树-T2
4> 珂朵莉树-T3
T1.混凝土粉末
concrete.cpp
题目大意
有一个长度为\(n\)的序列,\(m\)次操作,要求支持:
- 在区间\([l,r]\)中每个位置加入\(h\)个的\(x\)种类的方块
- 询问第\(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\)次操作,要求支持:
- 把指定区间内的数全部改为\(t\)
- 查询区间内的众数,若有多个,全部输出且满足众数出现的数量$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\)的线段树中修改
需要维护每一段连续的节点种类相同的区间
就需要用到珂朵莉树
总结一下:
- 需要用到一棵大的线段树
维护区间内的众数 - 需要对于每一个\(a_i\)有一棵小的线段树
维护\(a_i\)在每一个区间内出现的次数
来判断答案的合理性 - 需要一棵珂朵莉树
维护连续的\(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;
}