数据结构——树状数组和在线、离线操作

数据结构------树状数组和在线、离线操作

  • [数据结构 树状数组](#数据结构 树状数组)
    • 树状数组和线段树的对比
    • 树状数组维护区间的原理
    • 一维树状数组
      • [单点修改 + 区间查询](#单点修改 + 区间查询)
        • [#130. 树状数组 1 - LibreOJ](#130. 树状数组 1 - LibreOJ)
      • [区间修改 + 单点查询](#区间修改 + 单点查询)
        • [131. 树状数组 2 - LibreOJ](#131. 树状数组 2 - LibreOJ)
      • [区间修改 + 区间查询](#区间修改 + 区间查询)
        • [#132. 树状数组 3 - LibreOJ](#132. 树状数组 3 - LibreOJ)
    • 二维树状数组
      • [单点修改 + 区间查询](#单点修改 + 区间查询)
        • [#133. 二维树状数组 1 - LibreOJ](#133. 二维树状数组 1 - LibreOJ)
      • [区间修改 + 单点查询](#区间修改 + 单点查询)
        • [#134. 二维树状数组 2 - LibreOJ](#134. 二维树状数组 2 - LibreOJ)
      • [区间修改 + 区间查询](#区间修改 + 区间查询)
        • [#135. 二维树状数组 3 - LibreOJ](#135. 二维树状数组 3 - LibreOJ)
  • 树状数组的应用列举
    • [P1908 逆序对 - 洛谷](#P1908 逆序对 - 洛谷)
    • [P1966 火柴排队 - 洛谷](#P1966 火柴排队 - 洛谷)
    • [P10589 楼兰图腾 - 洛谷](#P10589 楼兰图腾 - 洛谷)
    • [P3605 Promotion Counting P - 洛谷](#P3605 Promotion Counting P - 洛谷)
    • [P4054 计数问题 - 洛谷](#P4054 计数问题 - 洛谷)
    • [P3586 物流 Logistics - 洛谷 前缀和](#P3586 物流 Logistics - 洛谷 前缀和)
  • 在线操作与离线操作
  • OJ参考

数据结构 树状数组

树状数组(Binary Indexed Tree 简称:BIT)是一种只支持"单点修改"和"区间查询"的数据结构,修改和查询的时间复杂度为 log ⁡ n \log n logn 级别。

但有的问题能通过转化转换成单点修改和区间查询时也能用树状数组解决。

树状数组尽管在使用上和线段树没有联系,但理解上需要有线段树的基础,且要求读者会使用差分和前缀和解决问题。

树状数组能解决的问题是线段树能解决的问题的子集:

  • 树状数组能解决的,线段树一定能解决;
  • 线段树能解决的,树状数组不一定可以。

树状数组的代码要远比线段树短,虽然时间复杂度都是 O ( log ⁡ n ) \text{O}(\log n) O(logn) ,但树状数组的时间效率常数也更小,因为线段树在最差情况下需要遍历左、右子树加右子树的根结点,实际枚举次数是 2 log ⁡ n + m 2\log n+m 2logn+m ,而树状数组则是 ≤ log ⁡ n \le \log n ≤logn 。此外有的问题会将数据量控制在使用线段树会超时,但使用树状数组刚好能过的范围。

树状数组一般维护的信息需要满足结合律 以及可差分(或者说是可减性),比如区间,大区间的和减去小区间的和可得到另一些小区间的和,以及区间乘积。如果不满足交换律和可差分,树状数组就不能维护,比如区间最值以及区间 gcd 等。

树状数组和线段树的对比

对于一个长度为 n n n 的序列,执行 m m m 次操作,每次操作为:

  • 单点修改:修改某一个位置 i i i 的值;
  • 区间查询:查询区间 l , r l, r l,r 的和。

若 n = 8 n = 8 n=8,用线段树可以这样维护:

其中,每一个结点存储的就是区间和信息。但若维护的是区间和很多区间其实没有存储数据的必要 。比如 3 , 4 3, 4 3,4 的区间和,可以通过 1 , 4 1, 4 1,4 的区间和减去 1 , 2 1, 2 1,2 的区间和得出。此时删掉不必要的信息之后,可以得到如下结构,仅需用数组就可以存储:

通过上述过程可知,树状数组可以看作是"简化版的线段树"。在维护信息时,发现某些信息不需要去维护,进而将线段树中某些结点删掉,仅用一个数组就可以维护出所有想要的区间信息。

此时也可以发现,若查询的是区间最大值最小值,树状数组就不可行了。原因就是不能通过区间相减得出被删除区间的最小值。

树状数组维护区间的原理

树状数组可以通过用二进制将一段区间和拆分成若干没有交集的区间,进而得到树状数组。但通过线段树删减结点的方式更容易理解树状数组是如何维护信息的。

树状数组的下标需要从 1 开始计数。此时才能有如下性质:

  1. 向上爬公式(找父亲):结点编号为 x x x 时, x + lowbit ( x ) x + \text{lowbit}(x) x+lowbit(x) 等于父结点的编号。 lowbit ( x ) \text{lowbit}(x) lowbit(x) 为获取 x x x 的二进制 01 序列里最右边的 1 。

    例如 3 的二进制是 011 ,它最右边的 1 是 001 ,则它的父结点就是 3 + lowbit ( 3 ) = 4 3+\text{lowbit}(3)=4 3+lowbit(3)=4 , 父结点的父结点就是 4 + lowbit ( 4 ) = 8 4+\text{lowbit}(4)=8 4+lowbit(4)=8。

  2. 向前跳公式(找前一个相邻区间):结点编号为 x x x 时, x − lowbit ( x ) x - \text{lowbit}(x) x−lowbit(x) 等于前一个相邻区间所在的编号。

    例如 7 的二进制是 111 , 7 − lowbit ( 7 ) 7-\text{lowbit}(7) 7−lowbit(7) 后是 6,正好是区间 5 , 6 5,6 5,6 。同理 7 − lowbit ( 7 ) − lowbit ( lowbit(7) ) = 4 7-\text{lowbit}(7)-\text{lowbit}(\text{lowbit(7)})=4 7−lowbit(7)−lowbit(lowbit(7))=4 ,正好是 1 , 4 1,4 1,4 区间。

  3. 维护的区间:结点编号为 x x x 时,维护的区间信息为 x − lowbit ( x ) + 1 , x x - \\text{lowbit}(x) + 1, x x−lowbit(x)+1,x

    例如 6 ,它维护的区间是 6 − lowbit ( 6 ) + 1 , 6 = 5 , 6 6-\\text{lowbit}(6)+1,6=5,6 6−lowbit(6)+1,6=5,6

所以任意正整数都有关于 2 的不重复次幂的唯一分解序列,一个正整数最多可分成 O ( log ⁡ x ) \text{O}(\log x) O(logx) 个小区间。

即 x = 2 i 1 + 2 i 2 + ⋯ + 2 i m x=2^{i_1}+2^{i_2}+\dots+2^{i_m} x=2i1+2i2+⋯+2im ,则 1 , x 1,x 1,x 可分成的小区间个数为 x x x 的二进制序列中 1 的个数。例如不同长度的区间:

  • 长度为 2 i 1 2^{i_1} 2i1 的小 区间 1 , 2 i 1 1,2\^{i_1} 1,2i1

  • 长度为 2 i 2 2^{i_2} 2i2 的小 区间 2 i 1 + 1 , 2 i 1 + 2 i 2 2\^{i_1}+1,2\^{i_1}+2\^{i_2} 2i1+1,2i1+2i2

  • 长度为 2 i 3 2^{i_3} 2i3 的小 区间 2 i 1 + 2 i 2 + 1 , 2 i 1 + 2 i 2 + 2 i 3 2\^{i_1}+2\^{i_2}+1,2\^{i_1}+2\^{i_2}+2\^{i_3} 2i1+2i2+1,2i1+2i2+2i3

    ... \dots ...

  • 长度为 2 i m 2^{i_m} 2im 的小 区间 2 i 1 + 2 i 2 + ⋯ + 2 i m − 1 + 1 , 2 i 1 + 2 i 2 + ⋯ + 2 i m 2\^{i_1}+2\^{i_2}+\\dots+2\^{i_{m-1}}+1,2\^{i_1}+2\^{i_2}+\\dots+2\^{i_m} 2i1+2i2+⋯+2im−1+1,2i1+2i2+⋯+2im

这里 1 , 8 1,8 1,8 就被划分成 1 , 2 1 1,2\^1 1,21 2 1 + 1 , 2 2 2\^1+1,2\^2 21+1,22 2 2 + 1 , 2 3 2\^2+1,2\^3 22+1,23 3 个区间。

其中 lowbit(x)=x&-x 。例如 3 在计算机中的存储是 0 ... 011 0\dots011 0...011 ,而 − 3 -3 −3 在计算机中存储的是补码,即 − 3 -3 −3 的原码是 10 ⋯ 011 10\cdots011 10⋯011 ,而它的补码是二进制原码先取反变成 1 ... 100 1\dots100 1...100 ,然后再加 1 1 1 变成 1 ... 101 1\dots101 1...101 。

这里通过一个代码直观了解这个操作的原理。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

template <typename T>
void getbit(T x) { // 函数模板,可接收任意整型
    for (int i = sizeof(T) * 8 - 1; i >= 0; i--) {
        if ((x >> i) & 1)
            cout << 1;
        else
            cout << 0;
    }
    cout << endl;
}

int main() {
    int x = 3, y = -3;
    getbit(x);
    getbit(y);
    return 0;
}

输出:

复制代码
00000000000000000000000000000011
11111111111111111111111111111101

这里可以看到, 3 在计算机中的存储方式就是 0 ... 011 0\dots011 0...011 , − 3 -3 −3 则是 1 ... 101 1\dots 101 1...101 ,二者相与就是 3 最右边的 1 。

一维树状数组

单点修改 + 区间查询

对于一个长度为 n n n 的序列,执行 m m m 次操作,每次操作为:

  • 修改某一个位置 i i i 的值;
  • 查询区间 l , r l, r l,r 的和。

这个操作是树状数组最核心的操作,几乎所有用树状数组解决的问题都围绕单点修改 + + + 区间查询展开。哪怕是区间修改也要转换成 2 个单点修改。

单点修改 :以 x x x 位置的数据增加一个值为例,当某一个位置 x x x 的值发生改变之后,沿着这个点一路向上的点都会发生改变。因此,从 x x x 开始,不断向上爬,一边爬一遍修改对应结点的值即可。

cpp 复制代码
inline int Bitree::lowbit(int x) {
    return x & -x;
}
void Bitree::modify(int x, int k) {
    for (int i = x; i < bt.size(); i += lowbit(i))
        bt[i] += k;
}

创建 :默认整个数组全部为 0,每读入一个数 a i ai ai,就相当于 i i i 位置的数增加了 a i ai ai。因此,调用 modify 函数就可以完成树状数组的创建。这里依旧延续线段树的进行封装的习惯。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

struct Bitree {
    vector<int> bt; // 树状数组本体
    Bitree(int sz) {
        bt.resize(sz + 1, 0); // 下标从1开始
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x, int k) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            bt[i] += k;
    }
};

int main() {
    int n;
    cin >> n;
    Bitree bt(n);
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        bt.modify(i, x);
    }
    return 0;
}

区间查询 :通过向前跳性质可以得出,树状数组可以快速查询 1 , x 1, x 1,x 区间的和。例如想查询 x , y x, y x,y 的区间和,可以用前缀和的思想,先求出 1 , x − 1 1, x - 1 1,x−1 的区间和,再求出 1 , y 1, y 1,y 的区间和,后者减去前者即可,即 s u m ( x , y ) = q u e r y ( y ) − q u e r y ( x − 1 ) sum(x,y)=query(y)-query(x-1) sum(x,y)=query(y)−query(x−1) 。

cpp 复制代码
inline int Bitree::lowbit(int x){
    return x&-x;
}
int Bitree::query(int x) {
    int sum = 0;
    for (int i = x; i; i -= lowbit(i))
        sum += bt[i];
    return sum;
}
#130. 树状数组 1 - LibreOJ

#130. 树状数组 1 :单点修改,区间查询 - 题目 - LibreOJ

模板题。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vl = vector<LL>;

struct Bitree {
    vl bt;// 树状数组本体
    Bitree(LL sz) {
        bt.resize(sz + 1, 0);
    }
    inline LL lowbit(LL x) {
        return x & -x;
    }
    void modify(LL x, LL k) {
        for (LL i = x; i < bt.size(); i += lowbit(i))
            bt[i] += k;
    }
    LL query(LL x) {
        LL sum = 0;
        for (LL i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    int n, q;
    cin >> n >> q;
    Bitree bt(n);
    for (int i = 1; i <= n; i++) {
        LL x;
        cin >> x;
        bt.modify(i, x);
    }
    for (int T = q; T--;) {
        int op, x, y;
        cin >> op >> x >> y;
        if (op == 1)
            bt.modify(x, y);
        else
            cout << bt.query(y) - bt.query(x - 1) << '\n';
    }
    return 0;
}

区间修改 + 单点查询

对于一个长度为 n n n 的序列,执行 m m m 次操作,每次操作为:

  • 将区间 x , y x, y x,y 上所有数都增加 d d d;
  • 查询 a i a_i ai 的值。

树状数组和线段树不同,因为很多结点被砍掉,所以无法使用延迟标记处理区间修改,若强行使用遍历区间的方式修改,则时间复杂度会退化到 O ( n log ⁡ n ) \text{O}(n\log n) O(nlogn) 。

此时利用差分,创建出原始数组的差分数组,使用树状数组维护差分数组,那么上述两个操作:

  • 原数组的区间修改变成:差分数组 x x x 位置增加 d d d, y + 1 y+1 y+1 位置增加 − d -d −d;
  • 原数组的单点查询变成:差分数组求 1 , i 1, i 1,i 的区间和。

此时就可以用树状数组来解决。

131. 树状数组 2 - LibreOJ

#131. 树状数组 2 :区间修改,单点查询 - 题目 - LibreOJ

模板题。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vl = vector<LL>;

struct Bitree {
    vl bt;
    Bitree(LL sz) {
        bt.resize(sz + 1, 0);
    }
    size_t size() {
        return bt.size();
    }
    inline LL lowbit(LL x) {
        return x & -x;
    }
    void modify(LL x, LL k) {
        for (LL i = x; i < bt.size(); i += lowbit(i))
            bt[i] += k;
    }
    LL query(LL x) {
        LL sum = 0;
        for (LL i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    int n, q;
    cin >> n >> q;
    Bitree bt(n + 1); // 差分数组需要多开一个单位
    for (int i = 1; i <= n; i++) {
        LL x;
        cin >> x;
        bt.modify(i, x); // 树状数组维护差分数组
        bt.modify(i + 1, -x);
    }
    for (int T = q; T--;) {
        int op, x, y, z;
        cin >> op >> x;
        if (op == 1) {
            cin >> y >> z;
            bt.modify(x, z);
            if (y + 1 < bt.size())
                bt.modify(y + 1, -z);
        } else {
            cout << bt.query(x) << '\n';
        }
    }
    return 0;
}

区间修改 + 区间查询

推荐这类问题优先使用线段树,除非超时。

对于一个长度为 n n n 的序列,执行 m m m 次操作,每次操作为:

  • 把区间 x , y x, y x,y 所有的数统一加 k k k;
  • 查询区间 x , y x, y x,y 所有元素的和。

可以利用差分,将区间修改改成两个单点修改。那么区间查询操作也需要在差分数组中进行计算。

设原始数组为 a a a,差分数组为 d d d,那么:

1 , i 1, i 1,i 的区间和:

s u m 1 , i = a 1 + a 2 + a 3 + ⋯ + a i = d 1 + ( d 1 + d 2 ) + ⋯ + ( d 1 + d 2 + ⋯ + d i ) = d 1 × ( i − 0 ) + d 2 × ( i − 1 ) + ⋯ + d i × ( i − ( i − 1 ) ) = ( d 1 + d 2 + ⋯ d i ) × i − ( d 1 × 0 + d 2 × 1 + d 3 × 2 + ⋯ + d i × ( i − 1 ) ) \begin{aligned}sum1, i &= a1 + a2 + a3 + \cdots + ai\\&=d1 + (d1 + d2) + \cdots + (d1 + d2 + \cdots + di)\\&=d1 \times (i-0) + d2 \times (i - 1) + \cdots + di \times (i-(i-1))\\&=(d1 + d2 + \cdots di) \times i - \\&(d1 \times 0 + d2 \times 1 + d3 \times 2 + \cdots + di \times (i - 1))\end{aligned} sum1,i=a1+a2+a3+⋯+ai=d1+(d1+d2)+⋯+(d1+d2+⋯+di)=d1×(i−0)+d2×(i−1)+⋯+di×(i−(i−1))=(d1+d2+⋯di)×i−(d1×0+d2×1+d3×2+⋯+di×(i−1))

所以对区间修改 + + + 区间查询的问题,可以创建 2 个树状数组,一个维护 { d i } \{di\} {di} 这个序列的区间和,另一个维护 { d i × ( i − 1 ) } \{di \times (i - 1)\} {di×(i−1)} 这个序列的区间和。

当把区间 x , y x, y x,y 所有的数统一加 k k k 时,区间和变成

s u m 1 , i = d 1 + ( d 1 + d 2 ) + ⋯ + ( d 1 + d 2 + ⋯ + ( d x + k ) + ... ( d y + 1 − k ) + ... d i ) = d 1 × ( i − 0 ) + d 2 × ( i − 1 ) + ⋯ + ( d x + k ) × ( i − x + 1 ) + ⋯ + ( d y + 1 − k ) × ( i − y ) + d i × ( i − ( i − 1 ) ) = ( d 1 + d 2 + ⋯ + ( d x + k ) + ⋯ + ( d y + 1 − k ) + ... + d i ) × i − ( d 1 × 0 + d 2 × 1 + ⋯ + ( d x + k ) × ( x − 1 ) + ⋯ + ( d y + 1 − k ) × y + ⋯ + d i × ( i − 1 ) ) \begin{aligned}sum1, i&=d1 + (d1 + d2) + \cdots + (d1 + d2 + \cdots\\&+ (dx+k)+\dots (dy+1-k) + \dots di)\\\\&=d1 \times (i-0) + d2 \times (i - 1) + \cdots\\&+(dx+k)\times(i-x+1)+\dots+(dy+1-k)\times (i-y)\\&+ di \times (i-(i-1))\\\\&=(d1 + d2 + \cdots + \\&(dx+k)+\dots+(dy+1-k)+\dots\\&+ di)\times i - \\\\&(d1 \times 0 + d2 \times 1 + \cdots +\\&(dx+k)\times (x-1) + \dots +(dy+1-k)\times y+\dots+\\&di \times (i - 1))\end{aligned} sum1,i=d1+(d1+d2)+⋯+(d1+d2+⋯+(dx+k)+...(dy+1−k)+...di)=d1×(i−0)+d2×(i−1)+⋯+(dx+k)×(i−x+1)+⋯+(dy+1−k)×(i−y)+di×(i−(i−1))=(d1+d2+⋯+(dx+k)+⋯+(dy+1−k)+...+di)×i−(d1×0+d2×1+⋯+(dx+k)×(x−1)+⋯+(dy+1−k)×y+⋯+di×(i−1))

所以第 1 个树状数组在使区间 x , y x,y x,y 的所有的数统一加 k k k 时,第 2 个树状数组需要做出对应的操作:在 d x × ( x − 1 ) dx\times (x-1) dx×(x−1) 的位置增加一个 k × ( x − 1 ) k\times (x-1) k×(x−1) ,在 d y + 1 × y dy+1\times y dy+1×y 减去一个 k × ( y + 1 − 1 ) k\times (y+1-1) k×(y+1−1) 。

查询区间 x , y x,y x,y 时,使用这里推导的公式:

s u m ( 1 , y ) − s u m ( 1 , x − 1 ) = b t 1 ( y ) × y − b t 2 ( y ) − b t 1 ( x − 1 ) × ( x − 1 ) + b t 2 ( x − 1 ) \begin{aligned}sum(1,y)-sum(1,x-1)&=bt_1(y)\times y-bt_2(y)-\\&bt_1(x-1)\times (x-1)+bt_2(x-1)\end{aligned} sum(1,y)−sum(1,x−1)=bt1(y)×y−bt2(y)−bt1(x−1)×(x−1)+bt2(x−1)

#132. 树状数组 3 - LibreOJ

#132. 树状数组 3 :区间修改,区间查询 - 题目 - LibreOJ

还是模板题。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vl = vector<LL>;

struct Bitree {
    vl bt;
    Bitree(LL sz) {
        bt.resize(sz + 1, 0);
    }
    size_t size() {
        return bt.size();
    }
    inline LL lowbit(LL x) {
        return x & -x;
    }
    void modify(LL x, LL k) {
        for (LL i = x; i < bt.size(); i += lowbit(i))
            bt[i] += k;
    }
    LL query(LL x) {
        LL sum = 0;
        for (LL i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    LL n, q;
    cin >> n >> q;
    Bitree bt1(n + 1), bt2(n + 1);
    vl d(n + 1, 0);
    for (LL i = 1; i <= n; i++) {
        LL x;
        cin >> x;
        d[i] += x; // 创造差分数组
        d[i + 1] -= x;
    }
    for (LL i = 1; i <= n; i++) {
        bt1.modify(i, d[i]);
        bt2.modify(i, d[i] * (i - 1)); // 差分数组带个系数就不再是差分数组
    }
    for (int T = q; T--;) {
        int op, l, r;
        LL x;
        cin >> op >> l >> r;
        if (op == 1) {
            cin >> x;
            bt1.modify(l, x);
            if (r + 1 < bt1.size())
                bt1.modify(r + 1, -1 * x);
            bt2.modify(l, x * (l - 1));
            if (r + 1 < bt2.size())
                bt2.modify(r + 1, -1 * x * r);
        } else {
            int &y = r, &x = l;
            LL part1 = bt1.query(y) * y - bt2.query(y);
            LL part2 = bt1.query(x - 1) * (x - 1) - bt2.query(x - 1);
            cout << (part1 - part2) << '\n';
        }
    }
    return 0;
}

二维树状数组

二维树状数组类比一维进行建模。甚至若有三维也是类比一维、二维。

线段树也可以设置二维,但时间复杂度和空间复杂度会膨胀,且分治建树的方式是每个子矩阵分成 4 份进行处理,数据结构维护更加困难。所以尽量使用更轻量化的数据结构。

单点修改 + 区间查询

已知一个二维数组,有两种操作:

  • 修改 x , y x, y x,y 位置的值;
  • 查询左上角为 x 1 , y 1 x_1, y_1 x1,y1,右下角为 x 2 , y 2 x_2, y_2 x2,y2 的子矩阵的所有元素的和。

可以用二维树状数组来维护。实现二维树状数组时,直接类比一维树状数组即可。二维树状数组是矩阵嵌套矩阵的形式,很难进行手绘,但可以进行类比为多层一维树状数组。

所以修改 x , y x, y x,y 位置的值:

cpp 复制代码
void Bitree::modify(vvl &bt, int x, int y, int k) {
    for (int i = x; i < bt.size(); i = lowbit(i))
        for (int j = y; j < bt.size(); j = lowbit(j))
            bt[i][j] += k;
}

对于查询,类似前缀和数组的处理方式:查询以 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) 为左上角, ( x 2 , y 2 ) (x_2, y_2) (x2,y2) 为右下角的子矩阵的和,然后使用二维前缀和的计算方式即可。

如图所示,紫色部分是要求的矩阵和,根据面积计算方式,可得:

紫色部分 = 全 − 绿 − 粉 − 黄 = 全 − ( 绿 + 粉 ) − ( 绿 + 黄 ) + ( 绿 ) = s u m x 2 y 2 − s u m x 1 − 1 y 2 − s u m x 2 y 1 − 1 + s u m x 1 − 1 y 1 − 1 \begin{aligned}\text{紫色部分}&=\text{全}-\text{绿}-\text{粉}-\text{黄}\\&=\text{全}-(\text{绿}+\text{粉})-(\text{绿}+\text{黄})+(\text{绿})\\&=sumx_2y_2-sumx_1-1y_2-sumx_2y_1-1\\&+sumx_1-1y_1-1\end{aligned} 紫色部分=全−绿−粉−黄=全−(绿+粉)−(绿+黄)+(绿)=sumx2y2−sumx1−1y2−sumx2y1−1+sumx1−1y1−1

查询单个矩阵和的方法:

cpp 复制代码
int Bitree::query(vvl &bt, int x, int y) {
    int sum = 0;
    for (int i = x; i; i -= lowbit(i))
        for (int j = y; j; j -= lowbit(j))
            sum += bt[i][j];
    return sum;
}

同样的矩阵和要查询 4 个,然后用二维前缀和的方式进行计算。

#133. 二维树状数组 1 - LibreOJ

#133. 二维树状数组 1:单点修改,区间查询 - 题目 - LibreOJ

模板题。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vl = vector<LL>;
using vvl = vector<vl>;

struct Bitree {
    vvl bt; // 树状数组本体
    Bitree(int n, int m) {
        bt.resize(n + 1, vl(m + 1, 0)); // 下标从1开始
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x, int y, LL k) { // 二维比一维多一层循环
        for (int i = x; i < bt.size(); i += lowbit(i))
            for (int j = y; j < bt[i].size(); j += lowbit(j))
                bt[i][j] += k;
    }
    LL query(int x, int y) {
        LL sum = 0;
        for (int i = x; i; i -= lowbit(i))
            for (int j = y; j; j -= lowbit(j))
                sum += bt[i][j];
        return sum;
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    int n, m, op;
    int x1, y1, x2, y2;
    LL k;
    cin >> n >> m;
    Bitree bt(n, m);
    while (cin >> op) {
        cin >> x1 >> y1;
        if (op == 1) {
            cin >> k;
            bt.modify(x1, y1, k);
        } else {
            cin >> x2 >> y2;
            LL p1 = bt.query(x2, y2);
            LL p2 = bt.query(x1 - 1, y2);
            LL p3 = bt.query(x2, y1 - 1);
            LL p4 = bt.query(x1 - 1, y1 - 1);
            cout << p1 - p2 - p3 + p4 << '\n';
        }
    }
    return 0;
}

区间修改 + 单点查询

已知一个二维数组,有两种操作:

  • 将左上角为 x 1 , y 1 x_1, y_1 x1,y1,右下角为 x 2 , y 2 x_2, y_2 x2,y2 的子矩阵内所有的数都加上 d d d;
  • 查询 x , y x, y x,y 格子的值。

区间修改可利用差分算法,创建出原始矩阵的差分矩阵,之后上述两个操作:

  • 原矩阵的区间修改变成:差分矩阵中四个位置的修改;
  • 原矩阵的单点查询变成:差分矩阵中求以 x , y x, y x,y 为右下角的子矩阵的和。

此时就可以用二维树状数组来解决。

差分矩阵的修改:假设原始矩阵 a a a 中,以 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) 为左上角, ( x 2 , y 2 ) (x_2, y_2) (x2,y2) 为右下角的子矩阵的每个元素都加上 k k k:

除了紫色部分,其余都是消除修改操作带来的影响。

cpp 复制代码
using vvl = vector<vector<long long>>;
void dif(vvl &d, int x1, int y1, int x2, int y2, long long k) {
    d[x1][y1] += k;
    d[x2 + 1][y1] -= k;
    d[x1][y2 + 1] -= k;
    d[x2 + 1][y2 + 1] += k;
}
#134. 二维树状数组 2 - LibreOJ

#134. 二维树状数组 2:区间修改,单点查询 - 题目 - LibreOJ

模板题。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vl = vector<LL>;
using vvl = vector<vl>;

struct Bitree {
    vvl bt; // 树状数组本体
    Bitree(int n, int m) {
        bt.resize(n + 1, vl(m + 1, 0)); // 下标从1开始
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x, int y, LL k) { // 二维比一维多一层循环
        for (int i = x; i < bt.size(); i += lowbit(i))
            for (int j = y; j < bt[i].size(); j += lowbit(j))
                bt[i][j] += k;
    }
    LL query(int x, int y) {
        LL sum = 0;
        for (int i = x; i; i -= lowbit(i))
            for (int j = y; j; j -= lowbit(j))
                sum += bt[i][j];
        return sum;
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    int n, m, op;
    int a, b, c, d, x, y;
    LL k;
    cin >> n >> m;
    Bitree bt(n, m);
    while (cin >> op) {
        if (op == 1) {
            cin >> a >> b >> c >> d >> k;
            bt.modify(a, b, k);
            bt.modify(a, d + 1, -k);
            bt.modify(c + 1, b, -k);
            bt.modify(c + 1, d + 1, k);
        } else {
            cin >> x >> y;
            cout << bt.query(x, y) << '\n';
        }
    }
    return 0;
}

区间修改 + 区间查询

已知一个二维数组,有两种操作:

  • 将左上角为 x 1 , y 1 x_1, y_1 x1,y1,右下角为 x 2 , y 2 x_2, y_2 x2,y2 的子矩阵内所有的数都加上 d d d;
  • 查询左上角为 x 1 , y 1 x_1, y_1 x1,y1,右下角为 x 2 , y 2 x_2, y_2 x2,y2 的子矩阵内所有的元素的和。

和之前一样,区间修改可以利用差分将区间修改操作改成四个单点修改。

区间查询操作也需要在差分矩阵中获得,方式是先求和得到原数组,原数组再通过前缀和的方式得到,但若直接从差分数组得到,式子太过庞大,需要寻找规律进行简化。

设原数矩阵为 a a a ,它的差分矩阵为 d d d 。原矩阵求 x , y x, y x,y 为右下角的区间和时:

  • 需要在差分矩阵中的每一个位置都求一次二维前缀和
  • 求原数组的前缀和的过程中,差分矩阵的某项 d i j dij dij 会在区间 i , j x , y i, j \sim x, y i,jx,y 的每一个位置求前缀和时都加一次,因此总共加进去的次数为 ( x − i + 1 ) × ( y − j + 1 ) (x - i + 1) \times (y - j + 1) (x−i+1)×(y−j+1) 次,即 ( x − i + 1 ) × ( y − j + 1 ) (x - i + 1) \times (y - j + 1) (x−i+1)×(y−j+1) 个 d i j dij dij 连续相加作为前缀和计算的一部分。

所以原始矩阵中, ( x , y ) (x,y) (x,y) 为右下角的区间和为

s u m x y = ∑ i = 1 x ∑ j = 1 y a i j = ∑ i = 1 x ∑ j = 1 y d i j × ( x − i + 1 ) × ( y − j + 1 ) = ∑ i = 1 x ∑ j = 1 y ( d i j × ( x y + x + y + 1 ) − d i j × i × ( y + 1 ) − d i j × j × ( x + 1 ) + d i j × i × j ) \begin{aligned}sumxy=&\sum\limits_{i=1}^x \sum\limits_{j=1}^y aij\\=&\sum\limits_{i=1}^x \sum\limits_{j=1}^y dij \times (x - i + 1) \times (y - j + 1)\\=&\sum\limits_{i=1}^x \sum\limits_{j=1}^y (dij \times (xy + x + y + 1) \\&- dij \times i \times (y + 1)\\&- dij \times j \times (x + 1)\\&+ dij \times i \times j)\end{aligned} sumxy===i=1∑xj=1∑yaiji=1∑xj=1∑ydij×(x−i+1)×(y−j+1)i=1∑xj=1∑y(dij×(xy+x+y+1)−dij×i×(y+1)−dij×j×(x+1)+dij×i×j)

x x x 和 y y y 都是已知常数,所以求目标矩阵需要使用 4 个树状数组,分别维护 d i j dij dij、 d i j × i dij \times i dij×i、 d i j × j dij \times j dij×j、 d i j × i × j dij \times i \times j dij×i×j 。

当然,这只是利用差分矩阵求左上角为 ( 1 , 1 ) (1,1) (1,1) ,右下角为 ( x , y ) (x,y) (x,y) 的原矩阵的区间和的求法,若要求左上角不为 ( 1 , 1 ) (1,1) (1,1) 的原矩阵的区间和,还得使用二维前缀和的方式求解。

#135. 二维树状数组 3 - LibreOJ

#135. 二维树状数组 3:区间修改,区间查询 - 题目 - LibreOJ

模板题,但思维量和整体难度放在洛谷的体系也是不低于蓝题,甚至紫题。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vl = vector<LL>;
using vvl = vector<vl>;

struct Bitree {
    vvl bt;
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x, int y, LL k) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            for (int j = y; j < bt[i].size(); j += lowbit(j))
                bt[i][j] += k;
    }
    LL query(int x, int y) {
        LL sum = 0;
        for (int i = x; i; i -= lowbit(i))
            for (int j = y; j; j -= lowbit(j))
                sum += bt[i][j];
        return sum;
    }
};
Bitree bt, bi, bj, bij;
// 对应4种形式的差分数组

void init(int n, int m) {
    bt.bt.resize(n + 2, vl(m + 2, 0));
    bi.bt = bj.bt = bij.bt = bt.bt;
}

void add(int x, int y, LL k) {
    bt.modify(x, y, k);
    bi.modify(x, y, x * k);
    bj.modify(x, y, y * k);
    bij.modify(x, y, x * y * k);
}

LL sum(int x, int y) {
    LL p1 = bt.query(x, y) * (x * y + x + y + 1);
    LL p2 = bi.query(x, y) * (y + 1);
    LL p3 = bj.query(x, y) * (x + 1);
    LL p4 = bij.query(x, y);
    return p1 - p2 - p3 + p4;
}

int main() {
    // freopen("in.in", "r", stdin);
    int n, m;
    int op, a, b, c, d;
    LL k;
    cin >> n >> m;
    init(n, m);
    while (cin >> op) {
        cin >> a >> b >> c >> d;
        if (op == 1) {
            cin >> k;
            add(a, b, k); // 不封装的话要写16个modify
            add(c + 1, b, -k);
            add(a, d + 1, -k);
            add(c + 1, d + 1, k);
        } else {
            LL p1 = sum(c, d), p2 = sum(a - 1, d), p3 = sum(c, b - 1),
               p4 = sum(a - 1, b - 1);
            cout << p1 - p2 - p3 + p4 << '\n';
        }
    }
    return 0;
}

树状数组的应用列举

树状数组只有单点修改和区间查询的操作,所以最大的作用是管理差分前缀和数据 ,以及权值树状数组作为计数器

这里列举的很多问题都可以使用线段树解决。但这里的主角是树状数组,所以优先考虑树状数组。

P1908 逆序对 - 洛谷

P1908 逆序对 - 洛谷

逆序对可用归并排序、线段树、树状数组求解。这里使用树状数组。

原理和线段树一样,都是先离散化处理,然后每遍历一个数,就用和权值线段树一样功能的树状数组进行计数,同时查询逆序对的数量并统计。处理速度比归并排序慢,但比线段树快。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vl = vector<LL>;

struct Bitree { // 模拟权值线段树的树状数组
    vl bt;
    Bitree(int sz) {
        bt.resize(sz + 2, 0); // 离散化后的数量可能等于原数组
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            bt[i]++;
    }
    LL query(int x) {
        LL sum = 0;
        for (int i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    int n;
    cin >> n;
    vl a(n + 1, 0), b;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    // 备份
    b = a;
    // 离散化处理
    sort(a.begin() + 1, a.end());
    unordered_map<LL, int> ump; // ump[a[i]]=ip
    for (int i = 1, ip = 0; i <= n; i++) {
        if (ump.count(a[i]) > 0)
            continue;
        ump[a[i]] = ++ip;
    }
    // 权值树状数组求逆序对
    Bitree bt(n);
    LL ans = 0;
    bt.modify(ump[b[1]]);
    for (int i = 2; i <= n; i++) {
        bt.modify(ump[b[i]]);
        // sum(1,ip)-sum(1,ump[b[i]]),
        // 即统计已被记录的大于b[i]的数的个数
        ans += bt.query(ump.size()) - bt.query(ump[b[i]]);
    }
    cout << ans;
    return 0;
}

P1966 火柴排队 - 洛谷

P1966 [NOIP 2013 提高组 火柴排队 - 洛谷](https://www.luogu.com.cn/problem/P1966)

NOIP 的题目大都有一个特点,为结局题目需要解决隐含的各个问题,每个问题解决之后,整个题目就有眉目。

解决这个题,就需要解决 2 个问题:

  1. 如何求得距离最小值

首先能想到的是贪心策略:当 a a a 、 b b b 均有序时,距离最小。

例如第 2 个测试样例 1 3 4 2 1 7 2 4 → 1 2 3 4 1 2 4 7 \begin{matrix}1&3&4&2\\1&7&2&4\end{matrix}\rightarrow\begin{matrix}1&2&3&4\\1&2&4&7\end{matrix} 11374224→11223447 ,此时的距离最小。

之后用反证法证明这个贪心策略是否正确(正式比赛时间不够的话不用证):

贪心策略的结论是 ∑ ( a i − b i ) 2 \sum(a_i-b_i)^2 ∑(ai−bi)2 是最小距离,则设 a i < a j , b i < b j a_i<a_j,b_i<b_j ai<aj,bi<bj 。

按照贪心策略, ( a i − b i ) 2 + ( a j − b j ) 2 (a_i-b_i)^2+(a_j-b_j)^2 (ai−bi)2+(aj−bj)2 是最小距离,则假设这个不是最小距离,真正的最小距离是 ( a i − b j ) 2 + ( a j − b i ) 2 (a_i-b_j)^2+(a_j-b_i)^2 (ai−bj)2+(aj−bi)2 (交换论证)。

则前者减去后者理应大于 0 。

( a i − b i ) 2 + ( a j − b j ) 2 − ( a i − b j ) 2 − ( a j − b i ) 2 = ( a i 2 + b i 2 − 2 a i b i ) + ( a j 2 + b j 2 − 2 a j b j ) − ( a i 2 + b j 2 − 2 a i b j ) − ( a j 2 + b i 2 − 2 a j b i ) = 2 a i b j + 2 a j b i − 2 a i b i − 2 a j b j = 2 ( a i ( b j − b i ) + a j ( b i − b j ) ) = 2 ( a i − a j ) ( b j − b i ) < 0 \begin{aligned}&(a_i−b_i)^2+(a_j−b_j)^2-(a_i-b_j)^2-(a_j-b_i)^2\\=&(a_i^2+b_i^2-2a_ib_i)+(a_j^2+b_j^2-2a_jb_j)-\\&(a_i^2+b_j^2-2a_ib_j)-(a_j^2+b_i^2-2a_jb_i)\\=&2a_ib_j+2a_jb_i-2a_ib_i-2a_jb_j\\=&2(a_i(b_j-b_i)+a_j(b_i-b_j))\\=&2(a_i-a_j)(b_j-b_i)<0\end{aligned} ====(ai−bi)2+(aj−bj)2−(ai−bj)2−(aj−bi)2(ai2+bi2−2aibi)+(aj2+bj2−2ajbj)−(ai2+bj2−2aibj)−(aj2+bi2−2ajbi)2aibj+2ajbi−2aibi−2ajbj2(ai(bj−bi)+aj(bi−bj))2(ai−aj)(bj−bi)<0

所以假设不成立,贪心策略正确。

  1. 如何交换才能使得距离最小

以第 2 个测试样例 1 3 4 2 1 7 2 4 → 1 2 3 4 1 2 4 7 \begin{matrix}1&3&4&2\\1&7&2&4\end{matrix}\rightarrow\begin{matrix}1&2&3&4\\1&2&4&7\end{matrix} 11374224→11223447 为例,从原数组看不出什么端倪。但若是将每个数和它的原始下标绑定作为一对映射:

( 1 ) 1 ( 1 ) 3 ( 2 ) 4 ( 3 ) 2 ( 4 ) 1 ( 1 ) 7 ( 2 ) 2 ( 3 ) 4 ( 4 ) → ( 2 ) 1 ( 1 ) 2 ( 4 ) 3 ( 2 ) 4 ( 3 ) 1 ( 1 ) 2 ( 3 ) 4 ( 4 ) 7 ( 2 ) (1)\begin{matrix}1(1)&3(2)&4(3)&2(4)\\1(1)&7(2)&2(3)&4(4)\end{matrix}\rightarrow (2)\begin{matrix}1(1)&2(4)&3(2)&4(3)\\1(1)&2(3)&4(4)&7(2)\end{matrix} (1)1(1)1(1)3(2)7(2)4(3)2(3)2(4)4(4)→(2)1(1)1(1)2(4)2(3)3(2)4(4)4(3)7(2)

然后将 a a a 或 b b b 进行还原,另一个也和绑定的映射一一还原:

( 2 ) 1 ( 1 ) 2 ( 4 ) 3 ( 2 ) 4 ( 3 ) 1 ( 1 ) 2 ( 3 ) 4 ( 4 ) 7 ( 2 ) → ( 3 ) 1 ( 1 ) 3 ( 2 ) 4 ( 3 ) 2 ( 4 ) 1 ( 1 ) 4 ( 4 ) 7 ( 2 ) 2 ( 3 ) (2)\begin{matrix}1(1)&2(4)&3(2)&4(3)\\1(1)&2(3)&4(4)&7(2)\end{matrix}\rightarrow(3)\begin{matrix}1(1)&3(2)&4(3)&2(4)\\1(1)&4(4)&7(2)&2(3)\end{matrix} (2)1(1)1(1)2(4)2(3)3(2)4(4)4(3)7(2)→(3)1(1)1(1)3(2)4(4)4(3)7(2)2(4)2(3)

所以完全可以做到一个数组不变,另一个数组做交换使得距离最小。

且从 ( 1 ) (1) (1) 到 ( 3 ) (3) (3) 看原数组时看不出什么规律,但看下标的话会发现交换使得距离最小的过程,实际上就是从有序变成想要的序列的逆过程 ,即排序的逆过程

所以这个测试样例的最小操作次数就是消灭 { 1 , 4 , 2 , 3 } \{1,4,2,3\} {1,4,2,3} 的逆序对的过程,这个数列的逆序对个数就是答案。逆序对有多种求法,这里使用树状数组。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const LL MOD = 1e8 - 3;
vector<pair<int, int>> a, b;
vector<int> aim;
int n;

struct Bitree { // 线段树辅助统计逆序对
    vector<LL> bt;
    Bitree(int sz) {
        bt.resize(sz + 1, 0);
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            bt[i]++;
    }
    LL query(int x) {
        LL sum = 0;
        for (int i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};

void input(vector<pair<int, int>> &a) {
    a.resize(n + 1, {0, 0});
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        a[i] = {x, i};
    }
}

int main() {
    // freopen("in.in", "r", stdin);
    cin >> n;
    aim.resize(n + 1, 0);
    input(a), input(b);
    sort(a.begin() + 1, a.end()); // 键值对按first进行排序
    sort(b.begin() + 1, b.end());
    for (int i = 1; i <= n; i++)
        aim[a[i].second] = b[i].second;
    // 答案就是消灭aim数组的逆序对的过程即统计逆序对
    Bitree bt(n);
    LL ans = 0;
    bt.modify(aim[1]);
    for (int i = 2; i <= n; i++) {
        bt.modify(aim[i]);
        ans = (ans + bt.query(n) - bt.query(aim[i])) % MOD;
    }
    cout << ans;
    return 0;
}

P10589 楼兰图腾 - 洛谷

P10589 楼兰图腾 - 洛谷

为方便描述,将尖刀简称 V,将铁锹简称 A(尖)。

V 图腾是对任意 3 个数 a i , a j , a k a_i,a_j,a_k ai,aj,ak , i < j < k i<j<k i<j<k ,有 a i > a j , a j < a k a_i>a_j,a_j<a_k ai>aj,aj<ak 的话,则这 3 个数组成一个 V 图腾。

A 图腾是对任意 3 个数 a i , a j , a k a_i,a_j,a_k ai,aj,ak , i < j < k i<j<k i<j<k ,有 a i < a j , a j > a k a_i<a_j,a_j>a_k ai<aj,aj>ak 的话,则这 3 个数组成一个 A 图腾。

所以对每个数可分别预处理出以它为起点、终点的逆序对数和以它为起点的顺序对数,然后对每个数求这些数对数,然后通过乘法原理统计以它为中心的图腾数,最后叠加即可。

顺序对是这里瞎编的定义,对任意 a i , a j ∈ { a } a_i,a_j\in \{a\} ai,aj∈{a} , i < j i<j i<j ,有 a i < a j a_i<a_j ai<aj 。也可求每个数左边比它小的数的个数、比它大的数的个数以及右边的,再用乘法原理求解也是一样的。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

struct Bitree { // 线段树辅助统计逆序对
    vector<LL> bt;
    Bitree(int sz) {
        bt.resize(sz + 1, 0);
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            bt[i]++;
    }
    LL query(int x) {
        LL sum = 0;
        for (int i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};
vector<LL> a, b; // 原始数组
int n;
LL ansv = 0, ansA = 0;
unordered_map<LL, LL> ump; // 离散化表

int main() {
    // freopen("in.in", "r", stdin);
    cin >> n;
    a.resize(n + 1, 0);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    // 离散化处理,因为题目没给a[i]的数据范围,不好确定
    b = a;
    sort(b.begin() + 1, b.end());
    for (int i = 1, ip = 0; i < b.size(); i++) {
        if (ump.count(b[i]) > 0)
            continue;
        ump[b[i]] = ++ip;
    }
    // 树状数组统计
    Bitree sn(n), fn(n), ss(n), fs(n);
    // 以a[i]为起点(start)的逆(n)序对和顺(s)序对的数量
    vector<LL> snn(n + 1, 0), fnn(n + 1, 0), ssn(n + 1, 0), fsn(n + 1, 0);
    // 查找以a[i]为终点的顺、序对的数量
    for (int i = 1; i <= n; i++) {
        fn.modify(ump[a[i]]);
        fs.modify(ump[a[i]]);
        fnn[i] = fn.query(ump.size()) - fn.query(ump[a[i]]); // 大于a[i]
        fsn[i] = fs.query(ump[a[i]] - 1);                    // 小于a[i]
    }
    // 查找以a[i]为起点的顺、序对的数量
    for (int i = n; i >= 1; i--) {
        sn.modify(ump[a[i]]);
        ss.modify(ump[a[i]]);
        snn[i] = ss.query(ump[a[i]] - 1);
        ssn[i] = sn.query(ump.size()) - sn.query(ump[a[i]]);
    }
    for (int i = 1; i <= n; i++) {
        ansv += ssn[i] * fnn[i];
        ansA += snn[i] * fsn[i];
    }
    cout << ansv << ' ' << ansA << endl;
    return 0;
}

P3605 Promotion Counting P - 洛谷

P3605 [USACO17JAN Promotion Counting P - 洛谷](https://www.luogu.com.cn/problem/P3605)

题目问的问题是给一个树,问对每一个父节点,它的所有子节点中,大于父结点自身能力值(权值)的子结点数。

所以这题的思路是通过 dfs 遍历这个树,每层递归代表一个父结点,它的任务是遍历搜索它的所有子结点,以及将自身能力值和超过自身的子结点数进行统计。这里的数据量偏大,于是使用树状数组辅助统计。注意每遍历一个新的父结点,统计比它大的结点数时,会将已统计过的结点数进行重复统计,所以每层递归一开始就先减去已经统计过的大于它的子结点数,遍历完所有子树后再统计回来。

cpp 复制代码
void dfs(指定奶牛){
    num[指定奶牛]-=已统计过大于当前奶牛的子结点数;
    for(auto&x:edge[指定奶牛])
        dfs(x);
    num[指定奶牛]-=新统计的大于当前奶牛的子结点数;
    将这层递归写入树状数组进行统计;
}

此外这题的奶牛能力值达到 10 9 10^9 109 ,程序开不了这么大的数组,所以需要先离散化处理。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
using vvi = vector<vi>;

struct Bitree { // 线段树辅助统计逆序对
    vector<int> bt;
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            bt[i]++;
    }
    int query(int x) {
        int sum = 0;
        for (int i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};
vi t, p, num;                // 离散化用,存储能力排位,排位大于自身的子结点数
vvi edge;                    // 存储多叉树的关系
unordered_map<int, int> ump; // 离散化用
int n;
Bitree bt; // 统计大于自身的奶牛能力的下属数

void dfs(int start) {
    num[start] -= bt.query(n) - bt.query(p[start]);
    for (auto &x : edge[start])
        dfs(x);
    num[start] += bt.query(n) - bt.query(p[start]);
    bt.modify(p[start]);
}

int main() {
    // freopen("in.in", "r", stdin);
    cin >> n;
    t.resize(n + 1, 0);
    bt.bt.resize(n + 1, 0);
    num = t;
    edge.resize(n + 1, vi());
    for (int i = 1; i <= n; i++)
        cin >> t[i];
    for (int i = 2; i <= n; i++) {
        int fa = 0;
        cin >> fa;
        edge[fa].push_back(i);
    }
    // 离散化处理
    p = t;
    sort(t.begin() + 1, t.end());
    for (int i = 1; i <= n; i++)
        ump[t[i]] = i;
    for (int i = 1; i <= n; i++) // 原始能力值没p用,数值小好比较
        p[i] = ump[p[i]];
    // dfs遍历并统计子结点中能力值大于自身的奶牛个数
    dfs(1);
    for (int i = 1; i <= n; i++)
        cout << num[i] << '\n';
    return 0;
}

P4054 计数问题 - 洛谷

P4054 [JSOI2009 计数问题 - 洛谷](https://www.luogu.com.cn/problem/P4054)

矩阵的单点修改 + + + 区间查询。但查询的不是区间和,而是指定权值的个数,这种情况下没办法使用单个树状数组解决。

但换个思路一想,这题给的权值最大只有 100,所以定义 101 个权值树状数组,每个树状数组统计同一个矩阵的指定权值的数量,然后修改上只出现 1 和 -1 表示增加或减少这个数在指定区间的数量,此时就能使用树状数组的单点修改 + + + 区间查询解决。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using vi = vector<int>;
struct Bitree {
    vector<vi> bt;
    Bitree(int n, int m) {
        bt.resize(n + 1, vi(m + 1, 0));
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x, int y, int k) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            for (int j = y; j < bt[i].size(); j += lowbit(j))
                bt[i][j] += k;
    }
    int query(int x, int y) {
        int sum = 0;
        for (int i = x; i; i -= lowbit(i))
            for (int j = y; j; j -= lowbit(j))
                sum += bt[i][j];
        return sum;
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    int n, m;
    cin >> n >> m;
    vector<vi> a(n + 1, vi(m + 1, 0));
    vector<Bitree> bt(101, Bitree(n, m)); // 101个树状数组分别维护100个值

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            cin >> a[i][j];
            bt[a[i][j]].modify(i, j, 1);// 指定权值进行统计
        }
    // 在线操作
    int q;
    cin >> q;
    while (q--) {
        int op, x, y, k;
        cin >> op >> x >> y >> k;
        if (op == 1) {
            bt[a[x][y]].modify(x, y, -1);
            bt[k].modify(x, y, 1);
            a[x][y] = k;
        } else {
            int &x1 = x, &x2 = y, &y1 = k;
            int y2, c;
            cin >> y2 >> c;
            int p1 = bt[c].query(x2, y2);
            int p2 = bt[c].query(x1 - 1, y2);
            int p3 = bt[c].query(x2, y1 - 1);
            int p4 = bt[c].query(x1 - 1, y1 - 1);
            cout << p1 - p2 - p3 + p4 << '\n';
        }
    }
    return 0;
}

P3586 物流 Logistics - 洛谷 前缀和

P3586 [POI 2015 R2 物流 Logistics - 洛谷](https://www.luogu.com.cn/problem/P3586)

前几个题都是树状数组当成计数器来使用,这次用树状数组维护前缀和。

这题作为紫题,难点在于对题目的理解。或许以后这题的难度会降至蓝题,但现在就看成紫题。

读题:这里重点解读第 2 个操作,每次选 c c c 个数,将它们同时减 1 ,够减的话继续减,不够的话另选 1 个数继续减,问能否减 s s s 次。例如 { 5 , 2 , 3 } \{5,2,3\} {5,2,3} ,对于操作 { Z , 2 , 5 } \{Z,2,5\} {Z,2,5} ,第 1 次选 { 5 , 3 } \{5,3\} {5,3} 只能减 3 次,剪完后再选 2 进行补充: { 2 , 2 } \{2,2\} {2,2} ,刚好可以减 5 5 5 次。形象表示:

{ 5 , 2 , 3 } → 选数 选出的数 { 5 , 3 } 选完后 { 2 } → 同时相减 选出的数 { 2 , 0 } 选完后 { 2 } → 再选数 \{5,2,3\}\xrightarrow{\text{选数}}\begin{matrix}\text{选出的数}&\{5,3\}\\\text{选完后}&\{2\}\end{matrix}\xrightarrow{\text{同时相减}}\begin{matrix}\text{选出的数}&\{2,0\}\\\text{选完后}&\{2\}\end{matrix} \xrightarrow{\text{再选数}} {5,2,3}选数 选出的数选完后{5,3}{2}同时相减 选出的数选完后{2,0}{2}再选数

选出的数 { 2 , 2 } 选完后 { } → 再同时相减 \begin{matrix}\text{选出的数}&\{2,2\}\\\text{选完后}&\{\}\end{matrix}\xrightarrow{\text{再同时相减}} 选出的数选完后{2,2}{}再同时相减

对第 2 个操作,首先需要统计大于等于操作次数 s s s 的数,记为 c n t cnt cnt ;然后再在剩下的数里去挑选 c − c n t c-cnt c−cnt 个数 ,因为剩下的数基本都是小于 s s s 的,所以挑选时需要判断选出来的 c − c n t c-cnt c−cnt 个数,它们的和是否都是大于等于 s s s ,用区间和表示的话则是 s u m ≥ ( c − c n t ) × s sum\geq (c-cnt)\times s sum≥(c−cnt)×s 。

所以这里可以安排 2 个树状数组,一个维护区间和,另一个维护每个数据出现的次数,如此便可实现查询不等式 s u m ≥ ( c − c n t ) × s sum\geq (c-cnt)\times s sum≥(c−cnt)×s 是否满足。第 1 个操作需要根据离散化后的数值,去讲原来统计过的数移除,再进行修改操作。

这题的单个元素数量能达到 10 9 10^9 109 ,数组开不了这么大的空间,需要使用离散化处理。但原始数组全是 0 ,只有经历多次修改才会最终成型,所以可以先把操作记录下来,构建最终的数组,然后进行离散化处理之后,再进行模拟。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
using LL = long long;
using vl = vector<LL>;
struct Bitree {
    vl bt;
    Bitree(int n) {
        bt.resize(n + 1, 0);
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x, LL k) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            bt[i] += k;
    }
    LL query(int x) {
        LL sum = 0;
        for (int i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};
struct Question {
    char op;
    int x, y;
};
vector<Question> q;
vi a, b;
int n, m;
unordered_map<int, int> ump;

int main() {
    // freopen("in.in", "r", stdin);
    cin >> n >> m;
    a.resize(m + 1, 0);
    b = a;
    q.resize(m + 1, {0, 0, 0});
    for (int i = 1; i <= m; i++) {
        cin >> q[i].op >> q[i].x >> q[i].y;
        b[i] = q[i].y; // 将每一个出现过的数都进行离散化处理
    }
    // 离散化处理
    sort(b.begin() + 1, b.end());
    for (int i = 1, ip = 0; i <= m; i++) {
        if (ump.count(b[i]) > 0)
            continue;
        ump[b[i]] = ++ip;
    }
    // 2个树状数组进行统计
    Bitree area_sum(ump.size()), area_cnt(ump.size());
    for (int i = 1; i <= m; i++) {
        char op = q[i].op;
        int x = q[i].x, y = q[i].y;
        if (op == 'U') {
            // 把x改成y
            if (a[x]) {
                area_sum.modify(ump[a[x]], -a[x]);
                area_cnt.modify(ump[a[x]], -1);
            }
            area_sum.modify(ump[y], y);
            area_cnt.modify(ump[y], 1);
            a[x] = y;
        } else {
            // 选出x个数,问能否进行y次减1
            LL sum = area_sum.query(ump[y] - 1);
            LL cnt = area_cnt.query(ump.size()) - area_cnt.query(ump[y] - 1);
            if (sum >= (x - cnt) * y)
                cout << "TAK\n";
            else
                cout << "NIE\n";
        }
    }
    return 0;
}

在线操作与离线操作

时间轴和动、静态问题

在算法题中,很多操作其实都会有一个时间轴这样的维度。

所有的问题根据修改和查询两种操作在时间轴上分布的不同,可以分为两类:

  • 动态问题边修改边查询 ,也就是修改操作和查询操作穿插在一起
  • 静态问题不包含修改 ,或者所有查询都在修改之后,也就是统一处理查询。

线段树和树状数组的题,它们的操作都是按照时间的先后顺序给列举出来的,即使没给也会有隐藏的操作符合时间顺序,例如求逆序对时,统计当前的数和查询已记录在案的大于当前数的数的个数,也是先记录已经遍历过的数,再去查询。这里和之前的线段树列举的 OJ 题,基本都是动态问题。

静态问题则是曾经的预处理前缀和、差分数组和使用单调栈预处理最近最值问题。静态问题很明显比动态问题简单。

在线、离线操作

在线操作与离线操作也叫作在线算法与离线算法,这是解决问题的思想而并不是某种具体的算法,但可以和很多算法与数据结构结合在一起。

如果我们给修改和查询操作定义一个时间轴,按照一次读进来的顺序从小到大,其中:

  • 在线操作 :需要对于每一次询问马上回答出它的答案;

  • 离线操作 :当结果与操作顺序无关 时,在已知所有的操作的情况下,安排合理的顺序去统计答案 ,再去按时间轴上的位置输出。这里的合理指的是使用这个顺序能优化时间复杂度或只有这个顺序才能解决问题。

离线操作一般是将操作按某个标准进行排序,得到先后循序,再进行统一操作。离线操作有可能将静态问题转化成动态问题从而增加难度。

之前的线段树和树状数组的 OJ 基本都是在线操作。这里列举 2 道离线操作的题。

P1972 HH 的项链 - 洛谷

P1972 [SDOI2009 HH 的项链 - 洛谷](https://www.luogu.com.cn/problem/P1972)

  1. 只有 1 个区间,如何快速求出区间内的贝壳种类数。

最笨的办法:遍历区间内的贝壳数。然后使用红黑树(set)或哈希表(unordered_set)。当然这会超时,耗时来自数据结构对数据的管理。

假设只有 1 次询问,也就只查询 1 个区间。则从头开始遍历每个贝壳,若当前贝壳对整个区间的贝壳种类数有贡献,就记为 1;若当前贝壳和之前的贝壳出现重复,则记为 0 。遍历完成后对每个位置的贝壳的贡献值求前缀和,就能求得这 1 个区间的贝壳总数。

  1. 如何处理多个区间查询的问题。

例如对测试样例进行补充,则有 3 次查询:

这时就有 3 个区间,若按只有 1 个区间的方式统计种数,则 { 2 , 3 } \{2,3\} {2,3} 这次查询会少统计 1 个贝壳。所以查询方式还需要再改进。

重新读题发现,这个题只有查询操作,且对原数组没有任何修改,所以可进行离线操作

  1. 对每次询问,按照右端点进行排序。
  2. 对排序好的每次询问,按照只有 1 个区间的方式统计种数。若查到之前已统计过贝壳数,则将之前的贝壳的贡献值记为 0 ,将当前的贝壳的贡献值记为 1 。因为已经排序,之前的查询已获得结果,所以修改贡献值对后续查询不会造成影响。

统计贝壳数可使用权值线段树或权值树状数组解决。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
struct Bitree {
    vi bt;
    Bitree(int n) {
        bt.resize(n + 1, 0);
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x, int k) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            bt[i] += k;
    }
    int query(int x) {
        int sum = 0;
        for (int i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};
struct Question {
    int l, r, id;
    bool operator<(const Question &aim) const {
        return r < aim.r;
    }
};
vi a, last;
int n, m, pos = 1;
vector<Question> ques;
vector<int> ans;

void IOinit() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
}

int main() {
    // freopen("in.in", "r", stdin);
    IOinit();
    cin >> n;
    a.resize(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        a[0] = max(a[0], a[i]);
    }
    last.resize(a[0] + 1, 0); // 这里不做离散化处理
    cin >> m;
    ques.resize(m + 1, {0, 0, 0});
    ans.resize(m + 1, 0);
    for (int i = 1; i <= m; i++) {
        int &l = ques[i].l, &r = ques[i].r, &id = ques[i].id;
        cin >> l >> r;
        id = i;
    }
    // 离线操作,先将问题排序
    sort(ques.begin() + 1, ques.end());
    Bitree bt(n);
    // 再遍历问题和统计贝壳数
    for (int i = 1; i <= m; i++) {
        int &l = ques[i].l, &r = ques[i].r, &id = ques[i].id;
        while (pos <= r) { // 统计贝壳数
            if (last[a[pos]])
                bt.modify(last[a[pos]], -1);
            bt.modify(pos, 1);
            last[a[pos]] = pos;
            ++pos;
        }
        // 每个问题都有各自的编号,输出答案时要按编号输出
        ans[id] = bt.query(r) - bt.query(l - 1);
    }
    for (int i = 1; i <= m; i++)
        cout << ans[i] << '\n';
    return 0;
}

P4113 采花 - 洛谷

P4113 [HEOI2012 采花 - 洛谷](https://www.luogu.com.cn/problem/P4113)

中译中:统计一个区间内出现次数大于等于 2 的某种颜色的花的种数。

P1972 [SDOI2009 HH 的项链 - 洛谷](https://www.luogu.com.cn/problem/P1972) 类似的离线操作。但统计的东西不一样,很多细节也不一样。这里依旧按照之前的思路分析。

  1. 只有 1 个区间,如何快速求出区间内的符合要求的花的种类数。

依旧引入贡献值的概念。以题目给的测试样例 { 1 , 2 , 2 , 3 , 1 } \{1,2,2,3,1\} {1,2,2,3,1} 为例。

则从头开始遍历每朵花,若当前的花和之前的花出现重复,则将当前的花记为 1 。遍历完成后对每个位置的花的贡献值求前缀和,就能求得这 1 个区间的花总数

例如测试样例经过一轮遍历下来的数据: 花色 1 2 2 3 1 贡献 0 0 1 0 1 \begin{matrix}\text{花色}&1&2&2&3&1\\\text{贡献}&0&0&1&0&1\end{matrix} 花色贡献1020213011 此时这个区间内的符合条件的花的种数就是 2 。

  1. 如何处理多个区间查询的问题,以及同种颜色的花出现的次数超过 2 种,如何快速求出区间内的符合要求的花的种类数。

这里引入一个新的例子: { 1 , 2 , 3 , 2 , 3 , 4 , 2 , 3 , 4 , 2 } \{1,2,3,2,3,4,2,3,4,2\} {1,2,3,2,3,4,2,3,4,2} ,因为这个例子会出现重复次数超过 2 的花。则对新例子和多种区间查询而言,原来的贡献设置方法不适用。

例如 花色 1 2 3 2 3 4 2 3 4 2 贡献 0 0 0 1 1 0 1 ? \begin{matrix}\text{花色}&1&2&3&2&3&4&2&3&4&2\\\text{贡献}&0&0&0&1&1&0&1?\end{matrix} 花色贡献10203021314021?342

颜色为 2 的花第 3 次出现,若将第 3 次出现的花的贡献值设置为 1 ,则查询区间 4 , 7 4,7 4,7 时会出错。但若将第 2 次出现的花的贡献改为 0 ,第 3 次设置为 1 的话,则查询区间 3 , 7 3,7 3,7 时会出错。所以需要改进统计方法。

对每种花:

  • 若第 1 次遇到时不用更新贡献值。
  • 若第 2 次遇到时,更新第 1 次遇到的花的贡献值。例如 { 1 , 2 , 3 , 2 } \{1,2,3,2\} {1,2,3,2} ,若更新当前位置的贡献值,则出现查询 3 , 4 3,4 3,4 时会出错。
  • 若第 3 次遇到时,更新第 2 次遇到的花的贡献值,同时清空第 1 次遇到的花的贡献值。
  • 若第 4 次遇到时,更新第 3 次的贡献值,同时清空第 2 次的贡献值。
  • ... \dots ...
  • 若第 k k k 次遇到时,更新第 k − 1 k-1 k−1 次的贡献值,同时清空第 k − 2 k-2 k−2 次的贡献值。

此时若按照离线操作对每次询问按右端点进行排序,则会发现排序靠前的询问已获得答案,再清空第 k − 2 k-2 k−2 次的贡献值时不会多后续的查询造成影响。

因此在 P1972 [SDOI2009 HH 的项链 - 洛谷](https://www.luogu.com.cn/problem/P1972) 的参考程序的基础上,修改对花的贡献设置判断逻辑即可。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
struct Bitree {
    vi bt;
    Bitree(int n) {
        bt.resize(n + 1, 0);
    }
    inline int lowbit(int x) {
        return x & -x;
    }
    void modify(int x, int k) {
        for (int i = x; i < bt.size(); i += lowbit(i))
            bt[i] += k;
    }
    int query(int x) {
        int sum = 0;
        for (int i = x; i; i -= lowbit(i))
            sum += bt[i];
        return sum;
    }
};
struct Question {
    int l, r, id;
    bool operator<(const Question &aim) const {
        return r < aim.r;
    }
};
vi a, last, llt;
int n, c, m, pos = 1;
vector<Question> ques;
vector<int> ans;

void IOinit() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
}

void Viinit() {
    a.resize(n + 1, 0);
    last.resize(c + 1, 0);
    llt = last;
    ques.resize(m + 1, {0, 0, 0});
    ans.resize(m + 1, 0);
}

int main() {
    // freopen("in.in", "r", stdin);
    IOinit();
    cin >> n >> c >> m;
    Viinit();
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = 1; i <= m; i++) {
        int &l = ques[i].l, &r = ques[i].r, &id = ques[i].id;
        cin >> l >> r;
        id = i;
    }
    // 离线操作,先将问题排序
    sort(ques.begin() + 1, ques.end());
    Bitree bt(n);
    for (int i = 1; i <= m; i++) {
        int &l = ques[i].l, &r = ques[i].r, &id = ques[i].id;
        while (pos <= r) {
            if (last[a[pos]]) {// 第2及以上次遇到此花
                bt.modify(last[a[pos]], 1);//记录上次遇到的花对种数的贡献
                if (llt[a[pos]])// 清空上上次遇到的花对种数的贡献
                    bt.modify(llt[a[pos]], -1);
                llt[a[pos]] = last[a[pos]];
            }
            last[a[pos]] = pos;
            ++pos;
        }
        ans[id] = bt.query(r) - bt.query(l - 1);
    }
    for (int i = 1; i <= m; i++)
        cout << ans[i] << '\n';
    return 0;
}

OJ参考

  1. 一维树状数组构建

#130. 树状数组 1 :单点修改,区间查询 - 题目 - LibreOJ

#131. 树状数组 2 :区间修改,单点查询 - 题目 - LibreOJ

#132. 树状数组 3 :区间修改,区间查询 - 题目 - LibreOJ

  1. 二维树状数组构建

#133. 二维树状数组 1:单点修改,区间查询 - 题目 - LibreOJ

#134. 二维树状数组 2:区间修改,单点查询 - 题目 - LibreOJ

#135. 二维树状数组 3:区间修改,区间查询 - 题目 - LibreOJ

  1. 树状数组应用列举

P1908 逆序对 - 洛谷 一题多解

P1966 [NOIP 2013 提高组 火柴排队 - 洛谷](https://www.luogu.com.cn/problem/P1966)

P10589 楼兰图腾 - 洛谷

P3605 [USACO17JAN Promotion Counting P - 洛谷](https://www.luogu.com.cn/problem/P3605)

P4054 [JSOI2009 计数问题 - 洛谷](https://www.luogu.com.cn/problem/P4054)

P3586 [POI 2015 R2 物流 Logistics - 洛谷](https://www.luogu.com.cn/problem/P3586)

  1. 树状数组辅助离线操作

P1972 [SDOI2009 HH 的项链 - 洛谷](https://www.luogu.com.cn/problem/P1972)

P4113 [HEOI2012 采花 - 洛谷](https://www.luogu.com.cn/problem/P4113)

相关推荐
一只旭宝1 小时前
【C++入门精讲22】常见设计模式
c++·设计模式
JAVA面经实录9172 小时前
Java 数据结构与算法 (终极完整学习文档)
java·数据结构·算法
c++之路3 小时前
Bazel C++ 构建系列文档(三):构建第一个 C++ 项目
开发语言·c++
旖-旎3 小时前
《LeetCode 695 岛屿的最大面积 FloodFill DFS 解法》
c++·算法·力扣·深度优先遍历·floodfill
影视飓风TIM4 小时前
数据结构 | 链表超全笔记(单链表+双链表+高频算法题)
数据结构·笔记·链表
森G4 小时前
61、信号与槽机制在 TCP 编程中的应用---------网络编程
网络·c++·qt·网络协议·tcp/ip
syagain_zsx4 小时前
STL 之 vector 讲练结合
c++·算法
牛油果子哥q4 小时前
STL set与map底层精讲,红黑树适配原理、有序去重特性、迭代器遍历、API实战与面试核心考点全解
开发语言·数据结构·c++·面试
奇妙方程式5 小时前
2026年第九届GXCPC广西大学生程序设计大赛(热身赛)题解
c++·编程比赛·编程竞赛·gxcpc
Tian_Hang5 小时前
C++原型模式(Protype)
开发语言·c++·算法