月度归档: 2018 年 2 月

可持久化线段树(含主席树)原理与实现

可持久化线段树(含主席树)原理与实现

概述

可持久化线段树是一类线段树的实现方式,用于保存线段树的历史版本。本文后半部分所介绍的一种用法是主席树这一类可持久化线段树。主席树是以下标为时间加入元素的一类可持久化线段树,常见的用法是静态查询区间第k大值。下面我们介绍它的原理和实现。

原理

什么叫“可持久”?

可持久的意思是保存历史版本,可以查询到历史操作的结果。比如,对于一个序列进行更改,你可以选择每一次更改前将上次更改后的序列复制一份,再在新序列中更改,以保留这个序列的所有历史版本。同样地,你也可以建一堆线段树来保留线段树的历史版本。但是,这样做的空间是O(Q * 4N)的,显然很难接受,因此我们要使用可持久化线段树这种实现方法。

结构

一棵可持久化线段树的结构如图所示。
180226a 1 - 可持久化线段树(含主席树)原理与实现
如图,黑色的为最初版本的线段树,蓝色的为版本2。

修改

如示例图所示,线段树的修改应该是修改一条链,也就是两个相邻版本的的线段树只会有一条链有差别。那我们考虑动态开点,将后面版本的线段树那些没有更改的儿子指向前一个版本的线段树的节点,这样每次只会新建一条链的节点,空间降低为O(4N + Q \log N),是可以接受的了。

查询

每次修改后存储这一次修改的新线段树的根,然后按照根下去正常查询即可。

实现

修改

每次修改前令本次的根为上一次的根,再执行下面的操作。

inline void insert(int &o, int l, int r, int x) {
    tree[++tot] = tree[o];
    o = tot;
    tree[o].val++;
    if(l == r) return;
    int mid = (l + r) >> 1;
    if(x <= mid) insert(tree[o].lch, l, mid, x);
    else insert(tree[o].rch, mid + 1, r, x);
}

查询

常规操作,不提供实现细节。

主席树(查询区间k小值)

原理

我们考虑使用权值线段树,对原序列一个一个插入可持久化线段树。这样,线段树对应区间就是前若干数中该区间内数的个数。对于查询,我们考虑第r和第l-1棵线段树,对应区间值的差就是询问区间中的答案。所以我们的查询操作可以同时沿着这两棵线段树走。

实现

以下是查询操作的实现。

inline int query(int o1, int o2, int l, int r, int k) {
    if(l == r) return l;
    int mid = (l + r) >> 1;
    int num = tree[tree[o2].lch].val - tree[tree[o1].lch].val;
    if(k <= num) return query(tree[o1].lch, tree[o2].lch, l, mid, k);
    else return query(tree[o1].rch, tree[o2].rch, mid + 1, r, k - num);
}

代码

本代码可以通过【P3834】【模板】可持久化线段树 1(主席树) – 洛谷一题。

