二分法

定义

二分查找(英语:binary search),也称折半搜索(英语:half-interval search)、对数搜索(英语:logarithmic search),是用来在一个有序数组中查找某一元素的算法。

过程

以在一个升序数组中查找一个数为例。

它每次考察数组当前部分的中间元素,如果中间元素刚好要找的,就结束搜索过程;如果中间元素小于查找的值,那么左侧的只会更小,不会有所查找的元素,只需到右侧查找;如果中间元素大于所查找的值同理,只需到左侧查找。

性质

时间复杂度

二分查找的最优时间复杂度为 \(O(1)\)。

二分查找的平均时间复杂度和最坏时间复杂度均为 O(\log n)。因为在二分搜索过程中,算法每次都把查询的区间减半,所以对于一个长度为 \(n\) 的数组,至多会进行 \(O(\log n)\) 次查找。

空间复杂度

迭代版本的二分查找的空间复杂度为 \(O(1)\)。

递归(无尾调用消除)版本的二分查找的空间复杂度为 \(O(\log n)\)。

实现

int binary_search(int start, int end, int key) {
int ret = -1; // 未搜索到数据返回-1下标
int mid;
while (start <= end) {
mid = start + ((end - start) >> 1); // 直接平均可能会溢出,所以用这个算法
if (arr[mid] < key)
start = mid + 1;
else if (arr[mid] > key)
end = mid - 1;
else { // 最后检测相等是因为多数搜索情况不是大于就是小于
ret = mid;
break;
}
}
return ret; // 单一出口
}

最大值最小化

注意,这里的有序是广义的有序,如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看做是一种有序(如果把满足天剑看做 \(1\),不满足看做 \(0\),至少对于这个条件的这一维度是有序的)。换言之,二分搜索法可以用来满足某种条件的最大(最小)的值。

要求满足某种条件的最大值的最小可能情况(最大值最小化),首先的想法是从小到大枚举这个作为答案的 \(\lceil\) 最大值 \(\rfloor\),然后去判断是否合法。若答案单调,就可以使用二分搜索法来更快地找到答案。因此,要想使用二分搜索法来解这种 \(\lceil\) 最大值最小化 \(\rfloor\) 的题目,需要满足以下 \(3\) 个条件:

  1. 答案在一个固定区间内;
  2. 可能查找一个符合条件的值不是很容易,但是要求能比较容易得判断某个值是否是符合条件的;
  3. 可行解对于区间满足一定的单调性。换而言之,如果 \(x\) 是符合条件的,那么有 \(x+1\) 或者 \(x-1\) 也符合条件。(这样下来就满足了上面提到的单调性)

当然,最小值最大化也是同理的。

STL的二分查找

c++ 标准库中实现了查找首个不小于(大于等于)给定值的元素的函数 std::lower_bound 和查找首个大于给定值的元素的函数 std::upper_bound,二者均定义于头文件 <algorithm>

二者均采用二分实现,所以调用前必须保证元素有序。

二分答案

解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。若满足单调性,则满足使用二分法的条件。把这里的枚举换成二分,就变成了 \(\lceil\) 二分答案 \(\rfloor\)。

例题:洛谷P1873

解题思路:

我们可以在 \(1\sim10^9\) 中枚举答案,但是这种朴素写法坑定拿不到满分,因为从 \(1\) 枚举到 \(10^9\) 太耗时间。我们可以再 \(1\sim10^9]\) 的区间上进行二分作为答案,然后检查各个答案的可行性(一半使用贪心法)。这就是二分答案

参考解法:

#include<bits/stdc++.h>

using namespace std;

long long n, m, a[1000005], ma = -1;

bool check(int x) {
long long sum = 0;
for (int i = 1; i <= n; i++) {
if (a[i] >= x) sum += a[i] - x;
}
return sum < m;
} int main() {
scanf("%lld%lld", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
}
int i = -1, j = 1e9+1;
while (i + 1 < j) {
int mid = (i + j) / 2;
if (check(mid)) j = mid;
else i = mid;
}
printf("%lld", i);
return 0;
}

整体二分

引入

在信息学竞赛中,有一部分题目可以使用二分的办法来解决。但是当这种题目有多次询问且我们每次查询都直接二分可能导致TLE是,就会用到整体二分。整体二分的主题思路就是把多个查询一起解决。(所以这是一个离线算法)

