整体二分

news/发布时间2024/5/18 23:43:19

1 概念

在很多题目中,我们可以使用二分法来得出答案。但是如果说这一类题目有多次询问,并且多次询问分别二分会 TLE 时,我们就需要引入一个东西叫整体二分。

整体二分的主要思路就是将多个查询一起解决,因此它是一种离线算法。

整体二分的具体操作步骤如下:

首先记 \([l,r]\) 为答案的值域,\([L,R]\) 是答案的定义域。这代表着我们在求答案时考虑下标在 \([L,R]\) 上的操作,这当中的询问的答案都在 \([l,r]\)

首先我们现将所有操作按照时间轴存入数组,然后开始分治。在每一层分治中,我们利用一些东西统计当前查询的答案和 \(mid\) 的关系。

根据这个关系(小于等于 \(mid\) 和大于 \(mid\)),我们将操作序列分成两半,然后递归处理。

那么我们通过例题来具体了解整体二分的过程。

2 基础例题

2.1 静态全局第 k 小

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

显然我们可以直接排序。那如果用二分呢?我们可以二分数字,然后查询这个数字的排名;这样看上去有点多此一举,我们看下一题。

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

我们可以分开二分,但是也可以放在一起二分。

首先我们可以假设当前所有询问的答案都是 \(mid\),然后我们一次判断真正的答案与 \(mid\) 的关系。也就是应该小于等于 \(mid\) 还是大于 \(mid\),并分成两个部分。假如原先我们查询的值域为 \([l,r]\),那么现在两个区间的值域就是 \([l,mid],(r,mid]\)。在值域里继续二分查找,直到 \(l=r\)

可以理解为我们本来是一个一个二分,现在我们将他们放到一起同时做,这样可以省去当中重复运算的时间。

2.2 静态区间第 k 小

我们来看一道模板题:【模板】可持久化线段树 2。我们发现这是一道静态查询区间第 \(k\) 小问题,可以考虑整体二分。

我们在每一次二分中利用树状数组记录下当前区间内小于等于 \(mid\) 的数有哪些,用这个来帮助计算区间中小于等于指定数的数量。同时,为了提高效率,我们可以在统计时只对值域在 \([l,r]\) 之间的数进行统计,将他们单独拿出来之后在上面做二分。

时间复杂度 \(O(n\log ^2 n)\),比主席树 \(O(n\log n)\) 较劣。但是仍然可以过掉上面的模板题。

#include <bits/stdc++.h>using namespace std;typedef long long LL;
const int Maxn = 5e5 + 5;int n, m;
int mn = 2e9, mx;
struct node {int opt, l, r, k, id;
}q[Maxn], q1[Maxn], q2[Maxn];int tot = 0;
int ans[Maxn];struct BIT {int c[Maxn];int lowbit(int x) {return x & (-x);}void mdf(int x, int val) {for(int i = x; i <= n; i += lowbit(i)) {c[i] += val;}}int query(int x) {int sum = 0;for(int i = x; i; i -= lowbit(i)) {sum += c[i];}return sum;}
}B;void obs(int l, int r, int pl, int pr) {//[l,r] 是答案值域,[pl,pr] 是当前二分的查询区间if(pl > pr) return ;if(l == r) {for(int i = pl; i <= pr; i++) {//答案全部为 lif(q[i].opt == 2) {ans[q[i].id] = l;}}return ;}int mid = (l + r) >> 1, p1 = 0, p2 = 0;for(int i = pl; i <= pr; i++) {if(q[i].opt == 1) {//是修改操作if(q[i].k <= mid) {//与 mid 比较B.mdf(q[i].id, 1);//更新树状数组q1[++p1] = q[i];//比 mid 小的放到左半部分}else {q2[++p2] = q[i];//比 mid 大的放到右半部分}}else {int x = B.query(q[i].r) - B.query(q[i].l - 1);//查询当前区间内 mid 的排名if(q[i].k <= x) {q1[++p1] = q[i];//比 mid 小的放到左半部分} else {q[i].k -= x;//注意右半部分在计算之前要减掉左半部分的贡献q2[++p2] = q[i];//比 mid 大的放到右半部分}}}for(int i = 1; i <= p1; i++) {if(q1[i].opt == 1) {B.mdf(q1[i].id, -1);}}for(int i = 1; i <= p1; i++) {q[pl + i - 1] = q1[i];}for(int i = 1; i <= p2; i++) {q[pl + p1 + i - 1] = q2[i];}obs(l, mid, pl, pl + p1 - 1);obs(mid + 1, r, pl + p1, pr);//分治求解
}int main() {ios::sync_with_stdio(0);cin >> n >> m;for(int i = 1; i <= n; i++) {int p;cin >> p;mn = min(mn, p), mx = max(mx, p);q[++tot] = {1, -1, -1, p, i};}for(int i = 1; i <= m; i++) {int l, r, k;cin >> l >> r >> k;q[++tot] = {2, l, r, k, i};}obs(mn, mx, 1, tot);for(int i = 1; i <= m; i++) {cout << ans[i] << '\n';}return 0;
}

二维区间最小值例题:[国家集训队] 矩阵乘法。

2.3 带修区间第 k 小

例题:Dynamic Rankings。

