[暴力数据结构] 珂朵莉树

珂朵莉树

0x00 绪言

Update:2022/11/23 原来文章大部分都是拼凑内容(现在也是.....),质量过低,所以进行了一次小换血,主要是对代码风格以及阅读体验进行了优化。

温馨提示:如果你幻想找到非指针珂朵莉树代码,那你可以停下了,就看这一篇文章即可,虽然这篇文章代码实现也是指针。但你不可能找到数组实现......

0x01 珂朵莉树的起源

珂朵莉树原名老司机树 (Old Driver Tree,ODT),是一种基于 std::set 的暴力数据结构,由 2017 年一场 CF 比赛中提出的数据结构,因为题目背景主角是《末日时在做什么?有没有空?可以来拯救吗?》的主角珂朵莉,因此该数据结构被称为珂朵莉树。

0x02 应用

解决各种线段树无法完成的操作。

注意珂朵莉树保持复杂度主要依靠 assign 操作,所以题目中必须有区间赋值。

还有很重要的一点:数据需纯随机。

0x03 什么时候用珂朵莉树

关键操作:推平一段区间,使一整段区间内的东西变得一样。保证数据随机。

n 个数,m 次操作。 \(n,m\leq10^5\)

操作:

  • 区间加

  • 区间赋值

  • 区间第 \(k\)

  • 求区间幂次和

  • 数据随机

0x04 构造

用一个带结构体的集合 set 维护序列

集合中的每个元素有左端点,右端点,值

下面展示该结构体的构造:

struct node
{
    int l, r;
    mutable int val;
    friend bool operator<(node a, node b)
    {
        return a.l < b.l;
    }
    node(int l, int r = 0, int val = 0) : l(l), r(r), val(val) {}
};
//mutale,意为可变的,即不论在哪里都是可修改的,用于突破C++带const函数的限制。

0x05 Split 操作

操作过程

set::iterator split(int pos)

将原来含有 pos 的区间分为 [l,pos)[pos,r] 两段。

返回一个 std::set 的迭代器,指向 [pos,r]

可能有些抽象,详细解如下:

split 函数的作用就是查找 set 中第一个左端点不小于 pos 的结点,如果找到的结点的左端点等于 pos 便直接返回指向该结点的迭代器,如果不是,说明 pos 包含在前一个结点所表示的区间之间,此时便直接删除包含 pos 的结点,然后以 pos 为分界点,将此结点分裂成两份,分别插入 set 中,并返回指向后一个分裂结点的迭代器。

首先我们假设 set 中有三个 node 结点,这三个结点所表示的区间长度为 14 ,如下图:

