尺取法学习笔记

时间:2021-08-07
本文章向大家介绍尺取法学习笔记,主要包括尺取法学习笔记使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

什么是尺取法

Codeforces 中显示它的算法名称叫做 "two pointers"

直译成中文的话叫双指针法

这个算法 hin 有意思,由于在某些巨佬眼中过于简单,以至于都没把尺取法当成一个算法

如何进行尺取法

尺取法的思想是维护两个指针 \(l,r\)​ ,分别为左端点与右端点,每当确定左端点时,尝试将右端点一直移动,直到不满足条件为止

,让我们来看这一道例题

P1638 逛画展

求刚好有 \(m\) 种数字的最短区间

我们发现,当这个区间的左端点向右移动时,右端点一定不会向左移动,所以我们在枚举 \(r\) 时,不需要从 \(l\) 开始枚举,我们可以从上一次枚举到的 \(r\) 开始枚举

我们记录一个 \(cnt\) 数组, \(cnt_i\) 表示第 \(i\) 种数字在 \(l \sim r-1\) 这个区间内的出现次数

用一个变量 \(sum\) 来表示这个区间不同画的数量

维护一下,即可AC

具体细节见代码:

#include <cstdio>
using namespace std;
const int N=1e6+7,MAX=2e3+7;

int a[N];
int cnt[MAX];

int n,m;
int ansl,ansr=N; // 初始化区间长度为最长

signed main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
		scanf("%d",a+i);
	for(int l=1,r=1,sum=0;l<=n;) {
		while(r<=n && sum<m) { // 判断是否满足条件
			++cnt[a[r]]; // 数量+1
			if(cnt[a[r]]==1) // 如果数量+1后只有一个,说明出现了新的数,++sum
				++sum;
			++r; // 移动右端点指针
		}
		
		// 这里的右端点指针指向的是最长能向右扩展的位置的下一位
		// 所以区间长度是(r-1)-l+1=r-l
		// 而不是r-l+1
		if(sum==m && r-l<ansr-ansl)
			ansl=l,ansr=r; // 如果有更短的满足题设的区间,更新答案
		
		--cnt[a[l]]; // 移动左端点指针
		if(!cnt[a[l]]) // 如果移动后这个数的数量为0,说明减少了一个数,--sum
			--sum;
		++l;
	}
	printf("%d %d",ansl,ansr-1); // 根据右端点的定义,输出时右端点要-1
    return 0;
}

习题:

UVA11572 唯一的雪花 Unique Snowflakes

求没有重复数字的最长区间

与第一题思路类似,由于值域范围较大,我使用的是 set 维护

#include <set>
#include <cstdio>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=1e6+7;

set<int> s;

int a[N];

int T,n,ans;

signed main() {
	scanf("%d",&T);
	for(;T;--T) {
		scanf("%d",&n);
		for(int i=1;i<=n;++i)
			scanf("%d",a+i);
		s.clear(); // 清空
		ans=-inf;
		for(int l=1,r=1;l<=n;) {
			while(s.find(a[r])==s.end() && r<=n)
				s.insert(a[r]),++r; // 移动右端点指针
			ans=max(ans,r-l); // 计算答案
			s.erase(a[l]);
			++l; // 移动左端点
		}
		printf("%d\n",ans);
	}
    return 0;
}

AT4142 [ARC098B] Xor Sum 2

求有多少个区间 \([l,r]\) ,满足 \(a_l \ xor \ a_{l+1} \ xor \dots xor \ a_r = a_l + a_{l+1} + ... +a_r\)

注意到一个性质,当 \(a \ and \ b=0\) 时,\(a \ xor \ b=a+b\)

那么 \(l \sim r\) 所有数的异或值等于所有数的和,必须要每一个二进制位上该区间所有数加起来最多只有一个 \(1\) 才行.

直接用尺取法做,记得开 long long

#include <cstdio>
typedef long long ll;
using namespace std;
const int N=2e5+7;

int a[N];

ll ans;
int n;

signed main() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d",a+i);
	for(int l=1,r=1,tmp=0;r<=n;) {
		while(!(tmp&a[r]) && r<=n) { // tmp&a[r]!=0,则tmp^a[r]!=tmp+a[r]
			tmp+=a[r],++r; // 移动右端点指针
			ans+=r-l; // 统计答案
		}
		tmp^=a[l],++l; // 移动左端点指针
	}
	printf("%lld",ans);
    return 0;
}

P3143 [USACO16OPEN]Diamond Collector S

求最长的两端不相交的区间,每个区间的极差不大于 \(k\)

本题我们可以用尺取法做

要求区间互不相交,我们统计答案就要变成左端点前的最大区间+当前区间

#include <cstdio>
#include <algorithm>
using namespace std;
const int N=5e4+7;

int a[N];
int c[N];

int k;
int n,ans;

signed main() {
	scanf("%d%d",&n,&k);
	for(ll i=1;i<=n;++i)
		scanf("%d",a+i);
	sort(a+1,a+1+n); // 因为放置与顺序无关,所以我们可以先排序,使得相邻两数之差变小
	for(int l=1,r=2,maxx=-1;l<=n;) {
		while(a[r]-a[l]<=k && r<=n)
			++r; // 移动右端点指针
		c[r]=max(c[r],r-l); // 记录以r为右端点向左可以扩展的最大区间
		maxx=max(maxx,c[l]); // 更新之前的最长区间
		ans=max(ans,maxx+(r-l)); // 更新答案为之前的最长区间+当前区间
		++l;
	}
	printf("%d",ans);
    return 0;
}

原文地址:https://www.cnblogs.com/wshcl/p/two-pointers.html