1. 程式人生 > >poj 2104 <排序分塊,區間第k大>/<第一次用主席樹>2個方法+整體二分

poj 2104 <排序分塊,區間第k大>/<第一次用主席樹>2個方法+整體二分

給一個序列,查詢區間第k大,用分塊來實現

首先將區間分為每塊block大小,也就有num=n/block塊,if(n%block==0)num++.

然後每次在定義每個塊其左右邊界的時候進行排序,那麼就得到一個每塊內排好序的塊。

查詢的時候因為是查詢區間第k大,那麼我們可以二分出這個區間最大能滿足(區間小於其的數<k),因為區間小於其的數<k,也就是這個數排第k個(這裡可以好好想想為什麼)。

對於一個查詢的區間,如果其包括一個完整的塊就是在這個塊中二分,對於左右邊界就線性查詢。

TLE-1 分塊的大小可以分成sqrt(n*log(n)),這樣查詢的速度會塊許多,但如果n==1,那麼log(1)=0,注意這個點(re,就因為除0和陣列下標越界)

TLE-2 二分的時候,如果是確定某個陣列的值,可以二分這個陣列(排好序)的值,而不用從left1=-1e9,right1=1e9

wa1 排序了q1作為分塊的時候使用,二分的時候就不能用q1,應該另外設定一個數組q3

#include<iostream>
#include <algorithm>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <queue>
#include <vector>
using namespace std;
int l[5500];
int r[5500];
int belong[105000];
int q1[105000];
int q2[105000];
int q3[105000];
int n,block,num;
void build(){
	double f1=n;
	block=sqrt(f1*log(f1));
	if(block==0)block=1;
	num=n/block;if(n%block!=0)num++;
	int i;
	for(i=1;i<=num;i++){
		l[i]=(i-1)*block+1;
		r[i]=min(n,i*block); //每個block個邊界,是包括的 
		sort(q1+l[i],q1+r[i]+1); //注意這裡的排序,後面是需要+1的 
	//	cout << l[i] <<"    "<< r[i] <<  " 排序了"<<endl;
		//for(j=l[i];j<=r[i];j++)
		 //cout << j << "   " << endl;
	}
	for(i=1;i<=n;i++){
	belong[i]=(i-1)/block+1;
	 }
}
int find2(int x,int y,int t){
	int i;
	int res=0,ans=0;
	if(belong[x]==belong[y]){
		for(i=x;i<=y;i++){
			if(q2[i]<t)res++;
		}
		return res;
	}else{
		for(i=x;i<=r[belong[x]];i++)
			if(q2[i]<t)res++;
		for(i=l[belong[y]];i<=y;i++)
			if(q2[i]<t)res++;
			//cout << res<< "個了" << endl;
		int left1,right1,mid1;
		for(i=belong[x]+1;i<belong[y];i++){
			left1=l[i];right1=r[i];
			ans=0;
			//cout << "左邊是" << l[i] << "   " << r[i] << endl;
			while(right1>=left1){
				mid1=(right1+left1)/2;
			//	cout << "進來是" << q1[mid1] <<"    " <<mid1<< endl;
				if(q1[mid1]<t){
					left1=mid1+1;
					ans=max(ans,mid1-l[i]+1);
				}else{
					right1=mid1-1;
				}
			}
			//cout << "++了" << ans << endl;
			res+=ans;
		}
		return res;
	}
}
int find1(int x,int y,int k){
	int left1,right1,mid1;
	left1=-(1e9+7);right1=1e9+7; //每次都二分n 
	int res=-(1e9+7);
	while(right1>=left1){
		mid1=(right1+left1)/2;
//	cout << "找" << q1[mid1] <<"    " << mid1 << "  " <<find2(x,y,q1[mid1])<<endl; 
		if(find2(x,y,mid1)<k){
	//		cout <<" 符合" << endl;
			res=max(res,mid1);
			left1=mid1+1;
		}else{
			right1=mid1-1;
		}
	}
	return res;
}
int main(){
	freopen("in.txt","r",stdin);
	int i,k,j,f1,f2,f3,f4,t1,t2,t3,t4,l2,m;
	long long c1,c2,c3,c4;
	cin >> n>> m;
	for(i=1;i<=n;i++){
	scanf("%d",&q1[i]);
	q3[i]=q2[i]=q1[i];}
	sort(q3+1,q3+1+n);
	build();
	for(i=1;i<=m;i++){
		scanf("%d %d %d",&t1,&t2,&t3);
		//cout << find2(t1,t2,5) << endl;
		printf("%d\n",find1(t1,t2,t3));
	}
	return 0;
}

此外,可以這樣進行離散化。

for(i=1;i<=n;i++){
	scanf("%d",&q1[i]);
	q2[i]=q1[i];
	q3.push_back(q1[i]);  //用map來進行判斷而離散的速度是非常慢的。 
	}
	sort(q3.begin(),q3.end());
	q3.erase(unique(q3.begin(),q3.end()),q3.end());

主席樹,也就是可持久化線段樹。

