算法學習筆記: 珂朵莉樹

珂朵莉樹(Chtholly Tree)起源於CF896C,那道題要求咱們實現一種數據結構,能夠較快地實現:node

  • 區間加
  • 區間賦值
  • 求區間第k大值
  • 求區間n次方和

原題以下:c++

(CF896C Willem, Chtholly and Seniorious)git

img

Seniorious is made by linking special talismans in particular order.
After over 500 years, the carillon is now in bad condition, so Willem decides to examine it thoroughly.
Seniorious has n pieces of talisman. Willem puts them in a line, the i-th of which is an integer ai.
In order to maintain it, Willem needs to perform m operations.
There are four types of operations:算法

1 l r x: For each i such that l ≤ i ≤ r, assign ai + x to ai.
2 l r x: For each i such that l ≤ i ≤ r, assign x to ai.
3 l r x: Print the x-th smallest number in the index range [l, r], i.e. the element at the x-th position if all the elements ai such that l ≤ i ≤ r are taken and sorted into an array of non-decreasing integers. It's guaranteed that 1 ≤ x ≤ r - l + 1.
4 l r x y: Print the sum of the x-th power of ai such that l ≤ i ≤ r, modulo y, i.e. [公式] .編程

Input
The only line contains four integers n, m, seed, vmax (1 ≤ n, m ≤ 105, 0 ≤ seed < 109 + 7, 1 ≤ vmax ≤ 109).
The initial values and operations are generated using following pseudo code:數組

def rnd():數據結構

ret = seed
seed = (seed * 7 + 13) mod 1000000007
return retide

for i = 1 to n:函數

a[i] = (rnd() mod vmax) + 1學習

for i = 1 to m:

op = (rnd() mod 4) + 1
l = (rnd() mod n) + 1
r = (rnd() mod n) + 1

if (l > r):
swap(l, r)

if (op == 3):
x = (rnd() mod (r - l + 1)) + 1
else:
x = (rnd() mod vmax) + 1

if (op == 4):
y = (rnd() mod vmax) + 1

Here op is the type of the operation mentioned in the legend.
Output
For each operation of types 3 or 4, output a line containing the answer.

就此需求來講,普通的樹狀數組、線段樹等顯然難以勝任,看上去須要一種至關複雜的數據結構。然而,在這位珂學家出題人的題解中,他實現的數據結構卻很簡單,甚至看起來有點……暴力。他一開始用本身的id把這種數據結構命名爲ODT,後又改而定名爲珂朵莉樹。

珂朵莉樹的適用範圍是有區間賦值操做且數據隨機的題目。其實珂朵莉樹看上去並不像是樹狀數據結構,但由於通常要用到std::set,而std::set是用紅黑樹實現的,因此也不算名存實亡。在隨機數據下,珂朵莉樹能夠達到 [公式] 的複雜度(參見這篇文章)。


珂朵莉樹的思想在於隨機數據下的區間賦值操做極可能讓大量元素變爲同一個數。因此咱們以三元組<l,r,v>的形式保存數據(區間 [公式] 中的元素的值都是v):

img

翻譯成代碼:

struct node
{
    ll l, r;
    mutable ll v; // 這裏mutable要寫否則可能會CE
    node(ll l, ll r, ll v) : l(l), r(r), v(v) {} // 構造函數
    bool operator<(const node &o) const { return l < o.l; } // 重載小於運算符
};

而後把這些三元組存儲到set裏(也能夠用鏈表,但set更優秀):

set<node> tree;

要把結構體放進set裏須要重載小於運算符,set會保證內部元素有序(插入、刪除和查詢的時間複雜度都是 [公式] )。而mutable使得當整個結構體爲const時,標爲mutable的成員仍可變(由於可能有區間加等操做)。

然而,咱們進行區間操做時並不老是那麼幸運,可能會把本來連續的區間斷開。咱們須要一個函數來實現「斷開」的操做,把<l,r,v>斷成<l,pos-1,v>和<pos,r,v>:

auto split(ll pos)
// 若不支持C++14,auto須改成set<node>::iterator
{
    auto it = tree.lower_bound(node(pos, 0, 0)); // 尋找左端點大於等於pos的第一個節點
    // 若不支持C++11,auto須改成set<node>::iterator
    if (it != tree.end() && it->l == pos) // 若是已經存在以pos爲左端點的節點,直接返回
        return it;
    it--; // 不然往前數一個節點
    ll l = it->l, r = it->r, v = it->v;
    tree.erase(it); // 刪除該節點
    tree.insert(node(l, pos - 1, v)); // 插入<l,pos-1,v>和<pos,r,v>
    return tree.insert(node(pos, r, v)).first; // 返回以pos開頭的那個節點的迭代器
                                               // insert默認返回值是一個pair,第一個成員是咱們要的
}

