基于链表实现的珂朵莉树

以下内容基于 CF896C Willem, Chtholly and Seniorious 来介绍。

珂朵莉树实质上是一种可以维护区间上的分裂与合并的数据结构,但要求数据是随机的,或者有大量的随机合并操作,这样才能保证维护的区间个数是一个很小的值。

一开始,我们用不同的节点表示 [ 1 , 1 ] , [ 2 , 2 ] , . . . , [ n , n ] [1,1],[2,2],...,[n,n] [1,1],[2,2],...,[n,n] 以及该区间上的值。

本题中的“把区间 [ l , r ] [l,r] [l,r] 赋值为 x x x”对应着一个合并操作,若随机到的 [ l , r ] [l,r] [l,r] 范围比较大,则意味着有大量的节点会合并成一个节点。经测试,在若干次随机合并后,区间个数会骤降至一个稳定的范围(大约几十个),这是理解珂朵莉树的关键。

图例:横轴为操作次数,纵轴为区间个数

数据定义

目前主流的实现是基于 set 来维护节点,但由于平均维护的区间个数很小,set 的优势并不明显。相比之下,链表(或数组)能更简洁地维护分裂与合并操作。

typedef long long int64;

struct Block {
    Block *next; // 链表下一节点
    int l, r; // 区间范围
    int64 val; // 区间上的值

    Block(Block *next, int l, int r, int64 val): next(next), l(l), r(r), val(val) {}
    bool operator<(const Block &b) const { return val < b.val; }
} *root;

基本操作

分裂区间

void split(int mid) {
	// 遍历链表
    for (Block *b = root; b; b = b->next) {
    	// 寻找能包含 mid 和 mid+1 的 [l, r],将其被拆分成 [l, mid] 和 [mid+1, r]
        if (b->l <= mid && mid + 1 <= b->r) { 
            b->next = new Block(b->next, mid + 1, b->r, b->val);
            b->r = mid;
            break;
        }
    }
}

在操作区间时,由于不能只维护区间的一部分,所以下面的操作进行之前都需要预先分裂区间,再完成相应操作。

// 预分裂,保证后续操作在 [l, r] 内部
void prepare(int l, int r) {
    split(l - 1);
    split(r);
}

合并区间

void merge(int l, int r, int64 val) {
	prepare(l, r)
	// 遍历链表
    for (Block *b = root; b; b = b->next) {
    	// 寻找区间左端点
        if (b->l == l) {
        	// 将区间 [b.l, b.r] 修改成 [b.l, r]
            b->r = r;
            b->val = val;
            // 然后寻找与 [b.l, r] 右侧相邻的区间,将当前节点链至该区间
            Block *tmp = b->next;
            while (tmp && tmp->l <= r) tmp = tmp->next;
            b->next = tmp;
            break;
        }
    }
}
// 注:这里没有释放被删除节点的内存,若有需要可自行添加

区间修改与计算

由于数据量很小,枚举整个链表即可。

// 区间更新
void add(int l, int r, int64 val) {
	prepare(l, r)
    for (Block *b = root; b; b = b->next) {
        if (l <= b->l && b->r <= r) b->val += val;
    }
}

// 区间第 k 小
int64 kth(int l, int r, int k) {
	prepare(l, r)
    vector<Block> blocks;
    for (Block *b = root; b; b = b->next) {
        if (l <= b->l && b->r <= r) {
            blocks.emplace_back(*b);
        }
    }
    sort(blocks.begin(), blocks.end());
    k--;
    for (Block b: blocks) {
        int cnt = b.r - b.l + 1;
        if (k >= cnt) k -= cnt;
        else return b.val;
    }
}

// 快速幂
int64 quick_pow(int64 x, int n, int64 mod) {
    x %= mod;
    int64 res = 1 % mod;
    for (; n; n >>= 1) {
        if (n & 1) res = res * x % mod;
        x = x * x % mod;
    }
    return res;
}

// 区间幂和
int64 pow_sum(int l, int r, int n, int64 mod) {
	prepare(l, r)
    int64 sum = 0;
    for (Block *b = root; b; b = b->next) {
        if (l <= b->l && b->r <= r) {
            sum += int64(b->r - b->l + 1) * quick_pow(b->val, n, mod);
        }
    }
    return sum % mod;
}

AC 代码

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;
typedef long long int64;

int64 seed;

// 生成 [0, n-1] 的随机数
int rand(int n) {
    int64 ret = seed;
    seed = (seed * 7 + 13) % int64(1e9 + 7);
    return int(ret) % n;
}

struct Block {
    Block *next;
    int l, r;
    int64 val;

    Block(Block *next, int l, int r, int64 val): next(next), l(l), r(r), val(val) {}
    bool operator<(const Block &b) const { return val < b.val; }
} *root;

void split(int mid) {
    for (Block *b = root; b; b = b->next) {
        if (b->l <= mid && mid + 1 <= b->r) {
            b->next = new Block(b->next, mid + 1, b->r, b->val);
            b->r = mid;
            break;
        }
    }
}

void prepare(int l, int r) {
    split(l - 1);
    split(r);
}

void add(int l, int r, int64 val) {
    for (Block *b = root; b; b = b->next) {
        if (l <= b->l && b->r <= r) b->val += val;
    }
}

