程序设计竞赛常用技巧—— 尺取法 讲解


一、尺取法的基本概念

算法描述

尺取法,在Codeforces中它的算法名称叫做Two Pointers,在《挑战程序设计竞赛》一书中,译者直接使用了日文原文的汉字即尺取法,因为该算法的操作很像是尺蠖(日语中称尺取虫)爬行的方式而得名。

尺取法顾名思义,通常是指对数组保存一对下标(起点、终点),然后根据实际情况交替推进两个端点直接得出答案的方法

基本框架

	//Alun版本
	int _beg = 0, _end = 0//需要保存起点和终点
	while(1)
	{
		// 推进终点,可以使用while()持续推进
		if()	break;	//判断能否构成满足条件序列
		// do something...
		// 推进起点
	}

二、尺取法的经典例题

A. POJ 3061 Subsequence

→ 原题传送门
Time Limit:1000MS
Memory Limit: 65536K
Source: Southeastern Europe 2006

Description

A sequence of N positive integers (10 < N < 100 000), each of them less than or equal 10000, and a positive integer S (S < 100 000 000) are given. Write a program to find the minimal length of the subsequence of consecutive elements of the sequence, the sum of which is greater than or equal to S.

大意:给出N个正整数序列(10 < N < 100,000),每一个正整数都小于或等于10,000,接下来会给出一个正整数S (S < 100,000,000)。要求编写一个程序,求出总和不小于S的连续子序列的长度的最小值。

Input

The first line is the number of test cases. For each test case the program has to read the numbers N and S, separated by an interval, from the first line. The numbers of the sequence are given in the second line of the test case, separated by intervals. The input will finish with the end of file.

大意:第一行为一个整数,表示测试用例的数量。对于每一个测试用例都含有两行输入,首行为两个整数N和S,紧跟着的一行为N个正整数,中间都用一个空格隔开。

Output

For each the case the program has to print the result on separate line of the output file.if no answer, print 0.

大意:一行一个测试用例的答案,如果某一用例解不存在,则输出0。

Sample Input

2
10 15
5 1 3 5 10 7 4 9 2 8
5 11
1 2 3 4 5

Sample Output

2
3

思路概要

对于该题来讲,绝对不可能使用暴力枚举攻克,粗略预估一下,暴力枚举需要不断枚举起点和终点,复杂度应该为 O ( n 2 ) O(n^2) ,大致为 1 e 5 2 1e5^2 ,显然会造成超时。

那么如何进行优化呢?
这里有两个思路可以选择:

思路一、前缀和 + 二分答案

前缀和的运用可以快速得到某一区间的答案,再利用二分答案的思想不断枚举总和不小于S的连续子序列的长度即可,每次查找的时间复杂度可以降至 O ( l o g n ) O(logn)

思路二、尺取法

虽然前缀和 + 二分答案的做法已经可以通过该题,但是还可以更加高效地求解该题。

假设以 a s a_s 元素开始总和不小于S时的最初连续子序列为 a s + + a t a_s + \cdots + a_t , 此时起点若向前推进一个,出现如下情况:
a s + 1 + + a t &lt; a s + + a t &lt; S a_{s+1} + \cdots + a_{t} \lt a_s + \cdots + a_{t} \lt S
这时,可以选择推进终点位置 t t ,直至得到 t t&#x27; 位置使得 a s + 1 + + a t S a_{s+1} + \cdots + a_{t&#x27;} \ge S ,比较原序列长度与新序列长度更新答案。

其实,更可以得到,每一次起点向前推进,都应存在 t t t \le t&#x27; ,使得新序列 a s + 1 + + a t S a_{s+1} + \cdots + a_{t&#x27;} \ge S ,若不存在,便是剩余序列不满足条件,便可以退出当前整体操作。

据此思路,可以整理为以下步骤:

  1. 得到最初总和不小于 S S 序列,起点终点分别为 s s t t ,总和为 s u m sum
  2. 推进起点 s s ,并且 s u m sum 减去原 s s 位置上的值
  3. 根据新序列的总和是否大于 S S ,推进 t t ,直至得到满足条件的新序列,若 t t 一直推进到序列末尾都无法满足条件,则直接退出;否则,将新的序列长度与历史长度比优,并重复执行步骤2