之所以是可持久化線段樹,是因為其可以儲存其所有歷史變化情況。

也就是說,一個線段樹,每次一個結點的更改(如更改10次),就產生了10棵線段樹(第i棵線段樹與第i-1棵只有log(n)個結點的區別),分別對應每次更改後線段樹的樣子。

如果每次更改都把整棵樹儲存下來是非常浪費空間的,主席樹的作用就是對第i棵樹,進行一次更新後,只重新儲存這次更新的全部結點,那麼這就只是一顆只有一個方向走

到底的樹(只有log(n)個結點),那麼可以用指標進行,把其沒更新的結點用指標指過去。那麼就由改變和未改變的組成了一顆完整的樹啦。

可能這個用指標進行,把其沒更新的結點用指標指過去比較難理解,對著程式碼手動過一遍就好了。

#include <iostream>
#include <stdio.h>
#include <vector>
#include <string.h>
#include <algorithm>
using namespace std;
const int maxn=1e5+7;
int n,m,cnt;
struct ttt{
	int l,r,sum;  //sum表示這個區間裡面數字的數量 
};
int a[maxn];
int root[maxn];
ttt T[maxn*40];
vector<int>v;
int getid(int x){
	return lower_bound(v.begin(),v.end(),x)-v.begin()+1; //錯1次,返回下標需要-v.begin()+1 
}
void update(int l,int r,int &x,int y,int pos){
	T[++cnt]=T[y]; //把T[y]全部給T[cnt],也就是全部給x 
	T[cnt].sum++;x=cnt; //x表明的是下標 
	if(l==r)return ;
	int mid=(l+r)/2;
	if(pos<=mid) update(l,mid,T[x].l,T[y].l,pos);
	else update(mid+1,r,T[x].r,T[y].r,pos);
}
int query(int l,int r,int x,int y,int k){//query為查詢,root查的是下標,那麼輸入就為下標 
	if(l==r)return l;
	int mid=(l+r)/2;
	int sum=T[T[y].l].sum-T[T[x].l].sum;//差值為在這個期間新增的結點
	if(sum>=k)return query(l,mid,T[x].l,T[y].l,k);
	else return query(mid+1,r,T[x].r,T[y].r,k-sum); //sum是指這個區間左邊加了多少個值。							
}
int main(){
	int i,j,k,f1,f2,f3,f4,t1,t2,t3,t4;
	//freopen("in.txt","r",stdin);
	cin >> n>> m;
	for(i=1;i<=n;i++){
		scanf("%d",&a[i]);
		v.push_back(a[i]);
	}
	sort(v.begin(),v.end());
	v.erase(unique(v.begin(),v.end()),v.end());
	for(i=1;i<=n;i++){
	update(1,n,root[i],root[i-1],getid(a[i])); //給線段樹加入一個下標為a[i]的數 
//	cout << i << "    !!!  " << cnt << endl
	}
	//v.erase(i),為刪除下標是i的數字。
	//v.erase(beg,end),刪除從beg到end區間的資料。
		for(i=1;i<=m;i++){
		scanf("%d %d %d",&t1,&t2,&t3);
		printf("%d\n",v[query(1,n,root[t1-1],root[t2],t3)-1]);
		// 得到的下標是數字的下標,但是要在v中輸出就必須-1 
	}
	return 0;
}

之前分塊的方法錯了,這個是正確的,改變的只有find1()函式,二分判斷結果的是下標,然後把這個下標放入值中,而不是二分值。

