问题

问题描述

有一个有序序列a1,a2,a3,,ana_1,a_2,a_3,\cdots ,a_n, 问第一个大于kk的元素与位置是哪里.

样例输入

  • 第一行两个整数n(105),m(105)n(\leqslant 10^5),m(\leqslant 10^5),表示序列元素的数量,和有多少询问
  • 第二行,nn个数表示序列的元素,保证有序
  • 接下来mm行,询问的值
5 3
1 2 5 9 100
2
3
20

样例输出

  • 每行两个数,表示每个询问的第一个\geqslant的值和对应的位置
2 2
5 3
100 5

解析1: 暴力

本问题是:在一个单调序列上查找xxxx的后缀(当xx不存在时)

显示对于每个询问,最简单的方法就是使用暴力查询,代码如下

cpp
copy
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream> using namespace std; const int maxn=qe5+5; int a[maxn]; int n,m; int find(int val) { for (int i = 1; i <= n; i++) { if (a[i] >= val) return i; } return n+1; } int main() { cin >> n >> m; for(int i =1;i<=n;i++ ) cin >> a[i]; for(int i =1;i<=m;i++) { int num; cin >> num; int pos = find(num); if( pos == n+1) //没有找到 cout << "not found\n"; else cout << a[pos] << " " << pos << "\n"; } return 0; }

上面的代码的时间复杂度为O(n2)O(n^2),显然会超时.

解析2: 二分法

面对样例1,2,5,9,1001,2,5,9,100,这个序列是有序的

我们可以利用元素有序这条性质来加快我们的查询呢?

一个朴素的想法如下

面对样例,我们查询的位置的范围是[1,6][1,6],为什么最后一个位置是66,不应该是55吗?因为我们有可能会查询到比最大值100100还要大的元素,例如,200200,那这个时候,我们应该返回的位置是66,100100位置的后面一个位置.

同样,我们可以这样思考,我们把原序列末尾添加一个无穷大的值,改成

元素1259100位置123456 \begin{array}{lcccccc} \text{元素}& 1&2&5&9&100&\infin \\ \hline \text{位置}& \tt{1}&\tt2&\tt3&\tt4&\tt5&\tt6 \end{array}

那么如果出现比最大的一个元素还大的查询,返回的位置就是n+1n+1,同样如果得到返回值为n+1n+1则知道,没有查询到需要的值,输出not found

如果需要查询的元素是66, 可以描述我们的问题为:f(1,6,6)f(1,6,6),表示在区间[1,6][1,6]上找到第一个6\geqslant 6位置,我们挑选到中间位置(1+6)/2=3(1+6) / 2 = 3,值为55,又发则元素565 \leqslant 6,所以55和左边的元素都是比66小,所以范围[1,3][1,3]是对我们无用的范围,那么新的问就变成了:f(4,6,6)f(4,6,6),在区间[4,6][4,6]上查询第一个6\geqslant 6的位置

  • 我们成功的将问题缩小了一半,将问题变得到更简单了
  • 新的问题与原问题是相似的,这显然是递归
  • 小朋友法:如果找到足够多的小朋友来帮我们解题,一定可以解出来.所以问题有解.

下面采用更抽象的方法,来思考问题,设问题为f(l,r,val)f(l,r,val),表示在区间[l,r][l,r]上第一个val\geqslant val的位置 .条件:保证区间[l,r][l,r]上必然存在一个值val\geqslant val

则得到公式如下

f(l,r,val)={f(l,mid,val),a[mid]>=valf(mid+1,r,val),a[mid]<val(1) f(l,r,val) = \begin{cases} f(l,mid,val) ,& a[mid] >= val\\ f(mid+1,r,val) ,& a[mid] < val \end{cases} \tag1

其中mid=(l+r)÷2mid = \lfloor(l+r) \div 2\rfloor,且显然f(l,l,val)=lf(l,l,val) = l

于是我们写出如下代码:

cpp
copy
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream> using namespace std; const int maxn=1e5+5; int a[maxn]; int n,m; int mid(int l,int r) { return (l+r) >> 1; //这是最快的写法 } //检查pos位置的值是否符合要求 bool check(int pos,int val){ return a[pos] >= val; } //bs_find = binary search find int bs_find(int l,int r,int val) { while( l < r) { int m = mid(l,r); if( check(m,val)) //成立 r = m; else //不成立,抛弃左半边 l = m+1; } return l ; } int main() { cin >> n >> m; for(int i =1;i<=n;i++ ) cin >> a[i]; for(int i =1;i<=m;i++) { int num; cin >> num; int pos = bs_find(1,n+1,num); if( pos == n+1) //没有找到 cout << "not found\n"; else cout << a[pos] << " " << pos << "\n"; } return 0; }

总结

定义1 二分性

在一个序列A=a1,a2,,anA = a_1,a_2,\cdots,a_n上,有一个函数叫做check(ai)check(a_i),针对序列AA形成的新的序列B=b1,b2,,bnB = b_1,b_2,\cdots,b_n,其中bi=check(ai){true,false}b_i = check(a_i) \in \{true,false\},且序列bb的相同值的位置都是连续的,如下所示:

b1,b2,b3,,bmcheck=false,bm+1,,bncheck=true \color{blue}{ \underbrace{b_1,b_2,b_3,\cdots,b_m}_{check = false}}, \color{red}{ \underbrace{b_{m+1},\cdots,b_n}_{check = true} }

得到一个类似的如下的图:

如果序列b满足两个条件

  • 只有两个值
  • 相同值的位置都是连续

则说明序列AA在函数checkcheck是具有二分性.其中bm,bm+1b_m,b_{m+1}称为分界点

引理11 二分性等价定义1

一个具有二分性的序列不会出现蓝红蓝红蓝红的连续三个相邻值

证明:

必要性显然.充分性: 根据描述,序列最多出现蓝红红蓝,这种情况可以是边界,其它地方要么蓝,要么红,符合二分性的定义.

引理22 二分性等价定义2

一个序列AA符合下面两条性质,则序列AA与二分性等价.

  1. 若某个位置ii,则所有小于位置ii的都是
  2. 若某个位置ii,则所有大于位置ii的都是

证明:

必要性显然. 充分性: 使用反证法: 假设序列AA不符合引理11的描述,也就是会出现蓝红蓝的情况,但此时违反了引理22的描述,所以序列AA符合引理11,引理22可以推出引理11.证毕.

推论1

具有二分性的序列可以使用二分查找算法来查找分界点的位置

推论2

对于序列AA,若函数f(ai)f(a_i)成立,则f(aj),jif(a_j),j \geqslant i都成立,则会序列AA满足二分性.

推论2证明:

二分查找的正确性证明

开始证明:Proof.\cal{Proof}.

使用归纳法,证明bs_find(l,r,val)代码的正确性

只有1个元素的时候a1a_1,需要创建一个元素a2=a_2 = \infin, 问题就变成求f(1,2,val)f(1,2,val),取m=(1+2)1==1m = (1+2) \gg 1 == 1

分类讨论如下

  1. 情况1: check(1,val)=true    a1valcheck(1,val) = true \iff a_1 \geqslant val,check函数成立,则问题变成f(1,1,val)==1f(1,1,val) == 1,得到正确答案
  2. 情况2: check(1,val)=false    a1valcheck(1,val) = false \iff a_1 \ngeqslant val,check函数不成立,则问题变成f(2,2,val)==2f(2,2,val) == 2,得到正确答案

结论:当只有一个元素a1a_1时,函数bs_find可以得到正确答案

有两个元素ai,ai+1a_i,a_{i+1},且保证check(i+1,val)=truecheck(i+1,val) = true成立,取m=(i+(i+1)/2=im = (i+(i+1) / 2 = i,说明当两个相当位置i,i+1i,i+1,进行mid函数的运算后一定得到ii,也就是较小的那个位置.

分类讨论如下

  1. 情况1: check(m=i,val)=true    aivalcheck(m=i,val) = true \iff a_i \geqslant val,check函数成立,则问题变成f(i,i,val)==if(i,i,val) == i,得到正确答案
  2. 情况2: check(m=i,val)=false    aivalcheck(m=i,val) = false \iff a_i \ngeqslant val,check函数不成立,则问题变成f(i+1,i+1,val)==i+1f(i+1,i+1,val) == i+1,得到正确答案

结论:当只有两个元素ai,ai+1a_i,a_{i+1}时,且check(i+1,val)=truecheck(i+1,val)=true成立时,函数bs_find可以得到正确答案

同理,有33个元素:ai,ai+1,ai+1a_i,a_{i+1},a_{i+1},且保证check(i+2,val)=truecheck(i+2,val)=true成立时,函数bs_find(i,i+2,val)可以得到正确的答案

2k2k(偶数)个元素的时候a1,a2,,a2ka_1,a_2,\cdots,a_{2k}, 且保证check(2k,val)=truecheck(2k,val)=true成立时,取m=(1+2k)1=km = (1+2k) \gg 1 = k

分类讨论如下

  1. 情况1: check(m=k,val)=true    akvalcheck(m=k,val) = true \iff a_k \geqslant val,check函数成立,则问题变成f(1,k,val)f(1,k,val),
  2. 情况2: check(m=k,val)=false    akvalcheck(m=k,val) = false \iff a_k \ngeqslant val,check函数不成立,则问题变成f(k+1,2k,val)f(k+1,2k,val)

于是问题就变成是一个长度为kk的子问题,问题正确性由子问题的正确性保证

同理,当有2k+12k+1(奇数)个元素的时候a1,a2,,a2ka_1,a_2,\cdots,a_{2k}, 且保证check(2k+1,val)=truecheck(2k+1,val)=true成立时,取m=(1+2k+1)1=k+1m = (1+2k+1) \gg 1 = k+1,问题就变成是一个长度为k+1k+1,或kk的子问题,问题正确性由子问题的正确性保证

这样一直到分解,最后剩余的元素个数,一定会变成2233,而这两个长度,已经验证成功.

证明完毕.Q.E.D\cal{Q.E.D}

一般化

TODO 没有解释清楚

具体什么样的情况下可以使用二分查询算法呢?

在一个序列A=a1,a2,,anA = a_1,a_2,\cdots,a_n上,有一个函数叫做check(ai)check(a_i),针对序列AA形成的新的序列B=b1,b2,,bnB = b_1,b_2,\cdots,b_n,其中bi=check(ai){true,false}b_i = check(a_i) \in \{true,false\}

序列

b1,b2,b3,,bmcheck=false,bm+1,,bncheck=true \color{blue}{ \underbrace{b_1,b_2,b_3,\cdots,b_m}_{check = false}}, \color{red}{ \underbrace{b_{m+1},\cdots,b_n}_{check = true} }

得到一个类似的如下的图:

若序列 存在某个位置

什么是单调性?

单调性的性质:

在一个单调性的序列上

  1. 查询使函数check(pos)check(pos)第一个成立的位置
  2. 查询使函数check(pos)check(pos)最后一个失败的后一个的位置

可以使用二分查询法

什么题目使用

  1. 第一个x\geqslant x元素的位置
  2. 第一个=x= x元素的位置
  3. 最后一个x\leqslant x元素的位置
  4. 求解数字xx的数量
  5. 求解满足数字val[l,r]val \in [l,r]的数量

经典模型

  • 最小值最大,最大值最小问题
    • 1.序列划分

二分的应用

  • 在有序的数组里查找东西

    • 查找一个特定的数字。
    • 查找第一个大于或等于某个数的元素。
    • 查找第一个大于某个数的元素。
    • 查找最后一个小于或等于某个数的元素。
    • 统计某个数出现了多少次。
  • 猜答案(答案二分)

    • 解决“最大值最小化”问题。
      • 例子:把一根很长的木头切成 k 段,怎么切才能让最长的那一段尽可能短?
    • 解决“最小值最大化”问题。
      • 例子:在一条路上装 k 个路灯,怎么装才能让两盏灯之间的最小距离尽可能远?
  • 在小数(实数)上查找

    • 计算一个数的平方根,精确到指定的小数位。
    • 求解方程的近似解。
  • 其他常见应用

    • 寻找“山峰”数组的顶点(一个数组先递增后递减,找最大值)。

      这种问题的关键在于,虽然整个数组不是单调的,但我们可以利用山峰的性质来确定二分的搜索方向。山峰的左边是“上坡路”,右边是“下坡路”。

      核心思想: 我们每次取中间位置 mid,通过比较 A[mid] 和它右边紧邻的元素 A[mid+1] 的关系,就能判断出 mid 正处于上坡还是下坡,从而确定真正的山峰在它的左边还是右边。

      具体步骤:

      1. 设置查找区间 [left, right] 为整个数组的下标范围。
      2. 取中间位置 mid
      3. 比较 A[mid]A[mid+1]
        • 如果 A[mid] < A[mid+1]:这说明 mid 正处在上坡阶段,那么真正的山峰一定在 mid 的右边(可能是 mid+1 或更远)。因此,我们可以安全地舍弃包括 mid 在内的左半部分,更新 left = mid + 1
        • 如果 A[mid] > A[mid+1]:这说明 mid 正处在下坡阶段,那么山峰就是 mid 本身,或者在 mid 的左边。我们不能排除 mid 本身,因此可以舍弃 mid 右边的部分,更新 right = mid
      4. leftright 相遇时,那个位置就是山峰的顶点。

      示例代码 (C++):

      cpp
      copy
              
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      #include <iostream> #include <vector> // 寻找山峰元素的下标 int findPeakElement(const std::vector<int>& nums) { int left = 0; int right = nums.size() - 1; while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] < nums[mid + 1]) { // mid 在上坡,山峰一定在右侧 left = mid + 1; } else { // mid 在下坡或就是山峰,答案在左侧或就是 mid right = mid; } } // 循环结束时,left == right,指向山峰 return left; } int main() { std::vector<int> mountain = {1, 3, 5, 8, 10, 7, 4, 2}; int peak_index = findPeakElement(mountain); std::cout << "山峰的下标是: " << peak_index << std::endl; std::cout << "山峰的值是: " << mountain[peak_index] << std::endl; return 0; }
    • 加速某些算法,比如用 O(n log n) 的时间复杂度求解“最长上升子序列”。

练习题目

  • luogu P1083
  • luogu P2678
  • luogu P1314
  • luogu P1868
  • luogu P1493
  • hdu 6231
  • poj 3273
  • poj 3258
  • poj 1905
  • poj 3122
  • luogu P1419 二分+单调队列