例如剛剛的狀況,若是要split(4)會發生什麼?

img再放一次圖省得往上滑

首先lower_bound,找到左端點大於等於4的節點,<5,6,3>。它的左端點不是4,因此回退,得<2,4,2>。咱們把節點<2,4,2>刪除,而後插入<2,3,2>及<4,4,2>便可。是否是很簡單?

img


珂朵莉樹的精髓在於區間賦值。而區間賦值操做的寫法也極其簡單:

void assign(ll l, ll r, ll v)
{
    auto end = split(r + 1), begin = split(l); // 順序不能顛倒,不然可能RE
    tree.erase(begin, end); // 清除一系列節點
    tree.insert(node(l, r, v)); // 插入新的節點
}

無非就是把範圍內的節點所有刪除,而後換上新的(範圍較大的)節點而已。只是須要注意求end和begin的順序不能顛倒,由於split(end)可能把begin原來所在的節點斷開。

到此爲止,已經能夠輕鬆A掉這道紫題了:

CF915E Physical Education Lessons 洛谷@小粉兔譯)

Alex高中畢業了,他如今是大學新生。雖然他學習編程,但他仍是要上體育課,這對他來講徹底是一個意外。快要期末了,可是不幸的Alex的體育學分仍是零蛋!
Alex可不但願被開除,他想知道到期末還有多少天的工做日,這樣他就能在這些日子裏修體育學分。可是在這裏計算工做日可不是件容易的事情:
從如今到學期結束還有 [公式] 天(從 [公式][公式] 編號),他們一開始都是工做日。接下來學校的工做人員會依次發出 [公式] 個指令,每一個指令能夠用三個參數 [公式] 描述:
若是 [公式] ,那麼從 [公式][公式] (包含端點)的全部日子都變成工做日。
若是 [公式] ,那麼從 [公式][公式] (包含端點)的全部日子都變成工做日
幫助Alex統計每一個指令下發後,剩餘的工做日天數。
輸入格式
第一行一個整數 [公式] ,第二行一個整數 [公式] ,分別是剩餘的天數和指令的個數。
接下來 [公式] 行,第 [公式] 行有 [公式] 個整數 [公式] ​,描述第 [公式] 個指令 [公式]
輸出格式
輸出 [公式] 行,第 [公式] 行表示第 [公式] 個指令被下發後剩餘的工做日天數。

只須要在assign過程當中求一下和便可,部分代碼以下:

int sum;
void assign(int l, int r, int v)
{
    int tot = 0, len = 0;
    auto end = split(r + 1), it = split(l), begin = it;
    for (it; it != end; it++)
    {
        len += (it->r - it->l + 1);
        tot += it->v * (it->r - it->l + 1);
    }
    tree.erase(begin, end);
    tree.insert(node(l, r, v));
    if (v == 1)
        sum += (len - tot);
    else
        sum -= tot;
}
int main()
{
    int n = read(), q = read();
    tree.insert(node(1, n, 1));
    sum = n;
    while (q--)
    {
        int l = read(), r = read(), k = read();
        assign(l, r, k == 1 ? 0 : 1);
        printf("%d\n", sum);
    }
    return 0;
}

那麼,文章開頭那些複雜的操做,要如何實現呢?實際上一點都不復雜,就兩個字,暴力

區間加(就挨個加):

void add(ll l, ll r, ll v)
{
    auto end = split(r + 1);
    for (auto it = split(l); it != end; it++)
        it->v += v;
}

求區間k大值(直接扔到vector裏排下序):

ll kth(ll l, ll r, ll k)
{
    auto end = split(r + 1);
    vector<pair<ll, ll>> v; // 這個pair裏存節點的值和區間長度
    for (auto it = split(l); it != end; it++)
        v.push_back(make_pair(it->v, it->r - it->l + 1));
    sort(v.begin(), v.end()); // 直接按節點的值的大小排下序
    for (int i = 0; i < v.size(); i++) // 而後挨個丟出來,直到丟出k個元素爲止
    {
        k -= v[i].second;
        if (k <= 0)
            return v[i].first;
    }
}

求區間n次方和(用快速冪直接求):