可以使用整体二分解决的题目需要满足以下性质:

  1. 询问的答案具有可二分性
  2. 修改对判定答案的贡献互相独立,修改之间互补影响效果
  3. 修改如果对判定答案有贡献,则贡献为一确定的与判定标准无关的值
  4. 贡献满足交换律、结合律、具有可加性
  5. 题目允许使用离线算法

——许昊然《浅谈数据结构题几个非经典解法》

解释

记 \([l,r]\) 为答案的值域,\([L,R]\) 为答案的定义域。(也就是说求答案是仅考虑下标在区间 \([L,R]\) 内大操作和询问,这其中询问的答案在 \([l,r]\) 内)

  • 我们首先把所有操作按时间顺序存入数组中,然后开始分治,
  • 在每一层分治中,利用数据结构(常见的是树状数组)统计当前查询的答案和 \(mid\) 之间的关系。
  • 根据查询出来的答案和 \(mid\) 间的关系(小于等于 \(mid\) 和大于 \(mid\))将当前处理的操作序列氛围 \(q1\) 和 \(q2\) 两份,并分别递归处理。
  • 当 \(l=r\) 是,找到答案,记录答案并返回即可。

需要注意的是,在整体二分中,若当前处理的值域为 \([l,r]\),则此时最终答案不在 \([l,r]\) 的询问会在其他时候处理。

过程

从普通二分说起:

查询全局第 \(k\) 小

题1 在一个数列中查询第 \(k\) 小的数。

当然可以直接排序。如果用二分,可以用数据结构记录每个大小范围内有多少个数,然后用二分猜测,利用数据结构检验。

题2 在一个数列中多次查询第 \(k\) 小的数。

可以对于每个询问进行一次二分;但是,也可以把所有的询问放在一起二分。

先考虑二分的本质:假设要猜一个 \([l,r]\) 之间的数,猜测之后会知道是猜大了,猜小了还是刚好。当然可以从 \(l\) 枚举到 \(r\),但更优秀的方法是二分:猜测答案是

$ m = \lfloor\frac{l + r}{2}\rfloor$,然后去验证 m 的正确性,再调整边界。这样做每次询问的复杂度为 \(O(\log n)\),若询问次数为 \(q\),则时间复杂度为 \(O(q\log n)\)。

回过头来,对于当前的所有询问,可以去猜测所有询问的答案都是 \(mid\),然后去依次验证每个询问的答案应该是小于等于 \(mid\) 的还是大于 \(mid\) 的,并将询问分为两个部分(不大于/大于),对于每个部分继续二分。注意:如果一个询问的答案是大于 \(mid\) 的,则在将其划至右侧前需更新它的 \(k\),即,如果当前数列中小于等于 \(mid\) 的数有 \(t\) 个,则将询问划分后实际是在右区间询问第 \(k-t\) 小数。如果一个部分的 \(l=r\) 了,则结束这个部分的二分。利用线段树的相关知识,我们每次将整个答案可能在的区间 \([1,maxans]\) 划分成了若干个部分,这样的划分共进行了 \(O(\log maxans)\) 次,一次划分会将整个操作序列操作一次。若对整个序列进行操作,并支持对应的查询的时间复杂度为 \(O(T)\),则整体二分的时间复杂度为 \(O(T\log n)\)。

参考代码:

struct Query {
int id, k; // 这个询问的编号, 这个询问的k
}; int ans[N], a[N]; // ans[i] 表示编号为i的询问的答案,a 为原数列 int check(int l, int r) {// 返回原数列中值域在 [l,r] 中的数的个数
int res = 0;
for (int i = 1; i <= n; i++) {
if (l <= a[i] && a[i] <= r) res++;
}
return res;
} void solve(int l, int r, vector<Query> q)
{
int m = (l + r) / 2;
vector<Query> q1, q2; // 将被划到左侧的询问和右侧的询问
if (l == r) {
for (unsigned i = 0; i < q.size(); i++) ans[q[i].id] = l;
return;
return;
}
int t = check(l, m);
for (unsigned i = 0; i < q.size(); i++) {
if (q[i].k <= t){
q1.push_back(q[i]);
}else{
q[i].k -= t, q2.push_back(q[i]);
}
}
solve(l, m, q1), solve(m + 1, r, q2);
return;
}

查询区间第 \(k\) 小

题3 在一个数列中多次查询区间第 \(k\) 小的数。

