学习笔记 #4:树状数组和 LIS

学习笔记#4 :树状数组和 LIS

前言:

树状数组是和线段树类似的数据结构,更确切的说,树状数组能解决的问题,线段树都能解决,而线段树还能解决一些树状数组所不能解决的问题。因此线段树的应用范围比树状数组更广泛。但是,树状数组的常数更小,在同样的 \(\text{O}(n\log n)\) 复杂度下,树状数组普遍运行得更快。虽然线段树中有 zkw 线段树这样的小常数线段树,但树状数组仍然是常数之王,并且它的码量也极其之小,因此还是有学习的必要。

原理 & 实现:

有图为证:

首先介绍 \(\text{lowbit}(x)\) 是什么:

\(\text{lowbit}(x)\),顾名思义,就是二进制数 \(x\) 最低位的 \(1\) 所代表的十进制数,求法是 \(\text{lowbit}(x)=x\&-x\),具体原理手玩一下就知道。

代码:

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

而上图中树状数组的结构,从十进制数看起来没什么规律,但如果转为二进制呢?

\(c[00001]=a[00001]\)
\(c[00010] = a[00001] + a[00010]\)
\(c[00011] = a[00011]\)
\(c[00100] = a[00001] + a[00010] + a[00011] + a[00100]\)
\(\dots\)

有什么规律?

从单个的 \(a\)\(c\) 来看找不到规律,放眼全局,则可以看出:

  1. \(1\) 加到 \(i\) 就是 \(i\) 一直 -= \(\text{lowbit}(i)\) 直到 \(i = 0\)
  2. 每一个 \(a[i]\)\(i\) 一直 += \(\text{lowbit}(i)\) 直到 \(i = n\)\(c[i]\) 所管辖。

因此,树状数组的单点修改和区间查询就可以写出:

单点修改:

void modify(int i, int c) {for(; i <= n; i += lowbit(i)) C[i] += c;}

区间查询:

int query(int i) {int res = 0; for(; i; i -= lowbit(i)) res += C[i]; return res;}
int Query(int l, int r) {return Query(r) - Query(l - 1);}

树状数组与 LIS

LIS,即最长不下降子序列。

譬如这样一个序列:

1 2 4 1 3 4

显然,它的最长不下降子序列就是 1 2 3 4 ,长度为 \(4\)

我们设 \(f[i]\) 为以 \(i\) 结尾的最长不下降子序列的长度,则有:

\(f[i] = \max(f[j] + 1) (j < i \&\& a[j] \le a[i])\)

很明显,复杂度 \(\text{O}(n^2)\),对于很大的数据是过不了的。

那么怎么将复杂度降到 \(\text{O}(n \log n)\) 呢?

就要请出我们今天的主角:树状数组了。

首先,建立一棵值域树状数组:(如果 \(a[i]\) 过大就需要离散化)

这棵树状数组所维护的是以 \(a[i]\) 结尾的最长不下降子序列的长度,所以添加的时候 \(C[i] = \max(C[i], c)\)。而我们扫到哪加到哪,就保证了无后效性。

然后区间查询比 \(a[i]\) 小的,即以 \([1, a[i]]\) 结尾的最长不下降子序列的长度,加 \(1\) 即可。

\(Q.E.D.\)

posted @ 2024-02-18 19:49  水晶矩阵锭  阅读(37)  评论(0编辑  收藏  举报