..

第七章 树状数组与字典树

树状数组可以很方便的维护序列的前缀和等信息。因此,可以使用树状数组高效完成单点修改、区间求和的任务。虽然线段树也可以完成这些任务,而且时间复杂度都是 $\mathcal{O} (\log n)$,但是树状数组运行时间的常数更小,所需空间更小,代码也更加短小精悍。

字典树可以方便的储存、索引和查找字符串。当然除了字符串,还可以将数字拆成二进制,存入字典树中,常常可以高效处理涉及到异或的问题。

树状数组

例1:P3374 【模板】树状数组 1

例2:P3368 【模板】树状数组 2

例3:P1908 逆序对

字典树

例4:P1481 魔族密码

node[u].val 为以 $u$ 节点为末字符的单词个数,则在字典树上访问单词 $str$ 时,所经过的各个 $u$ 节点的 $val$ 之和,即为以 $str$ 为词链末单词时,该词链所包含的单词数。

需要注意的是,题目保证输入顺序按字典顺序排列,故在添加单词 $str$ 时,所有可能对 $res$ 产生贡献的单词都已经被添加过了,因此可以在添加单词时统计 $res$,并将 $ans$ 与之取 $\max$ 即可。

struct TrieTree {
    struct Node {
        int next[26];
        int val;
    } node[maxN * maxS + 10];

    int tot;

    void Insert(std::string str) {
        int u = 0;
        int res = 1;
        for (int i = 0; i < str.size(); i++) {
            int ch = str[i] - 'a';
            if (node[u].next[ch] == 0) node[u].next[ch] = ++tot;
            u = node[u].next[ch];
            res += node[u].val;
        }
        node[u].val++;
        ans = std::max(ans, res);
        return;
    }
} TT;

例5:P2580 于是他错误的点名开始了

字典树基本操作。

需要注意的是,点名时查找字典树都节点,即时已经枚举完名字的所有字符,还是需要判断这个名字是否存在,否则遇到名字时花名册上名字的前缀的情况就出错了。

暑假好像用 $\text{map}$ 艹过去的。

例7:P4551 最长异或路径

SXB1 的作业题。

题目中给出的树没有指定根节点,但可以指定节点 $1$ 为根节点,使其变为有根树。建立这个树,求的第 $i$ 个节点到根节点到异或路径 $s_i$。而节点 $i$ 到节点 $j$ 之间的异或路径就是 $s_i \oplus s_j$。这是因为这两个节点分别到根节点到路径,在他们的最近公共祖先再往上的路径是重叠的,而将相同的边权异或两次就会归零。因此对于所有 $s_i$,找到一个 $s_j$ 使 $s_i \oplus s_j$ 的值最大并记录之,即可求解。

问题在于,如果两两枚举,效率很低,因此可以用 $\text{01-Trie}$ 来维护所有数字的异或和。将每个数字都变成 $31$ 位的二进制数(和值域相关),将其视为一个字符串来建立字典树。所有叶子节点的深度都是一致的。

从二进制数最左边开始,同时字典树从根节点开始,每一位使用贪心策略。如果字典树当前节点只有一个子节点,则直接往下走,否则选择和这一位不同的数对应节点(这样可以使它们的异或值为 $1$)。

struct Trie {
    struct Node {
        int next[2];
    } node[maxN << 5];

    int cnt;

    void Insert(int val) {
        int u = 0;
        for (int p = 30; p >= 0; p--) {
            bool i = (val >> p) & 1;
            if (node[u].next[i] == 0) node[u].next[i] = ++cnt;
            u = node[u].next[i];
        }
        return;
    }

    int Query(int val) {
        int ret = 0;
        int u = 0;
        for (int p = 30; p >= 0; ip--) {
            bool i = (val >> p) & 1;
            if (node[u].next[!i]) {
                ret += 1 << p;
                u = node[u].next[!i];
            } else u = node[u].next[i];
        }
        return ret;
    }
} TT;

