学习笔记——区间第k小算法

前置知识:值域线段树,可持久化线段树,树状数组

动态整体第k小

题目:给定一个序列和m次操作,每次操作修改单点或者询问整个序列第k小的数

首先考虑暴力,对于每次修改都直接排序的话,复杂度为O(nmlogn),也可以魔改一下排序方法,不过一般的暴力还是没办法过

整体第k小带修改很明显可以用平衡树做,不过编程较麻烦(而且大材小用),所以不考虑

值域线段树

值域线段树可以很方便的O(logn)查询一次所有数中比某个值小的数的个数,于是我们可以考虑用它解决这一类问题,当然一般来说值域线段树是和离散化配套使用的

做法:

将所有数离散化后加入值域线段树,修改操作就直接删除旧的,加入新的

对于查询操作,从根节点开始,当前节点的左儿子保存着\(≤\)mid的数的个数sum,如果sum\(≥\)k,就说明第k小应该在左边,递归到左儿子,否则k-=sum,递归给右儿子(k-=sum是因为在整个区间找第k小等价于在右区间找第k-sum小)

时间复杂度为O(nlogn),空间复杂度O(n*4)


静态前缀第k小

题目:每次查询前x个数中的第k小,无修改

做法:这里改变一下上面的方法。上面的做法中,可以发现,sum的大小表示的是所有数\(≤\)mid的数的个数,而这里是要求前x个数\(≤\)mid的数的个数,于是我们需要对每一个数a[i]加入之后都对前i个数建立一颗值域线段树,询问前x个数的时候就使用第x个线段树

可持续化线段树

显然不可能真的建立n个值域线段树

链接

时空复杂度O(nlogn)


静态区间第k小

题目:查询改为[ l , r ]区间,无修改

上面的前缀第k小相当于把主席树看做前缀和,那么区间第k小相当于使用差分相减

首先明确一件事,对于上面建的n个值域线段树(假装把n个树都单独拆出来),形态完全相同,并且对于每一个树的相同位置,意义几乎一样,比如,第x个树和第y个树的某个位置都表示不大于c的数的个数,只不过一个是针对前a[1x],另一个a[1y]。所以可以考虑前缀和的思想,假设y \(>\) x,用y树一个节点减去x树上对应节点就可以表示a[ x+1 ~ y ]这一段上不大于c的数

做法:

对于查询[ l , r ],同时使用l-1和r两个值域线段树,每次的sum由r树的左儿子减去l-1树的左儿子得到,向下递归时两个根要一起向同一个方向走

时空复杂度O(nlogn)

Code:

#include<bits/stdc++.h>
#define N 200005
using namespace std;
int n,m;
int ref[N],len;
int a[N],ndsum;
int root[N],ls[N*20],rs[N*20],sum[N*20];

template <class T>
void read(T &x)
{
    char c;int sign=1;
    while((c=getchar())>'9'||c<'0') if(c=='-') sign=-1; x=c-48;
    while((c=getchar())>='0'&&c<='9') x=(x<<1)+(x<<3)+c-48; x*=sign;
}

void build(int &rt,int l,int r)
{
    rt=++ndsum;
    if(l==r) return;
    int mid=(l+r)>>1;
    build(ls[rt],l,mid);
    build(rs[rt],mid+1,r);
}
void copynode(int x,int y)
{
    ls[x]=ls[y];
    rs[x]=rs[y];
    sum[x]=sum[y]+1;//复制的链上都会增加 1 
}
int modify(int rt,int l,int r,int x,int val)
{
    int t=++ndsum;
    copynode(t,rt);
    if(l==r) return t;
    int mid=(l+r)>>1;
    if(mid>=x) ls[t]=modify(ls[rt],l,mid,x);
    else rs[t]=modify(rs[rt],mid+1,r,x);
    return t;
}
int query(int rt1,int rt2,int l,int r,int k)
{
    if(l==r) return l;
    int x=sum[ls[rt2]]-sum[ls[rt1]];
    int mid=(l+r)>>1;
    if(x>=k) return query(ls[rt1],ls[rt2],l,mid,k);
    else return query(rs[rt1],rs[rt2],mid+1,r,k-x);
}