void merge(int l, int r, int64 val) {
    for (Block *b = root; b; b = b->next) {
        if (b->l == l) {
            b->r = r;
            b->val = val;
            Block *tmp = b->next;
            while (tmp && tmp->l <= r) tmp = tmp->next;
            b->next = tmp;
            break;
        }
    }
}

int64 kth(int l, int r, int k) {
    vector<Block> blocks;
    for (Block *b = root; b; b = b->next) {
        if (l <= b->l && b->r <= r) {
            blocks.emplace_back(*b);
        }
    }
    sort(blocks.begin(), blocks.end());
    k--;
    for (Block b: blocks) {
        int cnt = b.r - b.l + 1;
        if (k >= cnt) k -= cnt;
        else return b.val;
    }
}

int64 quick_pow(int64 x, int n, int64 mod) {
    x %= mod;
    int64 res = 1 % mod;
    for (; n; n >>= 1) {
        if (n & 1) res = res * x % mod;
        x = x * x % mod;
    }
    return res;
}

int64 pow_sum(int l, int r, int n, int64 mod) {
    int64 sum = 0;
    for (Block *b = root; b; b = b->next) {
        if (l <= b->l && b->r <= r) {
            sum += int64(b->r - b->l + 1) * quick_pow(b->val, n, mod);
        }
    }
    return sum % mod;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, v_max;
    cin >> n >> m >> seed >> v_max;

    root = new Block(nullptr, 0, 0, rand(v_max) + 1);
    Block *b = root;
    for (int i = 1; i < n; i++) {
        b->next = new Block(nullptr, i, i, rand(v_max) + 1);
        b = b->next;
    }

    for (; m; m--) {
        int op = rand(4) + 1;
        int l = rand(n), r = rand(n);
        if (l > r) swap(l, r);
        int x = op == 3 ? rand(r - l + 1) + 1 : rand(v_max) + 1;
        prepare(l, r); // 在所有操作前预分裂
        switch (op) {
            case 1:
                add(l, r, x);
                break;
            case 2:
                merge(l, r, x);
                break;
            case 3:
                cout << kth(l, r, x) << '\n';
                break;
            default:
                int64 y = rand(v_max) + 1;
                cout << pow_sum(l, r, x, y) << '\n';
                break;
        }
    }
    return 0;
}
莉树(Old Driver Tree)是一种基于链表(list)的数据结构,主要用于高效处理区间赋值和区间查询问题。其核心思想是将连续的相同值区间合并为一个节点,从而减少不必要的操作。下面我们从数学角度严格证明其时间复杂度。 ### **1. 基本操作分析** 莉树的核心操作包括: - **`split(pos)`**:在位置 `pos` 处分割区间,将一个区间拆分成两个子区间。 - **`assign(l, r, val)`**:将区间 `[l, r]` 赋值为 `val`,并合并相邻的相同值区间。 ### **2. 时间复杂度证明** #### **(1) `split(pos)` 的时间复杂度** - 由于链表是线性结构,最坏情况下需要遍历整个链表才能找到 `pos` 所在的区间,因此 `split` 的时间复杂度为 **O(n)**,其中 `n` 是当前区间的数量。 - 但实际应用中,由于区间合并的存在,`n` 通常远小于原始数据规模。 #### **(2) `assign(l, r, val)` 的时间复杂度** - `assign` 操作通常需要先调用两次 `split`(在 `l` 和 `r` 处分割),然后删除 `[l, r]` 内的所有区间,并插入一个新的区间。 - 设 `m` 是被 `[l, r]` 覆盖的区间数量,则: - 两次 `split` 的时间为 **O(m)**。 - 删除 `m` 个区间的时间为 **O(m)**(链表删除是 O(1) 但需要遍历)。 - 插入新区间的时间为 **O(1)**。 - 因此,`assign` 的摊还时间复杂度为 **O(m + 1)**。 #### **(3) 摊还分析** - 每次 `assign` 操作会减少区间数量(合并相邻相同区间),因此 `m` 的上界受初始区间数和操作次数限制。 - 假设初始有 `n` 个区间,经过 `k` 次 `assign` 操作后,区间数最多为 `O(n + k)`。 - 因此,`assign` 的 **摊还时间复杂度为 O(log n)**(类似势能分析,但实际应用中常视为 O(1))。 ### **3. 结论** - **最坏情况下**,单次 `split` 或 `assign` 的时间复杂度为 **O(n)**。 - **摊还时间复杂度**(经过多次操作均摊后),`assign` 的时间复杂度可视为 **O(log n)** 或 **O(1)**(取决于实现和操作序列)。 - 由于莉树依赖数据随机性(如随机区间赋值),在 **随机数据** 下表现更优,接近 **O(log n)**。 ```python # 示例:莉树的简单实现(基于链表) class Node: def __init__(self, l, r, val): self.l = l self.r = r self.val = val class ODTree: def __init__(self, n, init_val): self.nodes = [Node(0, n-1, init_val)] def split(self, pos): # 在 pos 处分割区间,返回左半部分和右半部分 for i, node in enumerate(self.nodes): if node.l <= pos <= node.r: left = Node(node.l, pos, node.val) right = Node(pos+1, node.r, node.val) self.nodes[i:i+1] = [left, right] return ```
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值