Featured image of post 离散化,树状数组

离散化,树状数组

请输入文字描述

前言

学习网站:Starrycoding

离散化

离散化是一种处理数据的技巧,通俗地讲就是当我们需要处理的数据范围很大,但是数据的个数很少时,我们可以将数据的范围进行压缩,从而减少空间的占用。离散化是一种映射的思想。

将一个数组离散化,并进行查询是比较常用的应用场景。

离散化的步骤如下:

  1. 选择好相关点,创建原数组的副本。
  2. 将副本中的值从小到大排序
  3. 将排序好的副本去重
  4. 查找原数组的每一个元素在副本中的位置,位置即为排名,将其作为离散化后的值。(我们一般用二分查找)

例题

P63 我的很长,你算一下

分析:

根据题目,数组的下标范围是$[1,10^9]$,这么大的范围如果我们直接用数组来存储会爆内存,根本连暴力都用不了。

但是看到数据,个数只有$10^5$个,那么如果把下标作为值存储在数组中呢?

我们把这个数组叫做X,这些下标叫做相关点,X的下标叫做排名。接着我们再开一个数组Y,Y的下标和X的下标一一对应,而Y的值存储的是原数组的值,也可以用来加减了。

这样通过两个数组,我们就可以通过排名查询原数组的值了。

总结得到这样的映射关系:排名–>原数组的下标–>原数组的值

所以我们可以将数组的下标进行离散化,然后用数组来存储。那么我们就有办法存储相关点了,也可以再用二分得到相关点排名

这里要注意,既然离散化的是下标,那么q次询问中的l和r也要记录进X中.这样才能得到排名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 3e5 + 9;
ll arr[N];
vector<int> X;
struct Q // 存放操作和询问
{
    ll a, b;
} add[N], que[N];

ll getidx(int x)
{
    /*lower_bound下边界 up_bound 上边界
    由于计算机区间习惯左闭右开,所以lower_bound返回第一个等于x的元素(假如有),up_bound返回第一个大于x的元素
    <x, <x, <x, [x, x, x, >x) ,>x
    lower_bound找出数组中第一个大于等于x元素的迭代器,up_bound返回的是第一个严格大于给定值的元素的迭代器。*/
    return lower_bound(X.begin(), X.end(), x) - X.begin() + 1;
    // 返回值范围是[1,X.size()]
}

int main(void)
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int n, q;
    cin >> n >> q;
    for (int i = 1; i <= n; i++)
    {
        ll x, w;
        cin >> x >> w;
        X.push_back(x);
        add[i] = {x, w};
    }
    for (int i = 1; i <= q; i++)
    {
        ll l, r;
        cin >> l >> r;
        X.push_back(l);
        X.push_back(r);
        que[i] = {l, r};
    }
    // 排序去重
    sort(X.begin(), X.end());
    X.erase(unique(X.begin(), X.end()), X.end());

    for (int i = 1; i <= n; i++)
    {
        ll x = getidx(add[i].a);
        ll w = add[i].b;
        arr[x] += w;
    }

    // 前缀和

    for (int i = 1; i <= X.size(); i++)
    {
        arr[i] += arr[i - 1];
    }

    for (int i = 1; i <= q; i++)
    {
        ll l = getidx(que[i].a);
        ll r = getidx(que[i].b);
        cout << arr[r] - arr[l - 1] << "\n";
    }
    return 0;
}

树状数组

树状数组是一种支持单点修改,区间查询区间修改(区间修改运用了差分)的,代码量小的数据结构。树状数组简单来说,算是一种高级前缀和

假如有个数组$a$,我们想得到这个数组的前缀和很简单,只需要用一个数组$sum$来存储前缀和就可以了。但是,如果我们要修改数组$a$中的一个元素,然后马上查询那个区间的和,那么我们$sum$就要重新进行前缀和再输出,这样的话,时间复杂度就会很高。

想解决这种在线的单点修改,就得使用树状数组。