ll sum_of_pow(ll l, ll r, ll x, ll y)
{
    ll tot = 0;
    auto end = split(r + 1);
    for (auto it = split(l); it != end; it++)
        tot = (tot + qpow(it->v, x, y) * (it->r - it->l + 1)) % y; // qpow本身寫一下
    return tot;
}

真是一個比一個暴力……然而由於隨機數據中大量賦值操做的存在,整個珂朵莉樹上也沒有太多節點,因此速度仍是很可觀的。須要注意的是,若是題目不保證隨機數據,很是容易hack。因此不少時候,珂朵莉樹也許只能看成一種對拍方法或者騙分算法。

可是……事實上,還有一種暴力+暴力法。就是,先把數據離線下來,而後,根據輸入數據的特色(好比有多少次大範圍的賦值),選擇直接暴力或使用珂朵莉樹。由於,若是一個數據具備卡珂朵莉樹的特色,那麼它確定大範圍賦值較少,那麼暴力的複雜度也許就能夠接受。

CF896C的完整代碼以下(題目專門給了一個隨機數生成器就是防hack...):

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read()
{
    ll ans = 0;
    char c = getchar();
    while (!isdigit(c))
        c = getchar();
    while (isdigit(c))
    {
        ans = ans * 10 + c - '0';
        c = getchar();
    }
    return ans;
}
struct node
{
    ll l, r;
    mutable ll v;
    node(ll l, ll r, ll v) : l(l), r(r), v(v) {}
    bool operator<(const node &o) const { return l < o.l; }
};
set<node> tree;
auto split(ll pos)
{
    auto it = tree.lower_bound(node(pos, 0, 0));
    if (it != tree.end() && it->l == pos)
        return it;
    it--;
    ll l = it->l, r = it->r, v = it->v;
    tree.erase(it);
    tree.insert(node(l, pos - 1, v));
    return tree.insert(node(pos, r, v)).first;
}
void assign(ll l, ll r, ll v)
{
    auto end = split(r + 1), begin = split(l);
    tree.erase(begin, end);
    tree.insert(node(l, r, v));
}
ll qpow(ll a, ll n, ll p)
{
    ll ans = 1;
    a %= p;
    while (n)
    {
        if (n & 1)
            ans = ans * a % p;
        n >>= 1;
        a = a * a % p;
    }
    return ans;
}
ll n, m, seed, vmax;
ll rnd()
{
    ll ret = seed;
    seed = (seed * 7 + 13) % 1000000007;
    return ret;
}
void add(ll l, ll r, ll v)
{
    auto end = split(r + 1);
    for (auto it = split(l); it != end; it++)
        it->v += v;
}
ll kth(ll l, ll r, ll k)
{
    auto end = split(r + 1);
    vector<pair<ll, ll>> v;
    for (auto it = split(l); it != end; it++)
        v.push_back(make_pair(it->v, it->r - it->l + 1));
    sort(v.begin(), v.end());
    for (int i = 0; i < v.size(); i++)
    {
        k -= v[i].second;
        if (k <= 0)
            return v[i].first;
    }
}
ll sum_of_pow(ll l, ll r, ll x, ll y)
{
    ll tot = 0;
    auto end = split(r + 1);
    for (auto it = split(l); it != end; it++)
        tot = (tot + qpow(it->v, x, y) * (it->r - it->l + 1)) % y;
    return tot;
}
int main()
{
    n = read(), m = read(), seed = read(), vmax = read();
    for (int i = 1; i <= n; ++i)
    {
        int r = rnd();
        tree.insert(node(i, i, r % vmax + 1));
    }
    for (int i = 1; i <= m; ++i)
    {
        ll opr = rnd() % 4 + 1, l = rnd() % n + 1, r = rnd() % n + 1, x, y;
        if (l > r)
            swap(l, r);
        if (opr == 3)
            x = rnd() % (r - l + 1) + 1;
        else
            x = rnd() % vmax + 1;
        if (opr == 4)
            y = rnd() % vmax + 1;
        switch (opr)
        {
        case 1:
            add(l, r, x);
            break;
        case 2:
            assign(l, r, x);
            break;
        case 3:
            printf("%lld\n", kth(l, r, x));
            break;
        case 4:
            printf("%lld\n", sum_of_pow(l, r, x, y));
        }
    }
    return 0;
}

最後附上一張世界上最幸福的女孩珂朵莉

v2-e11a00675e5a239af4b771600fbaa129_1440w

相關文章
相關標籤/搜索