题解——Dynamic Rankings(树状数组+主席树)

题解——Dynamic Rankings(树状数组+主席树)

这道题可以整体二分过,但我写了树套树
而且这个外层树不是那么明显


题目传送门

给定一个含有n个数的序列a[1],a[2],a[3]……a[n],程序必须回答这样的询问:对于给定的i,j,k,在a[i],a[i+1],a[i+2]……a[j]中第k小的数是多少(1≤k≤j-i+1),并且,你可以改变一些a[i]的值,改变后,程序还能针对改变后的a继续回答上面的问题。你需要编一个这样的程序,从输入文件中读入序列a,然后读入一系列的指令,包括询问指令和修改指令。

第一行有两个正整数n(1≤n≤100000),m(1≤m≤100000)。分别表示序列的长度和指令的个数。

第二行有n个数,表示a[1],a[2]……a[n],这些数都小于10^9。接下来的m行描述每条指令,每行的格式是下面两种格式中的一种。 Q i j k 或者 C i t

Q i j k (i,j,k是数字,1≤i≤j≤n, 1≤k≤j-i+1)表示询问指令,询问a[i],a[i+1]……a[j]中第k小的数。

C i t (1≤i≤n,0≤t≤10^9)表示把a[i]改变成为t。

简化题意

带单点修改的区间第K小(大)

关于静态第K大(主席树)

默认各位是会主席树的,只是复习一下,具体讲解在代码中
就是对原数组离散化,每个状态为一颗新的权值线段树,维护一个类似于前缀和的东西,由于每次相同部分很多,考虑用可持久化线段树节约空间(每次只开一条链)。

AC code(静态):

#include<bits/stdc++.h>
using namespace std;
const  int  MAXN = 1000005 ;  
inline int read(){
    int s = 0,w = 1;char g = getchar();while(g<'0'||g>'9'){if(g=='-')w*=-1;g = getchar();}
    while(g>='0'&&g<='9'){s = s*10+g-'0';g = getchar();}return s*w;
}
int  b[ MAXN ] , a [ MAXN ] , root[ MAXN ] ;
int  N , M , cnt = 0 ;
struct Segment{
	int  ls , rs , num ;
}t[ MAXN<<5 ] ; 
void copyNode( int x , int y ){
	t[ x ].ls = t[ y ].ls , t[ x ].rs = t[ y ].rs , t[ x ].num = t[ y ].num+1 ;
}
void build( int p , int  l , int  r ){//初始建树
	t[ p ].num = 0 ;
	if( l == r )return ;
	int  mid = ( l+r )>>1 ;
	build( t[ p ].ls = ++cnt , l , mid ) ;
	build( t[ p ].rs = ++cnt , mid+1 , r ) ;
}
int New_made( int las , int  l , int r , int pos ){//新建,las为上一状态
	int p = ++cnt ; 
	copyNode( p , las ) ;//新建的链深=树深,别想多了 
	if( l == r )return p ;
	int mid = ( l+r )>> 1 ; 
	if( pos <= mid )t[ p ].ls = New_made( t[ p ].ls , l , mid , pos ) ;//二分查找
	else t[ p ].rs = New_made( t[ p ].rs , mid+1 , r , pos ) ;
	return p ; 
}
int  ask( int x , int  y , int  l , int  r , int rank ){
	if( l == r )return l ;
	int  mid = (l+r)>>1 ;
	int  dif = t[ t[x].ls ].num - t[ t[y].ls ].num ;
	if( rank <= dif )ask( t[ x ].ls , t[ y ].ls , l , mid , rank ) ;//值域上二分
	else  ask( t[ x ].rs , t[ y ].rs , mid+1 , r , rank-dif ) ;
}
int main(){
	N = read() , M = read() ;//平时不用register了 
	for( int i = 1 ; i <= N ; ++i )b[ i ] = a[ i ] = read() ; 
	sort( b+1 , b+N+1 ) ;
	int  tot = unique( b + 1 , b + 1 + N ) - b - 1 ;//自带的去重 ,返回的是数组的尾地址
	build( root[0] = ++cnt , 1 , tot ) ; //root , l , r
	for( int i = 1 ; i <= N ; ++i ){//nlogn,等效Hash
		int pos = lower_bound( b+1 , b+tot+1 , a[ i ] ) - b ; 
		root[ i ] = New_made( root[i-1] , 1 , tot , pos ) ; // 上一版本root , l , r , pos  
	}
	for( int i = 1 ; i <= M ; ++i ){
		int m1 = read() , m2 = read() , m3 = read() ;
		int t = ask( root[ m2 ] , root[ m1-1 ] , 1 , tot , m3 ) ; //前缀和思想 
		printf("%d\n",b[ t ] ) ;
	}
	return 0 ;
}

关于动态第K大(树状数组套主席树)

上面每棵树都是一个历史状态,即第i棵树代表了第i个状态时序列 1~i 所对应的值域线段树树上形态,就是维护了一个前缀和。

然后,带修改,如果单纯的修改前缀包含该位置的所有树,明显T飞的。

然后,某些神仙们并不满足,既然每棵树和序列上的一个前缀区间可以相互对应,那为何不能用树状数组像维护 n 个数的序列一样维护 n 棵主席树呢?(请回顾下图)

