尺取法
1、算法概述
尺取法简单来说是一种利用双指针遍历获取满足条件的区间的算法。其为一种线性算法。记(l, r)
为一个序列内以l
为起点的最短合法区间,如果有r
随l
的增大而增大,使用尺取法。具体做法就是不断枚举l
,同时求出r
,由于r
随l
增大而增大,所以r
只有 n 次变化机会,所以时间复杂度为O(n)
。
尺取法需要满足的条件: 区间权值大小满足随区间长度单调变化,即区间越长区间权值越小(或越大)。
尺取法的优点: 不会去枚举到一定不满足条件的区间。
2、算法模板
for(int l = 1; l <= n; ++l)
{
while(r < n && sum < s)
{
++r;
sum += arr[r];
}
if(sum >= s && r - l + 1 <= n)
{
ans = min(ans, r - 1 + 1);
}
sum -= arr[l];
}
说明: 首先枚举l
,就是相当于给所有状态按左端点分类,去枚举以l
为起点的第一个满足条件的区间。确定l
后,++r
就相当于剪枝一定不满足条件的区间,在此过程中维护区间和sum
,当我们发现终于枚举到第一个满足条件的区间时,去统计一下区间长度最小值。sum -= arr[l]
逻辑很关键,它利用了尺取法的单调性,sum
减去后,下一轮枚举新的l
,这样l
和r
是以新的l
为起点的最接近满足条件的区间,这样省得去枚举以新l
为起点,右端点为r
之前得一定不满足条件得区间。
3、算法示例
1、找到指定和的整数对
- 问题描述
输入n(n <= 100000)
个整数,放在数组arr
中,找出其中的两个数,它们的和等于整数m
(假设肯定有解)。
- 输入
21 4 5 6 13 65 32 9 23
28
- 输出
5 23
- 说明
输入第一行是n
个整数,第二行是m = 28
,输出5 + 23 = 28
。
题解
该题有多种解法:
1)两重循环暴力枚举,复杂度为O(n^2)
,超时。
2)二分法,首先对数组从小到大排序,复杂度O(nlogn)
,然后从头到尾处理数组中的每个元素,在大于arr[i]
的数中使用二分查找一个等于m - arr[i]
的数,复杂度O(nlogn)
,总复杂度仍然为O(nlogn)
。
3)哈希表,分配一个哈希表,把n
个数放进去,逐个检查arr
中的n
个数,检查m - arr[i]
在哈希表中是否有值,复杂度O(n)
,空间换时间,复杂度最低。
4)尺取法,首先对数组从小到大排序,然后,设置两个变量i
和j
,分别指向头和尾,i
初值是 0,j
初值是n - 1
,然后让i
和j
逐渐向中间移动,检查arr[i] + arr[j]
,如果大于m
,就让j
减 1,如果小于m
,就让i
加 1,直至arr[i] + arr[j] = m
,排序复杂度O(nlogn)
,检查的复杂度O(n)
,合起来总复杂度O(nlogn)
。
void FindSum(int *arr, int n, int m)
{
if(arr == nullptr)
{
return;
}
sort(arr, arr + n - 1); // 排序
int i = 0;
int j = n - 1;
while(i < j)
{
int sum = arr[i] + arr[j];
if(sum > m)
{
--j;
}
if(sum < m)
{
++i;
}
if(sum == m)
{
std::cout << arr[i] << " " << arr[j] << "\n";
++i;
}
}
}
说明
在这个题目中,尺取法不仅效率高,而且不需要额外的空间。把题目的条件改变一下,可以变化为类似的问题,例如:判断一个数是否为两个数的平方和。其实也能用同向扫描来做。
2、子序列
- 问题描述
有一个长度为 N 的正整数序列(10 < N < 100000),每个数字都小于等于 10000,再给定一个正整数 S(S < 100000000),试求一个连续子序列,使得该序列的数字之和大于或等于 S,并要求该子序列尽量短。
- 输入
输入多组数据,每组数据第一行为两个正整数 N 和 S,中间用空格隔开。第二行给出这个序列,每两个整数之间用空格隔开。输入文件以 EOF 结尾。当这个连续子序列不存在时输出 0。
- 输出
对于每组数据,输出一个整数,代表满足要求的最短子序列长度,每组数据输出占一行。
题解
const int N = 500007;
const int M = 510007;
const int INF = 0x3f3f3f3f;
int n = 0;
int S = 0;
int arr[N] = {0};
int Solve()
{
int res = INF;
int s = 1;
int t = 1;
int sum = 0;
while(1)
{
while(t <= n && sum < S)
{
sum += arr[t++];
}
if(sum < S)
{
break;
}
res = min(res, t - s);
sum -= arr[s++];
}
if(res == INF)
{
res = 0;
}
return res;
}
int main()
{
int t = 0;
std::cin >> t;
while(t--)
{
std::cin >> n >> S;
for(int i = 1; i <= n; ++i)
{
std::cin >> arr[i];
}
std::cout << Solve() << "\n";
}
return 0;
}