涉及到给定区间的查询,再按之前的方法进行二分就会导致 \(check\) 函数的时间复杂度爆炸。仍然考虑询问与值域中点 \(m\) 的关系:若询问区间内小于等于 \(m\) 的数有 \(t\) 个,询问的是区间内的 \(k\) 小数,则当 \(k\leq t\) 时,答案应小于等于 \(m\);否则,答案应大于 \(m\)。(注意边界问题)此处需记录一个区间小于等于指定数的数的数量,即单点加,求区间和,可用树状数组快速处理。为提高效率,只对数列中值在值域区间 \([l,r]\) 的数进行统计,即,在进一步递归之前,不仅将询问划分,将当前处理的数按值域范围划为两半。

参考代码(关键部分):

struct Num {
int p, x;
}; // 位于数列中第 p 项的数的值为 x struct Query {
int l, r, k, id;
}; // 一个编号为 id, 询问 [l,r] 中第 k 小数的询问 int ans[N];
void add(int p, int x); // 树状数组, 在 p 位置加上 x
int query(int p); // 树状数组, 求 [1,p] 的和
void clear(); // 树状数组, 清空 void solve(int l, int r, vector<Num> a, vector<Query> q)
// a中为给定数列中值在值域区间 [l,r] 中的数
{
int m = (l + r) / 2;
if (l == r) {
for (unsigned i = 0; i < q.size(); i++) ans[q[i].id] = l;
return;
}
vector<Num> a1, a2;
vector<Query> q1, q2;
for (unsigned i = 0; i < a.size(); i++)
if (a[i].x <= m)
a1.push_back(a[i]), add(a[i].p, 1);
else
a2.push_back(a[i]);
for (unsigned i = 0; i < q.size(); i++) {
int t = query(q[i].r) - query(q[i].l - 1);
if (q[i].k <= t)
q1.push_back(q[i]);
else
q[i].k -= t, q2.push_back(q[i]);
}
clear();
solve(l, m, a1, q1), solve(m + 1, r, a2, q2);
return;
}

下面提供洛谷P3834【模板】可持久化线段树 2 一题使用整体二分,偏向竞赛风格的写法。

#include<bits/stdc++.h>

using namespace std;
constexpr int N = 2e5 + 10;
int n, m, ans[N], t[N], a[N],toRaw[N],tot;
pair<int, int> b[N]; int sum(int p) {
int ans = 0;
while (p) {
ans += t[p];
p -= p & (-p);
}
return ans;
} void add(int p, int x) {
while (p <= n) {
t[p] += x;
p += p & (-p);
}
return ;
} struct node {
int l, r, k, id, type;
} q[N * 2], q1[N * 2], q2[N * 2]; void solve(int l, int r, int ql, int qr) {
if (ql > qr) return ;
if (l == r) {
for (int i = ql; i <= qr; i++){
if (q[i].type == 2) ans[q[i].id] = l;
}
return ;
}
int mid = (l + r) / 2, cnt1 = 0, cnt2 = 0;
for (int i = ql; i <= qr; i++) {
if (q[i].type == 1) {
if (q[i].l <= mid) {
add(q[i].id, 1);
q1[++cnt1] = q[i];
} else{
q2[++cnt2] = q[i];
}
} else {
int x = sum(q[i].r) - sum(q[i].l - 1);
if (q[i].k <= x)
q1[++cnt1] = q[i];
else {
q[i].k -= x;
q2[++cnt2] = q[i];
}
}
}
for (int i = 1; i <= cnt1; i++) {
if (q1[i].type == 1) {
add(q1[i].id, -1);
}
}
for (int i = 1; i <= cnt1; i++) {
q[i + ql - 1] = q1[i];
}
for (int i = 1; i <= cnt2; i++) {
q[i + cnt1 + ql - 1] = q2[i];
}
solve(l, mid, ql, cnt1 + ql - 1);
solve(mid + 1, r, cnt1 + ql, qr);
return ;
} int main() {
cin >> n >> m;
for (int i = 1,x; i <= n; i++) {
cin >> x;
b[i].first = x;
b[i].second = i;
}
sort(b + 1, b + n + 1);
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (b[i].first != b[i - 1].first) cnt++;
a[b[i].second] = cnt;
toRaw[cnt] = b[i].first;
}
for (int i = 1; i <= n; i++) {
q[++tot] = {a[i], -1, -1, i, 1};
}
for (int i = 1; i <= m; i++) {
int l, r, k;
cin >> l >> r >> k;
q[++tot] = {l, r, k, i, 2};
}
solve(0, cnt + 1, 1, tot);
for (int i = 1; i <= m; i++) {
cout << toRaw[ans[i]] << '\n';
}
return 0;
}

