双指针 & 双向搜索
双指针
根据人类直觉这个东西需要满足单调性,所以预处理的时候大概率需要排序。
好像常与二分结合使用?
可以用在序列、链表(存储位置)或者树、图上(存储结点)。
或者用于其他算法(eg:单调队列、差分),还有主播没学过的莫队。
正题
顾名思义双指针是两个指针,通常是外层一个内层一个(依靠相对移动去维护区间信息,从而满足题意),写个伪代码:
int j = () ;
for (i : a -> b) {
while (j ...) {
...
}
}
可以看出其实并不是指针,只是用下标实现了类似指针的功能。
根据伪代码不难发现,\(j\) 并不会每次都更新,手模一下发现复杂度 \(\mathcal{O}(n)\),很好。
举个栗子:
给定一个升序排列的数组,请从中找出两个数使得他们的和满足目标数 \(t\)。
如果二分,显然复杂度 \(\mathcal{O}(n\log n)\),与双指针比较显然更劣,说明这个玩意还是有用的。
板题没写过,懒得放代码。
题目
洛谷 P3143
奶牛 Bessie 很喜欢闪亮亮的东西(Baling~ Baling~),所以她喜欢在她的空余时间开采钻石!她现在已经收集了 \(n\) 颗不同大小的钻石(\(n\le50000\)),现在她想在谷仓的两个陈列架上摆放一些钻石。
Bessie 想让这些陈列架上的钻石保持相似的大小,所以她不会把两个大小相差 \(k\) 以上的钻石同时放在一个陈列架上(如果两颗钻石的大小差值不大于 \(k\),那么它们可以同时放在一个陈列架上)。现在给出 \(k\),请你帮 Bessie 确定她最多一共可以放多少颗钻石在这两个陈列架上。(样例就不放了)
思路
显然需要先将原数组排序使其满足单调性(不然怎么算),考虑枚举断点 & 端点,假设以 \(i\) 为断点,设 \(ansl_i\) 表示以 \(i\) 为右端点时区间 \([1,i]\) 中最多能放多少钻石,\(ansr_i\) 表示以 \(i\) 为左端点时区间 \([i,r]\) 中最多能放多少(dp ?)。
这时候双指针的用处就来了,我们以每个钻石为断点,通过维护一个指针 \(t\),去求满足 \(a_i - a_t\le k\) 或者 \(a_t - a_i\le k\) 的最小或者最大的 \(t\)(这样可以保证答案最优)。
如何更新?假如当前不符合要求,我们就将 \(t\) 指针向左或向右移即可,因为原数组满足单调性。
最后答案显然是 \(\max\{ansl_i+ansr_{i+1}|i\in [1,n]\}\)。
放上代码:
#define f(i ,m ,n ,x) for (int i = (m) ;i <= (n) ;i += (x))
sort (a + 1 ,a + n + 1) ;
ansl[1] = 1 ;
int l = 1 ;
f (i ,2 ,n ,1) {
while (l <= i && a[i] - a[l] > k) l ++ ;
ansl[i] = max (ansl[i - 1] ,i - l + 1) ;
}
ansr[n] = 1 ;
int r = n ;
for (int i = n - 1 ;i >= 1 ;i --) {
while (i <= r && a[r] - a[i] > k) r -- ;
ansr[i] = max (ansr[i + 1] ,r - i + 1) ;
}
f (i ,1 ,n ,1) {
maxx = max (maxx ,ansl[i] + ansr[i + 1]) ;
}
仔细观察发现:\(ansl\) 和 \(ansr\) 状态转移的顺序是不一样的(一个正着一个倒着),为肾摸?手模一下。
- 对 \(ansl\) 来说,我们正着枚举,由于我们已经
sort
过,原数组单增,那么我们的 \(i\) 不断增大,\(a_i\) 显然也不断增大,既然 \(a_i - a_t \le k\),那么 \(a_t\) 也理应不断增大,这样我们的l++
就非常合理了。什么意思?假设我们上一问指针的值是 \(t\),那么我们下一次 \(i\) 更大时 \(t\) 只会比现在大而不会比现在小,还是那句话,原数组单调递增。 - \(ansr\) 同理。
这就是为什么双指针优于二分(但有时不一定),它依靠题目的单调性和我们合理的循环顺序,避免了每次重复更新而多跑很多次没用的肯定不行的情况,从而使得时间复杂度 \(\mathcal{O}(n^2)/\mathcal{O}(n\log n)->\mathcal{O}(n)\)。
我们增加一些思维难度:
CF1744F
多组数据,对于每组数据,给出一个 \(0\) 到 \(n-1\) 的排列 \(p\),询问有多少个区间 \([l,r]\) 满足 \(mex(p_l,p_{l+1}\cdots p_{r-1},p_r)>med(p_l,p_{l+1}\cdots p_{r-1},p_r)\)。
其中 \(mex(p)\) 表示未在 \(p\) 中出现的最小非负整数,\(med(p)\) 表示 \(p_{\lfloor{\frac{|p|+1}{2}}\rfloor}\)。
需要一些脑子,设满足条件的序列为 \(S\),则:
\([0,med(S)]\in S\)。(不然 \(mex(p_l,p_{l+1}\cdots p_{r-1},p_r)\) 一定小于 \(med(p_l,p_{l+1}\cdots p_{r-1},p_r)\),仔细思考)
\(med(S)\) 在实际上必须是 \(S\) 排序后的中位数。
如何满足第一个条件?
我们枚举左右端点,从数字 \(0\) 开始遍历至 \(n-1\),\(l\) 记录最左侧的下标,\(r\) 记录最右侧的下标,可以保证对于当前数字 \(i\),\([0,i]\in [l,r]\)。同时,当 \(i\) 扩大时,由于 \(l\) 和 \(r\) 只会向两侧扩展,所以比 \(i\) 小的数一定仍在区间 \([l,r]\) 中,这样的话 \(l\) 和 \(r\) 可以直接更新而不用重新赋值(这是一个双指针),整体复杂度降至 \(\mathcal{O}(n)\)。
那第二个呢?
在满足第一个条件的情况下,如果我们想保证此时的 \(i=med(S)\),那么 \(|S|=i\times2+1\) 或 \(i\times 2+2\)(手模一下就能发现这个规律)。
如何统计答案?
我们假设 \(S\) 的左端点为 \(T\),那么 \(T\) 显然应该满足 \(1\le T\le l\),同时右端点(\(T+|S|-1\))应该满足 \(r\le T+|S|-1\le n\)。解这个不等式,得出 \(T\) 的值域是 \([\max(1,r-|S|+1),\min(l,n-|S|+1)]\),换言之,这个区间内的所有数均满足条件。
注意把不合理的答案(区间长小于等于 \(0\))排除。
代码:
inline void init () {
l = INT_MAX ;
r = 0 ;
ans = 0 ;
}
inline void solve (int &x) {
if (r - l + 1 > x) return ; // 一个小剪枝:如果包含 [0 ,med (S)] 的最小区间长已经大于当前长度,那么不合法,舍去
int L = max (1 ,r - x + 1) ,R = min (l ,n - x + 1) ;
R - L + 1 > 0 ? ans += (ll) (R - L + 1) : 0 ;
}
read (n) ;
f (i ,1 ,n ,1) {
read (a[i]) ;
p[a[i]] = i ;
}
init () ;
f (i ,0 ,n ,1) {
l = min (l ,p[i]) ;
r = max (r ,p[i]) ;
int len = i * 2 + 1 ;
solve (len) ;
solve (++ len) ; // i * 2 + 1 和 i * 2 + 2 都要计算
}
我们会发现有时双指针的需要满足的单调性并不是那么好找。
然后就没有例题了。
不知道为什么,这个人认为双指针与两个 _bound
有相似之处。
就是 upper_bound
和 lower_bound
。
对于 \(i\) 位置,upper_bound
返回的是之后第一个大于 \(a_i\) 的位置,lower_bound
则是大于等于。
其形式为:
upper_bound/lower_bound (a.begin () ,a.end () + 1 ,x) - a ;
即查询 \(a\) 的 begin
到 end
中第一个大于或大于等于 \(x\) 的位置。
进化应用:弗洛伊德判圈法(龟兔赛跑算法)
先放图:
不难看出↑是由一条链和一个环组成的,那么我们可以用这种方法求出环的起点(还有环和链的长度)。
实现
设计一个快指针和慢指针(其实和双指针并没有什么关系),快指针每次走两步,慢指针每次走一步,当它们相遇时,让快指针(慢的也行)返回起点,再让两个指针同时变成慢指针(也就是同时走一步),最后一定会在环的起点处相遇。
听起来有点糊,所以:
证明
设上图链长(红色的)长 \(n\),慢指针在环上走过的部分(青色的)长 \(p\),剩余的(橙色的)长 \(t\),慢指针相遇时已经走了 \(k\) 圈,那么它的总路程是 \(k\times (p+t)+p+n=(k+1)\times p+k\times t+n\)。由于快指针的速度是它的两倍,那么快指针的路程是 \((2\times k+2)\times p+2\times k\times t+2\times n\),让上述两式相减得 \((k+1)\times p+k\times t+n\)。
为了获得更多条件,我们考虑 \(k\) 的值域,显然在慢指针至多走了一条链长加一整圈时,快指针已经走了两条链长和两整圈,比它多走了一条链长一整圈,根据人类智慧,此时它们肯定相遇过了,即 \(k = 1\)。也就是说,第一次相遇时快指针比慢指针多走的路程不会多于一条链长加一圈。
那么上述式子就少了一个参数,变成:\(2\times p+t+n\)。
又出现一个显然:相遇时快指针一定比慢指针多走整数圈,请自行思考。
所以 \(2\times p+t+n\equiv0(\mod p+t)\),化简得 \(p+n\equiv0(\mod p+t)\),根据上述结论,\(p+n\) 一定等于 \(p+t\),即 \(n=t\)。
又因为此时快慢指针速度一样,所以容易得证。
实际应用:
洛谷 CF1137D(交互题)
再挂一遍题目太麻烦了,请自行阅读。
其实就是判圈法的一个板题,首先随机指定两个棋子分别当做快、慢指针,走到它们相遇,这时将所有棋子都看做慢指针一起走,一定会在环的起点相遇,步数符合要求。代码:
for ( ; ; ) {
puts ("next 0") ;
fflush (stdout) ;
in () ;
puts ("next 0 1") ;
fflush (stdout) ;
if (in () == 2) {
break ; // 第二次 n = 2 时说明 0、1 号相遇,跳出
}
}
for ( ; ; ) {
puts ("next 0 1 2 3 4 5 6 7 8 9") ;
fflush (stdout) ;
if (in () == 1) { // 所有棋子相遇,结束
break ;
}
}
puts ("done") ;
fflush (stdout) ;
双指针 & 双向搜索的更多相关文章
- [LeetCode] #167# Two Sum II : 数组/二分查找/双指针
一. 题目 1. Two Sum II Given an array of integers that is already sorted in ascending order, find two n ...
- [LeetCode] #1# Two Sum : 数组/哈希表/二分查找/双指针
一. 题目 1. Two SumTotal Accepted: 241484 Total Submissions: 1005339 Difficulty: Easy Given an array of ...
- Leetcode解题思想总结篇:双指针
Leetcode解题思想总结篇:双指针 1概念 双指针:快慢指针. 快指针在每一步走的步长要比慢指针一步走的步长要多.快指针通常的步速是慢指针的2倍. 在循环中的指针移动通常为: faster = f ...
- FZU 11月月赛D题:双向搜索+二分
/* 双向搜索感觉是个不错的技巧啊 */ 题目大意: 有n的物品(n<=30),平均(两个人得到的物品差不能大于1)分给两个人,每个物品在每个人心目中的价值分别为(vi,wi) 问两人心目中的价 ...
- Longest Substring Without Repeating Characters - 哈希与双指针
题意很简单,就是寻找一个字符串中连续的最长包含不同字母的子串. 其实用最朴素的方法,从当前字符开始寻找,找到以当前字符开头的最长子串.这个方法猛一看是个n方的算法,但是要注意到由于字符数目的限制,其实 ...
- leetcode 15. 3Sum 双指针
题目链接 给n个数, 找出三个数相加结果为0的所有的组, 不可重复. 用双指针的思想,O(n^2)暴力的找, 注意判重复. class Solution { public: vector<vec ...
- hdu_5806_NanoApe Loves Sequence Ⅱ(双指针)
题目链接:hdu_5806_NanoApe Loves Sequence Ⅱ 题意: 给你一段数,问你有多少个区间满足第K大的数不小于m 题解: 直接双指针加一下区间就行 #include<cs ...
- BZOJ_2679_[Usaco2012 Open]Balanced Cow Subsets _meet in middle+双指针
BZOJ_2679_[Usaco2012 Open]Balanced Cow Subsets _meet in middle+双指针 Description Farmer John's owns N ...
- BZOJ_3048_[Usaco2013 Jan]Cow Lineup _双指针
BZOJ_3048_[Usaco2013 Jan]Cow Lineup _双指针 Description Farmer John's N cows (1 <= N <= 100,000) ...
- BZOJ_4653_[Noi2016]区间_线段树+离散化+双指针
BZOJ_4653_[Noi2016]区间_线段树+离散化+双指针 Description 在数轴上有 n个闭区间 [l1,r1],[l2,r2],...,[ln,rn].现在要从中选出 m 个区间, ...
随机推荐
- 改造 Kubernetes 自定义调度器
原文出处:改造 Kubernetes 自定义调度器 | Jayden's Blog (jaydenchang.top) Overview Kubernetes 默认调度器在调度 Pod 时并不关心特殊 ...
- Tkinter禁止用户调整窗口尺寸大小
禁止用户调整窗口尺寸大小的方式: root.resizable(False,False) 例子: from tkinter import * from tkinter import ttk impor ...
- 原型工具--canva可画
Canva 是一个功能强大的在线设计平台,提供了丰富的设计工具和素材,包括原型设计.尽管 Canva 在原型设计方面并不像专门的原型设计工具(如Sketch.Figma.Adobe XD等)那样功能全 ...
- nginx重载流程nginx请求处理流程nginx单进程和多进程
nginx重载流程 首先nginx会向master进程发送HUP信号[reload命令] master进程校验配置语法是否正确 master进程打开新的监听端口 master进程用心配置启动新的wor ...
- 11种排序算法(Python实现)
10种排序算法(Python实现) 冒泡排序 1. 两重循环,每次都将一个点移动到最终位置 def BubbleSort(lst): n=len(lst) if n<=1: return lst ...
- LeetCode 675. Cut Off Trees for Golf Event 为高尔夫比赛砍树 (C++/Java)
题目: You are asked to cut off trees in a forest for a golf event. The forest is represented as a non- ...
- kettle从入门到精通 第十二课 kettle java代码过滤记录、利用Janino计算Java表达式
1.下图通过简单的示例讲解了根据java代码过滤记录和利用Janino计算Java表达式两个组件. 2.根据java代码过滤记录 1)步骤名称:自定义 2)接收匹配的行的步骤(可选):下面条件(jav ...
- ABC347题解
省流:输+赢 D 按位分析. 既然两个数异或后的结果是 \(C\),那就考虑 \(C\) 中为 \(1\) 的数中有几个是在 \(X\) 当中的. 假如 \(\text{a - popcnt(X) = ...
- Unity3D 内存管理非代码技巧
在场景管理器新建 gameobjct 使用代码在类初始化时 NEW 普肉fai包(包)然后将相同的类NEW够挂载到 gameobjct子节点上 在操控列表中类的时候用for循环遍历操作移动还是怎么样( ...
- EF EntityFramework 强制从数据库中取数据,而不是上下文
场景:插入了一条数据到数据库,这条数据会有其它程序修改,接着程序想获取最新数据.此时不加额外处理,取的仍是旧的. t_task ta = new t_task(); ta.item_id = item ...