如图,开一个数组$t$视为树状数组,$t_1$存的是$a_1$,$t_2$存的是$a_1+a_2$,$t_4$存的是$a_1+a_2+a_3+a_4$,$t_8$存的是$a_1+a_2+a_3+a_4+a_5+a_6+a_7+a_8$。别的也是如图一一对应的。

这样存储会带来什么性质?

我们把$t$存的数的区间称为管辖区间。这个管辖区间为$[i-lowbit(i)+1,i]$。长度为lowbit(i)

什么是lowbit(i)?

lowbit(i)是i的二进制表示中最低位的1所代表的数值

例子:lowbit(1100100)=0000100

lowbit公式:lowbit(i)=i&(-i)

我们把这个公式代入到例子看,$t_8$长度就为8,它的管辖区间为$[8-lowbit(8)+1,8]=[1,8]$。因此他是$a_1+a_2+a_3+a_4+a_5+a_6+a_7+a_8$。

那么我们如何进行单点修改呢?

可以看出,如果我们对$a_3$进行修改,那么$t_3,t_4,t_8$都要进行修改。这样的影响是有规律的。

我们可以发现,3+lowbit(3)=4,4+lowbit(4)=8,所以我们可以用一个循环来实现单点修改。

那么接下来我们就要实现区间查询了。

虽然$t_8$把全部元素包括了,但假如我们要的和是部分的话该怎么办呢?例如,我们要的是$a_1$到$a_7$的和。观察一下,可以发现$a_1$到$a_7$可以分为$t_4$+$t_6$+$t_7$。

这种无缝连接的块也是有规律的。

我们可以发现,7-lowbit(7)=6,6-lowbit(6)=4,所以我们可以用一个循环来实现区间查询。

最后,如何初始化树状数组呢?

我们可以把$t$数组初始化为0,然后用单点修改来实现初始化,就可以了。(和差分的思想有点类似)。

例题1

P40 【模板】树状数组(单点修改)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 9;
typedef long long ll;
ll a[N], t[N];
int n, q;

int lowbit(int x)
{
    return x & -x;
}

void update(int k, ll x)//单点修改,也负责初始化
{
    for (int i = k; i <= n; i += lowbit(i))
    {
        t[i] += x;
    }
}

ll getsum(int k)//区间查询求前缀和
{
    ll ans = 0;
    for (int i = k; i > 0; i -= lowbit(i))
    {
        ans += t[i];
    }
    return ans;
}

int main(void)
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> q;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    // 初始化树状数组
    for (int i = 1; i <= n; i++)
    {
        update(i, a[i]);
    }
    // 操作
    for (int i = 1; i <= q; i++)
    {
        int x;
        cin >> x;
        if (x == 1)
        {
            int k;
            ll v;
            cin >> k >> v;
            update(k, v);
        }
        else
        {
            int l, r;
            cin >> l >> r;
            ll ans = 0;
            ans = getsum(r) - getsum(l - 1);
            cout << ans << "\n";
        }
    }
    return 0;
}

接下来讲区间修改。

如果是区间修改,离线的话我们可以用差分来实现,时间复杂度比树状数组更加优秀。

但是,如果是要求在线的区间修改,即修改一次,就要我们出一次值,我们就必须用树状数组来实现了。

树状数组想要实现区间修改,就是从维护普通的前缀和变成维护差分数组的前缀和。

这里出现的n是错误,应该是r

如图,我们得出推广公式:

$$\sum_{i=1}^{r}a_i=\sum_{i=1}^{r} (r+1)d_i-\sum_{i=1}^{r}id_i$$

那么我们用两个树状数组来维护这两个差分前缀和就可以了。

我们可以先初始化出差分数组$d$,然后用树状数组来维护$d$的前缀和。也可以直接就用树状数组来得出$d$的前缀和。

例题2

P41 树状数组(区间修改)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 9;
typedef long long ll;
ll a[N], dt[N], dti[N];
int n, q;

int lowbit(int x)
{
    return x & -x;
}

void update(int k, ll x)  //单点修改,一次对两个树状数组进行修改
{
    for (int i = k; i <= n; i += lowbit(i))
    {
        dt[i] += x;
        dti[i] += k * x;     //注意这里是k*x,不是i*x
    }
}

