title: 专题2 - 双指针、折半搜索
tags: [双指针, 搜索, 折半搜索]
categories: [OI, 算法专题]

双指针

也叫做 Two Pointers ,是一种很常见的基础算法思想。
指针一般代表着节点,确定一个指针,另一个指针存在单调性,移动的少了(通常复杂度从单个操作 \(O(n)\) ,总操作 \(O(n^2)\) )优化到总操作 \(O(n)\) 。

思考

双指针的精髓是确定一端,另外一端具有单调性。这样另外一端就不需要枚举很多无用的节点了。
这不一定在一维序列上进行,一些题目需要建模出来、

题目

P1102 A-B数对

原题 · 标签:#双指针

考点

双指针的入门题目

题意

给出一串正整数数列以及一个正整数 \(C\),要求计算出所有满足 \(A - B = C\) 的数对的个数(不同位置的数字一样的数对算不同的数对)。
把式子变为 \(A=B+C\) ,发现 \(A\) 和 \(B\) 有一个同向的变化关系。但是会有一个相等的情况,而我们要统计等的数量。

双指针做法

一个解决方法是去重,统计个数。下述代码的思路是再加一个指针,l和r表示这个区间,i表示 \(A\) 。
那就好写了,来看眼代码。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define REP(i,a,b) for(int i=(a);i<=(b);i++)
const int N = 200010;
int s[N];
int n,c;
int ans;
signed main(){
	cin >> n >> c;
	REP(i,1,n) cin >> s[i];
	sort(s+1,s+1+n);
	int l=1,r=1;
	REP(i,1,n){
		while(s[l]<s[i]-c && l<=n) l++;
		while(s[r]<=s[i]-c&& r<=n) r++;
		ans += r-l;
	}
	cout << ans;
	return 0;
}

P8708 蓝桥杯 2020 省 A1 整数小拼接

原题 · 标签:#双指针

考点

从基础的题目性质接触双指针的本质

题意

给定一个长度为 \(n\) 的数组 \(A_1,A_2,\cdots,A_n\)。你可以从中选出两个数 \(A_i\) 和 \(A_j\)(\(i\neq j\)),然后将 \(A_i\) 和 \(A_j\) 一前一后拼成一个新的整数。例如 12 和 345 可以拼成 12345 或 34512。注意交换 \(A_i\) 和 \(A_j\) 的顺序总是被视为 \(2\) 种拼法,即便是 \(A_i=A_j\) 时。

请你计算有多少种拼法满足拼出的整数小于等于 \(K\)。

双指针做法

首先将数组排序。
如果最后的数是 \(A\) + \(B\) ,循环 \(A\) 的位置i 。 \(B\) 的位置是j 。j是越来越小的。
怎么理解呢?
如果j所代表的数的位数没变,那么A变大了,数需要不变或变小,则 \(B\) 要变小。
如果j所代表的数的位数变了,位数变小,这个数自然就变小了。
从 \(1\) 到 \(j\) 就是答案了。
对了还有取等的边界,这个得额外注意。

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define ll long long
#define pii pair<int,int>
#define DEBUG 1
#if DEBUG
#define d(x) cout << #x << " = " << x << endl;
#define da(a,l,r) cout << #a << "[" << l << "-" << r << "]:"; f(i,l,r) cout << a[i] << " "; cout << endl;
#else
#define d(x)
#define da(a,l,r) 
#endif
const int N = 1e5+10;
ll n,k,p,ans;
int a[N];
ll connect(ll a,ll b){
	return stoll(to_string(a)+to_string(b));
}
signed main() {
	cin >> n >> k;
	f(i,1,n){
		cin >> a[i];
	}
	sort(a+1,a+1+n);
	p = n;
	f(i,1,n){
		while (p > 0 && connect(a[p],a[i]) > k) --p;
		ans += p,ans -= (p >= i);
	}
	cout << ans;
	return 0;
}

P8783 蓝桥杯 2022 省 B 统计子矩阵

原题 · 标签:#双指针 #前缀和

考点

从这道题学会建模出双指针

题意

给定一个 \(N \times M\) 的矩阵 \(A\),请你统计有多少个子矩阵 (最小 \(1 \times 1\), 最大 \(N \times M\))满足子矩阵中所有数的和不超过给定的整数 \(K\)。

双指针做法

