双指针

根据人类直觉这个东西需要满足单调性,所以预处理的时候大概率需要排序。

好像常与二分结合使用?

可以用在序列、链表(存储位置)或者树、图上(存储结点)。

或者用于其他算法(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_boundlower_bound

对于 \(i\) 位置,upper_bound 返回的是之后第一个大于 \(a_i\) 的位置lower_bound 则是大于等于。

其形式为:

upper_bound/lower_bound (a.begin () ,a.end () + 1 ,x) - a ;

即查询 \(a\) 的 beginend 中第一个大于或大于等于 \(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) ;

双指针 & 双向搜索的更多相关文章

  1. [LeetCode] #167# Two Sum II : 数组/二分查找/双指针

    一. 题目 1. Two Sum II Given an array of integers that is already sorted in ascending order, find two n ...

  2. [LeetCode] #1# Two Sum : 数组/哈希表/二分查找/双指针

    一. 题目 1. Two SumTotal Accepted: 241484 Total Submissions: 1005339 Difficulty: Easy Given an array of ...

  3. Leetcode解题思想总结篇:双指针

    Leetcode解题思想总结篇:双指针 1概念 双指针:快慢指针. 快指针在每一步走的步长要比慢指针一步走的步长要多.快指针通常的步速是慢指针的2倍. 在循环中的指针移动通常为: faster = f ...

  4. FZU 11月月赛D题:双向搜索+二分

    /* 双向搜索感觉是个不错的技巧啊 */ 题目大意: 有n的物品(n<=30),平均(两个人得到的物品差不能大于1)分给两个人,每个物品在每个人心目中的价值分别为(vi,wi) 问两人心目中的价 ...

  5. Longest Substring Without Repeating Characters - 哈希与双指针

    题意很简单,就是寻找一个字符串中连续的最长包含不同字母的子串. 其实用最朴素的方法,从当前字符开始寻找,找到以当前字符开头的最长子串.这个方法猛一看是个n方的算法,但是要注意到由于字符数目的限制,其实 ...

  6. leetcode 15. 3Sum 双指针

    题目链接 给n个数, 找出三个数相加结果为0的所有的组, 不可重复. 用双指针的思想,O(n^2)暴力的找, 注意判重复. class Solution { public: vector<vec ...

  7. hdu_5806_NanoApe Loves Sequence Ⅱ(双指针)

    题目链接:hdu_5806_NanoApe Loves Sequence Ⅱ 题意: 给你一段数,问你有多少个区间满足第K大的数不小于m 题解: 直接双指针加一下区间就行 #include<cs ...

  8. 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 ...

  9. BZOJ_3048_[Usaco2013 Jan]Cow Lineup _双指针

    BZOJ_3048_[Usaco2013 Jan]Cow Lineup _双指针 Description Farmer John's N cows (1 <= N <= 100,000) ...

  10. BZOJ_4653_[Noi2016]区间_线段树+离散化+双指针

    BZOJ_4653_[Noi2016]区间_线段树+离散化+双指针 Description 在数轴上有 n个闭区间 [l1,r1],[l2,r2],...,[ln,rn].现在要从中选出 m 个区间, ...

随机推荐

  1. 改造 Kubernetes 自定义调度器

    原文出处:改造 Kubernetes 自定义调度器 | Jayden's Blog (jaydenchang.top) Overview Kubernetes 默认调度器在调度 Pod 时并不关心特殊 ...

  2. Tkinter禁止用户调整窗口尺寸大小

    禁止用户调整窗口尺寸大小的方式: root.resizable(False,False) 例子: from tkinter import * from tkinter import ttk impor ...

  3. 原型工具--canva可画

    Canva 是一个功能强大的在线设计平台,提供了丰富的设计工具和素材,包括原型设计.尽管 Canva 在原型设计方面并不像专门的原型设计工具(如Sketch.Figma.Adobe XD等)那样功能全 ...

  4. nginx重载流程nginx请求处理流程nginx单进程和多进程

    nginx重载流程 首先nginx会向master进程发送HUP信号[reload命令] master进程校验配置语法是否正确 master进程打开新的监听端口 master进程用心配置启动新的wor ...

  5. 11种排序算法(Python实现)

    10种排序算法(Python实现) 冒泡排序 1. 两重循环,每次都将一个点移动到最终位置 def BubbleSort(lst): n=len(lst) if n<=1: return lst ...

  6. 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- ...

  7. kettle从入门到精通 第十二课 kettle java代码过滤记录、利用Janino计算Java表达式

    1.下图通过简单的示例讲解了根据java代码过滤记录和利用Janino计算Java表达式两个组件. 2.根据java代码过滤记录 1)步骤名称:自定义 2)接收匹配的行的步骤(可选):下面条件(jav ...

  8. ABC347题解

    省流:输+赢 D 按位分析. 既然两个数异或后的结果是 \(C\),那就考虑 \(C\) 中为 \(1\) 的数中有几个是在 \(X\) 当中的. 假如 \(\text{a - popcnt(X) = ...

  9. Unity3D 内存管理非代码技巧

    在场景管理器新建 gameobjct 使用代码在类初始化时 NEW 普肉fai包(包)然后将相同的类NEW够挂载到 gameobjct子节点上 在操控列表中类的时候用for循环遍历操作移动还是怎么样( ...

  10. EF EntityFramework 强制从数据库中取数据,而不是上下文

    场景:插入了一条数据到数据库,这条数据会有其它程序修改,接着程序想获取最新数据.此时不加额外处理,取的仍是旧的. t_task ta = new t_task(); ta.item_id = item ...