ll getsum(int k)    //区间查询,运用刚才的公式得出
{
    ll ans = 0;
    for (int i = k; i > 0; i -= lowbit(i))
    {
        ans += (k + 1) * dt[i] - dti[i];
    }
    return ans;
}

int main(void)
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> q;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    // 初始化树状数组
    for (int i = 1; i <= n; i++)
    {
        update(i, a[i]);
        update(i + 1, -a[i]);      //根据差分,我们这样单点修改就可以实现区间修改了
    }
    // 操作
    for (int i = 1; i <= q; i++)
    {
        int x;
        cin >> x;
        if (x == 1)
        {
            int l, r;
            ll v;
            cin >> l >> r >> v;
            update(l, v);       //区间修改
            update(r + 1, -v);
        }
        else
        {
            int l, r;
            cin >> l >> r;
            ll ans = 0;
            ans = getsum(r) - getsum(l - 1);
            cout << ans << "\n";
        }
    }
    return 0;
}

附加题

P31 求逆序对个数

题面:

给定一个长度为$n$的数组$a$,求$a$的逆序对个数。

逆序对的定义是一个二元组$(a_i,a_j)$,满足:$i<j$且$a_i>a_j$。

输入格式:

一个整数n.($1\leq n\leq 2\times10^5$)

一个长度为$n$的数组$a$。($1\leq a_i\leq 10^9$)

输出格式:

一个整数,表示逆序对的个数。

分析:

此题求解逆序对,可以使用权值数组来求解,权值数组其实就是一个桶,桶的下标就是数组的值,桶的元素就是数组的值的个数。

如图,我们把2,3,1,1,6依次归入桶中,先进2,$a_2$值为1,然后遍历$a_2$之后的桶,发现桶里面个数都为0,说明2之前没有比2大的。然后进3,然后进1,发现有两个逆序对,再进1,发现有两个逆序对,如此结束。

接着我们发现这样虽然找得到,但时间复杂度很高,而且(10^9)的范围太大,所以我们需要用离散化来优化。

至于如何优化时间复杂度,可以这样逆向的求解,同样是2,3,1,1,6,2进桶之前,先判断下所有桶里面的元素个数,然后再减去其中比2小的元素个数,这样就可以得到2之前的逆序对个数。

这样的话就要有个前缀和,我们可以用树状数组来实现(时间复杂度优秀,总共$O(nlogn)$)。并且建立在离散化处理的数组的基础上。

树状数组<–排名<–值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 9;
typedef long long ll;

ll a[N], t[N];
vector<int> X;

int getidx(int x)
{
    return lower_bound(X.begin(), X.end(), x) - X.begin() + 1;
}

int lowbit(int x)
{
    return x & -x;
}

int getsum(int k)
{
    int res = 0;
    for (int i = k; i > 0; i -= lowbit(i))
    {
        res += t[i];
    }
    return res;
}

void update(int k, int x)
{
    for (int i = k; i <= X.size(); i += lowbit(i))
    {
        t[i] += x;
    }
}

int main(void)
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        X.push_back(a[i]);
    }
    // 排序去重
    sort(X.begin(), X.end());
    X.erase(unique(X.begin(), X.end()), X.end());

    /*
    树状数组是建立在离散化数组上的
    getsum(X.size())返回当前位置之前的所有元素的和
    getsum(getidx(a[i]))返回当前位置之前小于等于a[i]的元素的个数
    两者相减即为比 a[i] 大的元素的数量。
    */
    ll ans = 0;
    for (int i = 1; i <= n; i++)
    {
        ans += 1ll * getsum(X.size()) - getsum(getidx(a[i]));
        update(getidx(a[i]), 1);
    }
    cout << ans << "\n";
    return 0;
}
根据CC BY-NC-SA 4.0协议授权
最后更新于 2025-01-28
我等生来追逐辉光,一如火花向上飞舞
使用 Hugo 构建
主题 StackJimmy 设计