int main()
{
    read(n);read(m);
    for(int i=1;i<=n;++i) read(a[i]),ref[++len]=a[i];
    sort(ref+1,ref+len+1);
    len=unique(ref+1,ref+len+1)-ref-1;
    build(root[0],1,len);//先建立一个空树 
    for(int i=1;i<=n;++i)
    {
        int t=lower_bound(ref+1,ref+len+1,a[i])-ref;//找到要加入的a[i]在ref中对应的下标 
        root[i]=modify(root[i-1],1,len,t);
    }
    for(int i=1;i<=m;++i)
    {
        int x,y,k;
        read(x);read(y);read(k);
        printf("%d\n",ref[query(root[x-1],root[y],1,len,k)]);
    }
    return 0;
}

区间静态第k小运用了前缀和差分,所以显然这个问题可以上树

将一个点到根节点的路径建值域线段树,对于父亲\(rt\)和儿子\(v\)\(v\)的值域线段树从\(rt\)扩展一个点而来;查询路径\((u,v)\)用差分转换成\(root[u]+root[v]-root[lca(u,v)]-root(fa[lca(u,v)])\)四棵线段树加减

#include<bits/stdc++.h>
#define N 100005
#define Min(x,y) ((x)<(y)?(x):(y))
#define Max(x,y) ((x)>(y)?(x):(y))
using namespace std;
typedef long long ll;
int n,m,a[N],b[N],len;
int f[N][18],dep[N];
int root[N],ndsum; 
int sum[N*100],ls[N*100],rs[N*100];
struct Edge
{
	int next,to;
}edge[N<<1];int head[N],cnt=1;
void add_edge(int from,int to)
{
	edge[++cnt].next=head[from];
	edge[cnt].to=to;
	head[from]=cnt;
}

template <class T>
void read(T &x)
{
	char c; int sign=1;
	while((c=getchar())>'9'||c<'0') if(c=='-') sign=-1; x=c-48;
	while((c=getchar())>='0'&&c<='9') x=(x<<1)+(x<<3)+c-48; x*=sign;
}
void build(int &rt,int las,int l,int r,int x)
{
	if(!rt) rt=++ndsum;
	ls[rt]=ls[las];
	rs[rt]=rs[las];
	sum[rt]=sum[las]+1;
	if(l==r) return;
	int mid=(l+r)>>1;
	if(x<=mid) ls[rt]=0,build(ls[rt],ls[las],l,mid,x);
	else rs[rt]=0,build(rs[rt],rs[las],mid+1,r,x);
}
void dfs(int rt,int fa)
{
	dep[rt]=dep[fa]+1;
	build(root[rt],root[fa],1,n,a[rt]);
	for(int i=head[rt];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa) continue;
		f[v][0]=rt;
		for(int j=1;j<18;++j) f[v][j]=f[f[v][j-1]][j-1];
		dfs(v,rt);
	}
}
int lca(int x,int y)
{
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=17;i>=0;--i) if(dep[f[x][i]]>=dep[y]) x=f[x][i];
	if(x==y) return x;
	for(int i=17;i>=0;--i) if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
	return f[x][0];
}
int query(int rt1,int rt2,int rt3,int rt4,int l,int r,int k)
{
	if(l==r) return l;
	int L=sum[ls[rt1]]+sum[ls[rt2]]-sum[ls[rt3]]-sum[ls[rt4]];
	int mid=(l+r)>>1;
	if(L>=k) return query(ls[rt1],ls[rt2],ls[rt3],ls[rt4],l,mid,k);
	else return query(rs[rt1],rs[rt2],rs[rt3],rs[rt4],mid+1,r,k-L);
}

int main()
{
	read(n);read(m);
	for(int i=1;i<=n;++i) read(a[i]),b[++len]=a[i];
	sort(b+1,b+len+1); len=unique(b+1,b+len+1)-b-1;
	for(int i=1;i<=n;++i) a[i]=lower_bound(b+1,b+len+1,a[i])-b;
	for(int i=1;i<n;++i)
	{
		int x,y;
		read(x);read(y);
		add_edge(x,y);
		add_edge(y,x);
	}
	dfs(1,0);
	for(int i=1;i<=m;++i)
	{
		int x,y,k;
		read(x);read(y);read(k);
		int l=lca(x,y);
		printf("%d\n",b[query(root[x],root[y],root[l],root[f[l][0]],1,n,k)]);
	}
	return 0;
}

动态区间第k小

对于上面的静态区间第k小问题,我们用类似前缀和的思想知道了区间[ l , r ]中不大于c的数的个数,于是对于带修改的前缀和问题,我们很容易想到用树状数组维护

