1. 程式人生 > 其它 >尺取法學習筆記

尺取法學習筆記

什麼是尺取法

Codeforces 中顯示它的演算法名稱叫做 "two pointers"

直譯成中文的話叫雙指標法

這個演算法 hin 有意思,由於在某些巨佬眼中過於簡單,以至於都沒把尺取法當成一個演算法

如何進行尺取法

尺取法的思想是維護兩個指標 \(l,r\)​ ,分別為左端點與右端點,每當確定左端點時,嘗試將右端點一直移動,直到不滿足條件為止

,讓我們來看這一道例題

P1638 逛畫展

求剛好有 \(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;
}

AT4142 [ARC098B] Xor Sum 2

求有多少個區間 \([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;
}