day15 树状数组 & Trie树 & AC自动机

树状数组 & Trie树


树状数组

要求

  • 满足结合律 & 可差分
  • 结合律: (x\ \circ\ y)\ \circ\ z ,其中 \circ 是一个二元运算符
  • 可差分:具有逆运算的运算,即已知 x\ \circ\ yx 可以求出 y
  • 在差分数组等的帮助下,还可解决更强的区间加单点值区间加区间和的问题

实现

  • c[x] 开始往前跳,由 c[x] 管辖 a[x-\text{lowbit}(x) + 1\dots x]
  • x\leftarrow x - \text{lowbit}(x) ,如果 x=0 说明已经跳到尽头了,终止循环
  • 否则回到第一步
  • 将跳到的 c 合并
  • l(x)=x-\text{lowbit}(x)+1l(x)c[x] 管辖范围左端点
  • 对于任意正整数 x ,总能将 x 表示成 s\times 2^{k+1}+2^k 的形式,其中 \text{lowbit}(x)=2^k

性质

  • 对于 x\le y ,要么有 c[x]c[y] 不交,要么有 c[x] 包含于 c[y]
  • c[x] 真包含于 c[x+\text{lowbit}(x)]
  • 对于任意 x<y<x+\text{lowbit}(x) ,有 c[x]c[y] 不交
  • fa[u] 表示 u 的直系父亲:
    • u<fa[u]
    • u 大于任何一个 u 的后代,小于任何一个 u 的祖先
    • u\text{lowbit} 严格小于 fa[u]\text{lowbit}
    • 如果 c[1] 的高度是 0 ,则点 x 的高度是 \log_2\text{lowbit}(x)
    • c[u] 真包含于 c[fa[u]]
    • c[u] 真包含于 c[v] ,其中 vu 的任意祖先
    • c[u] 真包含于 c[v] ,其中 vu 的任意后代
    • 对于任意 v'>u ,若 v' 不是 u 的祖先,则 c[u]c[v'] 不交
  • 对于任意 v<u ,如果 v 不在 u 的子树上,则 c[u]c[v] 不交
  • u = s \times 2^{k+1}+2^k ,则其儿子数量为 k = log_2\,\text{lowbit}(x) ,编号分别为 u-2^t\,(0 \leq t \leq k)
  • u 的所有儿子对应 c 的管辖区间恰好拼成 [l(u),u-1]

时空复杂度

  • 空间复杂度 O(n)
  • 时间复杂度:查询 O(\log n) ,单点修改 O(\log n)

区间加 & 区间求和

  • 考虑序列 a 的差分数组 d ,其中 d[i]=a[i]-a[i - 1]

  • 由于差分数组的前缀和就是原数组,所以 a_i=\sum^i_{j=1}d_j

  • 考虑将查询区间和通过差分转化为查询前缀和

  • 那么考虑查询 a[1\dots r] 的和,即 \sum^r_{i=1}a_j ,进行推导:

    \sum^r_{i=1}a_i=\sum^r_{i=1}\sum^i_{j=1}d_i\times(r-i+1)=\sum^r_{i=1}d_i\times(r+1)-\sum^r_{i=1}d_i\times i
  • \sum^r_{i=1}d_i 并不能推出 \sum_{i=1}^rd_i\times i 的值,所以要用两个树状数组分别维护

二维树状数组

  • 也被称作树状数组套树状数组,用来维护二维数组上的单点修改和前缀信息问题

  • c(x,y) 表示 a(x-\text{lowbit}(x)+1,y-\text{lowbit}(y)+1)\dots a(x,y) 的矩阵总信息

  • 即一个以 a(x,y) 为右下角,高 \text{lowbit}(x) ,宽 \text{lowbit}(y) 的矩阵的总信息

  • 对于单点修改,设:

    f(x,i)=\left\{\begin{matrix}x &i=0 \\f(x,i-1)+\text{lowbit}(f(x,i-1)) &i>0 \end{matrix}\right.
  • f(x,i)x 在树状数组形态上的第 i 级祖先(第 0 级祖先是自己)

  • 则只有 c(f(x,i),f(y,j)) 中的元素管辖 a(x,y) ,修改 a(x,y) 时只需要修改所有 c(f(x,i),f(y,j))

  • 其中 f(x,i)\le n,f(y,j)\le m

    // 修改
    for (int i = x;i <= n;i += lowbit(i))
        for (int j = y;j <= m;j += lowbit(j))
            c[i][j] += v;
    // 询问
    int ans = 0;
    for (int i = x;i > 0;i -= lowbit(i))
        for (int j = y;j > 0;j -= lowbit(j))
            ans += c[i][j];
    return ans;
    

子矩形加、求子矩形和

  • 考虑维护差分数组:

    d(i,j)=a(i,j)-a(i-1,j)-a(i,j-1)+a(i-1,j-1)
  • 对左上角 (x_1,y_1) ,右下角 x_2,y_2 的子矩形区间加 v

  • 相当于在差分数组上,对 d(x_1,y_1)d(x_2+1,y_2+1) 分别单点加 v

  • d(x2+1,y_1)d(x_1,y_2+1) 分别单点加 -v

  • 对于点 (x,y) ,它的二位前缀和可以表示为

    \sum^x_{i=1}\sum^y_{j=1}\sum^i_{h=1}\sum^j_{t=1}d(h,k)=\sum^x_{i=1}\sum^y_{j=1}d(i,j)\times(x-i+1)\times(y-j+1)\\=\sum^x_{i=1}\sum_{i=1}^yd(i,j)\times(xy+x+y+1)-d(i,j)\times i\times(y+1)-d(i,j)\times j\times(x+1)+d(i,j)\times i\times j
  • 所以需要维护四个树

  • 状数组,分别维护 d(i,j),d(i,j)\times i,d(i,j)\times j,d(i,j)\times i\times j 的和信息

权值树状数组

  • 例:单点修改,查询全局第 k
  • 对于单点修改,只需要将对原序列的单点修改转化为对权值数组的单点修改即可
  • 具体来说,原数组 a[x]y 修改为 z ,转化为对权值数组 b 的单点修改就是 b[y] 单点减 1b[z] 单点加 1
  • 对于查询第 k 小,考虑二分 x ,查询权值数组中 [1,x] 的前缀和,找到 x_0 使得 [1,x_0] 的前缀和 <k
  • [1,x_o+1] 的前缀和 \ge k ,则第 k 大的数是 x_0+1 ,这里认为 [1,0] 的前缀和是 0
  • 时间复杂度 O(\log^n)
  • 也可以用倍增代替二分,时间复杂度降低为 O(\log n)

全局逆序对(全局二位偏序)

  • 给定长度为 n 的序列 a ,求 a 中满足 i<ja[i] > a[j] 的数对 (i,j) 的数量
  • 设当前 a[i] = x 查询 b[1\dots x-1] 的前缀和,即为左端点为 a[i] 的逆序对数量
  • b[x] 自增 1

Trie

定义

  • 一颗像字典一样的树

维护异或极值

  • 将数的二进制表示看作一个字符串,就可以建出字符集为 \{0,1\}\texttt{Trie}

  • 给你一棵带边权的树,求 (u,v) 使得 uv 之间的路径的边权异或和最大,输出这个最大值

  • 指定一个根 rootT(u,v) 表示 uv 之间的路径的边权异或和,那么 T(u,v)=T(root,u)\oplus T(root,v) ,因为 \text{lca} 以上的部分异或两次抵消了

  • 如果将所有 T(root,u) 插入到一棵 \texttt{Trie} 树中,就可以对每个 T(root,u) 快速求出和它异或和最大的 T(root,v)

  • \texttt{Trie} 的根开始,如果能向和 T(root,u) 的当前为不同的子树走,就向那边走,否则没有选择

    int T[maxn][2], cnt;
    void insert(int v,int rt) {
        for (int i = 29;i >= 0;i --) {
            if (!T[rt][(v >> i) & 1]) T[rt][(v >> i) & 1] = ++ cnt;
            rt = T[rt][(v >> i) & 1];
        }
    }
    int query(int v,int rt) {
        for (int i = 29,k;i >= 0i --) {
            k = !((v >> i) & 1);
            if (T[rt][k]) rt = T[rt][k], ans += (1 << i);
        } else rt = T[rt][k];
        return ans;
    }
    

有限状态自动机

  • \texttt{deterministic finite automaton,DFA}
  • 由:
    1. 状态集合 Q
    2. 字符集 \Sigma
    3. 状态转移函数 \delta:Q\times\Sigma\to Q ,即 \delta(q,\sigma)=q',\ q,q'\in Q,\sigma\in \Sigma
    4. 一个开始状态 s\in Q
    5. 一个接收的状态集合 F\subseteq Q
  • 组成的五元组 (Q,\Sigma,\delta,s,F)

AC 自动机

  • \texttt{Trie} 的结构为基础,结合 KMP 的思想建立的

  • 在初始时会将若干个模式串丢到一个 \texttt{Trie} 里,然后在 \texttt{Trie} 上建立 AC 自动机

  • 构建 fail 指针,可以参考 KMP 中构造 Next 指针的思想

    • 考虑字典树中当前的结点 uu 的父节点是 pp 通过字符 c 的边指向 u ,即 \operatorname{trie[p,\texttt{c}]=u}
    • 假设深度小于 u 的所有结点的 fail 指针都已求得。
    1. 如果 \operatorname{trie[fail[p],\texttt{c}]} 存在:则让 u 的 fail 指针指向 \operatorname{trie[fail[p],\texttt{c}]} 。相当于在 p\operatorname{fail[p]} 后面加一个字符 c,分别对应 ufail[u]
    2. 如果 trie[fail[p],\texttt{c}] 不存在:那么我们继续找到 trie[fail[fail[p]],\texttt{c}] 。重复 1 的判断过程,一直跳 fail 指针直到根节点
    3. 如果真的没有,就让 fail 指针指向根节点
    // 模板
    struct node {
    	node *fail;
    	node *next[26]; 
    	int count;
    	node() { 
    		count = 0; 
    		memset(next, NULL, sizeof(next)); 
    	}
    } *q[15];
    void insert(char *str, node *root) {
    	node *p = root; 
    	int i = 0, index; 
    	while (str[i]) {
    		index = str[i] - 'a'; 
    		if (p -> next[index] == NULL) 
    			p -> next[index] = new node();
    			p = p -> next[index]; 
    			i++;
    	}
    	p->count++;
    }
    void buildAcAutomation(node *root) {
    	int i; 
    	root -> fail = NULL;
    	q[head++] = root;
    	while (head != tail) {
    		node *temp = q[tail++]; 
    		node *p = NULL;
    		for (i = 0; i < 26; i++) {
    			if (temp -> next[i] != NULL) {
    				if (temp == root)
    					temp -> next[i] -> fail = root; 
    				else {
    					p = temp -> fail;
    					while (p != NULL) {
    						if (p -> next[i] != NULL) {
    							temp -> next[i] -> fail = p -> next[i];
    							break;
    						}
    						p = p -> fail;
    					}
    					if (p == NULL) 
    						temp -> next[i] -> fail = root;
    				}
    				q[head++] = temp -> next[i];
    			}
    		}
    	}
    } 
    int query(node *root) {
    	int i = 0, cnt = 0, index, len = strlen(str); 
    	node *p = root;
    	while (str[i]) {
    		index = str[i] - 'a';
    		while (p -> next[index] == NULL && p != root)
    			p = p -> fail;
    		p = p -> next[index]; 
    		p = (p == NULL) ? root : p;
    		node *temp = p;
    		while (temp != root && temp -> count != -1) {
    			cnt += temp -> count; 
    			temp -> count = -1; 
    			temp = temp -> fail; 
    		} 
    		i++; 
    	}
    	return cnt; 
    } 
    

希望大家给蒟蒻一个赞 qwq

5 个赞

%%%太强了

1 个赞

666
ooo

1 个赞

tjl

1 个赞