前缀树的数据结构以及O(n)解决子数组最大异或和的问题

什么是前缀树,比如你输入了一个字符串“abc”:那么前缀树如图所示:

依次在头节点下创建节点,然后线上是字母(并非节点是)

每个节点上面有两个值:path(代表有多少字符串经过了该节点)、end(有多少字符串以该节点结尾)。

现在依次输入:“abc”、“bce”、“abd”、“bef”

输入第二个字符串:

输入第三个字符串:实际上输入abd的时候,之前已经输入abc了,可以复用之前的前缀ab。更新path。

输入第四个字符串,符用b前缀:

我们的头节点其实是没用的,字符串的下方节点才是有用的。

我们来看下面的代码:

这个就是节点类型,一起前缀的构造方法就是建造一个root,表明根节点,这里的nexts就是下层节点的指针,我们假设这个字符串中只有二十六个字母,所以数组长度是26,也就是下面的节点有26个可能,当然你也可以用map结构来做。这样可以任意字符

public class Trip {

    public static class TripNode{
        //讲过此节点的字符串的数量
        public int path;
        //以节点结尾的字符串的个数
        public int end;
        public TripNode[] nexts;

        public TripNode(){
            this.path = 0;
            this.end = 0;
            nexts = new TripNode[26];
        }
    }

    private TripNode root;

    public Trip(){
        root = new TripNode();
    }

插入方法:

首先从字符串转换成字符,然后从根节点开始找next是否存在,利用下标是chars[i] - 'a',如果有就直接走进入这个节点,如果没有就创建这个节点。每经过一个节点path++,到达最后的时候end++

   /**
     * 插入
     */
    public void insert(String word){
        if(word == null){
            return ;
        }
        char[] chars = word.toCharArray();
        TripNode node = root;
        int index = 0;
        for(int i = 0;i < chars.length;i++){
            index = chars[i] - 'a';
            if(node.nexts[index] == null){
                node.nexts[index] = new TripNode();
            }
            node = node.nexts[index];
            node.path++;
        }
        node.end++;
    }

查找方法:

跟插入是一样的,查找的过程中,如果发现为遍历完,但是有一个节点是null,那么直接返回,不存在这个字符串。否则返回end

    /**
     * 查找
     */
    public int search(String word){
        if(word == null){
            return 0;
        }
        char[] chars = word.toCharArray();
        TripNode node = root;
        int index = 0;
        for(int i = 0;i < chars.length;i++){
            index = chars[i] - 'a';
            if(node.nexts[index] == null){
                return 0;
            }
            node = node.nexts[index];
        }
        return node.end;
    }

删除方法:

实际上就是先查找这个字符串有没有,如果有,对于path > 1的节点执行path--,当发现一个节点path=1的时候,说明这个节点一下是下面字符串的一个部分,直接让这个节点指向null,直接返回就可以了。

    /**
     *  删除
     */
    public void delete(String word){
        if(search(word) != 0){
            char[] chars = word.toCharArray();
            TripNode node = root;
            int index = 0;
            for(int i = 0;i < chars.length;i++){
                index = chars[i] - 'a';
                if(--node.nexts[index].path == 0){
                    node.nexts[index] = null;
                    return ;
                }
                node = node.nexts[index];
            }
            node.end--;
        }
    }

就是输入一个pre的前缀字符串,然后返回这个字符串前缀的path。就是作为了多少个字符串的前缀。

    /**
     * 查询前缀有多少个
     */
    public int prefixNumber(String pre){
        if(pre == null){
            return 0;
        }
        char[] chars = pre.toCharArray();
        TripNode node = root;
        int index= 0;
        for(int i = 0;i < chars.length;i++){
            index = chars[i] - 'a';
            if(node.nexts[index] == null){
                return 0;
            }
            node = node.nexts[index];
        }
        return node.path;
    }
题目:给定一个数组,求子数组的最大异或和。 一个数组的异或和为,数组中所有的数异或起来的结果。
 
现在给出暴力破解,枚举每一个子数组,然后算出子数组的异或和。O(n3)
 
    /**
     * O(n3)
     */
    public static int getMaxEor1(int[] arr) {
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j <= i; j++) {
                int res = 0;
                //计算异或和
                for (int start = j; start <= i; start++) {
                    res ^= arr[start];
                }
                max = Math.max(max, res);
            }
        }
        return max;
    }

优化:时间复杂度O(n2),这里每次都保存0 - i子数组的异或和,然后放到dp里面,之后枚举子数组,然后根据dp算出异或和,然后选出最大值,这里的优化主要在于不用每一次重复计算之前的异或和。

    /**
     * O(n2)
     */
    public static int getMaxEor2(int[] arr) {
        int max = Integer.MIN_VALUE;
        int[] dp = new int[arr.length];
        int eor = 0;
        for (int i = 0; i < arr.length; i++) {
            //0 - i 的异或和
            eor ^= arr[i];
            max = Math.max(max, eor);
            for (int start = 1; start <= i; start++) {
                //计算异或和
                int res = eor ^ dp[start - 1];
                max = Math.max(max, res);
            }
            dp[i] = eor;
        }
        return max;
    }

