zoukankan      html  css  js  c++  java
  • 玄学数据结构——珂朵莉树

    关于珂朵莉

    こんなにも、たくさんの幸せをあの人に分けてもらった
    だから、きっと
    今の、私は
    誰が何と言おうと

    世界一、 幸せな女の子だ

    ————クトリ

    美图欣赏:


    (图片来源:萌娘百科)

    中国珂学院

    好了好了说正事

    关于这奇怪的名字

    本来是起源于CF896C Willem, Chtholly and Seniorious(一个CF在国内的镜像网站 ),当时CF上有一名叫做Old Driver的用户(就是lxl)在提交了线段树的正解之后又补充了这么一份奇怪的数据结构,得以命名为珂朵莉树(又称ODT(老司机树))

    在某谷上的评级是一道黑题,但是日常被爆切

    前置知识

    std::set的简单操作,一点区间操作的知识,没了

    正篇

    珂朵莉树可以用来解决含有区间覆盖(或者是区间统一赋值)的操作,本质上是基于(暴力)std::set的数据结构,对于随机数据来说效率很高甚至有时候比正解还高

    注意这种数据结构只在数据随机的情况下表现良好

    珂朵莉树的定义非常简单,就是把一段有着相同值的区间变成set里面的一个元素,以左区间位置为关键字维护的一个set。所以我们就可以写出珂朵莉树的节点定义和初始化:

    struct node {
    	int l, r;//左右区间
    	mutable ll val;//值,这里的mutable意味着在集合的任何位置上都是课修改的
    	node(int l_ = -1, int r_ = -1, ll val_ = 0) {//构造函数
    		l = l_, r = r_, val = val_;
    	}
    	bool operator < (const node &a) const {//运算符重载
    		return l < a.l;
    	}
    };
    set<node> st;//搞一个集合
    

    split操作

    split操作是珂朵莉树里面最重要的操作,其实很简单。对于某一个位置pos,我们将其所处的区间分为[l,pos),[pos,r]并且返回后面那个区间的迭代器,具体的看代码吧:

    IT split(int pos) {
    	IT it = st.lower_bound(node(pos));//查找后继
    	if (it != st.end() && it->l == pos) return it;//如果此时这个区间的左边界就是pos的话就直接返回
    	--it;//否则就是前面的区间
    	node tmp = *it;//保存这个区间的信息
    	st.erase(it);//删除这个区间
    	st.insert(node(tmp.l, pos - 1, tmp.val));//插入两个新区间
    	return st.insert(node(pos, tmp.r, tmp.val)).first;
    }
    

    assign操作

    assign操作就是区间赋值/区间推平(像远野志贵那样),有了split操作之后我们就可以很简单地写出assign的代码了。

    珂朵莉树的复杂度全部来源于assign操作,如果我们只有split操作的化复杂度就螺旋上天

    由于数据随机,所以有1/4的操作都是assign, assign操作会合并具有相同值的区间,使set的大小下降,最后趋近于logn, 所以时间复杂度是mlogn

    void assign(int l, int r, ll val) {
    	IT itr = split(r + 1), itl = split(l);
    	st.erase(itl, itr);
    	st.insert(node(l, r, val));
    }
    

    这里要注意一点,就是但凡我们要split的话肯定是优先split(r+1),这里应该是为了保证不让在split(l)的时候把r的位置也给搞掉了。

    接下来就是一些非常暴力的运算方式了,这里也给一下代码:

    区间加:

    void add(int l, int r, ll val) {
    	IT itr = split(r + 1), itl = split(l);
    	for (IT it = itl; it != itr; ++it) {
    		it->val += val;
    	}
    }
    

    这里要注意,由于我们set存储的是区间,所以在解决区间求和之类的问题的时候不要忘记考虑区间长度

    区间求和:

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

    区间第k小:

    ll queryKth(int l, int r, int k) {
    	vector< pair<ll, int> > vec(0);
    	IT itr = split(r + 1), itl = split(l);
    	for (IT it = itl; it != itr; ++it) {
    		vec.push_back(make_pair(it->val, it->r - it->l + 1));
    	}
    	sort(vec.begin(), vec.end());
    	for (vector< pair<ll, int> >::iterator it = vec.begin(); it != vec.end(); ++it) {
    		if ((k -= it->second) <= 0) return it->first;
    	}
    	return -1;
    }
    

    这里我们其实就是直接把整个区间都push到一个vector里面,然后再暴力sort接着输出。。。注意我们要把值放在pair的第一个关键字哪里,因为我们是要给值排序的嘛

    这里给出一个板子:

    //Chtholly tree
    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    struct node {
    	int l, r;
    	mutable ll val;
    	node(int l_ = -1, int r_ = -1, ll val_ = 0) {
    		l = l_, r = r_, val = val_;
    	}
    	bool operator < (const node &a) const {
    		return l < a.l;
    	}
    };
    typedef set<node>::iterator IT;
    set<node> st;
    IT split(int pos) {
    	IT it = st.lower_bound(node(pos));
    	if (it != st.end() && it->l == pos) return it;
    	--it;
    	node tmp = *it;
    	st.erase(it);
    	st.insert(node(tmp.l, pos - 1, tmp.val));
    	return st.insert(node(pos, tmp.r, tmp.val)).first;
    }
    
    void assign(int l, int r, ll val) {
    	IT itr = split(r + 1), itl = split(l);
    	st.erase(itl, itr);
    	st.insert(node(l, r, val));
    }
    
    void add(int l, int r, ll val) {
    	IT itr = split(r + 1), itl = split(l);
    	for (IT it = itl; it != itr; ++it) {
    		it->val += val;
    	}
    }
    
    ll querySum(int l, int r) {
    	IT itr = split(r + 1), itl = split(l);
    	ll res = 0;
    	for (IT it = itl; it != itr; ++it) {
    		res += (it->r - it->l + 1) * it->val;
    	}
    	return res;
    }
    
    ll queryKth(int l, int r, int k) {
    	vector< pair<ll, int> > vec(0);
    	IT itr = split(r + 1), itl = split(l);
    	for (IT it = itl; it != itr; ++it) {
    		vec.push_back(make_pair(it->val, it->r - it->l + 1));
    	}
    	sort(vec.begin(), vec.end());
    	for (vector< pair<ll, int> >::iterator it = vec.begin(); it != vec.end(); ++it) {
    		if ((k -= it->second) <= 0) return it->first;
    	}
    	return -1;
    }
    
    int main(void) {
    	
    }
    

    复杂度证明

    虽然前面我们估算(瞎猜)了一下含有assign操作复杂度,但是这里我找了一份相对比较完全的复杂度证明

    复杂度证明
    (在骗分数据结构底下寻找复杂度证明是不是搞错了什么)

    后记

    还有很多别的操作,其本质就是对分离出的那一段区间进行暴力运算,这里就不再赘述了

    题目的话这里有一道:P4344 [SHOI2015]脑洞治疗仪
    2020/03/09的时候还没有卡珂朵莉树,但是如果要用珂朵莉树的话需要吸氧(开O2)才能过

    好多题目珂朵莉树都没法AC(可见还是好好打线段树才是王道)

    总的来说,珂朵莉树就是用来骗分的(一题骗到84分还是很可观的),学会打了之后可以应对某些题目的时候拿稍微多一点的分数

    可能有些地方讲的有点问题,欢迎dalao提出

  • 相关阅读:
    idea 将java导出为可执行jar及导入jar依赖
    使用idea 调试java -jar xxx.jar方式启动
    springboot 打成的jar包在ClassLoader().getResource方法读取文件为null
    maven 使用dependencyManagement统一管理依赖版本
    Win10系列:C#应用控件基础5
    Win10系列:C#应用控件基础4
    Win10系列:C#应用控件基础3
    Win10系列:C#应用控件基础2
    Win10系列:C#应用控件基础1
    Win10系列:UWP界面布局进阶9
  • 原文地址:https://www.cnblogs.com/jrdxy/p/12449102.html
Copyright © 2011-2022 走看看