具体操作如下表(表中_beg为s,指代起点;_end为t,指代终点):

尺取法表格分析
根据表格操作不难发现,对于该算法,因为 s ( _ b e g ) s(\_beg) 起点最多变化 n n 次,所以只需 O ( n ) O(n) 的复杂度便可以解决该问题了。
○( ^皿^)っHiahia…再来强调一下主题, 这样反复推进区间的起点和终点,来求取满足条件的区间,这一方法被称为尺取法。

参考代码

解法一、前缀和 + 二分答案

/*
	code by Alun 2019.4.19
	PS:前缀和 + 二分答案
	AC Memory: 568K
	AC Time: 360MS
*/
#include <iostream>
#include <algorithm>
#define MAXN 100000
using namespace std;
int a[MAXN + 1] = {};
bool check(int sum[], int n, int s, int mid);
int main()
{
	//freopen("1.in","r",stdin);
	//freopen("1.out","w",stdout);
	int t;
	cin >> t;
	while(t--)
	{
		int n, s, a;
		int sum[MAXN + 1] = {};
		cin >> n >> s;
		cin >> sum[0];
		for(int i = 1; i < n; i++)
		{
			cin >> a;
			sum[i] = sum[i-1] + a;
		}
		
		//序列判定是否小于s 
		if(sum[n-1] < s)
		{
			cout << 0 << endl;
			continue;
		}
		
		//二分答案
		int l = 0, r = n;
		while(l < r)
		{
			int mid = (l + r) / 2;
			if(check(sum,n,s,mid))
				r = mid;
			else
				l = mid + 1;
		}
		cout << r << endl;
	}
	return 0;
}

bool check(int sum[], int n, int s, int mid)
{
	for(int i = 0; i < n - mid; i++)
	{
		if(sum[i+mid] - sum[i] >= s)
			return 1;
	}
	return 0;
}

解法二 、尺取法

/*
	code by Alun 2019.4.18
	PS:尺取法
	AC Memory: 564K
	AC Time: 329MS
*/
#include <iostream>
#include <algorithm>
#define MAXN 100000
using namespace std;
int a[MAXN + 1] = {};
int main()
{
	//freopen("1.in","r",stdin);
	//freopen("1.out","w",stdout);
	int t;
	cin >> t;
	while(t--)
	{
		int n, s, ans, sum = 0;		//ans为最后答案,sum存序列总和	
		int _beg = 0, _end = 0;		//尺取法的两个指针起点begin和终点end 
		cin >> n >> s;
		ans = MAXN + 1;
		for(int i = 0; i < n; i++)
			cin >> a[i];
		
		while(1)
		{
			while(sum < s && _end < n)		//爬行得到大于s的终点 
				sum += a[_end++];
			if(sum < s)						//检查剩余序列是否小于s 
				break;
			ans = min(ans, _end - _beg); 
//			cout << _beg << " " << _end <<endl;			//debug 
			sum -= a[_beg++];				//每轮起点前进并减少sum的值 
		}
		
		if(ans == MAXN + 1)
			cout << 0 << endl;
		else
			cout << ans << endl;
	} 
	return 0;
}

三、尺取法练习题归总

A. POJ 3061 Subsequence

→ 返回本文例题
→ 原题传送门

B. POJ 3320 Jessica’s Reading Problem

→ 原题传送门
Time Limit:1000MS
Memory Limit: 65536K
Source: POJ Monthly–2007.08.05, Jerry

Description

Jessica’s a very lovely girl wooed by lots of boys. Recently she has a problem. The final exam is coming, yet she has spent little time on it. If she wants to pass it, she has to master all ideas included in a very thick text book. The author of that text book, like other authors, is extremely fussy about the ideas, thus some ideas are covered more than once. Jessica think if she managed to read each idea at least once, she can pass the exam. She decides to read only one contiguous part of the book which contains all ideas covered by the entire book. And of course, the sub-book should be as thin as possible.

A very hard-working boy had manually indexed for her each page of Jessica’s text-book with what idea each page is about and thus made a big progress for his courtship. Here you come in to save your skin: given the index, help Jessica decide which contiguous part she should read. For convenience, each idea has been coded with an ID, which is a non-negative integer.

