珂朵莉树

珂朵莉树

名称简介

Chtholly

老司机树,ODT(Old Driver Tree),又名珂朵莉树(Chtholly Tree)。起源自 CF896C。

核心思想

分块,把块按照顺序储存在一起。把值相同的区间合并成一个结点保存在 set 里面。

复杂度

骗分。只要是有区间赋值操作的数据结构题都可以用来骗分。在数据随机的情况下一般效率较高,但在不保证数据随机的场合下,会被精心构造的特殊数据卡到超时。

如果要保证复杂度正确,必须保证数据随机。如果数据不够随机,区间比较紧密,那么将会出现许多碎片化的区块,就像磁盘一样。对于 add,assign 和 sum 操作,用 set 实现的珂朵莉树的复杂度为 O ( n log ⁡ log ⁡ n ) O(n \log \log n) O(nloglogn),而用链表实现的复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)

数据结构

一个结构体表示区块。

struct Block
{
  int l;         // 区间左端点(包括)
  int r;         // 区间右端点(不包括)
  mutable int v; // 区间元素

  // 自定义比较函数,关键字为区间左端点。
  bool operator<(const Block &rhs) const
  {
    return l < rhs.l;
  }
};

其中,区间中的元素具有相同的元素的值。

mutable 的意思是“可变的”,让我们可以在后面的操作中修改 v 的值。在 C++ 中,mutable 是为了突破 const 的限制而设置的。被 mutable 修饰的变量(mutable 只能用于修饰类中的非静态数据成员),将永远处于可变的状态,即使在一个 const 函数中。

这意味着,我们可以直接修改已经插入 set 的元素的 v 值,而不用将该元素取出后重新加入 set。

这之后,我们使用set来维护一个区块序列。

set<Block> tree;
typedef set<Block>::iterator iter;

初始化

void init(int l, int r, int v)
{
  tree.insert(Block(l, r, v));
}

分裂

这是珂朵莉树的核心,将一个区块拆成两个区间,其中分裂点为 x x x

// 分裂区块,返回以x为左端点的区块的迭代器
// 分裂区块,返回以x为左端点的区块的迭代器
iter split(int x)
{
  // 寻找左端点第一个大于等于x区块
  iter it = tree.lower_bound(Block(x, 0, 0));
  // 如果存在区块正好是左端点,那么就不用分裂,直接返回迭代器即可
  if (it != tree.end() && it->l == x)
  {
    return it;
  }

  // 否则就要退回一个区块
  it--;
  Block o = *it;
  // 删除原来的区块,插入新的区块即可
  tree.erase(it);
  tree.insert(Block(o.l, x - 1, o.v));
  return tree.insert(Block(x, o.r, o.v)).first;
}

赋值

对区间 [ l , r ] [l,r] [l,r]进行元素复制,需要拆分区块一个以 l l l为左端点,一个以 r r r为左端点。

// 将区间[l,r]内的元素全部赋值为v
void assign(int l, int r, int v)
{
  // 切分区间
  iter itr = split(r + 1);
  iter itl = split(l);

  // 删除区间内的所有小区块
  tree.erase(itl, itr);

  // 插入目标区块
  tree.insert(Block(l, r, v));
}

注:珂朵莉树在进行求取区间左右端点操作时,必须先 split 右端点,再 split 左端点。若先 split 左端点,返回的迭代器可能在 split 右端点的时候失效,可能会导致 RE。

区间操作模板

其他操作都可也基于以下模板进行操作,根据需要的操作进行魔改。

// 将区间[l,r]操作
void proc(ll l, ll r)
{
  // 切分区间
  iter itr = split(r + 1);
  iter itl = split(l);

  for (iter i = itl; i != itr; i++)
  {
    // TODO 遍历区块
  }

  // 删除区间内的所有小区块
  tree.erase(itl, itr);

  // 插入目标区块
  tree.insert(Block(l, r, v));
}

区间加法

void add(ll l, ll r, int v)
{
  // 切分区间
  iter itr = split(r + 1);
  iter itl = split(l);
  // 挨个加
  for (iter i = itl; i != itr; i++)
  {
    i->v += v;
  }
}

求区间第K大值

直接扔进优先队列即可。

int kth(ll l, ll r, int k)
{
  // 切分区间
  iter itr = split(r + 1);
  iter itl = split(l);
  // 挨个加
  priority_queue<int> pq;
  for (iter i = itl; i != itr; i++)
  {
    pq.push(i->v);
  }

  for (int i = 0; i < k - 1; i++)
  {
    pq.pop();
  }
  return pq.top();
}

例题

P1065

理解了题目的操作之后,我们发现,我们每次插入一个序列操作,需要在一个时间线上寻找位置,而区间段本身就是有序的,我们可以使用珂朵莉树解决(骗分)此问题。

#include <bits/stdc++.h>

using namespace std;

#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)

typedef long long ll;

struct Segment
{
    int l;
    int r;
    bool operator<(const Segment &o) const
    {
        return l < o.l;
    }
};

typedef set<Segment>::iterator iter;

// Chtholly ODT
struct Machine
{
    set<Segment> sequence;
    Machine()
    {
        // ! insert an enough BIG SEGMENT
        sequence.insert({1, 50000});
    }
    int insert(int start, int len)
    {
        iter it = sequence.lower_bound({start, 0});

        if (it->l != start && it != sequence.begin())
            it--;

        for (; max(start, it->l) + len - 1 > it->r; it++)
            ;

        int ed = max(start, it->l) + len - 1;
        // delete

        Segment old = *it;
        sequence.erase(it);

        // insert

        if (old.l < start)
        {
            // before
            sequence.insert({old.l, start - 1});
        }

        // current
        // {max(start, old.l), ed};

        // after

        if (ed < old.r)
        {
            sequence.insert({ed + 1, old.r});
        }

        return ed;
    }
} Ma[20];

int que[405]; // queue

int match[20][20]; // pieces-machines match

int lens[20][20]; // time

int order[20]; // order
int prv[20];   // the last time of the i-th piece

int main()
{
    int m, n;
    // Read
    scanf("%d %d", &m, &n);
    for (int i = 1; i <= m * n; i++)
    {
        scanf("%d", que + i);
    }

    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            scanf("%d", &match[i][j]);
        }
    }

    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            scanf("%d", &lens[i][j]);
        }
    }

    // DO THAT
    int ans = 0;
    for (int i = 1; i <= m * n; i++)
    {
        int gid = que[i];
        int ord = ++order[gid];
        int mid = match[gid][ord];
        prv[gid] = Ma[mid].insert(prv[gid] + 1, lens[gid][ord]);
        ans = max(ans, prv[gid]);
    }
    printf("%d", ans);
    return 0;
}

总结

其实就是分块的思想,注意,珂朵莉树非常容易HACK,数据不够随机效率就会大大下降。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值