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
原文地址:https://www.cnblogs.com/handwer/p/15460299.html