带修改区间第 \(k\) 小

题4 给定一个数列,要支持单点修改,区间查第 \(k\) 小。

修改操作可以直接理解为从原数列中删去一个数再添加一个数,为方便起见,将询问和修改统称为 \(\lceil\) 操作 \(\rfloor\)。因后面的操作会依附于之前的操作,不能如题 3 一样将统计和处理询问分开,故可将所有操作存于一个数组,用标识区分类型,依次处理每个操作。为便于处理树状数组,修改操作可分拆为擦除操作和插入操作。

优化

  1. 注意到每次对于操作进行分类是,只会更爱操作顺序,故可直接在原数组上操作。具体实现,在二分是将记录操作的 \(q,a\) 数组换位一个大的全局数组,二分是记录信息变为 \(L,R\),记当前处理的操作是全局数组上的哪个区间。利用临时数组记录当前的分类情况,进一步递归前将临时数组信息协会原数组。
  2. 树状数组每次清空都会复杂度爆炸,可采用每次使用树状数组是记录当前修改位置(这已由1中提到的临时数组实现),本次操作结束后在原数组加 \(-1\) 的方法快速清零。
  3. 一开始对于数列的初始化操作可简化插入操作。

三分法

引入

如果需要求出单峰函数的极值点,通常使用二分法衍生出的三分法求单峰函数的极值点。

三分法与二分法的基本思想类似,但每次操作需在当前区间 \([l,r]\)(下图中除去虚线范围内的部分)内任取两点 \(lmid,rmid(lmid<rmid)\)(下图中的两个蓝点)。如下图,如果 \(f(lmid)<f(rmid)\),则在 \([rmid,r]\)(下图中的红色部分)中函数必然单调递增,最小值所在点(下图中的绿点)必然不在这一区间内,可舍去这一区间。反之亦然。

三分法每次操作会舍去两侧区间中的其中一个。为减少三分法的操作次数,应使两侧区间尽可能大。因此,每一次操作时的 \(lmid\) 和 \(rmid\) 分别取 \(mid-\varepsilon\) 和 \(mid-\varepsilon\) 是一个不错的选择。

实现

while (r - l > eps) {
mid = (l + r) / 2;
lmid = mid - eps;
rmid = mid + eps;
if (f(lmid) < f(rmid))
r = mid;
else
l = mid;
}

例题

例题:洛谷P3382

解题思路

本题要求求 \(N\) 次函数在 \([l,r]\) 取最大值时自变量的值,显然可以使用三分法。

参考代码

#include<bits/stdc++.h>

using namespace std;
const double eps = 1e-7;
int n;
double l, r, a[20], mid, lmid, rmid; double f(double x) {
double res = 0;
for (int i = n; i >= 0; i--) {
res += a[i] * pow(x, i);
}
return res;
} int main() {
cin >> n >> l >> r;
for (int i = n; i >= 0; i--){
cin >> a[i];
}
while (r - l > eps) {
mid = (l + r) / 2;
lmid = mid - eps;
rmid = mid + eps;
if (f(lmid) > f(rmid)){
r = mid;
}else{
l = mid;
}
}
// cout << fixed << setprecision(6) << l;
printf("%.6lf",l);
return 0;
}