大意:Jessica是一个非常可爱的女孩,很多男孩都在追求她。可是最近她有个一个问题。期末考试就要来了,但是她至今都还没有花时间去复习。如果她想通过考试,她必须掌握一本厚厚的教科书中包含的所有内容。而且那本书的作者和其他作者一样,对概念非常挑剔,因此有些概念被多次提及。Jessica认为如果她至少把每个概念读一遍,就能通过考试。所以她决定只阅读教科书中一个连续的部分,这部分应当包含了整本书涵盖的所有思想。当然,还要求这部分的页数尽可能的薄。

一个非常勤奋的男孩为Jessica整理出了她课本的每一页都是关于什么概念,并一一标记了页码,因此他的求爱取得了很大的进展。现在期望你能够帮助Jessica决定应该读取哪个连续的部分。为了方便起见,每个概念思想都用ID编码,ID是一个非负整数。

Input

The first line of input is an integer P (1 ≤ P ≤ 1000000), which is the number of pages of Jessica’s text-book. The second line contains P non-negative integers describing what idea each page is about. The first integer is what the first page is about, the second integer is what the second page is about, and so on. You may assume all integers that appear can fit well in the signed 32-bit integer type.

大意:输入的第一行是整数 P ( 1 P 1 , 000 , 000 ) P(1 \le P \le 1,000,000) ,指的是Jessica的课本页数。第二行包含 P P 个非负整数,描述了每页的内容。第一个整数是第一页的内容,第二个整数是第二页的内容,以此类推。假定出现的所有整数都在带符号的32位整型范围内 。

Output

Output one line: the number of pages of the shortest contiguous part of the book which contains all ideals covered in the book.

大意:输出一行:为包含书中所有知识点概念的最短连续部分的页数。

Sample Input

5
1 8 8 8 1

Sample Output

2

参考代码

/*
	code by Alun 2019.4.25
	PS:尺取法
	AC Memory: 1540K
	AC Time: 485MS
*/

#include <iostream>
#include <algorithm>
#include <cmath>
#include <set>
#include <map>
#define MAXP 1000000
using namespace std;

int a[MAXP+1] = {};
set<int> id;		//集合id,主要统计知识点数量
map<int,int> mark; 	//键值对,key - 知识点,value - 知识点在该序列出现的次数 
int main()
{
	//freopen("1.in","r",stdin);
	//freopen("1.out","w",stdout);
	int p;
	cin >> p;
	for(int i = 0; i < p; i++)
	{
		scanf("%d", &a[i]);
		id.insert(a[i]);
	}
	int cnt = 0;
	int ans = p;
	int _beg = 0, _end = 0;
	while(1)
	{
		//得到符合条件序列
		while(_end < p && cnt < id.size())
		{
			if(mark[a[_end]] == 0)
				++cnt;
			++mark[a[_end]], ++_end;
		}
		
//		cout << _beg << " " << _end << " " << cnt << endl; 		//debug 
		
		//判定
		if(cnt < id.size())
			break;

		ans = min(ans, _end - _beg);
		
		//起点位置推进
		--mark[a[_beg]];
		if(mark[a[_beg]] == 0)
			cnt--;
		_beg += 1;
	}
	
	cout << ans << endl;
	return 0;
}

/*
5
1 3 1 2 5
5
*/ 

C. CF660C Hard Process

→ 原题传送门
Time Limit:1 second
Memory Limit: 256 megabytes
Source: Codeforces contest 660 TC

Description

You are given an array a a with n n elements. Each element of a a is either 0 0 or 1 1 .

Let’s denote the length of the longest subsegment of consecutive elements in a, consisting of only numbers one, as f ( a ) f(a) . You can change no more than k k zeroes to ones to maximize f ( a ) f(a) .

大意:给定一个包含 n n 个元素的数组 a a 。数组a的每个元素不是 0 0 就是 1 1
现在,让我们用 f ( a ) f(a) 表示 a a 中连续元素序列中最长子段的长度,它只包含数字 1 1 。并且你最多可以将 k k 0 0 更改为 1 1 来得到最大的 f ( a ) f(a)

