一棵复杂的线段树
题目链接 复杂的线段树
Time Limit: 4000 MS Memory Limit: 64 MB
Submit Status
秦怼长是一个爱钦定的人, 一年一度的集训队选拔又来了, 秦怼决定钦定一些人品特别好的人进入集训队.
秦怼先是给出了一个数组 A[1..n]A[1..n], 初始元素为 a1,a2,...,an 是 1∼n 的一个排列. 然后秦怼迅速地对数组施以了 mm 个操作. 每个操作针对一个区间 [l,r] (1≤l≤r≤n)[l,r] (1≤l≤r≤n), 秦怼将区间内的元素从小到大排序或者从大到小排序.
如果你能猜出最终数组中第 k 个元素 A[k] 的值, 就能钦定进队啦(误
Input
输入第一行为两个整数 n(1≤n≤105)和 k(1≤k≤n), 表示数组的大小和需要猜的数的下标.
第二行有 n 个整数 a1,a2,...,an, 表示数组中初始的元素,且是 1∼n 的一个排列.
第三行一个整数 m(1≤m≤105), 表示操作的个数.
接下来有 m 行,每行三个整数 o l r(1≤l≤r≤n),表示一个操作. 如果 o=0,表示对区间 [l,r]从小到大排序. 如果 o=1,表示对区间 [l,r]从大到小排序.
Output
输出包含一个整数的一行, 为最终数组中 A[k] 的值.
Sample input and output
Sample Input | Sample Output |
---|---|
| |
对于一个区间[ L,R ] 从大到小排,从小到大排,最后的到的数组问第k个是什么?
这种瞎几把骚搞的问题第一眼看上去,线段树根本没这种操作啊!
但是由于答案一定在1 - n 中, 考虑是否可以用二分找出结果;
假设答案是一个值mid,如果把数组a中所有>=mid的元素置为1,把所有< mid 都元素置为0,这样对于一个区间排序操作,如果降序则左边全1右边全0,如果升序,左边全0,右边全1,
这样做有利于的线段树,如果一个子区间刚好全部要被修改成1,只需要把他的父节点标记为1即可(lazy操作);
然后对于每次排序就变为了线段树的区间维护+lazy标记,线段树根节点维护的是区间的1的个数
排序完成之后,单点查询k,如果得到的是1,表明这结果有可能是对的,或者0的个数偏少;
为增加0的个数就要把二分区间的L=mid+1;
另外关于lazy操作:这里lazy标记的是这个区间是否为全0或全1;修改及向下传递的时候,不能用+=,直接赋值就行了。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<set>
using namespace std;
const int maxn=1e5+7;
int n,k;
int a[maxn];
struct node
{
int o,l,r;
}op[maxn];
struct tree
{
int val;
int tag;
}b[maxn<<2];
void down(int l,int r,int rt)
{
if(b[rt].tag!=-1)
{
b[rt*2].tag=b[rt].tag;
b[rt*2+1].tag=b[rt].tag;//添加标记
int mid=(l+r)>>1;
b[rt*2].val=b[rt].tag*(mid-l+1);
b[rt*2+1].val=b[rt].tag*(r-mid);
b[rt].tag=-1;
}
}
void build(int l,int r,int rt,int val)
{
b[rt].tag=-1;
if(l==r)
{
b[rt].val=a[l]>=val?1:0;
return ;
}
int mid=(l+r)>>1;
build(l,mid,rt*2,val);
build(mid+1,r,rt*2+1,val);
b[rt].val=b[rt*2].val+b[rt*2+1].val;//记录区间1的个数
}
int query(int l,int r,int rt,int ll,int rr)
{
if(ll<=l&&r<=rr) return b[rt].val;
down(l,r,rt);
int mid=(l+r)>>1;
int ans=0;
if(ll<=mid) ans+=query(l,mid,rt*2,ll,rr);
if(rr>mid) ans+=query(mid+1,r,rt*2+1,ll,rr);
return ans;
}
void change(int l,int r,int rt,int ll,int rr,int val)//区间修改
{
if(ll<=l&&r<=rr)
{
b[rt].val=(r-l+1)*val;
b[rt].tag=val;
return ;
}
down(l,r,rt);
int mid=(l+r)>>1;
if(ll<=mid) change(l,mid,rt*2,ll,rr,val);
if(rr>mid) change(mid+1,r,rt*2+1,ll,rr,val);
b[rt].val=b[rt*2].val+b[rt*2+1].val;
}
int query1(int l,int r,int rt,int k)
{
if(l==r)
{
return b[rt].val;
}
down(l,r,rt);
int mid=(l+r)>>1;
if(k<=mid) return query1(l,mid,rt*2,k);
else return query1(mid+1,r,rt*2+1,k);
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
int m;
scanf("%d",&m);
for(int i=0;i<m;i++)
scanf("%d%d%d",&op[i].o,&op[i].l,&op[i].r);
int l=1;
int r=n;
while(l<r)
{
int mid=(l+r)>>1;
build(1,n,1,mid+1);
//for(int i=1;i<n*4;i++)
// cout<<b[i].val<<endl;
for(int i=0;i<m;i++)
{
int cnt1=query(1,n,1,op[i].l,op[i].r);
if(cnt1==0) continue;
if(op[i].o==0)
{
//把区间从小到大排序,0在前,1在后
change(1,n,1,op[i].l,op[i].r,0);//先把整个区间改成0
change(1,n,1,op[i].r-cnt1+1,op[i].r,1);//再修改应该是1的区间
}
else
{
change(1,n,1,op[i].l,op[i].r,0);//先把整个区间改成0
change(1,n,1,op[i].l,op[i].l+cnt1-1,1);
}
}
if(query1(1,n,1,k)) l=mid+1; //单点查询,原数组下标为k的值
else r=mid;
}
printf("%d\n",l);
return 0;
}
另一种二分的方式,这个好像是最基本的二分写法
while(l<=r)
{
int mid=(l+r)>>1;
build(1,n,1,mid);
//for(int i=1;i<n*4;i++)
// cout<<b[i].val<<endl;
for(int i=0;i<m;i++)
{
int cnt1=query(1,n,1,op[i].l,op[i].r);
if(cnt1==0) continue;
if(op[i].o==0)
{
//把区间从小到大排序,0在前,1在后
change(1,n,1,op[i].l,op[i].r,0);//先把整个区间改成0
change(1,n,1,op[i].r-cnt1+1,op[i].r,1);//再修改应该是1的区间
}
else
{
change(1,n,1,op[i].l,op[i].r,0);//先把整个区间改成0
change(1,n,1,op[i].l,op[i].l+cnt1-1,1);
}
}
//cout<<l<<' '<<r<<endl;
if(query1(1,n,1,k)) l=mid+1,ans=mid;
//单点查询,原数组下标为k的值//mid可能符合题目要求所以保存下来
else r=mid-1; //此时mid不符合,故ans还是上一个mid
}
printf("%d\n",ans);