[学习笔记] 二分答案&三分答案(写了我就能拿3分?)的更多相关文章

  1. java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

    java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...

  2. ELK学习笔记之logstash将配置写在多个文件

    0x00 概述 我们用Logsatsh写配置文件的时候,如果读取的文件太多,匹配的正则过多,会使配置文件动辄成百上千行代码,可能会造成阅读和修改困难.这时候,我们可以将配置文件的输入.过滤.输出分别放 ...

  3. SVM学习笔记(二)----手写数字识别

    引言 上一篇博客整理了一下SVM分类算法的基本理论问题,它分类的基本思想是利用最大间隔进行分类,处理非线性问题是通过核函数将特征向量映射到高维空间,从而变成线性可分的,但是运算却是在低维空间运行的.考 ...

  4. .net学习笔记---webconfig的读与写

    System.ConfigurationManager类用于对配置文件的读取.其具有的成员如下: 一.AppSettings AppSetting是最简单的配置节,读写非常简单. 名称 说明 AppS ...

  5. java jvm学习笔记五(实践自己写的类装载器)

     欢迎装载请说明出处:http://blog.csdn.net/yfqnihao 课程源码:http://download.csdn.net/detail/yfqnihao/4866501 前面第三和 ...

  6. 【DM642学习笔记四】flash烧写过程——错误记录…

    (欢迎批评指正) 一,打开.cdd配置文件时出错: 解决:在FlashBurn配置窗口中,Conversion Cmd一栏可不用管:      菜单Program—Download FBTC,load ...

  7. 《NVM-Express-1_4-2019.06.10-Ratified》学习笔记(6.15)-- 写命令

    6.15 Write command 写命令 写命令写数据和元数据,如果适用介质,发到逻辑块相应的I/O controller.主机也可以指定保护信息,作为操作的一部分包含进来. 命令用Command ...

  8. 这篇SpringBoot整合JSON的学习笔记,建议收藏起来,写的太细了

    前言 JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式,目前使用特别广泛. 采用完全独立于编程语言的文本格式来存储和表示数据. 简洁和清晰 ...

  9. $Min\_25$筛学习笔记

    \(Min\_25\)筛学习笔记 这种神仙东西不写点东西一下就忘了QAQ 资料和代码出处 资料2 资料3 打死我也不承认参考了yyb的 \(Min\_25\)筛可以干嘛?下文中未特殊说明\(P\)均指 ...

  10. OI知识点|NOIP考点|省选考点|教程与学习笔记合集

    点亮技能树行动-- 本篇blog按照分类将网上写的OI知识点归纳了一下,然后会附上蒟蒻我的学习笔记或者是我认为写的不错的专题博客qwqwqwq(好吧,其实已经咕咕咕了...) 基础算法 贪心 枚举 分 ...

随机推荐

  1. .NET + AI | Semantic Kernel vs Microsoft.Extensions.AI

    Microsoft.Extensions.AI 在 .NET AI 应用架构中的定位示意图:应用程序通过 Microsoft.Extensions.AI 调用下层各种 AI 服务(如 Semantic ...

  2. 信息资源管理综合题之“LJ集团信息化项目规划问题”

    一.LJ集团是北京的一家规模巨大的房地产投资公司,早在15年前,该公司出现了如下几个问题:每个业务员手上的用户资料,其他人无法得知,从而导致员工离职时会流失大量潜在客户:业务员繁忙的时候,无法满足客户 ...

  3. SQL 强化练习 (二)

    继续 sql 搞起来, 面向过程来弄, 重点是分析的思路, 涉及的的 left join, inner join, group by +_ having, case when ... 等场景, 也是比 ...

  4. 网络编程:CMD命令

    要求: 写一个客户端程序和服务器程序,客户端程序连接上服务器之后,通过敲命令和服务器进行交互,支持的交互命令包括: pwd:显示服务器应用程序启动时的当前路径. cd:改变服务器应用程序的当前路径. ...

  5. Qt图像处理技术四:图像二值化

    Qt图像处理技术四:图像二值化 github 如果你觉得有用的话,期待你的小星星 实战应用项目: github :https://github.com/dependon/simple-image-fi ...

  6. 用bat脚本启动和停止系统服务,如oracle等

    启动脚本 启动oracle.bat :: 取得管理员权限 :Main @echo off cd /d "%~dp0" cacls.exe "%SystemDrive%\S ...

  7. python基础—内置函数

    1.数学运算类 (1).abs(x) 求绝对值,参数x可以是整形,也可也是复数,如果是复数,则返回复数的模 abs(-1) >> 1 (2).divmod(x,y) 返回两个数值的商和余数 ...

  8. helm常用操作整理

    说明 下面是整理的日常常用的一些helm操作,后面会持续更新 下载chart到本地 helm repo add bitnami https://charts.bitnami.com/bitnami # ...

  9. 袋鼠云秋季发布会圆满落幕,AI驱动让生产力数智化

    在当今时代,AI 的发展如汹涌浪潮,其速度之快超越了任何历史时期.它以前所未有的迅猛之势,渗入到各个领域的不同场景之中,悄然重塑着商业模式与人们的生活方式. 在 AI 逐渐成为企业基础属性的背景下,袋 ...

  10. 大数据计算引擎 EasyMR 如何简单高效管理 Yarn 资源队列

    设想一下,作为一个开发人员,你现在所在的公司有一套线上的 Hadoop 集群.A部门经常做一些定时的 BI 报表,B部门则经常使用软件做一些临时需求.那么他们肯定会遇到同时提交任务的场景,这个时候到底 ...