例7:P5283 [十二省联考 2019] 异或粽子

给定长度为 $n$ 的序列 $a_i$。一个区间 $[l, r] (1 \le r \le n)$ 的价值为从 $a_l$ 到 $a_r$ 之间的每个数字进行的异或值。求这个序列中价值最大的 $k$ 个区间的价值和。

设 $s_i$ 为序列中 $a_1$ 到 $a_i$ 的每个数字依次异或后的结果。则区间 $[l, r]$ 的价值就是 $s_r \oplus s_{l - 1}$。所以对于每一个 $s_i$,都可以找到一个 $s_j$ 使得 $s_i \oplus s_j$ 的值最大。如果能保证 $i \lt j$ 的话,固定 $i$ 为左端点,就可以找到以 $j$ 为右端点的区间 $[i, j]$,使其价值最大了。$s_i$ 和 $s_j$ 互为对偶,所以只需要找 $2k$ 个价值最大的 $(i, j)$ 配对,将其价值累加处以 $2$ 就是答案。

对于每一个 $i$ 找到对应的 $j$,并将其对应的价值放入优先队列中。每次从优先队列中取出价值最大的元素,累加进答案。如果去除的是和 $s_i$ 对应的第 $t$ 大值,则将和 $s_i$ 对应的第 $t + 1$ 大值放入优先队列,重复操作 $2k$ 次即可。

类似于主席树的思想,可以在字典树的插入过程中,记录每个节点的数字数量。在查询的过程中,根据优先往 $0$ 走还是往 $1$ 走,以及左右子节点数量和 $t$ 来决定下一步的方向。

注意 1 << pint 类型,需要写成 1ll << p

struct TrieTree {
    struct Node {
        int next[2];
        int size;
    } node[maxN * (maxP + 1) + 10];

    int ncnt;

    void Insert(lxl val) {
        int u = 0;
        for (int p = maxP; p >= 0; p--) {
            int i = (val >> p) & 1;
            if (node[u].next[i] == 0) {
                node[u].next[i] = ++ncnt;
            }
            u = node[u].next[i];
            node[u].size++;
        }
        return;
    }

    lxl Query(lxl val, int t) {
        lxl ret = 0;
        int u = 0;
        for (int p = maxP; p >= 0; p--) {
            int i = (val >> p) & 1;
            if (node[node[u].next[i ^ 1]].size >= t) {
                u = node[u].next[i ^ 1];
                ret |= 1ll << p;
            } else {
                t -= node[node[u].next[i ^ 1]].size;
                u = node[u].next[i];
            }
        }
        return ret;
    }
} TT;

由题意得:

\[1 \le l \le r \le n\]

\[w(l, r) = s_r \oplus s_{i - 1}\]

故对于一个

\[1 \le j \le n\]

与之配对的 $i$ 满足

\[0 \le i' \le n - 1, i' = i - 1\]

当 $i = 1$ 时,$[i, j]$ 的贡献为

\[\begin{aligned} w(i, j) & = s_j \oplus s_{i - 1} \\ & = s_j \oplus s_0 \\ & = s_j \oplus 0 \\ & = s_j \end{aligned}\]

也就是说,在字典树上寻找与 $s_j$ 配对的 $s_i$ 时,$s_0 = 0$ 是需要被考虑在内的。

    for (int i = 0; i <= n; i++) TT.Insert(x[i]);
    for (int i = 0; i <= n; i++) {
        node[i] = (Node) {i, 1, TT.Query(x[i], 1)};
        q.push(node[i]);
    }

因为在字典树的节点中可以记录所管辖的数字数量,而且对于一个非叶子节点来说,往 “$0$” 方向走可以找到更小的数字,往 “$1$” 方向走可以找到更大的数字,故 $\text{01-Trie}$ 可以完成一些平衡树的操作——插入、删除、根据数据查询排名和根据排名查询数据等。单次操作的复杂度和值域有关。