#edu1007. 尺取法
尺取法
尺取法
Codeforces 也把这种算法叫做 "two pointers" ,翻译为双指针。我觉得“双指针”的叫法比较宽泛,“尺取法”专指两个下标指针 和 单向移动,类似于“尺蠖[chǐ huò]”,移动过程,一般用于解决一类具有单调性的区间问题(区间左端点 l 往前移动,右端点 r 只会单调往前移动),当然这类问题很多情况下可以用二分解决,使用尺取法左右指针扫描一次区间,算法复杂度比二分优,能达到 . 算法基本过程可以描述为: 维护两个下标指针 和 , 每次 往前移动 个位置, 往前移动直到区间 题目要求,更新一下当前答案或输出。
例1 . 子序列
改编自 Sequence poj 3061 , UVA1121
题意:给定一个长度为 的正整数序列 和一个正整数 ,求一段最短的子区间(连续),区间和大于等于 ,输出最短子区间长度.
分析:
暴力枚举区间左右端点,使用前缀和计算区间和,时间复杂度为 ,显然 。
- 
(二分) 当固定区间左端点 ,由于所有数都是正整数,区间右端点 越大,对应区间值越大,存在单调性,可以二分找到最小的 使得区间 和大于等于 ,那么我们枚举区间左端点,二分查找区间右端点,时间复杂度为 . 
- 
(尺取法) 我们发现区间左端点 往前移动,区间右端点 只会往右移动(所有数大于等于 ),那么可以用尺取法。 
- 
- 初始化当前区间为空: ,区间和
 
- 
- 循环进行,直到 越界:
 - 循环移动  直到 区间  区间和  大于等于 S , 即  while(r+1<=n && sum<S)r++,sum+=a[r];
- 判断 如果小于 S,跳出循环
- 更新答案 ans=min(ans,r-l+1);
- 移动左指针 ,即 sum-=a[l],l++;
 
时间复杂度为: ,参考代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e7+10;
int n,a[N],s;
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	while(cin>>n>>s)
	{
		for(int i=1;i<=n;i++)cin>>a[i];
		
		int ans=N;
		int l=1,r=0,sum=0;   //初始化当前区间为空 
		while(l<=n)
		{
			while(r+1<=n && sum<s){r++;sum+=a[r]; }  //向前移动 r 直到符合要求
			
			if(sum<s)break;       //说明区间不可能满足答案
			
			ans=min(ans,r-l+1);     //更新答案 
			sum-=a[l];l++;        //移动左端点
		}
		
		if(ans<N) cout<<ans<<"\n";
		else  cout<<0<<"\n";
	}
	return 0;
}
反思: 本题中如果数字 可以是负数,那么尺取法还正确吗?想一想
练习题:P1147 连续自然数和
例2:P1638 逛画展
问题:给定一个长度为 由 构成的整数序列,求一个最短的子区间,子区间包括 中任意一个数。
分析:本题也存在单调性,当固定区间左端点 , 右端点 越大区间 越会满足条件(包含 中任意一个数),理论上也可以二分求得最小的 ,但判断一个区间 是否包含 中任意一个数比较麻烦(有 的时间复杂度),我们发现 每次往前移动, 也必然增大,存在单调性,尺取法可以派上用处。
- 初始化定义一个 num[1...m]数组,num[i]表示 出现的次数, 表示 数组中非零的个数。l=1,r=0,num[]=0,cnt=0
- 循环进行直到区间左端点  越界
- 循环移动 ,直到区间 符合题意,借助 计数数组
- 如果数量不够(cnt<m),跳出循环 (此时说明后续也不可能)
- 用当前区间 更新答案
- 往前移动 个位置
 
时间复杂度 ,参考代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
int n,m,a[N];
int num[2005],cnt; 
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);  //关闭流同步,优化读入输出速度
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	int len=n+1,l=1,r=0;
	int ansa,ansb;
	while(l<=n)
	{
		while(r+1<=n && cnt<m)   //移动 r 直到区间[l,r]满足题意 
		{
			++r;
			num[a[r]]++;
			if(num[a[r]]==1)cnt++;
		}
		if(cnt<m)break;
		if(r-l+1<len)   //更新答案 
		{
			len=r-l+1;
			ansa=l,ansb=r;
		}                
		//移动 l,移动前把个数减少 
		num[a[l]]--;
		if(num[a[l]]==0)cnt--;
		l++;
	} 
	cout<<ansa<<" "<<ansb; 
	return 0;
}
问题简述: 给定一个长度为 的序列,求一个最长的区间 使得区间内没有相同的数字,输出区间长度。
尺取法参考代码:点击
例3 [ABC098D] Xor Sum 2
题意:给定一个长度为 的数组 ,求有多少个非空子区间的 区间和等于区间异或和。
样例1
4
2 5 4 6
6
分析:首先找一些规律,样例  中  个数写成二进制 10,101,100,110,发现区间  是符合区间和等于区间异或和的,进而可以得到规律:如果一个区间中所有数对应二进制各个位上  只出现了  次,那么这个区间必然符合区间和等于区间异或和,这个规律是充要的。
当区间左端点 固定,寻找最大的右端点 ,那么区间 这些区间必然都符合要求,对答案的贡献增加 。 当 向前移动, 只会单调往前移动(移动长度大于等于 ),这是可以使用尺取法的根本原因。
参考代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10;
ll n,a[N],sum,s;   //sum为区间和 s为区间异或和 
//区间a[l...r] 中二进制各位上 1 只出现一次 就会符合题意 
// 固定l,r具有单调性 因此可以使用尺取法 
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	int l=1,r=0;
	ll ans=0;
	while(l<=n)
	{
		while(r+1<=n && (sum+a[r+1])==(s^a[r+1]))  // 异或运算优先级低 括号不能省
		{
			r++;
			sum+=a[r];
			s^=a[r];
		}
		ans+=r-l+1;
		sum-=a[l];
		s^=a[l];
		l++;
	} 
	cout<<ans;
	return 0;
}
学习完毕
{{ select(1) }}
- YES
- NO
