挑战程序设计竞赛常用技巧-尺取法(方法+模板例题+解答)

尺取法

利用双指针解决问题,通常利用指针保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。尺取法比直接暴力枚举区间效率高很多,尤其是数据量大的时候,所以说尺取法是一种高效的枚举区间的方法,是一种技巧,一般用于求取有一定限制的区间个数或最短的区间等等。当然任何技巧都存在其不足的地方,有些情况下尺取法不可行,无法得出正确答案,所以要先判断是否可以使用尺取法再进行计算。

尺取法通常适用于选取区间有一定规律,或者说所选取的区间有一定的变化趋势的情况,通俗地说,在对所选取区间进行判断之后,我们可以明确如何进一步有方向地推进区间端点以求解满足条件的区间,如果已经判断了目前所选取的区间,但却无法确定所要求解的区间如何进一步得到根据其端点得到,那么尺取法便是不可行的。首先,明确题目所需要求解的量之后,区间左右端点一般从最整个数组的起点开始,之后判断区间是否符合条件在根据实际情况变化区间的端点求解答案。

尺取法通常用whie循环代替for循环解决问题,大大减小复杂度


例题一

题意:给定无序数列和sum,要求选取其中和等于sum的两个数并且输出,位置交换属于一种情况
分析尺取法反向扫描问题,如果是常规解法我们肯定会用到两个for循环一次遍历枚举,时间复杂度会达到O(n2),但用尺取法可以将复杂度降到O(n)

首先对数组从小到大排序排序,用两个指针i,j分别指向头尾
if(a[i]+a[j])>sum 需要减少和,尾指针左移
if(a[i]+a[j])<sum 需要增大和,头指针右移
if(a[i]+a[j])=sum 满足情况,头指正向后移增大,尾指针向前移减小

/*
测试样例
8 9
1 5 9 7 4 6 2 8
*/

#include<bits/stdc++.h>
using namespace std;
int n,m,a[100005];
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		cin>>a[i];
	}
	sort(a,a+n);//使用尺取法之前先排序
	//排序后:1 2 4 5 6 7 8 9 
	int i=0;//头指针指向第一个数 
	int j=n-1;//尾指针指向最后一个数 
	while(i<j){//保证不重复 
		if(a[i]+a[j]==m){//找到了,前端点后移(数增大),后端点向前移(数减少) 
			printf("%d %d\n",a[i],a[j]);
			i++;
			j--;
		}
		else if((a[i]+a[j])<m){//和太小,前端点后移增大和(后端点后移也可以增大,但是讨论过了) 
			i++;
		}else if((a[i]+a[j])>m){//和太大,后端点前移减小和(前端点前移也可以减小,但是讨论过了) 
			j--;
		}
	} 
	return 0;
} 

例题二

题意:给定长度为n的数列整数a0 a1 ,… an-1以及整数S。求出总和不小于S的连续子序列的长度最小值,如果解不存在,则输出0

分析:首先,序列都是正数,如果一个区间其和大于等于S了,那么不需要在向后推进右端点了,因为不但会增加区间和还会增加区间长度(我们要的是区间长度最小),所以此时只有向后推动左端点才能使区间长度减少
所以,当区间和小于S时右端点向右移动,和大于等于S时,左端点向右移动以进一步找到最短的区间,如果右端点移动到区间末尾其和还不大于等于S,结束区间的枚举。

代码

/*测试样例:
10
15
5 1 3 5 10 7 4 9 2 8
*/
#include<bits/stdc++.h>
using namespace std;
int n,s,a[100005];
int main(){
	cin>>n;
	cin>>s;
	for(int i=0;i<n;i++){
		cin>>a[i];
	}
	int i=0,j=0,ans=9999,sum=a[0];//初始化为区间[0,0]的和 
	while(i<=j&&j<n){
		if(sum>=s){//此时区间和满足条件 
			if(j-i<ans)ans=j-i+1;//求区间长度 
			sum-=a[i];//减少区间长度左端点向右移动,区间sum和删去i端点值 
			i++;
		}else{//区间和不够大,右端点向右移动增减区间和 
			j++;
			sum+=a[j];
		}
		
	}
	if(ans==9999){//解不存在 
		cout<<0<<endl;
	}else{
		cout<<ans;
	}
	
	return 0;
}

例题三

题意:一本书总共有P页,第i页恰好有一个知识点ai(每个知识点都有一个整数编号)全书同一个知识点可能被多次提到,所以尽可能阅读其中一些连续的页把所有的知识点都覆盖到,给定每页写到的知识点,求要阅读的最少页数

