[学习笔记] 二分答案&三分答案(写了我就能拿3分?)
二分法
定义
二分查找(英语: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\) 个条件:
- 答案在一个固定区间内;
- 可能查找一个符合条件的值不是很容易,但是要求能比较容易得判断某个值是否是符合条件的;
- 可行解对于区间满足一定的单调性。换而言之,如果 \(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是,就会用到整体二分。整体二分的主题思路就是把多个查询一起解决。(所以这是一个离线算法)
可以使用整体二分解决的题目需要满足以下性质:
- 询问的答案具有可二分性
- 修改对判定答案的贡献互相独立,修改之间互补影响效果
- 修改如果对判定答案有贡献,则贡献为一确定的与判定标准无关的值
- 贡献满足交换律、结合律、具有可加性
- 题目允许使用离线算法
——许昊然《浅谈数据结构题几个非经典解法》
解释
记 \([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 一样将统计和处理询问分开,故可将所有操作存于一个数组,用标识区分类型,依次处理每个操作。为便于处理树状数组,修改操作可分拆为擦除操作和插入操作。
优化
- 注意到每次对于操作进行分类是,只会更爱操作顺序,故可直接在原数组上操作。具体实现,在二分是将记录操作的 \(q,a\) 数组换位一个大的全局数组,二分是记录信息变为 \(L,R\),记当前处理的操作是全局数组上的哪个区间。利用临时数组记录当前的分类情况,进一步递归前将临时数组信息协会原数组。
- 树状数组每次清空都会复杂度爆炸,可采用每次使用树状数组是记录当前修改位置(这已由1中提到的临时数组实现),本次操作结束后在原数组加 \(-1\) 的方法快速清零。
- 一开始对于数列的初始化操作可简化插入操作。
三分法
引入
如果需要求出单峰函数的极值点,通常使用二分法衍生出的三分法求单峰函数的极值点。
三分法与二分法的基本思想类似,但每次操作需在当前区间 \([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分?)的更多相关文章
- java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)
java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...
- ELK学习笔记之logstash将配置写在多个文件
0x00 概述 我们用Logsatsh写配置文件的时候,如果读取的文件太多,匹配的正则过多,会使配置文件动辄成百上千行代码,可能会造成阅读和修改困难.这时候,我们可以将配置文件的输入.过滤.输出分别放 ...
- SVM学习笔记(二)----手写数字识别
引言 上一篇博客整理了一下SVM分类算法的基本理论问题,它分类的基本思想是利用最大间隔进行分类,处理非线性问题是通过核函数将特征向量映射到高维空间,从而变成线性可分的,但是运算却是在低维空间运行的.考 ...
- .net学习笔记---webconfig的读与写
System.ConfigurationManager类用于对配置文件的读取.其具有的成员如下: 一.AppSettings AppSetting是最简单的配置节,读写非常简单. 名称 说明 AppS ...
- java jvm学习笔记五(实践自己写的类装载器)
欢迎装载请说明出处:http://blog.csdn.net/yfqnihao 课程源码:http://download.csdn.net/detail/yfqnihao/4866501 前面第三和 ...
- 【DM642学习笔记四】flash烧写过程——错误记录…
(欢迎批评指正) 一,打开.cdd配置文件时出错: 解决:在FlashBurn配置窗口中,Conversion Cmd一栏可不用管: 菜单Program—Download FBTC,load ...
- 《NVM-Express-1_4-2019.06.10-Ratified》学习笔记(6.15)-- 写命令
6.15 Write command 写命令 写命令写数据和元数据,如果适用介质,发到逻辑块相应的I/O controller.主机也可以指定保护信息,作为操作的一部分包含进来. 命令用Command ...
- 这篇SpringBoot整合JSON的学习笔记,建议收藏起来,写的太细了
前言 JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式,目前使用特别广泛. 采用完全独立于编程语言的文本格式来存储和表示数据. 简洁和清晰 ...
- $Min\_25$筛学习笔记
\(Min\_25\)筛学习笔记 这种神仙东西不写点东西一下就忘了QAQ 资料和代码出处 资料2 资料3 打死我也不承认参考了yyb的 \(Min\_25\)筛可以干嘛?下文中未特殊说明\(P\)均指 ...
- OI知识点|NOIP考点|省选考点|教程与学习笔记合集
点亮技能树行动-- 本篇blog按照分类将网上写的OI知识点归纳了一下,然后会附上蒟蒻我的学习笔记或者是我认为写的不错的专题博客qwqwqwq(好吧,其实已经咕咕咕了...) 基础算法 贪心 枚举 分 ...
随机推荐
- .NET + AI | Semantic Kernel vs Microsoft.Extensions.AI
Microsoft.Extensions.AI 在 .NET AI 应用架构中的定位示意图:应用程序通过 Microsoft.Extensions.AI 调用下层各种 AI 服务(如 Semantic ...
- 信息资源管理综合题之“LJ集团信息化项目规划问题”
一.LJ集团是北京的一家规模巨大的房地产投资公司,早在15年前,该公司出现了如下几个问题:每个业务员手上的用户资料,其他人无法得知,从而导致员工离职时会流失大量潜在客户:业务员繁忙的时候,无法满足客户 ...
- SQL 强化练习 (二)
继续 sql 搞起来, 面向过程来弄, 重点是分析的思路, 涉及的的 left join, inner join, group by +_ having, case when ... 等场景, 也是比 ...
- 网络编程:CMD命令
要求: 写一个客户端程序和服务器程序,客户端程序连接上服务器之后,通过敲命令和服务器进行交互,支持的交互命令包括: pwd:显示服务器应用程序启动时的当前路径. cd:改变服务器应用程序的当前路径. ...
- Qt图像处理技术四:图像二值化
Qt图像处理技术四:图像二值化 github 如果你觉得有用的话,期待你的小星星 实战应用项目: github :https://github.com/dependon/simple-image-fi ...
- 用bat脚本启动和停止系统服务,如oracle等
启动脚本 启动oracle.bat :: 取得管理员权限 :Main @echo off cd /d "%~dp0" cacls.exe "%SystemDrive%\S ...
- python基础—内置函数
1.数学运算类 (1).abs(x) 求绝对值,参数x可以是整形,也可也是复数,如果是复数,则返回复数的模 abs(-1) >> 1 (2).divmod(x,y) 返回两个数值的商和余数 ...
- helm常用操作整理
说明 下面是整理的日常常用的一些helm操作,后面会持续更新 下载chart到本地 helm repo add bitnami https://charts.bitnami.com/bitnami # ...
- 袋鼠云秋季发布会圆满落幕,AI驱动让生产力数智化
在当今时代,AI 的发展如汹涌浪潮,其速度之快超越了任何历史时期.它以前所未有的迅猛之势,渗入到各个领域的不同场景之中,悄然重塑着商业模式与人们的生活方式. 在 AI 逐渐成为企业基础属性的背景下,袋 ...
- 大数据计算引擎 EasyMR 如何简单高效管理 Yarn 资源队列
设想一下,作为一个开发人员,你现在所在的公司有一套线上的 Hadoop 集群.A部门经常做一些定时的 BI 报表,B部门则经常使用软件做一些临时需求.那么他们肯定会遇到同时提交任务的场景,这个时候到底 ...