#include<iostream>
#include <algorithm>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <queue>
#include <vector>
using namespace std;
int l[5500];
int r[5500];
int belong[105000];
int q1[105000];
int q2[105000];
int q3[105000];
int n,block,num;
void build(){
	double f1=n;
	block=sqrt(f1*log(f1));
	if(block==0)block=1;
	num=n/block;if(n%block!=0)num++;
	int i;
	for(i=1;i<=num;i++){
		l[i]=(i-1)*block+1;
		r[i]=min(n,i*block); //每個block個邊界,是包括的 
		sort(q1+l[i],q1+r[i]+1); //注意這裡的排序,後面是需要+1的 
	//	cout << l[i] <<"    "<< r[i] <<  " 排序了"<<endl;
		//for(j=l[i];j<=r[i];j++)
		 //cout << j << "   " << endl;
	}
	for(i=1;i<=n;i++){
	belong[i]=(i-1)/block+1;
	 }
}
int find2(int x,int y,int t){
	int i;
	int res=0,ans=0;
	if(belong[x]==belong[y]){
		for(i=x;i<=y;i++){
			if(q2[i]<t)res++;
		}
		return res;
	}else{
		for(i=x;i<=r[belong[x]];i++)
			if(q2[i]<t)res++;
		for(i=l[belong[y]];i<=y;i++)
			if(q2[i]<t)res++;
			//cout << res<< "個了" << endl;
		int left1,right1,mid1;
		for(i=belong[x]+1;i<belong[y];i++){
			left1=l[i];right1=r[i];
			ans=0;
			//cout << "左邊是" << l[i] << "   " << r[i] << endl;
			while(right1>=left1){
				mid1=(right1+left1)/2;
			//	cout << "進來是" << q1[mid1] <<"    " <<mid1<< endl;
				if(q1[mid1]<t){
					left1=mid1+1;
					ans=max(ans,mid1-l[i]+1);
				}else{
					right1=mid1-1;
				}
			}
			//cout << "++了" << ans << endl;
			res+=ans;
		}
		return res;
	}
}
int find1(int x,int y,int k){
	int left1,right1,mid1;
	left1=1;right1=n; //每次都二分n 
	int res=-(1e9+7);
	while(right1>=left1){
		mid1=(right1+left1)/2;
//	cout << "找" << q1[mid1] <<"    " << mid1 << "  " <<find2(x,y,q1[mid1])<<endl; 
		if(find2(x,y,q3[mid1])<k){
	//		cout <<" 符合" << endl;
			res=max(res,q3[mid1]);
			left1=mid1+1;
		}else{
			right1=mid1-1;
		}
	}
	return res;
}
int main(){
//	freopen("in.txt","r",stdin);
	int i,k,j,f1,f2,f3,f4,t1,t2,t3,t4,l2,m;
	long long c1,c2,c3,c4;
	cin >> n>> m;
	for(i=1;i<=n;i++){
	scanf("%d",&q1[i]);
	q3[i]=q2[i]=q1[i];}
	sort(q3+1,q3+1+n);
	build();
	for(i=1;i<=m;i++){
		scanf("%d %d %d",&t1,&t2,&t3);
		//cout << find2(t1,t2,5) << endl;
		printf("%d\n",find1(t1,t2,t3));
	}
	return 0;
}

整體二分:

思路:把所有值記錄下標id,按結構體排序,然後二分答案,把所有比答案小的值的下標放入樹狀陣列中,這裡有個小技巧。

那就是因為每次區間都是往左走,然後一點點往右邊走。那麼就只有從大區間到左邊小區間的時候,需要這個雙指標往左邊移動,把加過大於此時mid的值再退回來,那麼對於每次查詢summ(q[i].r)-summ(q[i].l-1)就是這個下標區間在樹狀陣列中的個數,也就是值<=mid的個數,如果個數大於等於k,說明結果值必然小於mid,否則大於mid,此時判斷丟到兩邊再進行查詢。

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn=1e5+7;
const int INF=1e9+7;
struct ttt{
    int id,key;
};
struct tt1{
    int l,r,k,id;
};
int res[maxn],NN;
ttt q1[maxn];
tt1 q2[maxn];
int lowbits(int x){
    return x&(-x);
}
int c1[maxn];//注意這裡空間可能要開大一些
int summ(int x){
    int sum1=0;
    while(x>0){
    sum1+=c1[x];
    x-=lowbits(x);
    }
    return sum1;
}
int add(int x,int y){
    while(x<=NN){
        c1[x]+=y;
        x+=lowbits(x);
    }
}
int n,m,tot;
tt1 L[maxn],R[maxn];
void solve(int l,int r,int ql,int qr){
    if(ql>qr||l>r)return ;
    if(l==r){
        for(int i=ql;i<=qr;i++){
            res[q2[i].id]=l;
        }return ;
    }
    int mid=(l+r)>>1;
    int tot1,tot2;tot1=tot2=0;
    while(tot<n&&q1[tot+1].key<=mid)tot++,add(q1[tot].id,1);
    //因為有序,把所有結果滿足的值放入樹狀陣列中
    while(tot>0&&q1[tot].key>mid)add(q1[tot].id,-1),tot--;
    int sum1;
    for(int i=ql;i<=qr;i++){
        sum1=summ(q2[i].r)-summ(q2[i].l-1);
        if(sum1>=q2[i].k)L[++tot1]=q2[i]; //個數太多值應該在左邊
        else R[++tot2]=q2[i];
    }
    for(int i=1,j=ql;i<=tot1;i++,j++)q2[j]=L[i];
    for(int i=1,j=ql+tot1;i<=tot2;i++,j++)q2[j]=R[i];
    solve(l,mid,ql,ql+tot1-1);
    solve(mid+1,r,ql+tot1,qr);
}
int cmp1(ttt x,ttt y){
    return x.key<y.key;
}
int main(){
    int i,j,k,f1,f2,f3,f4,t1,t2,t3,t4,t5;
    //freopen("in.txt","r",stdin);
    scanf("%d %d",&n,&m);
    NN=n;
    int min1=2e9,max1=-2e9;
    for(i=1;i<=n;i++){
        q1[i].id=i;scanf("%d",&q1[i].key);
        min1=min(min1,q1[i].key);max1=max(max1,q1[i].key);
    }
    sort(q1+1,q1+1+n,cmp1);
    for(i=1;i<=m;i++){
        scanf("%d %d %d",&q2[i].l,&q2[i].r,&q2[i].k);q2[i].id=i;
    }
    solve(min1,max1,1,m);
    for(i=1;i<=m;i++)
    printf("%d\n",res[i]);
    return 0;
}