可用來計算區間第\(K\)小的數
題目 : 求整個數列的第\(K\)小
排序
開1顆線段樹,依序把a_i是第k小的k加進去線段樹中,並記錄區個數
在查詢的時候,如果當前查詢的\(K\)小於等於\(L\_num\)(左子樹的值:第\(l\)小元素值到第\(m\)小元素值的個數總和),代表第\(K\)小在左子樹中,則遞迴左子樹\((l,mid,K)\),否則遞迴右子樹\((mid+1,r,K-L\_num)\),因為\(L\_num\)個元素已經在在左子樹了,所以在查詢右子樹的時候必須減掉\(L\_num\),用於查詢區間\([mid+1,r]\)的第\(K-L\_num\)小。
int query(int node,int l,int r,int k){
if(l==r) return l;
int mid=(l+r)/2,L_num=tree[node*2+1];
if(k<=L_num) return query(node*2+1,l,mid,k);
else return query(node*2+2,mid+1,r,k-L_num);
}
引用前綴和概念,當要求區間\([l,r]\)的時候可以用。
ans=pre[R]-pre[L-1]
合併以上兩個想法
因為要多了一個區間的條件,所以在計算\(L\_num\)時就要利用到subtask 2,可以建立\(n\)顆線段樹,每一顆線段樹紀錄前綴的資訊(也就是\(1\)到當前元素),樹中存放前綴的每個元素為第幾小的資訊,接著用subtask 1的方法遞迴,在計算\(L\_num\)時要用第\(R\)顆線段樹左子樹的值\(-\)第\(L-1\)顆線段樹左子樹的值來判斷區間左子樹的值。
但這樣會MLE,觀察一下,第\(i\)顆樹的更新只會跟左子樹或右子樹其中一個有關(看當前的第\(K\)小在左邊或右邊),另一邊不會動到,跟第\(i-1\)顆樹是一樣的,所以可共用節點,實作上就是先建立一個節點,然後初始為跟第\(i-1\)顆樹的同一個節點一樣,再看更新的值在左子樹或右子樹,直接覆蓋就好了。
int update(int pre,int l,int r,int Kth){
int root=node++;
//初始化為跟pre節點一樣,連到pre節點與之共用
L[root]=L[pre];
R[root]=R[pre];
//sum為pre的sum加現在更新的pos
sum[root]=sum[pre]+1;
if(l==r) return root;
int mid=(l+r)/2;
if(Kth<=mid) L[root]=update(L[pre],l,mid,Kth);
//如果第Kth小的元素比mid還小,代表要更新左子樹,右子樹與原來的共用
else R[root]=update(R[pre],mid+1,r,Kth);
//如果第Kth小的元素比mid還大,代表要更新右子樹,左子樹與原來的共用
return root;
}
基本上跟subtask 1一樣,差別只在於左子樹的計算要用\((segtree[R]'s\ L\_num)-(segtree[L-1]'s\ L\_num)\),也就是區間\([1,R]\)小於等於第\(m\)大的元素個數總和減掉區間\([1,L-1]\)小於等於第\(m\)大的元素個數總和,這樣就會是區間\([L,R]\)小於等於第\(m\)大的元素個數總和\(L\_num\)。
int query(int u,int v,int l,int r,int k){
if(l==r) return l;
int m=(l+r)/2;
int L_num=sum[L[v]]-sum[L[u]]; //計算區間[l,r]中左子樹的元素個數
if(k<=L_num) return query(L[u],L[v],l,m,k);
//如果左子樹的數量<=k代表第k大的元素在左半邊
else return query(R[u],R[v],m+1,r,k-L_num);
//否則在右半邊,要更新成查詢區間[m+1,r]第k-L_num大的元素
}
#include <bits/stdc++.h>
using namespace std;
#define MAXN 200005
//-----------------------------------
int a[MAXN],Hash[MAXN],node=1;
int sum[MAXN<<5]; //sum[i]為前i小元素的數量
int L[MAXN<<5]; //L[i]為i的左孩子的節點編號
int R[MAXN<<5]; //R[i]為i的右孩子的節點編號
int Root[MAXN]; //Root[i]為第i個元素的線段樹的Root
int built(int l,int r){ //返回自己節點編號
int root=node++;
if(l==r) return root;
int mid=(l+r)/2;
L[root]=built(l,mid);
R[root]=built(mid+1,r);
return root;
}
int update(int pre,int l,int r,int Kth){
int root=node++;
//初始化為跟pre節點一樣,連到pre節點與之共用
L[root]=L[pre];
R[root]=R[pre];
//sum為pre的sum加現在更新的pos
sum[root]=sum[pre]+1;
if(l==r) return root;
int mid=(l+r)/2;
if(Kth<=mid) L[root]=update(L[pre],l,mid,Kth);
//如果第Kth小的元素比mid還小,代表要更新左子樹,右子樹與原來的共用
else R[root]=update(R[pre],mid+1,r,Kth);
//如果第Kth小的元素比mid還大,代表要更新右子樹,左子樹與原來的共用
return root;
}
int query(int u,int v,int l,int r,int k){
if(l==r) return l;
int mid=(l+r)/2;
int L_num=sum[L[v]]-sum[L[u]]; //計算區間[l,r]中左子樹的元素個數
if(k<=L_num) return query(L[u],L[v],l,mid,k);
//如果左子樹的數量<=k代表第k大的元素在左半邊
else return query(R[u],R[v],mid+1,r,k-L_num);
//否則在右半邊,要更新成查詢區間[mid+1,r]第k-L_num大的元素
}
void solve(){
int n,q;
cin >> n >> q;
for(int i=1;i<=n;i++){
cin >> a[i];
Hash[i]=a[i];
}
//離算化+去除重複元素,此後為區間為[1,m]
sort(Hash+1,Hash+n+1);
int m=unique(Hash+1,Hash+n+1)-(Hash+1);
node=1;
Root[0]=built(1,m); //類似前綴和概念:pre[0]=0,Root[0]=空樹
for(int i=1;i<=n;i++){
int Kth=lower_bound(Hash+1,Hash+m+1,a[i])-Hash; //求元素a[i]為第幾小
Root[i]=update(Root[i-1],1,m,Kth); //更新元素a[i]的線段樹
}
while(q--){
int l,r,k;
cin >> l >> r >> k;
int Kth=query(Root[l-1],Root[r],1,m,k); //查詢線段樹(Root[r]-Root[l-1])
cout << Hash[Kth] << '\n'; //對應回元素a[i]
}
}
signed main(){
cin.sync_with_stdio(0),cin.tie(0);
int t;
cin >> t;
while(t--) solve();
}
主要建樹和上面都是大同小異,只差在有更新要用到BIT,因為如果用前綴和的概念去建樹,每次更新會影響到後面的每一棵樹,但是用BIT就不用每個節點都更新到了,時間複雜度大大減小
其概念就是每個BIT節點都是一棵線段樹,在更新和查詢時,都在BIT會跑的節點執行
在更新的時候,要更新的樹的根結點不再是由上面的單一節點,而是bit遍歷到的節點,每個節點都要更新
為了持久化,也就是後面的樹的節點如果跟前面的樹的節點一樣,可以共用,所以前面的節點不能被覆蓋或刪掉,這時候我們只要更新新樹的根結點就好了,並不會覆蓋掉前面的節點,因為節點編號是會一直遞增下去的
int update(int pre,int l,int r,int Kth,int change){
int now=++cnt;
L[now]=L[pre];
R[now]=R[pre];
number[now]=number[pre]+change;
if(l==r) return now;
int mid=(l+r)>>1;
if(Kth<=mid) L[now]=update(L[pre],l,mid,Kth,change);
else R[now]=update(R[pre],mid+1,r,Kth,change);
return now;
}
void add(int x,int change){ //bit
int Kth=lower_bound(all(disc),a[x])-disc.begin()+1; //求第x個元素為第K大
while(x<m){ //m為bit大小
root[x]=update(root[x],1,m,Kth,change); //把更新後的樹的根結點放到root[x]
x+=x&-x;
}
}
查詢的時候不再是上面的sum[L[v]]-sum[L[u]],而是BIT節點的相加,就跟一般查詢前綴和的作法是一樣的
首先我們需要先建立一個所有BIT會跑到的節點編號,有左邊界和右邊界,所以要建立兩個,接下來的查詢都在這些節點上
vector<int> v[2]; //0:左邊界 1:右邊界
void init(int x,int i){
while(x){
v[i].pb(root[x]); //紀錄BIT遍歷到的樹的根結點
x-=x&-x;
}
}
在查詢的時候就把這些節點的值相加,就跟BIT計算前綴和的作法是一樣的
int query(int l,int r,int Kth){
if(l==r) return l;
//計算左子樹的數量
int L_num=0;
for(auto i:v[1]) L_num+=number[L[i]]; //右邊界的BIT
for(auto i:v[0]) L_num-=number[L[i]]; //左邊界的BIT
int mid=(l+r)>>1;
if(Kth<=L_num){ //第K小在左邊,往左走
//更新每棵樹的根結點=>此節點的左節點
for(auto &i:v[0]) i=L[i];
for(auto &i:v[1]) i=L[i];
return query(l,mid,Kth);
}else{ //第K小在右邊,往右走
//更新每棵樹的根結點=>此節點的右節點
for(auto &i:v[0]) i=R[i];
for(auto &i:v[1]) i=R[i];
return query(mid+1,r,Kth-L_num);
}
}
#pragma GCC optimize("Ofast,unroll-loops")
#include <bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define F first
#define S second
#define pb push_back
#define all(a) (a).begin(),(a).end()
#define mem(a) memset(a,0,sizeof(a))
#define binary_search
#define inf LLONG_MAX
#define mod 1000000007
#define MAXN 32005
int N=1,n,m,x,k,ans;
//-----------------------------------
struct Que{
char info;
int l,r,i,w,k;
}que[5005];
int a[MAXN],root[MAXN*2],L[MAXN*400],R[MAXN*400],number[MAXN*400],cnt;
vector<int> disc; //Discretization
//---------------update-----------------
int update(int pre,int l,int r,int Kth,int change){
int now=++cnt;
L[now]=L[pre];
R[now]=R[pre];
number[now]=number[pre]+change;
if(l==r) return now;
int mid=(l+r)>>1;
if(Kth<=mid) L[now]=update(L[pre],l,mid,Kth,change);
else R[now]=update(R[pre],mid+1,r,Kth,change);
return now;
}
void add(int x,int change){
int Kth=lower_bound(all(disc),a[x])-disc.begin()+1;
while(x<m){
root[x]=update(root[x],1,m,Kth,change); //把更新後的樹的根結點放到root[x],為
x+=x&-x;
}
}
//--------------update-----------------
//--------------query------------------
vector<int> v[2];
void init(int x,int i){
while(x){
v[i].pb(root[x]);
x-=x&-x;
}
}
int query(int l,int r,int Kth){
if(l==r) return l;
int L_num=0;
for(auto i:v[1]) L_num+=number[L[i]]; //右邊界
for(auto i:v[0]) L_num-=number[L[i]]; //左邊界
int mid=(l+r)>>1;
if(Kth<=L_num){
for(auto &i:v[0]) i=L[i];
for(auto &i:v[1]) i=L[i];
return query(l,mid,Kth);
}else{
for(auto &i:v[0]) i=R[i];
for(auto &i:v[1]) i=R[i];
return query(mid+1,r,Kth-L_num);
}
}
//--------------query------------------
signed main(){
cin.sync_with_stdio(0),cin.tie(0);
int q;
//-------input-----------
cin >> n >> q;
for(int i=1;i<=n;i++){
cin >> a[i];
disc.pb(a[i]); //加入離散化的陣列
}
for(int i=1;i<=q;i++){
cin >> que[i].info;
if(que[i].info=='C'){ //更新
cin >> que[i].i >> que[i].w;
disc.pb(que[i].w); //加入離散化的陣列
}else{ //查詢
cin >> que[i].l >> que[i].r >> que[i].k;
}
}
//------------------------
//-----離散化------
sort(all(disc));
m=unique(all(disc))-disc.begin()+1;
//----------------
//------建樹-------
for(int i=1;i<=n;i++) add(i,1);
//-----------更新和查詢------------
for(int i=1;i<=q;i++){
if(que[i].info=='Q'){ //查詢
v[0].clear(),v[1].clear();
//初始化bit會用到的bit編號
init(que[i].l-1,0);
init(que[i].r,1);
cout << disc[query(1,m,que[i].k)-1] << '\n';
}else{ //更新
add(que[i].i,-1); //先把之前的元素刪掉
a[que[i].i]=que[i].w;
add(que[i].i,1); //再加入新的元素
}
}
}