zoukankan      html  css  js  c++  java
  • 01 Trie 专项题解

    思维路径:

    • xor 运算的特性
      • “是否不同”
      • 相同会消掉
      • 前缀和思想(处理区间查询、树上路径查询)
    • 逐位处理
    • 从高位到低位贪心

    HDU 4825 Xor Sum

    板子题。

    将所有数字二进制从高位到低位插入 Trie 中,从高到低贪心地能取到 1 就取 1.

    const int MAXN = 100000 + 10;
    
    namespace Trie {
    struct Node {
        int nxt[2]; Node() { nxt[0] = nxt[1] = 0; }
    } node[MAXN * 34]; int root = 1, cnt = 1;
    
    void Insert(lli x) {
        int u = root;
        for (int bit = 32; bit >= 0; --bit) {
            int &nxt = node[u].nxt[(x >> bit) & 1];
            if (!nxt) nxt = ++cnt;
            u = nxt;
        }
    }
    lli Query(lli x) {
        lli ret = 0; int u = root;
        for (int bit = 32; bit >= 0; --bit) {
            int xb = (x >> bit) & 1;
            int nxt = 0;
            ret <<= 1;
            if (node[u].nxt[xb ^ 1]) {
                nxt = node[u].nxt[xb ^ 1];
                ret |= xb ^ 1;
            } else {
                nxt = node[u].nxt[xb];
                ret |= xb;
            }
            u = nxt;
        }
        return ret;
    }
    void clear() {
        memset(node, 0, sizeof node); root = cnt = 1;
    }
    }
    
    int n, m;
    
    int main() {
        int T = read();
        for (int cas = 1; cas <= T; ++cas) {
            printf("Case #%d:\n", cas);
            n = read(); m = read();
            rep (i, 1, n) Trie::Insert(readll());
            rep (i, 1, m) printf("%lld\n", Trie::Query(readll()));
            Trie::clear();
        }
        return 0;
    }
    

    HDU 5536 Chip Factory

    基本思路仍然是把一个东西插入 Trie,再用另一个查询。

    \(a_i + a_j\) 插入 Trie 会占用大量空间,但总时间复杂度仍然不变,所以不如把 \(a_k\) 插入后枚举 \(a_i + a_j\)

    关键是在于如何判断 \(i \neq j \neq k\)

    这里可以借助树形 DP 的一个小技巧(类似补集转换):求出所有的,减去不要的,就是要求的。

    所以我们在枚举 \(a_i + a_j\) 时,对 \(a_i\)\(a_j\) 打一个标记,表示这个点不能走,求完了再恢复回来即可。

    因为一条边能经过多个数字,所以这个标记用 size 记录比较舒服,贪心的时候先判 size != 0

    const int MAXN = 1000 + 10;
    
    namespace Trie {
    struct Node {
        int nxt[2]; int passby; Node() { nxt[0] = nxt[1] = passby = 0; }
    } node[MAXN * 34]; int root = 1, cnt = 1;
    
    void Insert(lli x, int dt) {
        int u = root;
        for (int bit = 32; bit >= 0; --bit) {
            node[u].passby += dt;
            int &nxt = node[u].nxt[(x >> bit) & 1];
            if (!nxt) nxt = ++cnt;
            u = nxt;
        }
        node[u].passby += dt;
    }
    lli Query(lli x) {
        lli ret = 0; int u = root;
        for (int bit = 32; bit >= 0; --bit) {
            int xb = (x >> bit) & 1;
            int nxt = 0;
            ret <<= 1;
            if (node[u].nxt[xb ^ 1] && node[node[u].nxt[xb ^ 1]].passby) {
                nxt = node[u].nxt[xb ^ 1];
                ret |= 1;
            } else {
                nxt = node[u].nxt[xb];
            }
            u = nxt;
        }
        return ret;
    }
    void clear() {
        for (int i = 1; i <= cnt; ++i) node[i] = Node();
        root = cnt = 1;
    }
    }
    
    int n, m;
    int aa[MAXN];
    
    int main() {
        int T = read();
        for (int cas = 1; cas <= T; ++cas) {
            n = read();
            rep (i, 1, n) aa[i] = read();
            rep (i, 1, n) Trie::Insert(aa[i], 1);
            lli ans = 0;
            for (int i = 1; i <= n; ++i) {
                for (int j = i + 1; j <= n; ++j) {
                    Trie::Insert(aa[i], -1); Trie::Insert(aa[j], -1);
                    ans = std::max(ans, Trie::Query(aa[i] + aa[j]));
                    Trie::Insert(aa[i], 1); Trie::Insert(aa[j], 1);
                }
            }
            printf("%lld\n", ans);
            Trie::clear();
        }
        return 0;
    }
    

    BZOJ 4260 Codechef REBXOR

    首先这个区间查询可以用一个前缀优化搞掉,\(a_i \oplus \dots \oplus a_j = s_j \oplus s_{i - 1}\)

    所以答案就是让我们分别求两组 \(l, r\) 满足 \(r_1 < l_2\),而且各自的 \(s_r \oplus s_{l - 1}\) 最大。

    后面这个东西,固定一个端点可以求出前 / 后缀中选另一个端点的最大值,类似权值树状数组的思想。

    为了这个 \(r_1 < l_2\),我们可以考虑对于每一个点求出它前后缀的最大答案,两个相邻的加一下就是最终答案了。

    const int MAXN = 4e5 + 10;
    
    int n;
    int aa[MAXN];
    int pref[MAXN];
    int suff[MAXN];
    
    struct Trie {
        struct Node {
            int nxt[2];
        } node[MAXN * 32]; int root = 1, cnt = 1;
    
        void Insert(int x) {
            int u = root;
            for (int bit = 30; bit >= 0; --bit) {
                int xb = (x >> bit) & 1;
                int &nxt = node[u].nxt[xb];
                if (!nxt) nxt = ++cnt;
                u = nxt;
            }
        }
        int Query(int x) {
            int ret = 0; int u = root;
            for (int bit = 30; bit >= 0; --bit) {
                int xb = (x >> bit) & 1;
                int nxt = 0;
                ret <<= 1;
                if (node[u].nxt[xb ^ 1]) {
                    nxt = node[u].nxt[xb ^ 1];
                    ret |= 1;
                } else nxt = node[u].nxt[xb];
                u = nxt;
            }
            return ret;
        }
    } prefs, suffs;
    
    int prefans[MAXN], suffans[MAXN];
    
    int main() {
        n = read();
        prefs.Insert(0); suffs.Insert(0);
        rep (i, 1, n) aa[i] = read();
        rep (i, 1, n) {
            pref[i] = pref[i - 1] ^ aa[i];
            int maxp = prefs.Query(pref[i]);
            prefans[i] = maxp;
            prefans[i] = std::max(prefans[i], prefans[i - 1]);
            prefs.Insert(pref[i]);
        }
        for (int i = n; i >= 1; --i) {
            suff[i] = suff[i + 1] ^ aa[i];
            int maxs = suffs.Query(suff[i]);
            suffans[i] = maxs;
            suffans[i] = std::max(suffans[i], suffans[i + 1]);
            suffs.Insert(suff[i]);
        }
        int ans = 0;
        for (int i = 1; i <= n - 1; ++i) {
            ans = std::max(ans, prefans[i] + suffans[i + 1]);
        }
        printf("%d\n", ans);
        return 0;
    }
    

    POJ 3764 The xor-longest Path

    和上道题差不多的思想,使用树上前缀异或和 + 边插入边查询。

    两个点之间路径异或和就是它们到根节点异或和做异或。

    对整棵树做 DFS,依次把所有点到根节点的路径异或和插入 Trie 里,边插入边查询异或最大值,这个做法显然是可以遍历到所有路径的,具体可以通过枚举 LCA 来证明。

    听他们说用 vector 会 T,我也没试过。

    const int MAXN = 1e5 + 10;
    
    int n;
    
    struct Edge {
        int v, nxt; lli w;
    } edge[MAXN << 1]; int head[MAXN], cnt;
    
    void addEdge(int u, int v, lli w) {
        edge[++cnt] = {v, head[u], w}; head[u] = cnt;
    }
    
    namespace Trie {
    struct Node { int nxt[2]; Node() {nxt[0] = nxt[1] = 0;} } node[MAXN * 34];
    int root = 1, cnt = 1;
    
    void clear() {
        for (int i = 1; i <= cnt; ++i) node[i] = Node();
        cnt = 1;
    }
    void Insert(lli x) {
        int u = root;
        for (int bit = 32; bit >= 0; --bit) {
            int xb = (x >> bit) & 1;
            int &nxt = node[u].nxt[xb];
            if (!nxt) nxt = ++cnt;
            u = nxt;
        }
    }
    lli Query(lli x) {
        lli ret = 0;
        int u = root;
        for (int bit = 32; bit >= 0; --bit) {
            int xb = (x >> bit) & 1;
            int nxt = 0;
            ret <<= 1;
            if (node[u].nxt[xb ^ 1]) {
                nxt = node[u].nxt[xb ^ 1];
                ret |= 1;
            } else nxt = node[u].nxt[xb];
            u = nxt;
        } return ret;
    }
    }
    
    lli ans = 0;
    void dfs(int u, int fa, lli pref) {
        ans = std::max(ans, Trie::Query(pref));
        Trie::Insert(pref);
        for (int e = head[u]; e; e = edge[e].nxt) {
            int v = edge[e].v; lli w = edge[e].w;
            if (v == fa) continue;
            dfs(v, u, pref ^ w);
        }
    }
    void cleanup() {
        ans = 0;
        cnt = 0; for (int i = 1; i <= n; ++i) head[i] = 0;
        Trie::clear();
    }
    int _main() {
        Trie::Insert(0);
        for (int i = 1; i <= n - 1; ++i) {
            int u = read() + 1; int v = read() + 1; lli w = readll();
            addEdge(u, v, w); addEdge(v, u, w);
        }
        dfs(1, 0, 0);
        printf("%lld\n", ans);
        cleanup();
        return 0;
    }
    int main() {
        while (scanf("%d", &n) != EOF) _main();
    }
    
    

    Codeforces 842D Vitya and Strange Lesson

    还挺有意思的这题。

    首先全局异或可以用一个标记记住,因为异或具有结合律。

    然后就是这个鬼畜的全局 Mex。

    假设没有全局异或这个鬼东西,只让我们求全局 Mex,那么我们可以用一个桶存下来所有数,求这个东西就相当于是在求桶的第一个空位是谁,有一个 \(\log\) 做法是使用权值线段树上二分,每次判断左区间是不是满的(也就是 \(\mathrm{sum[lson]} = \mathrm{mid}\)),如果不是满的就说明 Mex 在左边,否则就在右边。然而权值线段树无法维护全局 xor。

    这个做法的关键是能快速求出左区间有多少数字(因此也不能出现重复数字,必须提前去重),于是我们需要一个支持查询左区间 size 和逐位异或的数据结构,而这个东西可以用 01 Trie 来完成。

    题外话,考虑到数字 Trie 从高位向低位存、不足位补前导零的这个特性,每一个数字的长度都是相同的,也就是说,所有数字都可以通过一个叶子结点唯一确定。因此,它类似于一个权值数据结构,也能完成一些权值数据结构的操作(求 rank,求前驱后继),只不过单次操作复杂度是固定的 \(O(\mathrm{len})\),而权值线段树、树状数组等的单次操作复杂度是固定的 \(O(\log M)\),其中 \(M\) 是值域。
    这个特性也决定了,在将 \(1\sim n\) 的所有数字插入时,树的结构成为满多叉树,空间复杂度会达到指数级别。

    先考虑没有异或(或者异或的这一位是 0)的情况,和上面类似,对于第 \(b(b\geq 0)\) 位我们先求出 0 子树的 size(可以在插入的时候记录一下),然后再和满的大小 \(2^b\) 判断一下,如果不满就往左走,否则就往右走。我们可以不建满二叉树,如果当前的 0 子树不存在,就直接 return。

    有异或的话,直接把 0 当 1 看,1 当 0 看即可。

    const int MAXN = 3e5 + 10;
    
    int n, m;
    
    int addition;
    
    namespace Trie {
    struct Node { int nxt[2]; int siz; Node() { nxt[0] = nxt[1] = siz = 0; } } node[MAXN * 20];
    int root = 1, cnt = 1;
    
    void Insert(int x) {
        int u = root;
        for (int bit = 21; bit >= 0; --bit) {
            ++node[u].siz;
            int xb = (x >> bit) & 1;
            int &nxt = node[u].nxt[xb];
            if (!nxt) nxt = ++cnt;
            u = nxt;
        }
        ++node[u].siz;
    }
    int Query() {
        int u = root;
        int ret = 0;
        for (int bit = 21; bit >= 0; --bit) {
            ret <<= 1;
            int xb = (addition >> bit) & 1;
            int nxt = 0;
            if (!node[u].nxt[xb]) {
                ret <<= bit; return ret; // 一定别忘了 << bit
            }
            if (node[node[u].nxt[xb]].siz < (1 << bit)) nxt = node[u].nxt[xb];
            else {
                nxt = node[u].nxt[xb ^ 1]; ret |= 1;
            }
            u = nxt; // 一定别忘了 u = nxt
        }
        return ret;
    }
    }
    
    int uniq[MAXN];
    
    int main() {
        n = read(); m = read();
        rep (i, 1, n) {
            int x = read();
            if (!uniq[x]) Trie::Insert(x);
            uniq[x] = 1;
        }
        while (m --> 0) {
            addition ^= read();
            printf("%d\n", Trie::Query());
        }
        return 0;
    }
    

    Codeforces 713A Sonya and Queries

    最后来一道水题收尾。

    \(a, b\) 奇偶性相同等价于 \(a \equiv b\ (\bmod 2)\),所以就把所有的数字都逐位模 2 插进 Trie 即可。

    const int MAXN = 1e5 + 10;
    
    namespace Trie {
    struct Node { int nxt[2], siz; Node() { nxt[0] = nxt[1] = siz = 0; } } node[MAXN * 20];
    int root = 1, cnt = 1;
    
    void Insert(lli x, int dt) {
        int u = root;
        for (int bit = 19; bit >= 0; --bit) {
            int &nxt = node[u].nxt[(x >> bit) & 1];
            if (!nxt) nxt = ++cnt;
            u = nxt;
        }
        node[u].siz += dt;
    }
    int Query(lli x) {
        int u = root;
        for (int bit = 19; bit >= 0; --bit) {
            u = node[u].nxt[(x >> bit) & 1];
        }
        return node[u].siz;
    }
    }
    
    lli Process(lli x) {
        std::vector<int> res;
        while (x) {
            res.push_back((x % 10) % 2); x /= 10;
        }
        std::reverse(ALL(res));
        lli ret = 0;
        for (auto v : res) ret = (ret << 1) + v;
        return ret;
    }
    
    int t;
    
    int main() {
        t = read();
        while (t --> 0) {
            char ss[2]; scanf("%s", ss);
            lli fx = readll();
            switch (ss[0]) {
                case '+': {
                    Trie::Insert(Process(fx), 1);
                    break;
                }
                case '-': {
                    Trie::Insert(Process(fx), -1);
                    break;
                }
                case '?': {
                    printf("%d\n", Trie::Query(Process(fx)));
                }
            }
        }
        return 0;
    }
    

    易错警示

    1. 老生常谈的 typo。
    2. 求完 nxt 一定不要忘了更新 u = nxt
    3. node 数组大小一定要乘上数字长度,或者干脆就再写一个 const int MAXNODE
    4. 多测清空的时候不要忘了 cnt = 1
  • 相关阅读:
    raspberry pi 4 上Opencv-3.4.1编译安装与错误记录
    Ubuntu20.04上安装搜狗输入法(親測有效)
    Ubuntu20.04系统配置ROS-noetic环境
    Windows10和ubuntu20.04 LTS 双系统安装(ubuntu20.04)
    C++中构造函数的理解
    C++中函数模板的理解
    C++中重载函数的理解
    Gazebo9的进程死亡问题解决
    ROS常见问题及解决方法
    Moveit+Gazebo联合仿真问题解决
  • 原文地址:https://www.cnblogs.com/handwer/p/15460299.html
Copyright © 2011-2022 走看看