上图中的 c[1]~c[8] 都会对应一颗主席树。然后,每次修改是 log 棵数一起修改 , 每次查询仍然是两个状态, 但每个状态都会由 log 棵树一起组成 。 所以.....时间和空间复杂度都是 NlogN^2 。

具体的我写在代码中了(参考来参考去,最后终于写了一个好点的板子)

#include<bits/stdc++.h>
using namespace std ;
const  int  MAXN = 100005 ;//log 16
inline int read(){
    int s = 0,w = 1;char g = getchar();while(g<'0'||g>'9'){if(g=='-')w*=-1;g = getchar();}
    while(g>='0'&&g<='9'){s = s*10+g-'0';g = getchar();}return s*w;
}
int  b[ MAXN*2 ] , a[ MAXN ] , root[ MAXN*18 ] ;
int  N , M  , cnt = 0 , totx = 0 , toty = 0  , xx[ 18 ] , yy[ 18 ] , tot = 0 ;
int  opa[ MAXN ] , opb[ MAXN ] , opc[ MAXN ] ;
char  op[ 20 ] ; 
struct  Segment{
	int ls , rs , num ;
}t[ MAXN*324 ] ; // 空间log^2 
void copyNode( int x , int y , int z ){
	if( x == 0 )return ; 
	t[ x ].ls = t[ y ].ls , t[ x ].rs = t[ y ].rs , t[ x ].num = t[ y ].num + z ;
}
int New_made( int las , int  l , int  r , int  pos , int val ){
	int  p = ++cnt  ;
	copyNode( p , las , val ) ;// las = 0 一样 
	if( l == r )return p ;
	int  mid = (l+r)>>1 ;
	if( pos <= mid )t[ p ].ls = New_made( t[ p ].ls , l , mid , pos , val ) ;
	else t[ p ].rs = New_made( t[ p ].rs , mid+1 , r , pos , val ) ;
	return p ;
}
int  query( int  l , int  r , int k ){//实现log棵树同时移动 
	if( l == r ) return l;
    int sum = 0 , mid = (l+r)/2; // sum是 log个 状态差 的和
    for( int i=1;i<=totx;i++) sum += t[t[ xx[i] ].ls].num ;
    for( int i=1;i<=toty;i++) sum -= t[t[ yy[i] ].ls].num ;
    if( sum >= k ){//二分,log棵树同时向下进入左区间或右区间,请读者自行脑补
        for(int i=1;i<=totx;i++) xx[i]=t[ xx[i] ].ls;
        for(int i=1;i<=toty;i++) yy[i]=t[ yy[i] ].ls;
        return query( l , mid , k ) ;
    }else{
        for(int i=1;i<=totx;i++) xx[i]=t[ xx[i] ].rs;
        for(int i=1;i<=toty;i++) yy[i]=t[ yy[i] ].rs;
        return query( mid+1 , r , k-sum ) ;
    }
}
void add( int x , int val ){//log次增加,就是多开了log条链
	int pos = lower_bound( b+1 , b+tot+1 , a[ x ] ) - b ; 
	for( int i = x ; i <= N ; i += i&-i )
	root[ i ] = New_made( root[ i ] , 1 , tot , pos , val ) ; // 上一版本root , l , r , pos  
}
int  ask( int x , int  y , int  z ){// 找出每次修改对应的log棵树
	totx = toty = 0 ;
    for( int j = y ; j ; j -=j&(-j) ) xx[++totx]=root[j];
    for( int j = x - 1 ; j ; j -=j&(-j) ) yy[++toty]=root[j];
    int ans = query( 1 , tot , z ) ;
    return b[ans] ;
}
int main(){
	N = read() , M = read() , tot = N ;
	for( int i = 1 ; i <= N ; ++i )b[ i ] = a[ i ] = read() ;
	for( int i = 1 ; i <= M ; ++i ){
		scanf("%s",op);
		opa[ i ] = read() , opb[ i ] = read() ;//离线后,方便进行离散化
		if( op[ 0 ]=='Q' )opc[ i ] = read() ;
		else b[ ++tot ] = opb[ i ] ; 
	}
	sort( b+1 , b+tot+1 ) ; 
	tot = unique( b+1 , b+tot+1 )-b-1 ; //离散化,同静态
	for( int i = 1 ; i <= N ; ++i )add( i , 1 ) ;
	for( int i = 1 ; i <= M ; ++i )
		if( opc[ i ] )printf("%d\n",ask( opa[ i ] , opb[ i ] , opc[ i ]) ) ;
		else {
			add( opa[ i ] , -1 ) ;//修改,树上对应的个数 -1 
			a[ opa[ i ] ] = opb[ i ] ;//序列上对应修改
			add( opa[ i ] , 1 ) ;//个数+1
		}
	return 0 ;
}

然后,记得树的空间要开log^2倍,别像ssw02一样挂了。

本文的不足 ,还请各位大佬指出,ssw02感谢您的阅读。

posted @ 2019-07-26 08:10  蓝银杏-SSW  阅读(291)  评论(2编辑  收藏  举报
//结束