分析:和上面一题的思路一样,利用set统计出整本书的所有的知识点,如果知识点没有学够,就将右端点向后推进继续学习新的,如果学够了,就将左端点向后推进减少区间长度,学了多少知识可以用map来统计

代码

/*
样例
5
1 8 8 8 1
*/
#include<bits/stdc++.h>
using namespace std;
set<int> zl;//知识点总类
map<int,int> fx;//first表示已经学到的知识,scond表示已经学习的知识总共学的次数
int p,a[100005];
//计算学到了多少知识点
int story(){
	int conutl=0;
	for(map<int,int> :: iterator it=fx.begin();it!=fx.end();it++){
		if(it->second>0)conutl++;//当second>0表示学了这个知识
	}
	return conutl;
} 
int main(){
	cin>>p;
	for(int i=0;i<p;i++){
		cin>>a[i];
		zl.insert(a[i]);//需要学的知识转入set中 
	}
	int num=zl.size();//一共有num个知识点需要复习 
	int i=0,j=0,ans=9999;
	fx[a[j]]++;//初始化为第一个知识点已学 
	while(i<=j&&j<p){
		if(story()<num){//学习的知识点不够继续向后学 
			j++;
			fx[a[j]]++;//学习 
		}
		if(story()>=num){//学习的知识点够了,缩短区间长度,i向后移动 
			ans=min(ans,j-i+1);//比较区间长度
			fx[a[i]]--;//删除掉i端点学到的知识点 
			i++;
		}
	}
	cout<<ans<<endl;
	return 0;
}


例题四

题意:给定一个长度为n的数组a[]和一个数s,在这个数组中找出和等于k的区间,输出区间的起点和终点位置。

分析:还有一样的思想,大于或等于K移动左端点减少,小于k移动右端点增大

代码

/*
测试样例
输入 
6 6
2 1 3 3 4 2
输出 
0 2
2 3
4 5
*/ 
#include<bits/stdc++.h>
using namespace std;
int n,k,a[1000];
int main(){
	cin>>n>>k;
	for(int i=0;i<n;i++){
		cin>>a[i];
	}
	int i=0,j=0,sum=a[0];//初始化 
	while(i<=j&&j<n){
		if(sum==k)printf("%d %d\n",i,j);//找到一个区间 
		if(sum>=k)sum-=a[i++];//太大或者找到了一个区间,就让左端点移动减小大小 
		if(sum<k)sum+=a[++j]; //太小就让右端点后移增加大小 
	}
	return 0;
}

滑动窗口实现:窗口就是区间[i,j],随着i和j从头到尾移动,窗口就“滑动”扫描了整个序列,检索了所有的数据。i和j并不是同步增加的,窗口像一只蚯蚓伸缩前进,它的长度是变化的,这个变化,正对应了对区间和的计算。


例题五

题意:给定数组a[],长度为n,把数组中重复的数去掉。
分析:
(1)将数组排序,这样那些重复的整数就会挤在一起。
(2)定义双指针i、j,初始值都指向a[0]。i和j都从头到尾扫描数组a[]。j指针走得快,逐个遍历整个数组;i指针走得慢,它始终指向当前不重复部分的最后一个数。也就是说,j用于获得不重复的数。
(3)扫描数组。快指针j++,如果此时a[j]不等于慢指针i指向的a[i],就把i++,并且把a[j]复制到慢指针j的当前位置a[i]。
(4)i扫描结束后,a[0]到a[i]就是不重复数组。
代码:

/*
样例:
输入
6
1 3 3 4 5 5
输出
1 3 4 5
*/
#include<bits/stdc++.h>
using namespace std;
int n,a[1000];
int main(){
	cin>>n;
	for(int i=0;i<n;i++){
		cin>>a[i];
	}
	sort(a,a+n);
	int i=0,j=0;
	while(j<n){//j不能扫描超过数组 
		if(a[j]!=a[i]){//发现新数 
			a[++i]=a[j];//将新数放到前面去 
		}
		j++;
	}
	for(int t=0;t<=i;t++){
		cout<<a[t]<<" ";
	}
	return 0;
}

例题六

题意: 给定一个序列,以及一个整数M;在序列中找M个连续递增的元素,使它们的区间和最大。

