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;
}
浙公网安备 33010602011771号