我们发现这样一个问题:上面我们求静态区间第 k 小的时候已经将初始序列当做了插入操作,那么我们再做带修区间第 k 小的时候应该比较容易。

首先,一次修改操作可以看做是一次删除和一次插入操作组成的。而删除与查询操作本质上都是一样的,无非就是在树状数组上加一减一的区别。

代码与静态区间第 k 小的非常相似,如下:

#include <bits/stdc++.h>using namespace std;typedef long long LL;
const int Maxn = 5e5 + 5;int n, m, a[Maxn];
int mn = 2e9, mx;
struct node {int opt, l, r, k, id;
}q[Maxn], q1[Maxn], q2[Maxn];int tot, cnt;struct BIT {int c[Maxn];int lowbit(int x) {return x & (-x);}void mdf(int x, int val) {for(int i = x; i <= n; i += lowbit(i)) {c[i] += val;}}int query(int x) {int sum = 0;for(int i = x; i; i -= lowbit(i)) {sum += c[i];}return sum;}
}B;int ans[Maxn];void obs(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].opt == 3) {ans[q[i].id] = l;}}return ;}int mid = (l + r) >> 1, p1 = 0, p2 = 0;for(int i = ql; i <= qr; i++) {if(q[i].opt == 3) {int x = B.query(q[i].r) - B.query(q[i].l - 1);if(q[i].k <= x) {q1[++p1] = q[i];}else {q[i].k -= x;q2[++p2] = q[i];}}else {if(q[i].k <= mid) {if(q[i].opt == 1) B.mdf(q[i].id, 1);else B.mdf(q[i].id, -1);q1[++p1] = q[i]; }else {q2[++p2] = q[i];}}}for(int i = 1; i <= p1; i++) {if(q1[i].opt == 1) B.mdf(q1[i].id, -1);else if(q1[i].opt == 2) B.mdf(q1[i].id, 1);}for(int i = 1; i <= p1; i++) {q[ql + i - 1] = q1[i]; }for(int i = 1; i <= p2; i++) {q[ql + p1 + i - 1] = q2[i];}obs(l, mid, ql, ql + p1 - 1);obs(mid + 1, r, ql + p1, qr);
}int main() {ios::sync_with_stdio(0);cin >> n >> m;for(int i = 1; i <= n; i++) {int p;cin >> p;mn = min(mn, p), mx = max(mx, p);a[i] = p;q[++tot] = {1, -1, -1, p, i};}for(int i = 1; i <= m; i++) {char opt;int l, r, k;cin >> opt >> l >> r;if(opt == 'C') {mn = min(mn, r), mx = max(mx, r);q[++tot] = {2, -1, -1, a[l], l};q[++tot] = {1, -1, -1, r, l};a[l] = r;}else {cin >> k;q[++tot] = {3, l, r, k, ++cnt};} }obs(mn, mx, 1, tot);for(int i = 1; i <= cnt; i++) {cout << ans[i] << '\n';}return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ulsteruni.cn/article/03712533.html

如若内容造成侵权/违法违规/事实不符,请联系编程大学网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

关于diffusion model一些统计和数学的基础知识

likelihood-based models,通过(近似)最大似然直接学习分布的probability density(或mass)函数。典型的基于似然的模型包括自回归模型、归一化流模型、基于能量的模型(EBMs)和变分自编码器(VAEs)。 概率质量函数(Probability Mass Function,PMF):概率质量函数用于描述离散随…

AutoCAD C# 两不平行直线倒圆弧算法

参考的博客:https://www.cnblogs.com/JJBox/p/14300098.html 下面是计算示例主要计算代码:var peo = new PromptEntityOptions("选择直线1"){AllowNone = false,AllowObjectOnLockedLayer = false};peo.SetRejectMessage("请选择直线Line");peo.AddAllow…

2. 基础配置

1. 配置文件格式 1.1 配置文件自动提示功能消失解决方案 ​​ 1.2 SpringBoot配置文件加载顺序(了解) application.properties > application.yml > application.yaml 1.3 注意事项 SpringBoot核心配置文件名为application SpringBoot内置属性过多,且所有属性集中…

搭建自己的博客

基于github和Hexo 搭建自己的博客 【摘要】该教程基于个人的虚拟机和个人的GitHub,过程会详细注明对应的安装包的版本。 1、搭建hexo环境 环境配置 本地虚拟机:ubuntu 20.4(也可以基于对应的服务器) Hexo搭建步骤 1. 安装nodejs和npm 由于Ubuntu20通过apt安装nodejs默认只能…

06.数组

1.数组概述 数组是相同类型数据的有序集合; 数组描述的是相同类型的若干各数据,按照一定的先后次序排列组合而成; 其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们。 2.数组声明创建 首先必须声明数组变量,才能在程序中使用数组: dataType[] a…

.Net 8.0 下的新RPC,IceRPC之如何创建连接connection

作者引言很高兴啊,我们来到了IceRPC之如何创建连接connection,基础引导,让自已不在迷茫,快乐的畅游世界。如何创建连接connection学习如何使用IceRPC,创建和接受连接。连接有什么用途? 连接在 IceRPC 中发挥着核心作用: 通过连接向服务端发送请求,然后通过同一连接收到响应…