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