• 学习笔记——区间第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
    ",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
    ",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
    ",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;
    }
    
  • 相关阅读:
    zipline自制data bundles
    zipline目录结构
    Zipline入门教程
    QuantStart量化交易文集
    如何学习量化投资
    数字货币量化分析报告[2018-02-07]
    用于金融分析的Python包
    时间序列模式——ARIMA模型
    一份数学小白也能读懂的「马尔可夫链蒙特卡洛方法」入门指南
    Python实现HMM(隐马尔可夫模型)
  • 原文地址:https://www.cnblogs.com/Chtholly/p/10740533.html
Copyright © 2020-2023  润新知