写在前面
从刚开始学平衡树的时候我就被平衡树的美感所打动,一直渴望着自己能够创造出一种新的平衡树并接受众人的膜拜。
标题这个 idea 大概是在 2020.9 左右学完分块的 (O(1)-O(sqrt n)) 求第 (k) 大后产生的。我认为每次查询时都要暴力枚举整块求和太蠢了,在考虑用各种奇怪的东西维护这样一个带修前缀权值区间和问题时,我想到对块建立一个 BST 的形式,于是就有了标题这玩意。
以下将对这种乱搞做法进行一些介绍。
这里提前说一下,这东西完全没有用。复杂度最优的情况是块大小为 1,即建一棵静态 BST 的时候。阅读下文可能会浪费您宝贵的 2 分钟时间。
问题
您需要写一种数据结构来维护一些数。
有 (n) 次操作,每种操作是下列 6 种之一:
- 插入 (x) 数。
- 删除 (x) 数(若有多个相同的数,因只删除一个)。
- 查询 (x) 数的排名(排名定义为比当前数小的数的个数 +(1))。
- 查询排名为 (x) 的数。
- 求 (x) 的前驱(前驱定义为小于 (x),且最大的数)。
- 求 (x) 的后继(后继定义为大于 (x),且最小的数)。
(1le nle 10^5)。
1S,128MB。
分块解法
这里介绍修改 (O(1)) 查询 (O(sqrt{n})) 的值域分块,在修改数大于查询数时可以获得更优的时间复杂度。
操作数量较少,先对出现的数离散化,设离散化后值域为 ([1,n])。查询数的排名与某排名对应的数,考虑对值域分块,设块大小为 (T)。维护每个数出现的次数,及值域分块后每块内所有数出现的个数。
操作 1,2,插入删除操作,(O(1)) 单点修改即可。
操作 3,查询排名操作,即查询该数左侧所有数的出现次数。整块直接查询,散块暴力,复杂度上界 (O(frac{n}{T} + T))。
操作 4,查询某排名对应的数,大力枚举即可。
从小到大枚举整块,累计维护值域内所有数出现次数之和。当累计值 + 最后一个枚举到的整块 (ge x) 时,说明答案就在该块中。再顺序枚举答案所在整块中的数,累计出现次数直至 (ge x) 即得。复杂度上界 (O(frac{n}{T} + T))。
操作 5,查询前驱。先枚举 (x) 所在散块内 (< x) 的数,检查是否存在。再枚举 (x) 之前的整块,直至找到一个内部有数的整块。答案就在该整块中,降序枚举找到最大的存在的数即可。复杂度上界 (O(frac{n}{T} + T))。
操作 6,查询后继,同操作 5 大力枚举,找到第一个存在的 (>x) 的数,复杂度相同。
设块大小为 (sqrt{n}),操作 3 ~ 6 的复杂度均为 (O(sqrt n)),总复杂度上界 (O(nsqrt{n}))。当修改次数 > 查询次数时复杂度较优。
参考代码:
//知识点:分块
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e5 + 10;
const int kMaxSqrtn = 320;
const int kInf = 1e9 + 2077;
//=============================================================
struct Operation {
int opt, x;
} q[kMaxn];
int n, block_size, block_num, L[kMaxSqrtn], R[kMaxSqrtn], bel[kMaxn];
int cnt[kMaxn], cntblock[kMaxSqrtn];
int data_num, max_data, data[kMaxn], map[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Prepare() { //离线操作,并离散化。
n = read();
for (int i = 1; i <= n; ++ i) {
q[i] = (Operation) {read(), read()};
if (q[i].opt != 4) data[++ data_num] = q[i].x; //注意操作 4 的参数不需要离散化。
}
data[0] = - kInf;
std :: sort(data + 1, data + data_num + 1);
for (int i = 1; i <= data_num; ++ i) {
if (data[i] != data[i - 1]) max_data ++;
data[max_data] = data[i];
}
for (int i = 1; i <= n; ++ i) {
if (q[i].opt == 4) continue;
int origin = q[i].x;
q[i].x = std :: lower_bound(data + 1, data + max_data + 1, q[i].x) - data;
map[q[i].x] = origin;
}
}
void PrepareBlock() {
block_size = (int) sqrt(max_data);
block_num = max_data / block_size;
for (int i = 1; i <= block_num; ++ i) {
L[i] = (i - 1) * block_size + 1;
R[i] = i * block_size;
}
if (R[block_num] < max_data) {
block_num ++;
L[block_num] = R[block_num - 1] + 1;
R[block_num] = max_data;
}
for (int i = 1; i <= block_num; ++ i) {
for (int j = L[i]; j <= R[i]; ++ j) {
bel[j] = i;
}
}
}
void Insert(int val_) { //O(1) 插入
cnt[val_] ++;
cntblock[bel[val_]] ++;
}
void Delete(int val_) { //O(1) 删除
cnt[val_] --;
cntblock[bel[val_]] --;
}
int QueryRank(int val_) { //查询给定数值的排名
int belval = bel[val_], ret = 0;
for (int i = L[belval]; i < val_; ++ i) ret += cnt[i]; //注意 <val_
for (int i = 1; i < belval; ++ i) ret += cntblock[i];
return ret + 1;
}
int QueryVal(int rank_) { //查询给定排名对应的数
int size = 0, ret, belval;
for (belval = 1; belval <= block_num; ++ belval) {
if (size + cntblock[belval] >= rank_) break;
size += cntblock[belval];
}
for (ret = L[belval]; ret <= R[belval]; ++ ret) {
if (size + cnt[ret] >= rank_) break;
size += cnt[ret];
}
return ret;
}
int QueryAhead(int val_) { //查询前驱
int belval = bel[val_];
for (int i = val_ - 1; i >= L[belval]; -- i) {
if (cnt[i]) return i;
}
for (int i = belval - 1; i; -- i) {
if (! cntblock[i]) continue;
for (int j = R[i]; j >= L[i]; -- j) {
if (cnt[j]) return j;
}
}
}
int QueryBack(int val_) { //查询后继
int belval = bel[val_];
for (int i = val_ + 1; i <= R[belval]; ++ i) {
if (cnt[i]) return i;
}
for (int i = belval + 1; i <= block_num; ++ i) {
if (! cntblock[i]) continue;
for (int j = L[i]; j <= R[i]; ++ j) {
if (cnt[j]) return j;
}
}
}
void koishi() {
int satori;
}
//=============================================================
int main() {
Prepare();
PrepareBlock();
for (int i = 1; i <= n; ++ i) {
int opt = q[i].opt, x = q[i].x;
if(opt == 1) Insert(x);
if(opt == 2) Delete(x);
if(opt == 3) printf("%d
", QueryRank(x));
if(opt == 4) printf("%d
", map[QueryVal(x)]);
if(opt == 5) printf("%d
", map[QueryAhead(x)]);
if(opt == 6) printf("%d
", map[QueryBack(x)]);
}
return 0;
}
二叉分块树
考虑将分块序列建立成一个二叉搜索树的形态。满足树的中序遍历是原有的分块序列。在每一个节点上储存该节点子树内所有块中数的个数和 (operatorname{size})。记树高为 (h),显然有 (h approx log_2{left(frac{n}{T} ight)}),(T) 为块大小。
操作 1,2,插入删除操作,修改对应块,并跳父亲维护 (operatorname{size}) 即可,时间复杂度 (O(h))。
操作 3,查询排名操作,考虑树上二分,将值小于对应值的子树 (operatorname{size}) 求和,权值所在散块暴力求和,时间复杂度 (O(h + T))。
操作 4,同样树上二分,不断减去左子树的 (operatorname{size}) 向下递归,权值所在散块暴力,时间复杂度 (O(h + T))。
操作 5、6,查询前驱后继。考虑直接查询指定值的排名 -1/+1 的数的值即可(感谢 xwmwr!)。
这里是一开始的更复杂的的实现:对于操作 5,由于树的形态固定,这里比较难以实现。首先考虑暴力查询散块,若不在散块中则检查被查询块左儿子内是否有数,如果有则找到左儿子内最靠右的节点即为答案所在块。否则答案在其它子树中,或者是原位置的某一祖先。考虑跳父亲找到对应子树/祖先,若在祖先中则暴力查散块,若在子树中则进入子树并找到子树中最靠右的节点即为答案所在块。操作 6 同理实现即可。
上述过程的时间复杂度仍为 (O(h + T))。
则总时间复杂度为 (Oleft(n(h + T) ight) approx O(n(log_2{left(frac{n}{T} ight) + T)}))。
离谱的是,由于 (log) 的增长率较小,则 (T = 1) 时复杂度最优,为 (O(nlog n)) 级别。此时相当于建立了一棵静态的 BST,并且相当于没有分块。当然这是在修改查询操作次数平衡时的估计,在本题中块大小取 10 时表现最好。提交记录:Link。
那这东西意义何在?
这一切,值得吗?
意义——这真的重要吗?
参考代码
优美实现
//知识点:分块
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
#define ls lson[now_]
#define rs rson[now_]
#define mid ((L_+R_)>>1)
const int kMaxn = 1e5 + 10;
const int kBlockNum = 1e5 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
struct Operation {
int opt, x;
} q[kMaxn];
int n, block_size, block_num, L[kBlockNum], R[kBlockNum], bel[kMaxn];
int cnt[kMaxn], cntblock[kBlockNum];
int node_num, root, fa[kBlockNum], lson[kBlockNum], rson[kBlockNum], sumcnt[kBlockNum];
int data_num, max_data, data[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void koishi() {
int saroti;
}
void Prepare() { //离线操作,并离散化。
n = read();
for (int i = 1; i <= n; ++ i) {
q[i] = (Operation) {read(), read()};
if (q[i].opt != 4) data[++ data_num] = q[i].x; //注意操作 4 的参数不需要离散化。
}
data[0] = - kInf;
std :: sort(data + 1, data + data_num + 1);
for (int i = 1; i <= data_num; ++ i) {
if (data[i] != data[i - 1]) ++ max_data;
data[max_data] = data[i];
}
for (int i = 1; i <= n; ++ i) {
if (q[i].opt == 4) continue;
q[i].x = std::lower_bound(data + 1, data + max_data + 1, q[i].x) - data;
}
}
void PrepareBlock() {
block_size = (int) std::min(max_data, 10);
block_num = max_data / block_size;
for (int i = 1; i <= block_num; ++ i) {
L[i] = (i - 1) * block_size + 1;
R[i] = i * block_size;
}
if (R[block_num] < max_data) {
block_num ++;
L[block_num] = R[block_num - 1] + 1;
R[block_num] = max_data;
}
for (int i = 1; i <= block_num; ++ i) {
for (int j = L[i]; j <= R[i]; ++ j) {
bel[j] = i;
}
}
}
void Pushup(int now_) {
sumcnt[now_] = sumcnt[ls] + sumcnt[rs];
}
void Build(int &now_, int L_, int R_) {
if (L_ > R_) return ;
int ls_ = 0, rs_ = 0;
Build(ls_, L_, mid - 1);
now_ = ++ node_num;
Build(rs_, mid + 1, R_);
fa[ls = ls_] = now_, fa[rs = rs_] = now_;
}
void Insert(int val_) {
++ cnt[val_];
++ cntblock[bel[val_]];
for (int pos_ = bel[val_]; pos_; pos_ = fa[pos_]) ++ sumcnt[pos_];
}
void Delete(int val_) {
-- cnt[val_];
-- cntblock[bel[val_]];
for (int pos_ = bel[val_]; pos_; pos_ = fa[pos_]) -- sumcnt[pos_];
}
int QueryRank(int now_, int val_) { //查询给定数值的排名
if (L[now_] <= val_ && val_ <= R[now_]) {
int ret = 0;
for (int i = L[now_]; i < val_; ++ i) ret += cnt[i];
return sumcnt[ls] + ret + 1;
}
int ret = 0;
if (val_ < L[now_]) ret = QueryRank(ls, val_);
else ret = sumcnt[ls] + cntblock[now_] + QueryRank(rs, val_);
return ret;
}
int QueryVal(int now_, int rank_) { //查询给定排名对应的数
if (rank_ <= sumcnt[ls]) return QueryVal(ls, rank_);
rank_ -= sumcnt[ls];
for (int i = L[now_]; i <= R[now_]; ++ i) {
rank_ -= cnt[i];
if (rank_ <= 0) return i;
}
return QueryVal(rs, rank_);
}
//=============================================================
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
Prepare();
PrepareBlock();
Build(root, 1, block_num);
for (int i = 1; i <= n; ++ i) {
int opt = q[i].opt, x = q[i].x;
if(opt == 1) Insert(x);
if(opt == 2) Delete(x);
if(opt == 3) printf("%d
", QueryRank(root, x));
if(opt == 4) printf("%d
", data[QueryVal(root, x)]);
if(opt == 5) printf("%d
", data[QueryVal(root, QueryRank(root, x) - 1)]);
if(opt == 6) printf("%d
", data[QueryVal(root, QueryRank(root, x + 1))]);
}
return 0;
}
菜鸡实现
仅有查询前驱后继不同。
//知识点:分块
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
#define ls lson[now_]
#define rs rson[now_]
#define mid ((L_+R_)>>1)
const int kMaxn = 1e5 + 10;
const int kBlockNum = 1e5 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
struct Operation {
int opt, x;
} q[kMaxn];
int n, block_size, block_num, L[kBlockNum], R[kBlockNum], bel[kMaxn];
int cnt[kMaxn], cntblock[kBlockNum];
int node_num, root, fa[kBlockNum], lson[kBlockNum], rson[kBlockNum], sumcnt[kBlockNum];
int data_num, max_data, data[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void koishi() {
int saroti;
}
void Prepare() { //离线操作,并离散化。
n = read();
for (int i = 1; i <= n; ++ i) {
q[i] = (Operation) {read(), read()};
if (q[i].opt != 4) data[++ data_num] = q[i].x; //注意操作 4 的参数不需要离散化。
}
data[0] = - kInf;
std :: sort(data + 1, data + data_num + 1);
for (int i = 1; i <= data_num; ++ i) {
if (data[i] != data[i - 1]) ++ max_data;
data[max_data] = data[i];
}
for (int i = 1; i <= n; ++ i) {
if (q[i].opt == 4) continue;
q[i].x = std::lower_bound(data + 1, data + max_data + 1, q[i].x) - data;
}
}
void PrepareBlock() {
block_size = (int) std::min(max_data, 10);
block_num = max_data / block_size;
for (int i = 1; i <= block_num; ++ i) {
L[i] = (i - 1) * block_size + 1;
R[i] = i * block_size;
}
if (R[block_num] < max_data) {
block_num ++;
L[block_num] = R[block_num - 1] + 1;
R[block_num] = max_data;
}
for (int i = 1; i <= block_num; ++ i) {
for (int j = L[i]; j <= R[i]; ++ j) {
bel[j] = i;
}
}
}
void Pushup(int now_) {
sumcnt[now_] = sumcnt[ls] + sumcnt[rs];
}
void Build(int &now_, int L_, int R_) {
if (L_ > R_) return ;
int ls_ = 0, rs_ = 0;
Build(ls_, L_, mid - 1);
now_ = ++ node_num;
Build(rs_, mid + 1, R_);
fa[ls = ls_] = now_, fa[rs = rs_] = now_;
}
void Insert(int val_) {
++ cnt[val_];
++ cntblock[bel[val_]];
for (int pos_ = bel[val_]; pos_; pos_ = fa[pos_]) ++ sumcnt[pos_];
}
void Delete(int val_) {
-- cnt[val_];
-- cntblock[bel[val_]];
for (int pos_ = bel[val_]; pos_; pos_ = fa[pos_]) -- sumcnt[pos_];
}
int QueryRank(int now_, int val_) { //查询给定数值的排名
if (L[now_] <= val_ && val_ <= R[now_]) {
int ret = 0;
for (int i = L[now_]; i < val_; ++ i) ret += cnt[i];
return sumcnt[ls] + ret + 1;
}
int ret = 0;
if (val_ < L[now_]) ret = QueryRank(ls, val_);
else ret = sumcnt[ls] + cntblock[now_] + QueryRank(rs, val_);
return ret;
}
int QueryVal(int now_, int rank_) { //查询给定排名对应的数
if (rank_ <= sumcnt[ls]) return QueryVal(ls, rank_);
rank_ -= sumcnt[ls];
for (int i = L[now_]; i <= R[now_]; ++ i) {
rank_ -= cnt[i];
if (rank_ <= 0) return i;
}
return QueryVal(rs, rank_);
}
int QueryAhead(int val_) { //查询前驱
int now_ = bel[val_];
for (int i = val_ - 1; i >= L[now_]; -- i) {
if (cnt[i]) return i;
}
if (sumcnt[ls]) {
now_ = ls;
} else {
for (; now_; now_ = fa[now_]) {
if (now_ == lson[fa[now_]]) continue;
if (cntblock[fa[now_]]) {
now_ = fa[now_];
for (int i = R[now_]; i >= L[now_]; -- i) {
if (cnt[i]) return i;
}
} else if (sumcnt[lson[fa[now_]]]) {
now_ = lson[fa[now_]];
break;
}
}
}
while (sumcnt[rs]) now_ = rs;
for (int i = R[now_]; i >= L[now_]; -- i) {
if (cnt[i]) return i;
}
}
int QueryBack(int val_) { //查询后继
int now_ = bel[val_];
for (int i = val_ + 1; i <= R[now_]; ++ i) {
if (cnt[i]) return i;
}
if (sumcnt[rs]) {
now_ = rs;
} else {
for (; now_; now_ = fa[now_]) {
if (now_ == rson[fa[now_]]) continue;
if (cntblock[fa[now_]]) {
now_ = fa[now_];
for (int i = L[now_]; i <= R[now_]; ++ i) {
if (cnt[i]) return i;
}
} else if (sumcnt[rson[fa[now_]]]) {
now_ = rson[fa[now_]];
break;
}
}
}
while (sumcnt[ls]) now_ = ls;
for (int i = L[now_]; i <= R[now_]; ++ i) {
if (cnt[i]) return i;
}
}
//=============================================================
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
Prepare();
PrepareBlock();
Build(root, 1, block_num);
for (int i = 1; i <= n; ++ i) {
int opt = q[i].opt, x = q[i].x;
if(opt == 1) Insert(x);
if(opt == 2) Delete(x);
if(opt == 3) printf("%d
", QueryRank(root, x));
if(opt == 4) printf("%d
", data[QueryVal(root, x)]);
if(opt == 5) printf("%d
", data[QueryAhead(x)]);
if(opt == 6) printf("%d
", data[QueryBack(x)]);
}
return 0;
}
写在最后
这是一个根本没有任何卵用的东西。单从结果上来看我没有获得任何收获。
但是我玩的很开心。
如果单从结果上来看,在弱校学 OI 又有什么意义呢?