尺取法

目录
  • 尺取法
    • 1. 算法分析
    • 2. 板子
      • 2.1 一维尺取
    • 3. 例题

尺取法

1. 算法分析

尺取法: 尺取法通常是对数组保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。尺取法通常可以把一个O(n^2^)的算法利用特殊性质优化到O(n)的复杂度。
for example:
给定一个数列a[n]和一个S, 要求找出连续的一段区间,使得区间和大于等于S,打印这个区间长度的最小值。

  • O(n^2^)算法:前缀和预处理,然后O(n^2^)枚举l和r,每次O(1)计算sum[r] - sum[l]=K的值,然后取得K >= S且长度最小的那个。
  • 尺取优化:数学性质:对于l1, r1 和l2, r2,如果l1 < l2, 那么当sum[r2] - sum[l2] >= S,则sum[r1] - sum[l1] >= S且r1 <= r2。由这个性质可以知道当l单调增加时,r也是单调不减的。因此利用这个可以优化算法。具体处理:l=1,r=1,然后不断将r向右移动,直到sum[r] - sum[l] >= S。然后右移l同时满足sum[r] - sum[l] >= S。更新一次区间长度。不断重复这个过程移动r和l,不断更新区间长度。

尺取流程
n = 5, S = 11
a[i]: 1 2 3 4 5
一开始:l = 1, r = 1, sum = 0
然后r不断右移:r = 5, sum = 15
右移l:l = 2, sum=15-1=14>=S
继续左移l直到:l = 3, sum = 12
此时找到最小区间长度为2。

2. 板子

2.1 一维尺取

#include 
#include 

using namespace std;

int const N = 1e5 + 10, INF = 1e9 + 10;
int a[N], n, S, T;

int main() {
    cin >> T;
    while (T--) {
        cin >> n >> S;
        for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
        
        int l = 1, r = 1, sum = 0, res = INF;  // 初始化左、右边界、区间和
        while (1) {
            while (r <= n && sum < S) sum += a[r++];  // 计算区间和
            if (sum < S) break;  // 如果小于S说明找不到>=S的情况,直接break
            res = min(res, r - l);  // 更新区间
            sum -= a[l++];  // 右移l
        }
        if (res == INF) res = 0;
        cout << res << endl;
    }
    return 0;
}

3. 例题

Poj3061 Subsequence
题意: 给定一个数列a[n]和一个S, 要求找出连续的一段区间,使得区间和大于等于S,打印这个区间长度的最小值。
题解: 尺取模板题,具体见代码,思路见1.1
代码:

#include 
#include 

using namespace std;

int const N = 1e5 + 10, INF = 1e9 + 10;
int a[N], n, S, T;

int main() {
    cin >> T;
    while (T--) {
        cin >> n >> S;
        for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
        
        int l = 1, r = 1, sum = 0, res = INF;  // 初始化左、右边界、区间和
        while (1) {
            while (r <= n && sum < S) sum += a[r++];  // 计算区间和
            if (sum < S) break;  // 如果小于S说明找不到>=S的情况,直接break
            res = min(res, r - l);  // 更新区间
            sum -= a[l++];  // 右移l
        }
        if (res == INF) res = 0;
        cout << res << endl;
    }
    return 0;
}

poj3320Jessica's Reading Problem
题意: 给定一个n,然后给定n个数字(可能重复),要求取得最小的连续区间,使得能够取到这n个数字中所有的数字。n <= 10^6^
题解: 和上题类似,先while循环判断是否已经取到所有出现的数字,然后收缩左端点。尺取法处理即可。

#include 
#include 
#include 
#include 

using namespace std;

int const N = 1e6 + 10, INF = 1e9 + 10;
set s;
int a[N], n;
map cnt;

int main() {
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        s.insert(a[i]);
    }
    int l = 1, r = 1, res = INF, sum = 0, S = s.size();  // S为需要达到的数目,sum记录当前总的数目
    while (1) {
        while (r <= n && sum < S) {
            if (cnt[a[r++]]++ == 0) sum++;  // 只有cnt中没出现过才能使得sum++
        }
        if (sum < S) break;
        res = min(res, r - l);  // 更新
        if (--cnt[a[l++]] == 0) sum--;  // l右移使得cnt[a[l]]为时才需要sum--
    }
    cout << res << endl;
return 0;
}

你可能感兴趣的:(尺取法)