尺取法小结

尺取法也叫双指针。

应用尺取法很重要的一个地方就是满足区间单调性。

什么叫区间单调性呢?

比如假设都是正数的数组,你选定了一段[L, R],当R变大时,这一段的区间和会不断增大,而当L增大时,区间和会不断减小,这就是区间单调性了。

还有就是出现 >= k个不同字符的区间,当R变大时,区间中的字符个数会变大或者不变,而当L增大时,区间中的字符个数会减小或者不变

这两个例子是不是能感受到一点尺取法的应用条件了呢。

大部分时间尺取法都会跟算贡献结合在一起(所以算贡献真的很重要啊喂)

试过来,好像是这种写法尺取的写法会快点,我想了一下可能是因为if 比 while 快吧。或者是因为if使得这个程序更快的跳出了

按理来说不会那么卡复杂度,所以还是挑选自己更好理解的方式来写。

(BTW,前++与前--会比后++与后--快哦~,也就几十ms(逃

while (1) {
    if (右边界不满足条件 && 不超过上界) {
        右界变动,计算右界改变对此时状态的影响,
    }
    if (退出条件) break;
    else if (满足条件) {
       计算贡献,左界变动,计算左界变动对此状态的影响
    }
}

现在就放几道题目来感受一下吧。

POJ - 2100

给出一个数n。你要求一段连续的数,这些数的平方和等于n。

/*poj headers*/
#include <map>
#include <set>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;

int main() {
    ll n;
    while (~scanf("%lld", &n)) {
        ll L = 0, R = 0, sum = 0;
        vector<pair<ll, ll> > p;
        while (1) {
            if (sum < n) {
                ++ R;
                sum += R*R;
            }
            if (R * R > n) break;
            else if (sum >= n) {
                if (sum == n) p.push_back({L+1, R});
                ++ L;
                sum -= L * L;
            }
        }
        printf("%d
", p.size());
        for(ll i = 0;i < p.size();i++)
        {
            printf("%lld",p[i].second-p[i].first+1);
            for(ll j = p[i].first;j <= p[i].second;j++)
            {
                printf(" %lld", j);
            }
            puts("");
        }

    }
    return 0;
}
View Code

POJ - 2739

现在给你一个整数 n,你要告诉他这个数字有多少种不同的连续素数和表示方式。

#include <map>
#include <set>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;
int tot, n;
int prime[maxn];

void init() {
    prime[++tot] = 2, prime[++tot] = 3;
    for (int i = 4; i <= 10000; ++ i) {
        int flag = 0;
        for (int j = 2; j*j <= i && !flag; ++ j)
            if (i % j == 0) flag = 1;
        if (!flag) prime[++tot] = i;
    }
}

int main() {
    init();
    while (~scanf("%d", &n), n) {
        int L = 0, R = 0, sum = 0;
        int cnt = 0;
        while (1) {
            if (sum < n) {
                ++ R;
                sum += prime[R];
            }
            if (R > tot) break;
            else if (sum >= n) {
                if (sum == n) ++ cnt;
                ++ L;
                sum -= prime[L];
            }
        }
        printf("%d
", cnt);

    }
    return 0;
}
View Code

HDU - 5672

给你一串字符,问你至少出现k个不同数字的区间。

这里用到了算贡献,问你至少为k,我们知道当[L, R]的区间满足==k这个条件时,

那么[L, R+1] , [L, R+2], [L, R+3] ....[L, n]的区间肯定都是满足条件的。所以就是n - R + 1。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

int main() {
    int T; scanf("%d", &T);
    while (T--) {
        char str[1000000+10];
        scanf("%s", str+1);
        int n = strlen(str+1);
        unordered_map<char, int> mp;
        int k; scanf("%d", &k);
        int L = 1, R = 0, cnt = 0;
        ll ans = 0;
        while (1) {
            if (cnt < k) {
                ++ R;
                ++ mp[str[R]];
                if(mp[str[R]] == 1) ++ cnt;
            }
            if (R > n) break;
            else if(cnt==k)  {
                ans = ans+n-R+1;
                -- mp[str[L]];
                if(mp[str[L]]==0) -- cnt;
                ++ L;
            }
        }
        printf("%lld
", ans);
    }
    return 0;
}
View Code

HDU - 5178

给你一个数组,问你有多少对a,b满足|x[b] - x[a]| <= k (a < b)

这题二分也可以做,lower_bound码量会小一点,但是时间会大比双指针多个log(也没小多少。

我们发现绝对值这个条件,其实是为了取消(a < b)这个条件的。

因为如果 |x[b] - x[a] | <= k,但是a > b 的话,其实把a,b交换就好了。所以我们可以把题目要求变得严格一点。

sort一遍变成, x[b] - x[a] <= k, a < b,这样就会非常好做了。剩下的就是双指针的的事了。

(sort一遍取消绝对值其实是非常常见的操作,当然如果没遇到过应该也能想到吧)

#include <map>
#include <set>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;
ll val[maxn];

int main() {
    int T; scanf("%d", &T);
    while (T--) {
        ll n, k;
        scanf("%lld%lld", &n, &k);
        for (int i = 1; i <= n; ++ i) {
            scanf("%lld", &val[i]);
        }
        sort(val+1, val+1+n);
        ll L = 1, R = 2, ans = 0;
        while (1) {
            if (R <= n && val[R]-val[L] <= k) ++ R;
            if (L == n+1) break;
            if (R == n+1 || val[R]-val[L] > k) {
                ans += (R-L-1);
                ++ L;
            }
        }
        printf("%lld
", ans);

    }
    return 0;
}
View Code

HDU - 5358

 题意非常的简单,就是求(所有(区间和取log)+1)*(区间端点和))的和。但是却很难想。

首先我们发现ai >= 0 所以前缀和是满足区间加法的。

观察发现其实这个式子就是位数。之后观察范围发现位数最大也就35位(10^10)

那么就找到了突破点了。枚举位数,之后用双指针确定范围即可。

这里具体讲讲双指针怎么做好了。因为求得是一段区间,之后区间的端点其实是不固定的。那么我们其实就可以枚举左端点,

之后通过双指针找使得sum[L] - sum[i-1] (前缀和相减就是区间和,所以这里是[i, L] )>= 当前枚举位数,R也是同理。

之后计算贡献即可。

这道题不是很好写成上面的双指针格式所以就没写。

#include <map>
#include <set>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;
ll val[maxn], dic[36], sum[maxn];

int main() {
    int T; scanf("%d", &T);
    dic[0] = 0, dic[1] = 2;
    for (int i = 2; i <= 35; ++ i)
        dic[i] = (dic[i-1] << 1);
    while (T--) {
        int n; scanf("%d", &n);
        for (int i = 1; i <= n; ++ i) {
            scanf("%lld", &val[i]);
            sum[i] = sum[i-1] + val[i];
        }
        ll ans = 0;
        for (int bit = 1; bit <= 35; ++ bit) {
            ll L = 1, R = 1;
            for (int i = 1; i <= n; ++ i) {
                L = max(1ll*i, L);
                while (L <= n && sum[L] - sum[i-1] < dic[bit-1]) ++ L;
                while (R <= n && sum[R] - sum[i-1] < dic[bit]) ++ R;
                if (L < R) ans += bit*(1ll*i*(R-L) + (R-L)*(R+L-1)/2);
            }
        }
        printf("%lld
", ans);
    }
    return 0;
}
View Code

HDU - 1937

找一个面积最小的矩阵,使得矩阵里的" . "的数量 >= k

二维前缀和预处理, 之后枚举矩阵的上界行与下界行,再通过双指针来寻找最小答案。

这里写成上面if格式会更慢。我想了一下,可能是因为返回前缀和常数比较大。

#include <map>
#include <set>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 3e2+10;
int sum[N][N];
int r, c, k;
int _range(int x1, int y1, int x2, int y2) {
    return sum[x2][y2]-sum[x1-1][y2]-sum[x2][y1-1]+sum[x1-1][y1-1];
}
int main() {
    while (~scanf("%d%d%d", &r, &c, &k)) {
        if (r == 0 && c == 0 && k == 0) break;
        char str[N];
        for (int i = 1; i <= r; ++ i) {
            scanf("%s", str+1);
            for (int j = 1; j <= c; ++ j) {
                sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + (str[j]=='.');
            }
        }
        int ans = r*c;
        for (int i = 1; i <= r; ++ i) {
            for (int j = i; j <= r; ++ j) {
                int L = 1, R = 1;
                while (1) {
                    while (R < c && _range(i, L, j, R) < k) ++ R;
                    if (_range(i, L, j, R) < k) break;
                    if (_range(i, L, j, R) >= k) ans = min(ans, (j-i+1)*(R-L+1));
                    ++ L;
                }
            }
        }
        printf("%d
", ans);
    }

    return 0;
}
View Code

双指针拓展:

HDU - 6231

求区间M大数组成的数组中,第K大的数。

原文地址:https://www.cnblogs.com/Vikyanite/p/13963324.html