// Code by KSkun, 2018/2
#include <cstdio>
#include <algorithm>

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline int readint() {
    register int res = 0, neg = 1;
    char c = fgc(); 
    while (c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while (c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 200005;

struct Node {
    int lch, rch, val;
} tree[MAXN << 5];
int tot = 0, rt[MAXN];

inline void insert(int &o, int l, int r, int x) {
    tree[++tot] = tree[o];
    o = tot;
    tree[o].val++;
    if(l == r) return;
    int mid = (l + r) >> 1;
    if(x <= mid) insert(tree[o].lch, l, mid, x);
    else insert(tree[o].rch, mid + 1, r, x);
}

inline int query(int o1, int o2, int l, int r, int k) {
    if(l == r) return l;
    int mid = (l + r) >> 1;
    int num = tree[tree[o2].lch].val - tree[tree[o1].lch].val;
    if(k <= num) return query(tree[o1].lch, tree[o2].lch, l, mid, k);
    else return query(tree[o1].rch, tree[o2].rch, mid + 1, r, k - num);
}

int n, m, a[MAXN], tmp[MAXN],ttot, l, r, k;

int main() {
    n = readint();
    m = readint();
    for(int i = 1; i <= n; i++) {
        a[i] = tmp[i] = readint();
    }
    std::sort(tmp + 1, tmp + n + 1);
    ttot = std::unique(tmp + 1, tmp + n + 1) - tmp - 1;
    for(int i = 1; i <= n; i++) {
        rt[i] = rt[i - 1];
        insert(rt[i], 1, ttot, std::lower_bound(tmp + 1, tmp + ttot + 1, a[i]) - tmp);
    } 
    while(m--) {
        l = readint();
        r = readint();
        k = readint();
        printf("%d\n", tmp[query(rt[l - 1], rt[r], 1, ttot, k)]);
    }
    return 0;
}
[THUT2012]序列操作 题解

[THUT2012]序列操作 题解

题目地址:洛谷:【P4247】[清华集训]序列操作 – 洛谷、BZOJ:Problem 2962. — 序列操作

题目描述

有一个长度为n的序列,有三个操作:

  1. I a b c表示将[a, b]这一段区间的元素集体增加c;
  2. R a b表示将[a, b]区间内所有元素变成相反数;
  3. Q a b c表示询问[a, b]这一段区间中选择c个数相乘的所有方案的和mod 19940417的值。

输入输出格式

输入格式:
第一行两个数n, q表示序列长度和操作个数。
第二行n个非负整数,表示序列。
接下来q行每行输入一个操作I a b c或者R a b或者Q a b c,意义如题目描述。

输出格式:
对于每个询问,输出选出c个数相乘的所有方案的和mod 19940417的值。

输入输出样例

输入样例#1:

5 5
1 2 3 4 5
I 2 3 1
Q 2 4 2
R 1 5
I 1 3 -1
Q 1 5 1

输出样例#1:

40
19940397

说明

样例说明:
做完第一个操作序列变为1 3 4 4 5
第一次询问结果为3 \times 4+3 \times 4+4 \times 4=40
做完R操作变成-1 -3 -4 -4 -5
做完I操作变为-2 -4 -5 -4 -5
第二次询问结果为-2-4-5-4-5=-20

数据范围:
对于100%的数据,n≤50000,q≤50000,初始序列的元素的绝对值≤109,保证[a, b]是一个合法区间,I操作中∣c∣≤109,Q操作中1≤c≤min⁡(b−a+1, 20)。

题解

这个题里面有两种标记(相反数、区间加)和一种询问。我们分开来看。

标记与标记的应用

这里我们规定相反数的标记比加法标记优先级更高。试想,如果应用的顺序相反,可能会造成-a_i+add_i变成-(a_i+add_i)
首先考虑区间信息的设置。应该记下本区间长度和一个f[21]数组,代表这个区间内对于每个c询问的答案。规定任意的f[0]为0。
如果要应用一个取相反数标记,应该将上述c为奇数时的情况的f值取相反数,因为偶数次相乘负号会抵消。
如果要应用一个区间加标记,思考对答案的改变,以f[15]这个值为例:
假如选择前15个值相乘,变化如下
[a_1, a_2, \cdots, a_{15}] \to [a_1 + c, a_2 + c, \cdots, a_{15} + c]
乘起来是这样的
f_{15} = C_{len-15}^0 \cdot c^0 \cdot f_{15} + C_{len-14}^1 \cdot c^1 \cdot f_{14} + C_{len-13}^2 \cdot c^2 \cdot f_{13} + \cdots + C_{len-0}^{15} \cdot c^{15} \cdot f_0
那我们就明白了,倒序求f值就好了。这里的C可以开始的时候预处理打个大表,空间是够的。区间加标记应用对应的代码长这样

inline void add(Data &dat, LL x) {
    for(int i = std::min(dat.len, 20ll); i >= 0; i--) {
        LL t = x;
        for(int j = i - 1; j >= 0; j--) {
            dat.f[i] = (dat.f[i] + dat.f[j] * C[dat.len - j][i - j] % MO * t % MO) % MO;
            dat.f[i] = (dat.f[i] + MO) % MO;
            t = t * x % MO;
        }
    }
    dat.toadd = (dat.toadd + x) % MO;
}

区间合并

重点是要计算f值。首先考虑大区间选若干个是可以拆成左边选若干个和右边选若干个乘在一起的,这样既然左右两边的答案都是一堆乘积加起来,两块直接乘在一起就相当于两两配对后加起来,因此我们合并的公式就是:
f_k = \sum_{i=0}^k fl_i \times fr_{k-i}
上面式子的fl是左儿子f值,fr是右儿子f值。两层循环加一下就好。

代码

// Code by KSkun, 2018/2
#include <cstdio>
#include <cstring> 
#include <algorithm>
typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline int readint() {
    register int res = 0, neg = 1;
    char c = fgc(); 
    while (c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while (c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 50005, MO = 19940417;

LL n, q, a, b, c, C[MAXN][21], val[MAXN];
char op;

inline bool isop(char c) {
    return c == 'I' || c == 'R' || c == 'Q';
}

inline char readop() {
    char c = fgc();
    while (!isop(c)) c = fgc();
    return c;
}

inline void calc() {
    C[0][0] = 1;
    for(int i = 1; i <= n; i++) {
        for(int j = 0; j <= std::min(i, 20); j++) {
            C[i][j] += C[i - 1][j];
            if(j - 1 >= 0) C[i][j] += C[i - 1][j - 1];
            C[i][j] %= MO;
        }
    }
}

#define lch o << 1
#define rch o << 1 | 1
#define mid ((l + r) >> 1)

struct Data {
    LL len, toadd, f[21];
    bool torev;
} tree[MAXN << 2];

inline void merge(Data &rt, Data ls, Data rs) {
    rt.len = ls.len + rs.len;
    memset(rt.f, 0, sizeof(rt.f));
    for(int i = 0; i <= std::min(ls.len, 20ll); i++) {
        for(int j = 0; j <= std::min(rs.len, 20ll); j++) {
            if(i + j <= 20) rt.f[i + j] = (rt.f[i + j] + ls.f[i] * rs.f[j] % MO) % MO;
        } 
    }
}

inline void build(int o, int l, int r) {
    if(l == r) {
        tree[o].len = 1;
        tree[o].f[0] = 1;
        tree[o].f[1] = val[l];
        return;
    }
    build(lch, l, mid);
    build(rch, mid + 1, r);
    merge(tree[o], tree[lch], tree[rch]);
}

inline void add(Data &dat, LL x) {
    for(int i = std::min(dat.len, 20ll); i >= 0; i--) {
        LL t = x;
        for(int j = i - 1; j >= 0; j--) {
            dat.f[i] = (dat.f[i] + dat.f[j] * C[dat.len - j][i - j] % MO * t % MO) % MO;
            dat.f[i] = (dat.f[i] + MO) % MO;
            t = t * x % MO;
        }
    }
    dat.toadd = (dat.toadd + x) % MO;
}

inline void rev(Data &dat) {
    for(int i = 0; i <= std::min(dat.len, 20ll); i++) {
        if(i & 1) {
            dat.f[i] = (-dat.f[i] % MO + MO) % MO;
        }
    }
    dat.torev = !dat.torev;
    dat.toadd = (-dat.toadd % MO + MO) % MO;
}

inline void pushdown(int o) {
    if(tree[o].torev) {
        rev(tree[lch]);
        rev(tree[rch]);
        tree[o].torev = false;
    } 
    if(tree[o].toadd != 0) {
        add(tree[lch], tree[o].toadd);
        add(tree[rch], tree[o].toadd);
        tree[o].toadd = 0;
    }
} 

inline void modifyI(int o, int l, int r, int ll, int rr, LL v) {
    if(l == ll && r == rr) {
        add(tree[o], v);
        return;
    }
    pushdown(o);
    if(rr <= mid) {
        modifyI(lch, l, mid, ll, rr, v);
    } else if(ll > mid) {
        modifyI(rch, mid + 1, r, ll, rr, v);
    } else {
        modifyI(lch, l, mid, ll, mid, v);
        modifyI(rch, mid + 1, r, mid + 1, rr, v);
    }
    merge(tree[o], tree[lch], tree[rch]);
}

inline void modifyR(int o, int l, int r, int ll, int rr) {
    if(l == ll && r == rr) {
        rev(tree[o]);
        return;
    }
    pushdown(o);
    if(rr <= mid) {
        modifyR(lch, l, mid, ll, rr);
    } else if(ll > mid) {
        modifyR(rch, mid + 1, r, ll, rr);
    } else {
        modifyR(lch, l, mid, ll, mid);
        modifyR(rch, mid + 1, r, mid + 1, rr);
    }
    merge(tree[o], tree[lch], tree[rch]);
}

inline Data query(int o, int l, int r, int ll, int rr) {
    if(l >= ll && r <= rr) {
        return tree[o];
    }
    pushdown(o);
    if(rr <= mid) {
        return query(lch, l, mid, ll, rr);
    } else if(ll > mid) {
        return query(rch, mid + 1, r, ll, rr);
    } else {
        Data res;
        merge(res, query(lch, l, mid, ll, mid), query(rch, mid + 1, r, mid + 1, rr));
        return res;
    }
}

int main() {
    n = readint();
    q = readint();
    calc();
    for(int i = 1; i <= n; i++) {
        val[i] = readint();
        val[i] = (val[i] % MO + MO) % MO;
    } 
    build(1, 1, n);
    while(q--) {
        op = readop();
        if(op == 'I') {
            a = readint();
            b = readint();
            c = readint();
            c = (c % MO + MO) % MO;
            modifyI(1, 1, n, a, b, c);
        }
        if(op == 'R') {
            a = readint();
            b = readint();
            modifyR(1, 1, n, a, b);
        }
        if(op == 'Q') {
            a = readint();
            b = readint();
            c = readint();
            Data res = query(1, 1, n, a, b);
            printf("%lld\n", res.f[c]);
        }
    }
    return 0;
} 
[JLOI2015]城池攻占 题解

[JLOI2015]城池攻占 题解

题目地址:洛谷:【P3261】[JLOI2015]城池攻占 – 洛谷、BZOJ:Problem 4003. — [JLOI2015]城池攻占

题目描述

小铭铭最近获得了一副新的桌游,游戏中需要用 m 个骑士攻占 n 个城池。这 n 个城池用 1 到 n 的整数表示。除 1 号城池外,城池 i 会受到另一座城池 fi 的管辖,其中 fi < i。也就是说,所有城池构成了一棵有根树。这 m 个骑士用 1 到 m 的整数表示,其中第 i 个骑士的初始战斗力为 si,第一个攻击的城池为 ci。
每个城池有一个防御值 hi,如果一个骑士的战斗力大于等于城池的生命值,那么骑士就可以占领这座城池;否则占领失败,骑士将在这座城池牺牲。占领一个城池以后,骑士的战斗力将发生变化,然后继续攻击管辖这座城池的城池,直到占领 1 号城池,或牺牲为止。
除 1 号城池外,每个城池 i 会给出一个战斗力变化参数 ai;vi。若 ai =0,攻占城池 i 以后骑士战斗力会增加 vi;若 ai =1,攻占城池 i 以后,战斗力会乘以 vi。注意每个骑士是单独计算的。也就是说一个骑士攻击一座城池,不管结果如何,均不会影响其他骑士攻击这座城池的结果。
现在的问题是,对于每个城池,输出有多少个骑士在这里牺牲;对于每个骑士,输出他攻占的城池数量。

输入输出格式

输入格式:
第 1 行包含两个正整数 n;m,表示城池的数量和骑士的数量。
第 2 行包含 n 个整数,其中第 i 个数为 hi,表示城池 i 的防御值。
第 3 到 n +1 行,每行包含三个整数。其中第 i +1 行的三个数为 fi;ai;vi,分别表示管辖这座城池的城池编号和两个战斗力变化参数。
第 n +2 到 n + m +1 行,每行包含两个整数。其中第 n + i 行的两个数为 si;ci,分别表示初始战斗力和第一个攻击的城池。
输出格式:
输出 n + m 行,每行包含一个非负整数。其中前 n 行分别表示在城池 1 到 n 牺牲的骑士数量,后 m 行分别表示骑士 1 到 m 攻占的城池数量。

输入输出样例

输入样例#1:

5 5
50 20 10 10 30
1 1 2
2 0 5
2 0 -10
1 0 10
20 2
10 3
40 4
20 4
35 5

输出样例#1:

2
2
0
0
0
1
1
3
1
1

说明

对于 100% 的数据,1 <= n;m <= 300000; 1 <= fi<i; 1 <= ci <= n; -10^18 <= hi,vi,si <= 10^18;ai等于1或者2;当 ai =1 时,vi > 0;保证任何时候骑士战斗力值的绝对值不超过 10^18。

题解

考虑对城池构成的树进行DFS转移。维护一个可并小根堆,表示能够到达该节点的骑士,开始时将骑士加入开始节点的左偏树,DFS时合并儿子节点的左偏树,并且将那些战斗力达不到的骑士pop出,并计入该城池牺牲骑士数以及骑士牺牲的位置。本题输入比较大,所以临时换了个fread板子。

代码

// Code by KSkun, 2018/2 
#include <cstdio>
#include <algorithm>
typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0;
    register int neg = 1;
    char c = fgc(); 
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 300005;

int dis[MAXN], rt[MAXN], ch[MAXN][2];
LL val[MAXN], add[MAXN], mul[MAXN];

inline void pushdown(int x) {
    if(ch[x][0]) {
        val[ch[x][0]] *= mul[x];
        val[ch[x][0]] += add[x];
        mul[ch[x][0]] *= mul[x];
        add[ch[x][0]] *= mul[x];
        add[ch[x][0]] += add[x];
    }
    if(ch[x][1]) {
        val[ch[x][1]] *= mul[x];
        val[ch[x][1]] += add[x];
        mul[ch[x][1]] *= mul[x];
        add[ch[x][1]] *= mul[x];
        add[ch[x][1]] += add[x];
    }
    mul[x] = 1;
    add[x] = 0;
}

inline int merge(int x, int y) {
    if(!x) return y;
    if(!y) return x;
    if(val[x] > val[y]) std::swap(x, y);
    pushdown(x);
    pushdown(y);
    ch[x][1] = merge(ch[x][1], y);
    if(dis[ch[x][0]] < dis[ch[x][1]]) std::swap(ch[x][0], ch[x][1]);
    dis[x] = dis[ch[x][1]] + 1;
    return x;
}

struct Edge {
    int to, nxt;
} gra[MAXN];
int head[MAXN], tot = 1;

inline void addedge(int u, int v) {
    gra[tot].to = v;
    gra[tot].nxt = head[u];
    head[u] = tot++;
}

int n, m, f, a[MAXN], c[MAXN], dep[MAXN], die[MAXN], end[MAXN];
LL h[MAXN], v[MAXN];

inline void dfs(int u) {
    for(int i = head[u]; i; i = gra[i].nxt) {
        int v = gra[i].to;
        dep[v] = dep[u] + 1;
        dfs(v);
        rt[u] = merge(rt[u], rt[v]);
    }
    while(rt[u] && val[rt[u]] < h[u]) {
        pushdown(rt[u]);
        die[u]++;
        end[rt[u]] = u;
        rt[u] = merge(ch[rt[u]][0], ch[rt[u]][1]);
    }
    if(!a[u]) {
        val[rt[u]] += v[u];
        add[rt[u]] += v[u];
    } else {
        val[rt[u]] *= v[u];
        add[rt[u]] *= v[u];
        mul[rt[u]] *= v[u]; 
    }
}

int main() {
    dis[0] = -1;
    n = readint();
    m = readint();
    for(int i = 1; i <= n; i++) {
        h[i] = readint();
    }
    for(int i = 2; i <= n; i++) {
        f = readint();
        a[i] = readint();
        v[i] = readint();
        addedge(f, i);
    } 
    for(int i = 1; i <= m; i++) {
        val[i] = readint();
        c[i] = readint();
        mul[i] = 1;
        rt[c[i]] = merge(rt[c[i]], i);
    }
    dep[1] = 1;
    dfs(1);
    for(int i = 1; i <= n; i++) {
        printf("%d\n", die[i]);
    }
    for(int i = 1; i <= m; i++) {
        printf("%d\n", dep[c[i]] - dep[end[i]]);
    }
    return 0;
}