子矩阵的和怎么做?二维前缀和!
循环左上角的点 \((i,j)\) ,设右下角的点为 \((l,r)\) ,循环 \(l\) ,\(r\) 是具备单调性的。
这就是双指针了,\(O(n^3)\) 的复杂度。

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define ll long long
#define pii pair<int,int>
#define DEBUG 1
#if DEBUG
#define d(x) cout << #x << " = " << x << endl;
#define da(a,l,r) cout << #a << "[" << l << "-" << r << "]:"; f(i,l,r) cout << a[i] << " "; cout << endl;
#else
#define d(x)
#define da(a,l,r) 
#endif
const int N = 5e2+10;
int a[N][N];
ll p[N][N],k;
int n,m;
ll ans;
ll getsum(int l1,int r1,int l2,int r2){
	return p[l2][r2] - p[l1-1][r2] - p[l2][r1-1] + p[l1-1][r1-1];
}
signed main() {
	cin >> n >> m >> k;
	f(i,1,n){
		f(j,1,m){
			cin >> a[i][j];
		}
	}
	f(i,1,n){
		f(j,1,m){
			p[i][j] = p[i-1][j] + p[i][j-1] - p[i-1][j-1] + a[i][j];
		}
	}
	f(i,1,n){
		f(j,1,m){
			for(int l=i,r=m;l<=n;l++){
				while (r >= j && getsum(i,j,l,r) > k) r--;
				ans += r - j + 1;
//				if (getsum(i,j,l,r) <= k) ans++;
			}
		}
	}
	cout << ans;
	return 0;
}

P4805 CCC 2016 合并饭团

原题 · 标签:#区间DP #双指针 #DP #DP优化

考点

双指针优化区间DP

题意

Alphonse 有 \(N\) 个美味的饭团,它们大小不一,摆放成一行。他想把最大的饭团让给自己的基友。他可以执行以下操作:

  • 如果两个相邻的饭团大小相同,Alphonse 可以把它们合并成一个新的饭团。新饭团的大小是两个原饭团大小之和。它将占据两个原饭团先前占据的位置。

  • 如果两个饭团大小相同,且它们之间只有一个饭团,Alphonse 也可以把它们合并成一个新的饭团。(中间的饭团大小没有规定。)新饭团的大小是三个原饭团大小之和,并占据三个原饭团先前的位置。

Alphonse 可以按照他的意愿执行任意次操作。

在执行 0 或更多次操作后,确定他应该把哪个饭团让给基友。

思路

饭团的变化不会导致区间的值造成变化,所以 \(dp[i][j]\) 代表从 \(i\) 到 \(j\) 的区间可合并。
变化1很好写,在区间内再枚举一个 \(k\) 就可以,很显然的区间DP。
变化2怎么办呢?
设区间\([i,j]\) 是由\([i,l] [l+1,j] [r+1,j]\) 。将 \(l\) 和 \(r\) 优化掉一个。发现 \(l\) 增加的时候 \(r\) 会减少,双指针。
如此建模。

代码

#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define ll long long
#define DEBUG 1
#if DEBUG
#define d(x) cout << #x << " = " << x << endl;
#define da(a,l,r) cout << #a << "[" << l << "-" << r << "]:"; rep(i,l,r) cout << a[i] << " "; cout << endl;
#define dnl() cout << "\n";
#define dd() cout << "debug:";
#else
#define d(x) 
#define da(a,l,r) 
#define dnl() 
#define dd() 
#endif
const int N = 1e5+10;
int n,m;
int a[N];
int ans;
signed main() {
	cin >> n >> m;
	m *= 2;
	f(i,1,n) cin >> a[i];
	int i = 1,j = 0;
	int sum = 0;
	while (i <= n && j <= n){
		while (sum < m && j < n){
			j++;
			sum += a[j];
		}
		ans = max(j-i+1,ans);
		sum -= a[i];
        i++;
	}
	cout << ans;
	return 0;
}

P8775 蓝桥杯 2022 省 A 青蛙过河

原题 · 标签:#双指针 #思维

考点

思维推导用双指针解决推导后的问题

题意

小青蛙住在一条河边,它想到河对岸的学校去学习。小青蛙打算经过河里的石头跳到对岸。

河里的石头排成了一条直线,小青蛙每次跳跃必须落在一块石头或者岸上。不过,每块石头有一个高度,每次小青蛙从一块石头起跳,这块石头的高度就会下降 \(1\),当石头的高度下降到 \(0\) 时小青蛙不能再跳到这块石头上(某次跳跃后使石头高度下降到 \(0\) 是允许的)。

