sgt 大法好

正经原理

  • 我们学过了很多平衡树,像 Splay、Treap 等,无一是用很高端的方法维持树的平衡,这不是一般人能想到的。
  • 像分块这种优化暴力的算法就很容易想到。
  • 不如想想如何暴力维持树平衡。
  • 每次修改操作之后,如果「足够乱」,就将树拍扁(取中序遍历),再提起来(构建线段树式的树高平衡二叉树)。
  • 关键问题是如何定义「足够乱」。
  • 可以对于每一个结点记一个最大(小)深度,如果该深度距离 log 太远,说明不够平衡,将以该结点为根的子树拍扁重构。
  • 但这样往往会多记一个不大会用到的值。
  • 换一种方法。
  • 计算左(右)子树的大小与总大小的比值记为 a,如果该值大于一个固定的值 α,说明它不够平衡,拍扁重构。
  • 不难想到 a[0.5,1),则 α 的取值应该找一个折中的值,这里大概取 [0.7,0.8]
  • 此时替罪羊树的基本原理就成型了。

在此之前先介绍一下变量:

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); // 未删除数
}

重构操作

重构分为“拍扁(Flatten)”和“重构(Rebuild)”两部分,难度都不大。

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-FHQ

  • 因为裸的替罪羊树无法维护区间反转。
  • 所以做维护区间信息的题就没法用跑得快的 SGT 了。
  • 吗?
  • 类似的,我们在 SGT 里加入 splitmerge,此时就可以用类似 FHQ 的方法维护区间信息。
  • 但这只是图一乐,实际上常数要飞上天了。
  • 用 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 的精髓。

posted @   Archippus  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示