二分

二分

二分是一种基于一个具有非严格单调性的序列上进行搜索的算法,其复杂度为 \(O(logn)\),在单调性的前提下,效率碾压遍历不知道多少倍。

原理

我们以下图中长度 \(n=10\) 的非严格单调递增序列 \(P\) 为例

假设此时我们询问一个数 \(x\),我们需要在序列中找到一个数 \(y\) ,使得 \(y\) 是整个序列中所有大于等于 \(x\) 的树中最小的,即 \(y=\min\left \{ i \right \}[i \in P ,i \ge x]\)

现在思考如何找到这个数 \(y\)

一种极其明显的思路是,从 \(i=1\) 处开始遍历至 \(i=10\) 处,遍历过程中实时维护变量 \(sum\)

for(int i=1;i<=10;i++)
{
	if(P[i]>=x) sum=min(P[i],sum);
}

当然由于序列 \(P\) 单调递增,你完全可以在找到第一个大于等于 \(x\) 的数后立刻退出循环,但是尽管如此也不能改变复杂度为 \(O(n)\) 的事实。

如果我们想 \(O(logn)\) 地找 \(y\) 怎么办?

二分!

现在我们定义两个指针变量 \(L=1,R=10\),代表此时答案会包含在此区间内,同时算出区间 \([L,R]\) 的中点 \(mid\) (向下取整),如图

此时我们判断 \(mid\) 所在值 \(P[mid]\) 是否满足大于等于 \(x\) ,若满足,则根据序列 \(P\) 单调递增的性质,可得大于 \(x\) 中最小的树必定在区间 \([mid,R]\) 中,此刻我们更新 \(L=mid\),并进入下一次二分

若不满足,则答案必定位于区间 \([L,mid-1]\),为什么是 \(mid-1\)?,因为值 \(P[mid]\) 并不满足我们的条件,因此在下一次二分时,我们的答案区间不能包含 \(mid\)。此时更新 \(R=mid-1\)

那么我们如何知道是否找到答案了呢?

在上面的过程可知,我们二分的区间总满足 \(L<R\) ,这是必然的,若 \(L=R\) 了,则证明二分结束,找到答案或答案不存在,因此我们要用一个while语句将二分过程囊括起来,而while的循环条件就设为 \(L<R\)。因为最终 \(L=R\) ,所以我们只需要输出 \(L\) 即可。且此刻 \(\forall i < mid\),有 \(P[i] < x\)\(\forall i \ge mid\),有 \(p[i] > x\)

最终的答案会有三种情况:

  • \(L\in [1,n]\)

证明答案存在

  • \(L > n\)

证明答案不存在,且此时序列 \(P\) 中所有值都小于 \(x\)

  • \(R = 1\)

此刻你需要判断 \(P[R]\) 是否大于等于 \(x\),以此判断答案的存在性。(事实上你可以在最初初始化指针变量 \(L,R\) 时把 \(L\) 设为 \(0\),最终答案不存在的情况就会变成 \(R=0\)

代码

二分有多种不同的模板,其区别大多仅在于循环条件与 \(mid\) 计算上,使用时需要根据具体情况合理修改条件即可。

以下是上面的例子的代码,也是最通用的二分模板

int L=0,R=n;
while(L<R)
{
	int mid=(L+R)/2;//or mid=(L+R)>>1;
	if(P[mid]>=x) L=mid;
	else R=mid-1;
}
printf("%d",P[L]);

例题

(updating......)

posted @ 2023-10-17 08:23  An_Easy_Song  阅读(63)  评论(0)    收藏  举报