最优解:就是前缀树,时间复杂度O(n)。这里定义一个前缀树的黑盒,你只需要了解,这个东西能够给你0 - i的最大子数组的最大异或和,至于为什么能给?之后在分析黑盒的内容。

    public static int maxXorSubarray(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int max = Integer.MIN_VALUE;
        int eor = 0;
        NumTrie numTrie = new NumTrie();
        numTrie.add(0);
        for (int i = 0; i < arr.length; i++) {
            eor ^= arr[i];
            //比较大小。
            max = Math.max(max, numTrie.maxXor(eor));
            numTrie.add(eor);
        }
        return max;
    }

eor还是0 - i的数组的异或和,numTrie就是一个黑盒,numTrie.maxXor(eor)方法就能够给你返回0 - i的最优值,然后将每一个eor放入到黑盒中。我们看图分析黑盒:

如果黑盒里面有:0 -0 、0 - 1、0 - 2、0 - 3.......、0 - i - 1的异或和,那么当计算 0 - i的最优值的时候怎么做呢?他通过黑盒选举出一个最优的值,比如选举0 - 3,那么就是 0 - 3的异或和 和 0 - i的异或和 做异或最大,那么实际上就是子数组 4 -i 的异或和最大。因为 0 - i ^ 0 - 3 实际上就等于 4 - i的异或和。

看图分析如果建立前缀树,这里运用了位运算,一个int类型32位(我们以4位为例),最高位为符号位(1代表符数,0代表正数)。

现有如下值: 0 - 0 : 0001,0 - 1:0101,0 - 2:1011。要拿取这个最大异或和0 - 3 : 0101。

首先输入0001,建立前缀树:

再输入0101.
在输入1011:
 
至此前缀树建立完毕,然后我们来看如何找到0101的最大值:
(1)这个值的第一位是符号位,既然是符号位,那么要保证异或后的结果最大,一定是向正数看齐,所以从根节点要走0的位置,因为0异或上0才是0,保证之后的数是正数,实际上就是当前数x,(x >> 3) & 1,这个就是要走的正数位,即使来值是负数,我们也需要尽力的把他异或和之后的结果往正数上面靠。
(2)接下来0101,处理第一个1,我们下一步就是尽力保证这个一异或后的结果仍然是1,所以要走0 的路线。
(3)接下来是0,我们要保证走1,但是现在没有1呀,所以只能走0。
(4)接下来是1,我们要保证走0,依然是没有,只能走1。
所以如下图:
 
 
这就是要异或的东西,实际上就是0 - 0的异或,所以0 - 4最优解的异或是1 - 4的异或和。这个就是那个黑盒。之后依次比较大小,就能够找到最大的值。
 
    public static class Node {
        public Node[] nexts = new Node[2];
    }

    public static class NumTrie {
        public Node head = new Node();

        public void add(int num) {
            Node cur = head;
            for (int move = 31; move >= 0; move--) {
                int path = ((num >> move) & 1);
                cur.nexts[path] = cur.nexts[path] == null ? new Node() : cur.nexts[path];
                cur = cur.nexts[path];
            }
        }

        public int maxXor(int num) {
            Node cur = head;
            int res = 0;
            for (int move = 31; move >= 0; move--) {
                int path = (num >> move) & 1;
                int best = move == 31 ? path : (path ^ 1);//期待选择的路
                best = cur.nexts[best] != null ? best : (best ^ 1);//实际选择的路
                res |= (path ^ best) << move;//设置答案的一位
                cur = cur.nexts[best];//继续向下走
            }
            return res;
        }
    }

这个是前缀树的结构。add方法很简单,就是依次取出32位int整数的位值(0或者1),然后创建一个前缀树。

maxXor方法,就有点道行了,如何选取这个期待的呢?

(1)首先还是取出当前传入的节点num的高位,第一位是符号位, int path = (num >> move) & 1; path就能通过&1能够获取下走的路径,如果(num >> move)是1,那么下次走的路就是1,如果是0,那么走的路就是0。因为1 ^ 1是0,0 ^ 0是0。

(2)然后int best = move == 31 ? path : (path ^ 1);如果是符号位那么直接就是期待我要走path,如果不是符号位,我就想走,我相反的,因为相反才能保证异或是1,所以这个是期待路径。

(3)下一步就是即使你有期待的路径,但是未必前缀树中就有啊,所以呢下一个是实际的选择路径,best = cur.nexts[best] != null ? best : (best ^ 1);这个先判断期待路径是不是null,如果是null,那么走相反的路径(一个前缀树一定是有32高的一个层数的,因为我们起初add(0),所以保证了一定有0这个子树)。如果不是null,就说明有,所以直接走。

(4)res |= (path ^ best) << move;这个是设置答案,如果path(num的位值)与实际值做异或得出最优值,然后右移动放到对应的位数上,用或操作来弄。

(5)之后就是往下走!

 

这样就能够拿到0 - i的一个最优值。然后依次拿到 0 - i ,i = 1、2、3、......、n。的最优值,最后比较大小,取最大。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值