不妨以提取区间 [10,12] 为例详细展开(说好的查询 10 到 12呢,怎么下面扯了一堆13` ?别急,后续将会揭晓 ):

如果我们要查询序列第 13 个位置,首先执行 auto it = s.lower_bound(node(13)); 此时 it 将成为一个指向第三个结点的迭代器,为什么是第三个结点,而不是第二个结点呢,因为 lower_bound 这个函数获取的是第一个左端点 l 不小于 13 的结点,所以 it 是指向第三个结点的。

然后执行判断语句,发现第三个结点的左端点不是 13,不满足条件,说明 13 必包含在前一个结点中,继续向下执行,让 it 指向前一个结点:

先将该结点的信息保存下来:int l = 10, r = 13, val = 2
然后直接删除该结点

pos 为分界点,将被删除的结点分裂为 [l,pos-1] ,[pos,r] 这两块,并返回指向 [pos,r] 这个区间的迭代器,事实上 return s.insert(node(pos,r,val)).first; ,便做到了插入 [pos,r] 这端区间,并返回指向它的迭代器,有一个 insert 函数返回值为 pair 类型,其中 pair 的第一个元素就是元素插入位置的迭代器。

至此 13 位置已经分裂完成,然后是查询第 10 个位置,查询步骤同上,但是 10 号点满足 if 语句,便直接返回了

由上述步骤,为了提取区间 [10,12],我们执行了两次 split ,一次为 split(13) ,一次为 split(10),并获得了两个迭代器,一个指向第二结点,一个指向第三结点。

为什么要先分裂右端点,然后再分裂左端点呢?

因为如果先分裂左端点,返回的迭代器会位于所对应的区间以 l 为左端点,此时如果 r 也在这个节点内,就会导致分裂左端点返回的迭代器被 erase 掉,导致 RE

结合问题 1 和问题 2 ,获取区间迭代器 auto itr = split(r+1), itl = split(l);

代码

auto split(int p)
{
    auto it = s.lower_bound(node(p));
    if (it != s.end() && it->l == p)
    {
        return it;
    }
    it--;
    if (it->r < p)
    {
        return s.end();
    }
    int l = it->l;
    int r = it->r;
    int val = it->val;
    s.erase(it);
    s.insert(node(l, p - 1, val));
    return s.insert(node(p, r, val)).first;
}

0x06 Assign 操作

操作过程

注意:以后在使用 split 分裂区间的时候,请先右后左,一般情况不会出事,但你没有机会失误。

区间赋值操作,也是珂树维持其复杂度的关键函数

很暴力的思想,既然刚刚我们写了一个 split,那么就要把它用起来。

首先 splitl 并记返回值为 itl,然后 splitr+1 并记返回值为 itr,显然我们要操作的区间为 [itl,itr),那么我们将 [itl,itr) 删除 (std::set.erase(itl, itr)),再插入一个节点 Node,其 llrrval 为赋值的 val

我们注意到因为这个操作, [itl,itr) 中的所有节点合并为了一个节点,大大降低了集合的元素数量,因此调整了我们的复杂度

代码

void assign(int l, int r, int x)
{
    auto itr = split(r + 1);
    auto itl = split(l);
    s.erase(itl, itr);
    s.insert(node(l, r, x));
}
//将一个区间全部改为某个值。

0x07 其他操作

通用方法是 splitlsplitr+1,然后直接暴力扫描这段区间内的所有节点执行需要的操作

查询区间和

long long querySum(int l, int r)
{
    auto itr = split(r + 1);
    auto itl = split(l);
    long long res = 0;
    for (auto i = itl; i != itr; i++)
    {
        res += (i->r - i->l + 1) * i->val;
    }
    return res;
}

区间加:

void add(int l, int r, int x)
{
    auto itr = split(r + 1);
    auto itl = split(l);
    for (auto i = itl; i != itr; i++)
    {
        i->val += x;
    }
}

区间第 k 小:

algorithm 库中的 std::sort(快速排序)

std::map(方便起见使用其中的pair),std::vector(方便起见)

还是splitlsplitr+1,然后将每个节点的值和个数(即r-l+1)组成一个pair(注意为了排序,将值放在第一关键字),将pair加入一个vector

vector排序

vectorbegin开始扫描,不停的使k减去vector当前项的第二关键字,若 \(k\leq0\),返回当前项的第一关键字。

int kth_number(int l, int r, int k)
{
    auto itr = split(r + 1);
    auto itl = split(l);
    vector<rank> v;
    for (auto i = itl; i != itr; i++)
    {
        v.push_back(rank(i->val, i->r - i->l + 1));
    }
    sort(v.begin(), v.end());
    int i;
    for (i = 0; i < v.size(); i++)
    {
        if (v[i].cnt < k)
        {
            k -= v[i].cnt;
        }
        else
        {
            break;
        }
    }
    return v[i].num;
}

区间平方和

求区间所有数 x 次方的和模 y 的值

int qpow(int x, int b, int p)
{
    int res = 1;
    int a = x % p;
    for (; b; b >>= 1)
    {
        if (b & 1)
        {
            res = res * a % p;
        }
        a = a * a % p;
    }
    return res;
}

int calc(int l, int r, int x, int y)
{
    auto itr = split(r + 1);
    auto itl = split(l);
    int ans = 0;
    for (auto i = itl; i != itr; i++)
    {
        ans = (ans + qpow(i->val, x, y) * (i->r - i->l + 1) % y) % y;
    }
    return ans;
}

0x08 模板题代码实现CF896C

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <set>
 
#define rint register int
#define endl '\n'
#define int long long
 
using std::set;
using std::vector;
 
const int mod = 1e9 + 7;
const int N = 1e5 + 5;
 
int n, m, seed, vmax;
int a[N];
 
int rnd()
{
    int ret = seed;
    seed = (seed * 7 + 13) % mod;
    return ret;
}
 
struct Chtholly_Tree
{
    struct node
    {
        int l, r;
        mutable int val;
        friend bool operator<(node a, node b)
        {
            return a.l < b.l;
        }
        node(int l, int r = 0, int val = 0) : l(l), r(r), val(val) {}
    };
 
    set<node> s;
 
    auto split(int p)
    {
        auto it = s.lower_bound(node(p));
        if (it != s.end() && it->l == p)
        {
            return it;
        }
        it--;
        if (it->r < p)
        {
            return s.end();
        }
        int l = it->l;
        int r = it->r;
        int val = it->val;
        s.erase(it);
        s.insert(node(l, p - 1, val));
        return s.insert(node(p, r, val)).first;
    }
 
    void add(int l, int r, int x)
    {
        auto itr = split(r + 1);
        auto itl = split(l);
        for (auto i = itl; i != itr; i++)
        {
            i->val += x;
        }
    }
 
    void assign(int l, int r, int x)
    {
        auto itr = split(r + 1);
        auto itl = split(l);
        s.erase(itl, itr);
        s.insert(node(l, r, x));
    }
 
    struct rank
    {
        int num, cnt;
        friend bool operator<(rank a, rank b)
        {
            return a.num < b.num;
        }
        rank(int num, int cnt) : num(num), cnt(cnt) {}
    };
 
    int kth_number(int l, int r, int k)
    {
        auto itr = split(r + 1);
        auto itl = split(l);
        vector<rank> v;
        for (auto i = itl; i != itr; i++)
        {
            v.push_back(rank(i->val, i->r - i->l + 1));
        }
        sort(v.begin(), v.end());
        int i;
        for (i = 0; i < v.size(); i++)
        {
            if (v[i].cnt < k)
            {
                k -= v[i].cnt;
            }
            else
            {
                break;
            }
        }
        return v[i].num;
    }
 
    int qpow(int x, int b, int p)
    {
        int res = 1;
        int a = x % p;
        for (; b; b >>= 1)
        {
            if (b & 1)
            {
                res = res * a % p;
            }
            a = a * a % p;
        }
        return res;
    }
 
    int calc(int l, int r, int x, int y)
    {
        auto itr = split(r + 1);
        auto itl = split(l);
        int ans = 0;
        for (auto i = itl; i != itr; i++)
        {
            ans = (ans + qpow(i->val, x, y) * (i->r - i->l + 1) % y) % y;
        }
        return ans;
    }
 
    void build()
    {
        for (rint i = 1; i <= n; i++)
        {
            a[i] = (rnd() % vmax) + 1;
            s.insert(node(i, i, a[i]));
        }
    }
} tree;
 
signed main()
{
    scanf("%lld%lld%lld%lld", &n, &m, &seed, &vmax);
 
    tree.build();
 
    while (m--)
    {
        int op, l, r, x, y;
        op = (rnd() % 4) + 1;
        l = (rnd() % n) + 1;
        r = (rnd() % n) + 1;
        if (l > r)
        {
            std::swap(l, r);
        }
        if (op == 3)
        {
            x = (rnd() % (r - l + 1)) + 1;
        }
        else
        {
            x = (rnd() % vmax) + 1;
        }
        if (op == 4)
        {
            y = (rnd() % vmax) + 1;
        }
        if (op == 1)
        {
            tree.add(l, r, x);
        }
        if (op == 2)
        {
            tree.assign(l, r, x);
        }
        if (op == 3)
        {
            printf("%lld\n", tree.kth_number(l, r, x));
        }
        if (op == 4)
        {
            printf("%lld\n", tree.calc(l, r, x, y));
        }
    }
    return 0;
}

0x09 例题

CF915E

每个操作就是区间赋值 0 或 1,顺带把总和修改一下

//把 cin 改成快读就可以了,这个题卡常
#include <bits/stdc++.h>
 
#define rint register int
#define endl '\n'
#define int long long
 
using namespace std;
 
const int N = 1e5 + 5;
 
int n, m;

struct node
{
    int l, r;
    mutable int val;
    friend bool operator<(node a, node b)
    {
        return a.l < b.l;
    }
    node(int l, int r = 0, int val = 0) : l(l), r(r), val(val) {}
};
 
struct Chtholly_Tree
{
    set<node> s;
    int sum = 0;
 
    auto split(int p)
    {
        auto it = s.lower_bound(node(p));
        if (it != s.end() && it->l == p)
        {
            return it;
        }
        it--;
        if (it->r < p)
        {
            return s.end();
        }
        int l = it->l;
        int r = it->r;
        int val = it->val;
        s.erase(it);
        s.insert(node(l, p - 1, val));
        return s.insert(node(p, r, val)).first;
    }
 
    void assign(int l, int r, int x)
    {
        auto itr = split(r + 1);
        auto itl = split(l);
        auto it = itl;
        for( ;itl != itr; itl++) sum -= itl->val * (itl->r - itl->l + 1); 
        s.erase(it, itr);
        s.insert(node(l, r, x));
        sum += x * (r - l + 1);//这个跟模板不一样,要顺带计算一下
    }
} tree;
 
signed main()
{
    cin >> n >> m;
 	
 	tree.s.insert(node(1, n, 1));
 	tree.sum = n;
 	
 	while(m--)
 	{
 		int l, r, op;
 		cin >> l >> r >> op;
 		if(op == 1) 
 		{
 			tree.assign(l, r, 0);		 	
 		}
 		else
 		{
 			tree.assign(l, r, 1);		 	
		}
 		cout << tree.sum << endl;
 	}
    return 0;
}

0x10 后话

半年多了,终于重新更了,个人感觉质量涨了许多(最近没时间,0x09 我会尽快更的555.....)

posted @ 2022-02-11 21:23  PassName  阅读(1589)  评论(0编辑  收藏  举报