【Coel.学习笔记】【一个阶段的结束】01-Trie树(01字典树)求异或路径

题前闲语

是的,变成闲语了(别问我为什么要改)
今天考完了月考,虽然发挥得不是很好但终归是结束了,休息一下~
刚好深进也到货了,开始新一轮学习吧!

题目简介

题目描述

给定一棵 \(n\) 个点的带权树,结点下标从 \(1\) 开始到 \(n\)。寻找树中找两个结点,求最长的异或路径。
异或路径指的是指两个结点之间唯一路径上的所有边权的异或。

输入输出格式

输入格式

第一行一个整数 \(n\),表示点数。
接下来 \(n-1\) 行,给出 \(u,v,w\) ,分别表示树上的 \(u\) 点和 \(v\) 点有连边,边的权值是 \(w\)

输出格式

一行,一个整数表示答案。

解题思路

考虑用链式前向星存图,求出每个节点到根节点(自行定义根节点为\(1\))的异或路径,那么两个节点的异或路径就是它们与根节点路径的异或值
先用\(dfs\)初始化每个节点的异或值:

struct edge {
    int next, to, w;
} edge[maxn];

int p[maxn], cnt;

void add_edge(int u, int v, int w) {
    edge[++cnt].next = p[u];
    edge[cnt].to = v;
    edge[cnt].w = w;
    p[u] = cnt;
}

void init(int x, int f) {//dfs
    for (int i = p[x]; i != 0; i = edge[i].next) {
        int v = edge[i].to, w = edge[i].w;
        if (v != f) {
            s[v] = s[x] ^ w;
            init(v, x);
        }
    }
}

然后暴力枚举。当然直接暴力枚举的话复杂度是\(O(n^2)\),没法通过\(n=10^5\),所以需要用\(01-Trie\)优化。


\(01-Trie\)(即01字典树),是字典树的一种特殊形态,通过把一个数字分解成二进制来储存。
比如说$3=(0011)_2 $,在字典树上就按照插入\(0011\)字符串的方式储存。
因为往0走更大,往1走更小,这也意味着01字典树可以完成很多平衡树的操作,比如查前驱后继第K大。


01字典树的插入和查询方式和普通字典树差不多:

void insert(int v) {//插入值v
    int u = 0;
    for (int i = (1 << 30); i; i >>= 1) {//1<<30是int的值域
        bool c = v & i;//判断本位是0还是1
        if (!ch[u][c])
            ch[u][c] = ++tot;
        u = ch[u][c];
    }
}

根据贪心原理,我们优先走1更多的路径,这样结果尽可能大。

int find(int v) {
    int ans = 0, u = 0;
    for (int i = (1 << 30); i; i >>= 1) {
        bool c = v & i;
        if (ch[u][!c]) {
            ans += i;
            u = ch[u][!c];
        } else
            u = ch[u][c];
    }
    return ans;
}

最后把结果逐一比较即可。代码如下:

#include <cctype>
#include <cstdio>
#include <iostream>
#include <vector>

namespace FastIO {
inline int read() {
    int x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - '0';
        ch = getchar();
    }
    return x * f;
}
inline void write(int x) {
    if (x < 0) {
        x = -x;
        putchar('-');
    }
    static int buf[35];
    int top = 0;
    do {
        buf[top++] = x % 10;
        x /= 10;
    } while (x);
    while (top)
        putchar(buf[--top] + '0');
    puts("");
}
}  // namespace FastIO

using namespace std;
using namespace FastIO;

const int maxn = 1e5 + 10;

struct edge {
    int next, to, w;
} edge[maxn];

int p[maxn];
int n, cnt, tot, ans;
int s[maxn], ch[maxn * 32][2];

void add_edge(int u, int v, int w) {
    edge[++cnt].next = p[u];
    edge[cnt].to = v;
    edge[cnt].w = w;
    p[u] = cnt;
}

void init(int x, int f) {
    for (int i = p[x]; i != 0; i = edge[i].next) {
        int v = edge[i].to, w = edge[i].w;
        if (v != f) {
            s[v] = s[x] ^ w;
            init(v, x);
        }
    }
}

void insert(int v) {
    int u = 0;
    for (int i = (1 << 30); i; i >>= 1) {
        bool c = v & i;
        if (!ch[u][c])
            ch[u][c] = ++tot;
        u = ch[u][c];
    }
}

int find(int v) {
    int ans = 0, u = 0;
    for (int i = (1 << 30); i; i >>= 1) {
        bool c = v & i;
        if (ch[u][!c]) {
            ans += i;
            u = ch[u][!c];
        } else
            u = ch[u][c];
    }
    return ans;
}

int main() {
    n = read();
    for (int i = 1; i <= n - 1; i++) {
        int u = read(), v = read(), w = read();
        add_edge(u, v, w);
        add_edge(v, u, w);
    }
    init(1, -1);
    for (int i = 1; i <= n; i++)
        insert(s[i]);
    for (int i = 1; i <= n; i++)
        ans = max(ans, find(s[i]));
    write(ans);
    return 0;
}

顺便来分析一下时间复杂度吧?
初始化的时间复杂度为\(O(n)\),因为有\(n\)个数字。
接下来,对于01字典树来说,它的时间复杂度和值域大小有关。比如说int的值域大小是\(2^{31}\),那么它单次操作的时间复杂度就是\(O(31)\),即\(O(logA)\)\(A\)为值域大小。
很明显,这个复杂度相当于是\(O(logn)\)的最坏情况,所以01字典树的效率通常比其它平衡树慢许多。
总的时间复杂度即为\(O(n+nlog_{2}A)\)

posted @ 2022-03-31 20:16  秋泉こあい  阅读(32)  评论(0编辑  收藏  举报