sgt 大法好
正经原理
- 我们学过了很多平衡树,像 Splay、Treap 等,无一是用很高端的方法维持树的平衡,这不是一般人能想到的。
- 像分块这种优化暴力的算法就很容易想到。
- 不如想想如何暴力维持树平衡。
- 每次修改操作之后,如果「足够乱」,就将树拍扁(取中序遍历),再提起来(构建线段树式的树高平衡二叉树)。
- 关键问题是如何定义「足够乱」。
- 可以对于每一个结点记一个最大(小)深度,如果该深度距离
太远,说明不够平衡,将以该结点为根的子树拍扁重构。 - 但这样往往会多记一个不大会用到的值。
- 换一种方法。
- 计算左(右)子树的大小与总大小的比值记为
,如果该值大于一个固定的值 ,说明它不够平衡,拍扁重构。 - 不难想到
,则 的取值应该找一个折中的值,这里大概取 。 - 此时替罪羊树的基本原理就成型了。
在此之前先介绍一下变量:
int rt, tot, siz[N], szi[N], dsz[N], val[N], son[N][2], cnt[N];
/*
从左到右依次是:
根的编号、
目前的总结点个数、
子树种类数、
子树总数、
子树未删除总数(因为删除使用懒惰删除)、
结点权值、
左右儿子、
该权值一共有多少个数
*/
可得出对应上传代码:
inline void pushup(int u) {
siz[u] = siz[son[u][0]] + siz[son[u][1]] + 1; // 种类数
szi[u] = szi[son[u][0]] + szi[son[u][1]] + cnt[u]; // 总数
dsz[u] = dsz[son[u][0]] + dsz[son[u][1]] + (cnt[u] != 0); // 未删除数
}
重构操作
重构分为“拍扁(
inline bool ck_reb(int u) { // 检查是否需要重构
return cnt[u] && // 是否存在
(alpha * siz[u] <= (double)max(siz[son[u][0]], siz[son[u][1]]) || // 左右子树是否失衡
(double)dsz[u] <= alpha * (double)siz[u]); // 已删除元素是否失衡
}
inline void flatten(vector<int> &p, int u) { // 拍扁函数,p 为中序遍历数组
if (!u) return;
flatten(p, son[u][0]);
if (cnt[u]) p.push_back(u);
flatten(p, son[u][1]);
}
inline int re_bui(vector<int> p, int l, int r) { // 重构函数
if (l >= r) return 0; // 错位了
int mid = (l + r) >> 1;
son[p[mid]][0] = re_bui(p, l, mid); // 认左儿子、右儿子
son[p[mid]][1] = re_bui(p, mid + 1, r);
pushup(p[mid]); // 上传
return p[mid]; // 返回
}
最后还得把三个函数叠起来:
inline void rebuild(int &u) {
if (!ck_reb(u)) return;
vector<int> p(0); flatten(p, u);
u = re_bui(p, 0, p.size());
pushup(u);
}
其他操作没有什么特点,只不过要在所有涉及修改的操作后面加上一句 rebuild
。
总体代码:
#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define FRE(x) freopen(x ".in", "r", stdin), freopen(x ".out", "w", stdout)
#define ALL(x) x.begin(), x.end()
using namespace std;
inline void cmax(int& x, int c) {
x = max(x, c);
}
inline void cmin(int& x, int c) {
x = min(x, c);
}
int _test_ = 1;
const int N = 1e5 + 5;
const double alpha = 0.773;
int n;
struct scape_goat {
int rt, tot, siz[N], szi[N], dsz[N], val[N], son[N][2], cnt[N];
inline void pushup(int u) {
siz[u] = siz[son[u][0]] + siz[son[u][1]] + 1;
szi[u] = szi[son[u][0]] + szi[son[u][1]] + cnt[u];
dsz[u] = dsz[son[u][0]] + dsz[son[u][1]] + (cnt[u] != 0);
}
inline int nwnode(int v) {
tot++;
siz[tot] = szi[tot] = dsz[tot] = cnt[tot] = 1;
val[tot] = v;
return tot;
}
inline bool ck_reb(int u) {
return cnt[u] &&
(alpha * siz[u] <= (double)max(siz[son[u][0]], siz[son[u][1]]) ||
(double)dsz[u] <= alpha * (double)siz[u]);
}
inline void flatten(vector<int>& p, int u) {
if (!u)
return;
flatten(p, son[u][0]);
if (cnt[u])
p.push_back(u);
flatten(p, son[u][1]);
}
inline int re_bui(vector<int> p, int l, int r) {
if (l >= r)
return 0;
int mid = (l + r) >> 1;
son[p[mid]][0] = re_bui(p, l, mid);
son[p[mid]][1] = re_bui(p, mid + 1, r);
pushup(p[mid]);
return p[mid];
}
inline void rebuild(int& u) {
if (!ck_reb(u))
return;
vector<int> p(0);
flatten(p, u);
u = re_bui(p, 0, p.size());
pushup(u);
}
inline void ins(int& u, int v) {
if (!u) {
u = nwnode(v);
if (!rt)
rt = u;
return;
}
if (val[u] == v)
cnt[u]++;
else if (val[u] >= v)
ins(son[u][0], v);
else
ins(son[u][1], v);
pushup(u);
rebuild(u);
}
inline void del(int& u, int v) {
if (!u)
return;
if (val[u] == v)
cnt[u] = max(0LL, cnt[u] - 1);
else if (val[u] >= v)
del(son[u][0], v);
else
del(son[u][1], v);
pushup(u);
rebuild(u);
}
inline int pre_thk(int u, int k) {
if (!u)
return 0;
if (val[u] == k && cnt[u])
return szi[son[u][0]];
if (k <= val[u])
return pre_thk(son[u][0], k);
return szi[son[u][0]] + cnt[u] + pre_thk(son[u][1], k);
}
inline int nxt_thk(int u, int k) {
if (!u)
return 1;
if (val[u] == k && cnt[u])
return szi[son[u][0]] + 1 + cnt[u];
if (k < val[u])
return nxt_thk(son[u][0], k);
return szi[son[u][0]] + cnt[u] + nxt_thk(son[u][1], k);
}
inline int kth(int u, int k) {
if (!u)
return 0;
if (szi[son[u][0]] < k && k <= szi[son[u][0]] + cnt[u])
return val[u];
if (k <= szi[son[u][0]] + cnt[u])
return kth(son[u][0], k);
return kth(son[u][1], k - szi[son[u][0]] - cnt[u]);
}
inline int pre(int v) { return kth(rt, pre_thk(rt, v)); }
inline int nxt(int v) { return kth(rt, nxt_thk(rt, v)); }
} sgt;
void init() {}
void clear() {}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) {
int op, x;
cin >> op >> x;
if (op == 1)
sgt.ins(sgt.rt, x);
if (op == 2)
sgt.del(sgt.rt, x);
if (op == 3)
cout << sgt.pre_thk(sgt.rt, x) + 1 << "\n";
if (op == 4)
cout << sgt.kth(sgt.rt, x) << "\n";
if (op == 5)
cout << sgt.pre(x) << "\n";
if (op == 6)
cout << sgt.nxt(x) << "\n";
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
// cin >> _test_;
init();
while (_test_--) {
clear();
solve();
}
return 0;
}
扩展整活
你说的对,但是我不会时间复杂度分析。
- 因为裸的替罪羊树无法维护区间反转。
- 所以做维护区间信息的题就没法用跑得快的 SGT 了。
- 吗?
- 类似的,我们在
里加入 和 ,此时就可以用类似 的方法维护区间信息。 - 但这只是图一乐,实际上常数要飞上天了。
- 用 deepseek 帮我实现了一份代码:
#include <iostream>
#include <vector>
using namespace std;
struct Node {
int value;
int size; // 子树大小
bool reverse; // 翻转标记
Node *left, *right;
Node(int val) : value(val), size(1), reverse(false), left(nullptr), right(nullptr) {}
void updateSize() {
size = 1;
if (left) size += left->size;
if (right) size += right->size;
}
void pushDown() {
if (reverse) {
swap(left, right); // 交换左右子树
if (left) left->reverse ^= true;
if (right) right->reverse ^= true;
reverse = false; // 清除当前节点的标记
}
}
};
// 分割操作:将树分为前k个节点和剩余部分
pair<Node*, Node*> split(Node* root, int k) {
if (!root) return {nullptr, nullptr};
root->pushDown(); // 处理当前节点的标记
int leftSize = root->left ? root->left->size : 0;
if (k <= leftSize) {
auto [left, right] = split(root->left, k);
root->left = right;
root->updateSize();
return {left, root};
} else {
auto [left, right] = split(root->right, k - leftSize - 1);
root->right = left;
root->updateSize();
return {root, right};
}
}
// 合并操作:合并两棵树
Node* merge(Node* a, Node* b) {
if (!a) return b;
if (!b) return a;
a->pushDown();
b->pushDown();
// 启发式合并:选择较大的子树作为根
if (a->size > b->size) {
a->right = merge(a->right, b);
a->updateSize();
return a;
} else {
b->left = merge(a, b->left);
b->updateSize();
return b;
}
}
// 区间翻转操作
Node* reverseRange(Node* root, int l, int r) {
auto [left, right] = split(root, l - 1); // 分割出前l-1个节点
auto [mid, remaining] = split(right, r - l + 1); // 分割出区间[l, r]
if (mid) mid->reverse ^= true; // 打翻转标记
return merge(merge(left, mid), remaining); // 合并回原树
}
// 中序遍历拍平树,用于重构
void flatten(Node* node, vector<Node*>& nodes) {
if (!node) return;
node->pushDown();
flatten(node->left, nodes);
nodes.push_back(node);
flatten(node->right, nodes);
}
// 重构平衡的二叉树
Node* rebuild(vector<Node*>& nodes, int start, int end) {
if (start > end) return nullptr;
int mid = (start + end) / 2;
Node* node = nodes[mid];
node->left = rebuild(nodes, start, mid - 1);
node->right = rebuild(nodes, mid + 1, end);
node->updateSize();
node->reverse = false; // 重构后无需翻转
return node;
}
// 检查并重构不平衡的子树
Node* rebalance(Node* root) {
vector<Node*> nodes;
flatten(root, nodes);
return rebuild(nodes, 0, nodes.size() - 1);
}
// 中序遍历以验证结果
void inorder(Node* root, vector<int>& result) {
if (!root) return;
root->pushDown();
inorder(root->left, result);
result.push_back(root->value);
inorder(root->right, result);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
// 构建初始树,序列为1,2,3,...,n
Node* root = nullptr;
for (int i = 1; i <= n; ++i) {
root = merge(root, new Node(i));
}
// 处理m次操作
while (m--) {
int l, r;
cin >> l >> r;
root = reverseRange(root, l, r);
// 检查是否平衡,必要时重构
if (root->size > 2 * n) { // 简单的不平衡检查
root = rebalance(root);
}
}
// 输出最终序列
vector<int> result;
inorder(root, result);
for (int i = 0; i < n; ++i) {
cout << result[i] << " ";
}
cout << endl;
return 0;
}
但它并没有掌握 SGT 的精髓。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库