分析:滑动窗口维护一段递增数列,区间[i,j]是一段递增数列
(1)让i,j同时指向头部,假入a[j]<a[j+1]就将指针j向后推进,知道推进到递 增数列最后一个数,此时的[i,j]是这段递增区间最长的时候
(2)逐步向后推进i指针,减小递增区间长度(取子递增区间)
(3)如果i==j就让i,j同时指向下个数(下一段区间的开始)
(3)当一段区间长度等于M时取最大区间和
代码

/*
测试样例1
输入 
9 3
1 2 3 2 1 5 7 9 8
输出 
21

测试样例2
输入 
12 3
1 1 2 3 2 1 5 7 9 6 7 8
输出
21 
*/
#include<bits/stdc++.h>
using namespace std;
int n,a[10000],M;
int main(){
	cin>>n>>M;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	int i=1,j=1,sum=a[i],ans=0;
	while(i<=j&&j<=n){
		//printf("i=%d j=%d sum=%d\n",i,j,sum); 
		if(j-i+1==M){//满足区间和 	
			//printf("i=%d j=%d sum=%d\n",i,j,sum); 
				ans=max(ans,sum);//取最大区间和 
			
		}
		if(a[j]<a[j+1]){//本段递增区间还没有到结尾,继续推进j 
			sum+=a[++j];
		}else if(a[j]>=a[j+1]){ 
			if(i==j){//说明一段递增区间访问完,进入下一段
				j++;//指向下一段区间的开始
				sum=a[++i];//i指向下一段区间的开始,sum初始值设为下一段区间的开始值 
			}else{
				sum-=a[i++];//向后推进i,减小区间长度 
			} 
			
			
		}
	}
	cout<<ans<<endl;
	return 0;
}

例题七

题意:给定一个序列,以及一个整数K;求一个最短的连续子序列,其中包含至少K个不同的元素。
分析:和例三知识点题目差不多
代码

/*
测试样例1
输入
10 3
1 1 2 2 2 3 4 4 5 5
输出
2 3 4

测试样例2
输入
10 3
1 1 1 2 2 4 4 4 5 5
输出
1 2 2 4
*/
#include<bits/stdc++.h>
using namespace std;
int n,K,a[1000];
map<int,int> mp;
//统计序列中不同的数
int num(){
	int ans=0;
	for(map<int,int> ::iterator it=mp.begin();it!=mp.end();it++){
		if(it->second>0)ans++;
	}	
	return ans;
} 
int main(){
	cin>>n>>K;
	for(int i=0;i<n;i++){
		cin>>a[i];
	}
	int i=0,j=0,ans=999,le=0,en=0;
	mp[a[0]]++;//初始化 
	while(i<=j&&j<n){
		if(num()<K){//不同元素不够j向后推进 
			mp[a[++j]]++;//新元素导入map 
		}else{//不同元素足够,i向后推进 
			//printf("i=%d j=%d 长度=%d k=%d num()=%d\n",i,j,j-i+1,K,num());
			if(j-i+1<ans){//记录满足条件最小区间长度 
				ans=j-i+1;
				le=i;
				en=j;
			}
			mp[a[i++]]--;//删除a[i]元素 
		}
	}
	for(int i=le;i<=en;i++){
		cout<<a[i]<<" ";
	}
	return 0;
} 

例题八

题意:给定一个长度为n的数组a和正整数m,求有多少个连续子区间,其区间内存在某个元素出现的次数不小于m次?
分析:用滑动窗口求出维护满足条件的最小区间,最小区间右端点向右拓展一次就是一种情况(因为元素只会更多,一定符合情况),所以次数用ans+右端点的右边元素个数 就可以了,然后左端点在右移

/*
测试样例
输入 
5 2
1 2 1 2 3
输出
5 
*/
#include<bits/stdc++.h>
using namespace std;
int n,m,a[100005],ans;
map<int,int> mp;
bool check(int x,int y){//判断是否有元素出现次数大于等于m
	for(int i=x;i<=y;i++){
		if(mp[a[i]]>=m){
			//printf("%d %d %d\n",x+1,y+1,mp[a[i]]);	
			ans=ans+n-y;//如区间[x,y]满足,那么[x,y~n)任意的都满足,所以直接ans加上y后面的元素个数即可 
			return true;
		}
	}	
	return false;
}
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		cin>>a[i];
		mp[a[i]]=0;
	}
	int i=0,j=0;
	mp[a[i]]++;
	while(i<=j&&j<n){
		if(!check(i,j)){//不够后指针右移 
			mp[a[++j]]++;
		}else{
			mp[a[i++]]--;
		}
	}
	cout<<ans<<endl;
	return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值