问题 问题描述 样例输入 样例输出 解析1: 暴力 解析2: 二分法 总结 二分查找的正确性证明 一般化 什么题目使用 经典模型 练习题目 问题
问题描述
有一个有序序列a 1 , a 2 , a 3 , ⋯ , a n a_1,a_2,a_3,\cdots ,a_n a 1 , a 2 , a 3 , ⋯ , a n , 问第一个大于k k k 的元素与位置是哪里.
样例输入
第一行两个整数n ( ⩽ 10 5 ) , m ( ⩽ 10 5 ) n(\leqslant 10^5),m(\leqslant 10^5) n ( ⩽ 1 0 5 ) , m ( ⩽ 1 0 5 ) ,表示序列元素的数量,和有多少询问
第二行,n n n 个数表示序列的元素,保证有序
接下来m m m 行,询问的值
5 3
1 2 5 9 100
2
3
20
样例输出
每行两个数,表示每个询问的第一个⩾ \geqslant ⩾ 的值和对应的位置
2 2
5 3
100 5
解析1: 暴力
本问题是:在一个单调序列上查找x x x 或x x x 的后缀(当x x x 不存在时)
显示对于每个询问,最简单的方法就是使用暴力查询,代码如下
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 ( n 2 ) O(n^2) O ( n 2 ) ,显然会超时.
解析2: 二分法
面对样例1 , 2 , 5 , 9 , 100 1,2,5,9,100 1 , 2 , 5 , 9 , 100 ,这个序列是有序的
我们可以利用元素有序 这条性质来加快我们的查询呢?
一个朴素的想法如下
面对样例,我们查询的位置的范围是[ 1 , 6 ] [1,6] [ 1 , 6 ] ,为什么最后一个位置是6 6 6 ,不应该是5 5 5 吗?因为我们有可能会查询到比最大值100 100 100 还要大的元素,例如,200 200 200 ,那这个时候,我们应该返回的位置是6 6 6 ,100 100 100 位置的后面一个位置.
同样,我们可以这样思考,我们把原序列末尾添加一个无穷大的值,改成
元素 1 2 5 9 100 ∞ 位置 1 2 3 4 5 6
\begin{array}{lcccccc}
\text{元素}& 1&2&5&9&100&\infin \\
\hline
\text{位置}& \tt{1}&\tt2&\tt3&\tt4&\tt5&\tt6
\end{array}
元素 位置 1 1 2 2 5 3 9 4 100 5 ∞ 6
那么如果出现比最大的一个元素还大的查询,返回的位置就是n + 1 n+1 n + 1 ,同样如果得到返回值为n + 1 n+1 n + 1 则知道,没有查询到需要的值,输出not found
如果需要查询的元素是6 6 6 ,
可以描述我们的问题为:f ( 1 , 6 , 6 ) f(1,6,6) f ( 1 , 6 , 6 ) ,表示在区间[ 1 , 6 ] [1,6] [ 1 , 6 ] 上找到第一个⩾ 6 \geqslant 6 ⩾ 6 位置,我们挑选到中间位置( 1 + 6 ) / 2 = 3 (1+6) / 2 = 3 ( 1 + 6 ) /2 = 3 ,值为5 5 5 ,又发则元素5 ⩽ 6 5 \leqslant 6 5 ⩽ 6 ,所以5 5 5 和左边的元素都是比6 6 6 小,所以范围[ 1 , 3 ] [1,3] [ 1 , 3 ] 是对我们无用的范围,那么新的问就变成了:f ( 4 , 6 , 6 ) f(4,6,6) f ( 4 , 6 , 6 ) ,在区间[ 4 , 6 ] [4,6] [ 4 , 6 ] 上查询第一个⩾ 6 \geqslant 6 ⩾ 6 的位置
我们成功的将问题缩小了一半,将问题变得到更简单了
新的问题与原问题是相似的,这显然是递归
小朋友法:如果找到足够多的小朋友来帮我们解题,一定可以解出来.所以问题有解.
下面采用更抽象的方法,来思考问题,设问题为f ( l , r , v a l ) f(l,r,val) f ( l , r , v a l ) ,表示在区间[ l , r ] [l,r] [ l , r ] 上第一个⩾ v a l \geqslant val ⩾ v a l 的位置 .条件:保证区间[ l , r ] [l,r] [ l , r ] 上必然存在一个值⩾ v a l \geqslant val ⩾ v a l
则得到公式如下
f ( l , r , v a l ) = { f ( l , m i d , v a l ) , a [ m i d ] > = v a l f ( m i d + 1 , r , v a l ) , a [ m i d ] < v a l (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
f ( l , r , v a l ) = { f ( l , mi d , v a l ) , f ( mi d + 1 , r , v a l ) , a [ mi d ] >= v a l a [ mi d ] < v a l ( 1 ) 其中m i d = ⌊ ( l + r ) ÷ 2 ⌋ mid = \lfloor(l+r) \div 2\rfloor mi d = ⌊( l + r ) ÷ 2 ⌋ ,且显然f ( l , l , v a l ) = l f(l,l,val) = l f ( l , l , v a l ) = l
于是我们写出如下代码:
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 ;
}
bool check ( int pos, int val) {
return a[ pos] >= val;
}
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 = a 1 , a 2 , ⋯ , a n A = a_1,a_2,\cdots,a_n A = a 1 , a 2 , ⋯ , a n 上,有一个函数叫做c h e c k ( a i ) check(a_i) c h ec k ( a i ) ,针对序列A A A 形成的新的序列B = b 1 , b 2 , ⋯ , b n B = b_1,b_2,\cdots,b_n B = b 1 , b 2 , ⋯ , b n ,其中b i = c h e c k ( a i ) ∈ { t r u e , f a l s e } b_i = check(a_i) \in \{true,false\} b i = c h ec k ( a i ) ∈ { t r u e , f a l se } ,且序列b b b 的相同值的位置都是连续的,如下所示:
b 1 , b 2 , b 3 , ⋯ , b m ⏟ c h e c k = f a l s e , b m + 1 , ⋯ , b n ⏟ c h e c k = t r u e
\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}
}
c h ec k = f a l se b 1 , b 2 , b 3 , ⋯ , b m , c h ec k = t r u e b m + 1 , ⋯ , b n 得到一个类似的如下的图:
如果序列b满足两个条件
则说明序列A A A 在函数c h e c k check c h ec k 是具有二分性.其中b m , b m + 1 b_m,b_{m+1} b m , b m + 1 称为分界点
引理1 1 1 二分性等价定义1
一个具有二分性的序列不会出现蓝红蓝或红蓝红的连续三个相邻值
证明:
必要性显然.充分性: 根据描述,序列最多出现蓝红或红蓝,这种情况可以是边界,其它地方要么蓝,要么红,符合二分性的定义.
引理2 2 2 二分性等价定义2
一个序列A A A 符合下面两条性质,则序列A A A 与二分性等价.
若某个位置i i i 是蓝,则所有小于位置i i i 的都是蓝
若某个位置i i i 是红,则所有大于位置i i i 的都是红
证明:
必要性显然. 充分性: 使用反证法: 假设序列A A A 不符合引理1 1 1 的描述,也就是会出现蓝红蓝的情况,但此时违反了引理2 2 2 的描述,所以序列A A A 符合引理1 1 1 ,引理2 2 2 可以推出引理1 1 1 .证毕.
推论1
具有二分性的序列可以使用二分查找 算法来查找分界点的位置
推论2
对于序列A A A ,若函数f ( a i ) f(a_i) f ( a i ) 成立,则f ( a j ) , j ⩾ i f(a_j),j \geqslant i f ( a j ) , j ⩾ i 都成立,则会序列A A A 满足二分性.
推论2证明:
二分查找的正确性证明
开始证明:P r o o f . \cal{Proof}. P roo f .
使用归纳法,证明bs_find(l,r,val)代码的正确性
只有1个元素的时候a 1 a_1 a 1 ,需要创建一个元素a 2 = ∞ a_2 = \infin a 2 = ∞ , 问题就变成求f ( 1 , 2 , v a l ) f(1,2,val) f ( 1 , 2 , v a l ) ,取m = ( 1 + 2 ) ≫ 1 = = 1 m = (1+2) \gg 1 == 1 m = ( 1 + 2 ) ≫ 1 == 1
分类讨论如下
情况1: c h e c k ( 1 , v a l ) = t r u e ⟺ a 1 ⩾ v a l check(1,val) = true \iff a_1 \geqslant val c h ec k ( 1 , v a l ) = t r u e ⟺ a 1 ⩾ v a l ,check函数成立,则问题变成f ( 1 , 1 , v a l ) = = 1 f(1,1,val) == 1 f ( 1 , 1 , v a l ) == 1 ,得到正确答案
情况2: c h e c k ( 1 , v a l ) = f a l s e ⟺ a 1 ≱ v a l check(1,val) = false \iff a_1 \ngeqslant val c h ec k ( 1 , v a l ) = f a l se ⟺ a 1 v a l ,check函数不成立,则问题变成f ( 2 , 2 , v a l ) = = 2 f(2,2,val) == 2 f ( 2 , 2 , v a l ) == 2 ,得到正确答案
结论:当只有一个元素a 1 a_1 a 1 时,函数bs_find可以得到正确答案
有两个元素a i , a i + 1 a_i,a_{i+1} a i , a i + 1 ,且保证c h e c k ( i + 1 , v a l ) = t r u e check(i+1,val) = true c h ec k ( i + 1 , v a l ) = t r u e 成立,取m = ( i + ( i + 1 ) / 2 = i m = (i+(i+1) / 2 = i m = ( i + ( i + 1 ) /2 = i ,说明当两个相当位置i , i + 1 i,i+1 i , i + 1 ,进行mid函数的运算后一定得到i i i ,也就是较小的那个位置.
分类讨论如下
情况1: c h e c k ( m = i , v a l ) = t r u e ⟺ a i ⩾ v a l check(m=i,val) = true \iff a_i \geqslant val c h ec k ( m = i , v a l ) = t r u e ⟺ a i ⩾ v a l ,check函数成立,则问题变成f ( i , i , v a l ) = = i f(i,i,val) == i f ( i , i , v a l ) == i ,得到正确答案
情况2: c h e c k ( m = i , v a l ) = f a l s e ⟺ a i ≱ v a l check(m=i,val) = false \iff a_i \ngeqslant val c h ec k ( m = i , v a l ) = f a l se ⟺ a i v a l ,check函数不成立,则问题变成f ( i + 1 , i + 1 , v a l ) = = i + 1 f(i+1,i+1,val) == i+1 f ( i + 1 , i + 1 , v a l ) == i + 1 ,得到正确答案
结论:当只有两个元素a i , a i + 1 a_i,a_{i+1} a i , a i + 1 时,且c h e c k ( i + 1 , v a l ) = t r u e check(i+1,val)=true c h ec k ( i + 1 , v a l ) = t r u e 成立时,函数bs_find可以得到正确答案
同理,有3 3 3 个元素:a i , a i + 1 , a i + 1 a_i,a_{i+1},a_{i+1} a i , a i + 1 , a i + 1 ,且保证c h e c k ( i + 2 , v a l ) = t r u e check(i+2,val)=true c h ec k ( i + 2 , v a l ) = t r u e 成立时,函数bs_find(i,i+2,val)可以得到正确的答案
有2 k 2k 2 k (偶数)个元素的时候a 1 , a 2 , ⋯ , a 2 k a_1,a_2,\cdots,a_{2k} a 1 , a 2 , ⋯ , a 2 k , 且保证c h e c k ( 2 k , v a l ) = t r u e check(2k,val)=true c h ec k ( 2 k , v a l ) = t r u e 成立时,取m = ( 1 + 2 k ) ≫ 1 = k m = (1+2k) \gg 1 = k m = ( 1 + 2 k ) ≫ 1 = k
分类讨论如下
情况1: c h e c k ( m = k , v a l ) = t r u e ⟺ a k ⩾ v a l check(m=k,val) = true \iff a_k \geqslant val c h ec k ( m = k , v a l ) = t r u e ⟺ a k ⩾ v a l ,check函数成立,则问题变成f ( 1 , k , v a l ) f(1,k,val) f ( 1 , k , v a l ) ,
情况2: c h e c k ( m = k , v a l ) = f a l s e ⟺ a k ≱ v a l check(m=k,val) = false \iff a_k \ngeqslant val c h ec k ( m = k , v a l ) = f a l se ⟺ a k v a l ,check函数不成立,则问题变成f ( k + 1 , 2 k , v a l ) f(k+1,2k,val) f ( k + 1 , 2 k , v a l )
于是问题就变成是一个长度为k k k 的子问题,问题正确性由子问题的正确性保证
同理,当有2 k + 1 2k+1 2 k + 1 (奇数)个元素的时候a 1 , a 2 , ⋯ , a 2 k a_1,a_2,\cdots,a_{2k} a 1 , a 2 , ⋯ , a 2 k , 且保证c h e c k ( 2 k + 1 , v a l ) = t r u e check(2k+1,val)=true c h ec k ( 2 k + 1 , v a l ) = t r u e 成立时,取m = ( 1 + 2 k + 1 ) ≫ 1 = k + 1 m = (1+2k+1) \gg 1 = k+1 m = ( 1 + 2 k + 1 ) ≫ 1 = k + 1 ,问题就变成是一个长度为k + 1 k+1 k + 1 ,或k k k 的子问题,问题正确性由子问题的正确性保证
这样一直到分解,最后剩余的元素个数,一定会变成2 2 2 或3 3 3 ,而这两个长度,已经验证成功.
证明完毕.Q . E . D \cal{Q.E.D} Q . E . D
一般化
TODO 没有解释清楚
具体什么样的情况下可以使用二分查询算法呢?
在一个序列A = a 1 , a 2 , ⋯ , a n A = a_1,a_2,\cdots,a_n A = a 1 , a 2 , ⋯ , a n 上,有一个函数叫做c h e c k ( a i ) check(a_i) c h ec k ( a i ) ,针对序列A A A 形成的新的序列B = b 1 , b 2 , ⋯ , b n B = b_1,b_2,\cdots,b_n B = b 1 , b 2 , ⋯ , b n ,其中b i = c h e c k ( a i ) ∈ { t r u e , f a l s e } b_i = check(a_i) \in \{true,false\} b i = c h ec k ( a i ) ∈ { t r u e , f a l se }
序列
b 1 , b 2 , b 3 , ⋯ , b m ⏟ c h e c k = f a l s e , b m + 1 , ⋯ , b n ⏟ c h e c k = t r u e
\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}
}
c h ec k = f a l se b 1 , b 2 , b 3 , ⋯ , b m , c h ec k = t r u e b m + 1 , ⋯ , b n 得到一个类似的如下的图:
若序列
存在某个位置
什么是单调性?
单调性的性质:
在一个单调性的序列上
查询使函数c h e c k ( p o s ) check(pos) c h ec k ( p os ) 第一个成立的位置
查询使函数c h e c k ( p o s ) check(pos) c h ec k ( p os ) 最后一个失败的后一个的位置
可以使用二分查询法
什么题目使用
第一个⩾ x \geqslant x ⩾ x 元素的位置
第一个= x = x = x 元素的位置
最后一个⩽ x \leqslant x ⩽ x 元素的位置
求解数字x x x 的数量
求解满足数字v a l ∈ [ l , r ] val \in [l,r] v a l ∈ [ l , r ] 的数量
经典模型
练习题目
luogu P1083
luogu P2678
luogu P1314
luogu P1868
luogu P1493
hdu 6231
poj 3273
poj 3258
poj 1905
poj 3122
luogu P1419 二分+单调队列