这里采用类比法来方便理解

在树状数组中,查询前x个数的和的时候,我们将x二进制拆分使得遍历的点只有logn个,修改同理,这相当于将修改和查询的时间平均分到logn(暴力维护前缀和是查询O(1),修改O(n))。类似的,原来方法中询问的单纯从第x个树向下遍历(类比于直接输出sum[x])就变成了从logn颗树同时向下遍历,这样就可以使得修改操作变成修改logn颗树,将复杂度平均分给修改和查询。

Code:

#include<bits/stdc++.h>
#define N 100005
#define lowbit(x) ((x)&(-x))
using namespace std;
int n,m;
int a[N];
int exc[N<<1],len;//离散化数组 
int ls[N*400],rs[N*400],val[N*400],root[N<<1],sum;//值域线段树 
int son[2][100],s[2];//树状数组思路中应该处理的logn颗树(分为l,r) 

struct Order
{
	char o;
	int l,r,pos,val;
}order[N];

template <class T>
void read(T &x)
{
	char c;int sign=1;
	while((c=getchar())>'9'||c<'0') if(c=='-') sign=-1; x=c-48;
	while((c=getchar())>='0'&&c<='9') x=(x<<1)+(x<<3)+c-48; x*=sign;
}

void modify(int &rt,int l,int r,int pos,int v)//修改某一颗树 
{
	if(!rt) rt=++sum;
	val[rt]+=v;
	if(l==r) return;
	int mid=(l+r)>>1;
	if(mid>=pos) modify(ls[rt],l,mid,pos,v);
	else modify(rs[rt],mid+1,r,pos,v); 
}
int pre_modify(int pos,int v)//修改logn颗树 
{
	int k=lower_bound(exc+1,exc+len+1,a[pos])-exc;
	for(int i=pos;i<=n;i+=lowbit(i)) modify(root[i],1,len,k,v);
}
int query(int l,int r,int k)//询问 
{
	if(l==r) return l;
	int sum=0,mid=(l+r)>>1;
	for(int i=1;i<=s[1];++i) sum+=val[ls[son[1][i]]];//叠加每一颗树 (r)
	for(int i=1;i<=s[0];++i) sum-=val[ls[son[0][i]]];//减去每一颗树 (l) 
	if(k<=sum)//向左转,要处理的logn颗树都要转弯 
	{
		for(int i=1;i<=s[1];++i) son[1][i]=ls[son[1][i]];
		for(int i=1;i<=s[0];++i) son[0][i]=ls[son[0][i]];
		return query(l,mid,k);
	}
	else
	{
		for(int i=1;i<=s[1];++i) son[1][i]=rs[son[1][i]];
		for(int i=1;i<=s[0];++i) son[0][i]=rs[son[0][i]];
		return query(mid+1,r,k-sum);
	}
}

int main()
{
	read(n);read(m);
	for(int i=1;i<=n;++i) read(a[i]),exc[++len]=a[i];
	for(int i=1;i<=m;++i)
	{
		while((order[i].o=getchar())<40);
		if(order[i].o=='Q') {read(order[i].l);read(order[i].r);read(order[i].val);}
		if(order[i].o=='C') {read(order[i].pos);read(order[i].val);}
		exc[++len]=order[i].val;
	}
	sort(exc+1,exc+len+1);
	len=unique(exc+1,exc+len+1)-exc-1;
	for(int i=1;i<=n;++i) pre_modify(i,1);//加入a[i],建树 
	for(int opt=1;opt<=m;++opt)
	{
		if(order[opt].o=='Q')//询问 
		{
			s[1]=s[0]=0;
			for(int i=order[opt].r;i;i-=lowbit(i)) son[1][++s[1]]=root[i];
			for(int i=order[opt].l-1;i;i-=lowbit(i)) son[0][++s[0]]=root[i];
			//要同时处理的logn颗树,l,r 
			printf("%d\n",exc[query(1,len,order[opt].val)]);
		}
		else
		{
			pre_modify(order[opt].pos,-1);//删除目标 
			a[order[opt].pos]=order[opt].val;//修改 
			pre_modify(order[opt].pos,1);//加回去 
		}
	}
	return 0;
}
posted @ 2019-04-20 12:26  擅长平地摔的艾拉酱  阅读(1127)  评论(0编辑  收藏  举报
/*取消选中*/