小青蛙一共需要去学校上 \(x\) 天课,所以它需要往返 \(x\) 次,即过河 \(2x\) 次。当小青蛙具有一个跳跃能力 \(y\) 时,它能跳不超过 \(y\) 的距离。

请问小青蛙的跳跃能力至少是多少才能用这些石头上完 \(x\) 次课。

思路

题解说的太明白了:

容易发现,青蛙从左跳到右和从右到左没有区别,因此相当于青蛙从左到右跳 \(2x\) 次。

通过几个样例,可以发现这样一个规律:对于一个跳跃能力 \(y\),青蛙能跳过河 \(2x\) 次,当且仅当对于每个长度为 \(y\) 的区间,这个区间内 \(h\) 的和都大于等于 \(2x\)。

代码

#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define ll long long
#define DEBUG 1
#if DEBUG
#define d(x) cout << #x << " = " << x << endl;
#define da(a,l,r) cout << #a << "[" << l << "-" << r << "]:"; rep(i,l,r) cout << a[i] << " "; cout << endl;
#define dnl() cout << "\n";
#define dd() cout << "debug:";
#else
#define d(x) 
#define da(a,l,r) 
#define dnl() 
#define dd() 
#endif
const int N = 1e5+10;
int n,m;
int a[N];
int ans;
signed main() {
	cin >> n >> m;
	m *= 2;
	f(i,1,n) cin >> a[i];
	int i = 1,j = 0;
	int sum = 0;
	while (i <= n && j <= n){
		while (sum < m && j < n){
			j++;
			sum += a[j];
		}
		ans = max(j-i+1,ans);
		sum -= a[i];
        i++;
	}
	cout << ans;
	return 0;
}

TODO

P3089 USACO13NOV Pogo-Cow S

原题 · 标签:#DP

考点

太累了以后再整

折半搜索

在搜索的整体深度已经确定的情况下,我们可以从头和尾分别搜,再将答案合并,以降低复杂度。

题目

P4799 CEOI 2015 世界冰球锦标赛 (Day2)

原题 · 标签:#搜索 #折半搜索 #状压

考点

折半搜索的入门题目

题意

译自 CEOI2015 Day2 T1「Ice Hockey World Championship」

今年的世界冰球锦标赛在捷克举行。Bobek 已经抵达布拉格,他不是任何团队的粉丝,也没有时间观念。他只是单纯的想去看几场比赛。如果他有足够的钱,他会去看所有的比赛。不幸的是,他的财产十分有限,他决定把所有财产都用来买门票。

给出 Bobek 的预算和每场比赛的票价,试求:如果总票价不超过预算,他有多少种观赛方案。如果存在以其中一种方案观看某场比赛而另一种方案不观看,则认为这两种方案不同。

思路

整体的序列长度是确定的,也就是搜索的深度是确定的。
那我们可以分开搜前一半和后一半,再通过一个map来将答案合并。
本题中搜索也只是枚举选( \(1\) )与不选( \(0\) )这两种状态,所以用二进制状压实现还是更简单一点。

代码

#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define int long long
#define DEBUG 0
#if DEBUG
#define d(x) cout << #x << " = " << x << endl;
#define da(a,l,r) cout << #a << "[" << l << "-" << r << "]:"; rep(i,l,r) cout << a[i] << " "; cout << endl;
#define dnl() cout << "\n";
#define dd() cout << "debug:";
#else
#define d(x) 
#define da(a,l,r) 
#define dnl() 
#define dd() 
#endif
const int N = 41;
int a[N];
int m;
int n;
vector <int> v;
int getw(int st,int w){
	return (st >> w) & 1;
}
int ans;
signed main() {
	cin >> n >> m;
	rep(i,1,n){
		cin >> a[i];
	}
	rep(st,0,((1 << (n/2))-1)){
		int res = 0;
		rep(i,0,(n/2)-1){
			if (getw(st,i)){
				res += a[i+1];
			}
		}
		if (res <= m){
			d(res);
			v.push_back(res);
		}
	}
	sort(v.begin(),v.end());
	rep(st,0,((1 << (n-(n/2)))-1)){
		int res = 0;
		rep(i,0,(n-(n/2))-1){
			if (getw(st,i)){
				res += a[i + 1 + n/2];
			}
		}
		d(res);
		ans += upper_bound(v.begin(),v.end(),m-res) - v.begin();
		d(ans);
		d(upper_bound(v.begin(),v.end(),m-res) - v.begin());
	}
	cout << ans;
	return 0;
}