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;
}

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值