尺取法學習筆記
什麼是尺取法
Codeforces 中顯示它的演算法名稱叫做 "two pointers"
直譯成中文的話叫雙指標法
這個演算法 hin 有意思,由於在某些巨佬眼中過於簡單,以至於都沒把尺取法當成一個演算法
如何進行尺取法
尺取法的思想是維護兩個指標 \(l,r\) ,分別為左端點與右端點,每當確定左端點時,嘗試將右端點一直移動,直到不滿足條件為止
,讓我們來看這一道例題
求剛好有 \(m\) 種數字的最短區間
我們發現,當這個區間的左端點向右移動時,右端點一定不會向左移動,所以我們在列舉 \(r\) 時,不需要從 \(l\) 開始列舉,我們可以從上一次列舉到的 \(r\)
我們記錄一個 \(cnt\) 陣列, \(cnt_i\) 表示第 \(i\) 種數字在 \(l \sim r-1\) 這個區間內的出現次數
用一個變數 \(sum\) 來表示這個區間不同畫的數量
維護一下,即可AC
具體細節見程式碼:
#include <cstdio> using namespace std; const int N=1e6+7,MAX=2e3+7; int a[N]; int cnt[MAX]; int n,m; int ansl,ansr=N; // 初始化區間長度為最長 signed main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) scanf("%d",a+i); for(int l=1,r=1,sum=0;l<=n;) { while(r<=n && sum<m) { // 判斷是否滿足條件 ++cnt[a[r]]; // 數量+1 if(cnt[a[r]]==1) // 如果數量+1後只有一個,說明出現了新的數,++sum ++sum; ++r; // 移動右端點指標 } // 這裡的右端點指標指向的是最長能向右擴充套件的位置的下一位 // 所以區間長度是(r-1)-l+1=r-l // 而不是r-l+1 if(sum==m && r-l<ansr-ansl) ansl=l,ansr=r; // 如果有更短的滿足題設的區間,更新答案 --cnt[a[l]]; // 移動左端點指標 if(!cnt[a[l]]) // 如果移動後這個數的數量為0,說明減少了一個數,--sum --sum; ++l; } printf("%d %d",ansl,ansr-1); // 根據右端點的定義,輸出時右端點要-1 return 0; }
習題:
UVA11572 唯一的雪花 Unique Snowflakes
求沒有重複數字的最長區間
與第一題思路類似,由於值域範圍較大,我使用的是 set 維護
#include <set> #include <cstdio> using namespace std; const int inf=0x3f3f3f3f; const int N=1e6+7; set<int> s; int a[N]; int T,n,ans; signed main() { scanf("%d",&T); for(;T;--T) { scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",a+i); s.clear(); // 清空 ans=-inf; for(int l=1,r=1;l<=n;) { while(s.find(a[r])==s.end() && r<=n) s.insert(a[r]),++r; // 移動右端點指標 ans=max(ans,r-l); // 計算答案 s.erase(a[l]); ++l; // 移動左端點 } printf("%d\n",ans); } return 0; }
求有多少個區間 \([l,r]\) ,滿足 \(a_l \ xor \ a_{l+1} \ xor \dots xor \ a_r = a_l + a_{l+1} + ... +a_r\)
注意到一個性質,當 \(a \ and \ b=0\) 時,\(a \ xor \ b=a+b\)
那麼 \(l \sim r\) 所有數的異或值等於所有數的和,必須要每一個二進位制位上該區間所有數加起來最多隻有一個 \(1\) 才行.
直接用尺取法做,記得開 long long
#include <cstdio>
typedef long long ll;
using namespace std;
const int N=2e5+7;
int a[N];
ll ans;
int n;
signed main() {
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d",a+i);
for(int l=1,r=1,tmp=0;r<=n;) {
while(!(tmp&a[r]) && r<=n) { // tmp&a[r]!=0,則tmp^a[r]!=tmp+a[r]
tmp+=a[r],++r; // 移動右端點指標
ans+=r-l; // 統計答案
}
tmp^=a[l],++l; // 移動左端點指標
}
printf("%lld",ans);
return 0;
}
P3143 [USACO16OPEN]Diamond Collector S
求最長的兩端不相交的區間,每個區間的極差不大於 \(k\)
本題我們可以用尺取法做
要求區間互不相交,我們統計答案就要變成左端點前的最大區間+當前區間
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=5e4+7;
int a[N];
int c[N];
int k;
int n,ans;
signed main() {
scanf("%d%d",&n,&k);
for(ll i=1;i<=n;++i)
scanf("%d",a+i);
sort(a+1,a+1+n); // 因為放置與順序無關,所以我們可以先排序,使得相鄰兩數之差變小
for(int l=1,r=2,maxx=-1;l<=n;) {
while(a[r]-a[l]<=k && r<=n)
++r; // 移動右端點指標
c[r]=max(c[r],r-l); // 記錄以r為右端點向左可以擴充套件的最大區間
maxx=max(maxx,c[l]); // 更新之前的最長區間
ans=max(ans,maxx+(r-l)); // 更新答案為之前的最長區間+當前區間
++l;
}
printf("%d",ans);
return 0;
}