1. 程式人生 > 其它 >尺取法

尺取法

技術標籤:ACM--基礎演算法

文章目錄

尺取法

1. 演算法分析

尺取法: 尺取法通常是對陣列儲存一對下標,即所選取的區間的左右端點,然後根據實際情況不斷地推進區間左右端點以得出答案。尺取法通常可以把一個O(n2)的演算法利用特殊性質優化到O(n)的複雜度。之所以能夠使用尺取法,是因為元素的字首和具有單調性,才能使得雙指標不會回退。
for example:
給定一個數列a[n]和一個S, 要求找出連續的一段區間,使得區間和大於等於S,列印這個區間長度的最小值。

  • O(n2
    )演算法:字首和預處理,然後O(n2)列舉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 <iostream>
#include <cstdio>

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; }

2.2 二維尺取

#include <bits/stdc++.h>

using namespace std;
int const N = 305, INF = 1e9 + 7;

int r, c, kk, sum[N][N];
char mp[N][N];

int main() {
    while (scanf("%d%d%d", &r, &c, &kk), r) {
        memset(sum, 0, sizeof sum); 
        // 讀入元素,二維字首和維護點的數目
        for (int i = 1; i <= r; i++)
            for (int j = 1; j <= c; j++) {
                cin >> mp[i][j];
                if (mp[i][j] == 'X')
                    sum[i][j] = sum[i][j - 1];
                else
                    sum[i][j] = sum[i][j - 1] + 1;  //現在行上統計
            }

        for (int i = 1; i <= r; i++)
            for (int j = 1; j <= c; j++)
                sum[i][j] += sum[i - 1][j];  //再在列上統計

        // 二維尺取
        int ans = INF;
        for (int i = 1; i <= c; i++)  // i和j列舉矩形的左右邊界
            for (int j = i; j <= c; j++) {      
                int p = 1;                      
                for (int k = 1; k <= r; k++) {   // 尺取上下邊界
                    while (sum[k][j] - sum[k][i - 1] - sum[p - 1][j] + sum[p - 1][i - 1] >= kk) {
                        ans = min(ans, (j - i + 1) * (k - p + 1));
                        p++;
                    }
                }
            }
        printf("%d\n", ans);
    }
}

3. 例題

3.1 一維尺取

Poj3061 Subsequence
題意: 給定一個數列a[n]和一個S, 要求找出連續的一段區間,使得區間和大於等於S,列印這個區間長度的最小值。
題解: 尺取模板題,具體見程式碼,思路見1.1
程式碼:

#include <iostream>
#include <cstdio>

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 <= 106
題解: 和上題類似,先while迴圈判斷是否已經取到所有出現的數字,然後收縮左端點。尺取法處理即可。

#include <iostream>
#include <cstdio>
#include <set>
#include <map>

using namespace std;

int const N = 1e6 + 10, INF = 1e9 + 10;
set<int> s;
int a[N], n;
map<int, int> 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;
}

3.2 二維尺取

hdu1937 Finding Seats

題意:

給定一個R*C的矩陣,選擇一個面積最小的子矩陣,使得其內部‘.’的個數>=k。 1 < = R , C < = 300 1<=R, C <=300 1<=R,C<=300

…XX
.X.XX
XX…
5 6 6
…X.X.
.XXX…
.XX.X.
.XXX.X
.XX.XX

題解: 二維字首和模板,列舉矩形的上下邊界,然後尺取法處理左右邊界。

程式碼:

#include <bits/stdc++.h>

using namespace std;
int const N = 305, INF = 1e9 + 7;

int r, c, kk, sum[N][N];
char mp[N][N];

int main() {
    while (scanf("%d%d%d", &r, &c, &kk), r) {
        memset(sum, 0, sizeof sum); 
        // 讀入元素,二維字首和維護點的數目
        for (int i = 1; i <= r; i++)
            for (int j = 1; j <= c; j++) {
                cin >> mp[i][j];
                if (mp[i][j] == 'X')
                    sum[i][j] = sum[i][j - 1];
                else
                    sum[i][j] = sum[i][j - 1] + 1;  //現在行上統計
            }

        for (int i = 1; i <= r; i++)
            for (int j = 1; j <= c; j++)
                sum[i][j] += sum[i - 1][j];  //再在列上統計

        // 二維尺取
        int ans = INF;
        for (int i = 1; i <= c; i++)  // i和j列舉矩形的左右邊界
            for (int j = i; j <= c; j++) {      
                int p = 1;                      
                for (int k = 1; k <= r; k++) {   // 尺取上下邊界
                    while (sum[k][j] - sum[k][i - 1] - sum[p - 1][j] + sum[p - 1][i - 1] >= kk) {
                        ans = min(ans, (j - i + 1) * (k - p + 1));
                        p++;
                    }
                }
            }
        printf("%d\n", ans);
    }
}