Input

The first line contains two integers n n and k k ( 1 n 3 1 0 5 , 0 k n ) (1 \le n \le 3·10^5, 0 \le k \le n) — the number of elements in a a and the parameter k k .
The second line contains n n integers a i ( 0 a i 1 ) a_i (0 ≤ a_i ≤ 1) — the elements of a a .

大意:第一行包含两个整数 n n k k ( 1 n 3 1 0 5 , 0 k n ) (1 \le n \le 3·10^5, 0 \le k \le n) .
第二行为 n n 个整数,表示数组 a a 的每一个元素 a i ( 0 a i 1 ) a_i (0 \le a_i \le 1) .

Output

On the first line print a non-negative integer z z — the maximal value of f ( a ) f(a) after no more than k k changes of zeroes to ones.
On the second line print n n integers a j a_j — the elements of the array a a after the changes.
If there are multiple answers, you can print any one of them.

大意:
在第一行中打印一个非负整数 z z ,表示翻转不超过 k k 0 0 之后的 f ( a ) f(a) 的最大值。
在第二行中打印 n n 个整数 a j a_j ,表示修改之后数组 a a 的元素。
如果有多个答案,您可以打印其中任何一个。

Sample Input 1

7 1
1 0 0 1 1 0 1

Sample Output 1

4
1 0 0 1 1 1 1

Sample Input 2

10 2
1 0 0 1 0 1 0 1 0 1

Sample Output 2

5
1 0 0 1 1 1 1 1 0 1

参考代码

/*
	code by Alun 2019.4.26
	Tag: Two Points
	AC Time: 294ms
	AC Memory: 300KB
*/
#include <iostream>
#include <algorithm>
#define MAXN 300000
using namespace std;
bool a[MAXN+1] = {};
int main()
{
//	freopen("1.in","r",stdin);
//	freopen("1.out","w",stdout);
	int n, k;
	cin >> n >> k;
	for(int i = 0; i < n; i++)
		cin >> a[i];
	
	int ans = 0;
	int s = 0, t = 0;		
	int _beg = 0, _end = 0;	 
	while(1)
	{
		//Init interval
		while(_end < n)
		{
			if(!a[_end] && k == 0)
				break;
			else if(!a[_end] && k > 0)
				--k;
			++_end;
		}
		
		//break condition
		if(n - _beg < ans)
			break;

		//Compare and record answers
		if(ans < _end - _beg)
			s = _beg, t = _end, ans = _end - _beg;
		
		//push "begin" point
		if(!a[_beg])
			k++;
		++_beg;
	}
	
	//print answer
	cout << ans << endl;
	for(int i = s; i < t; i++)
		a[i] = 1;
	for(int i = 0; i < n; i++)
		cout << a[i] << " ";
	return 0;
}

D. 待阿伦日后更新○( ^皿^)っHiahia…

四、个人小结

整理下来,个人认为,重要的有以下两点:

  1. 尺取法可以运用在哪些题型上?
    个人认为尺取法可以运用的简单题型包括:求解某一区间的总和以及总和特征、元素种类或者元素共性等问题。
    其实同样也可以发现这一些题目都可以使用二分法去做。

  2. 尺取法的具体构思框架?

    Alun认为共四步:

    1. 必然存在起点与终点指针,并初始化
    2. 通过对终点指针的操作,得到符合条件的区间
    3. 对第二步的区间进行判定,检查是否真的符合条件,若不符合,退出;否则进行题意所需操作
    4. 起点位置进行移动(例如在例题中,采取+1),然后回到第二步操作

尺取法应该是属于比较基础的一个竞赛常用小技巧,据说很多大神虽然不认识尺取法,但是在编程中都有自己挖掘并运用到该方法(什么时候时候我要是能够成为大神就好)。

阿伦偶然在整理二分答案时发现尺取法,整理并加入自己思想写此Blog,以便自己拙笨遗忘,可日后翻阅。

发布了6 篇原创文章 · 获赞 5 · 访问量 953

猜你喜欢

转载自blog.csdn.net/weixin_36406616/article/details/89381176
今日推荐