又见神奇的异或。Trie树。今日头条。

又见神奇的异或。Trie树。

@(algorithm)

先来看一道题。异或最大值
http://acm.csu.edu.cn/OnlineJudge/problem.php?id=1216

Description
给定一些数,求这些数中两个数的异或值最大的那个值

Input
第一行为数字个数n,1 <= n <= 10 ^ 5。接下来n行每行一个32位有符号非负整数。

Output
任意两数最大异或值

Sample Input
3
3
7
9
Sample Output
14

参考了这位大神的博客:

http://blog.csdn.net/tc_to_top/article/details/49584013

第一次接触trie树,对这种数据结构不太熟,于是注释了一下大神的代码帮助理解:

在这个代码中,是如何表示trie树的呢?

是用二维数组next[MAX][2]和一维数组end[MAX]来表示的。

其中,next[i][0]表示索引为i的节点的左孩子的索引值,next[i][1]表示索引为i的节点的右孩子的索引值。

end[i]表示索引为i的节点是否是尾节点,如果end[i]==0则表示索引为i的节点不是尾部节点。否则,end[i]存入这个数的数值,便于查找到尾部的时候直接知道这个数是啥。

#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
int const MAX = 4e6;
int n;

struct Trie
{
    int root, tot, next[MAX][2], end[MAX];//next[i][0]表示节点i的左孩子,next[i][1]表示节点i的右孩子。
    inline int Newnode()//在数组中写入一个新的节点,然后返回idx(tot),再将tot++,指向下一个将要被写入的节点。
    {
        memset(next[tot], -1, sizeof(next[tot]));//左右孩子为空,-1
        end[tot] = 0;//不是终止的节点
        return tot ++;//返回新节点的索引,并将全局变量tot++。
    }

    inline void Init()
    {
        tot = 0;//初始化的索引为0
        root = Newnode();//申请新节点,根节点的索引就是新节点的索引。
    }

    inline void Insert(ll x)
    {
        int p = root;
        for(int i = 31; i >= 0; i--)
        {
            int idx = ((1 << i) & x) ? 1 : 0;
            if(next[p][idx] == -1)//如果节点没被写入
                next[p][idx] = Newnode();//申请新的节点
            p = next[p][idx];//更新当前节点
        }
        end[p] = x;//最后把尾部的节点写入这个数的值
    }

    inline int Search(int x)//找能与x异或出最大的数,使用贪心法,每次都要找这一位与x相应位不同的。
    {
        int p = root;
        for(int i = 31; i >= 0; i--)
        {
            int idx = ((1 << i) & x) ? 1 : 0;//x的当前位
            if(idx == 0)//x当前位是0,就要找1,实在不行才找0
                p = next[p][1] != -1 ? next[p][1] : next[p][0];
            else//x当前位是1,就要找0,实在不行才找1
                p = next[p][0] != -1 ? next[p][0] : next[p][1];
        }
        return x ^ end[p];//最后找到end[p]可以异或出最大的数
    }

}tr;

int main()
{
    while(cin >> n)
    {
        int ma = 0, x;
        tr.Init();
        for(int i = 0; i < n; i++)
        {
            cin >> x;
            tr.Insert(x);
            ma = max(ma, tr.Search(x));
        }
        printf("%d\n", ma);
    }
}

除了可以用数组实现Trie树之外,还可以用二叉树来实现,基本原理一致。代码如下:

const int NBITS = 32;
int n;

class Tnode
{
public:
    int val;
    Tnode *left, *right;
    Tnode(int v): val(v), left(NULL), right(NULL){}
};

class LTrie//链表式的Trie树
{
public:
    Tnode* root;

    LTrie()
    {
        root = new Tnode(0);
    }

    void insert(int x)
    {
        Tnode *iter = root;
        for (int i = NBITS-1; i >= 0; --i)
        {
            if ((x & (1 << i)))// 当前这一位是1
            {
                if (iter->right == NULL)
                    iter->right = new Tnode(NBITS-1-i);
                iter = iter->right;
            }
            else// 当前这一位是0
            {
                if (iter->left == NULL)
                    iter->left = new Tnode(NBITS-1-i);
                iter = iter->left;
            }
        }
        // 在尾部写入x
        iter->val = x;
    }

    int search(int x)// 用贪心法查找与x异或最大的值
    {
        Tnode *iter = root;
        for (int i = NBITS-1; i >= 0; --i)
        {
            int nowbit = (x & (1 << i));
            if (nowbit)
                iter = (iter->left != NULL)? iter->left: iter->right;
            else
                iter = (iter->right != NULL)? iter->right: iter->left;
        }
        return (x ^ iter->val);
    }

};

int main()
{
    while(cin >> n)
    {
        // 数组表示
// int ma = 0, x;
// tr.Init();
// for(int i = 0; i < n; i++)
// {
// cin >> x;
// tr.Insert(x);
// ma = max(ma, tr.Search(x));
// }
// printf("%d\n", ma);

        //链表表示
        LTrie lt;
        int x, res;
        res = 0;
        for (int i = 0; i < n; ++i)
        {
            cin >> x;
            lt.insert(x);
            //cout << lt.search(x)<< endl;
            res = max(res, lt.search(x));
        }
        cout << res << endl;
    }
}

到此,最大异或数的问题算是解决了。

那么我们现在来解决今日头条的笔试题。

题目

给出一堆数,找出这堆数两两异或后得到多少个大于n的数

样例:
输入:
3 10
10 5 6
输出:
2

样例说明:

这堆数有3个,找出这3个数中两两异或,有多少个大于10,结果是2

参考牛客网某位大神的提示:

异或那道题可以把每个数的二进制位求出来,用一个字典树维护,然后遍历每一个数按位贪心,比如这一位m是1,遍历的这个数这一位是0,那么和他异或的数就必须是1,如果这一位m是0,要大于m的话异或和的这一位可以是1也可以是零,ans加上之前维护的二进制位加上使这一位为1的数在字典树中查询有多少个数满足这个前缀的条件,然后在令这一位的异或和为0,继续向下遍历,最后的答案除以2.

实现代码:

//
// main.cpp
// toutiao2
//
// Created by SteveWong on 9/21/16.
// Copyright © 2016 SteveWong. All rights reserved.
//

#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
#include <time.h>

using namespace std;
const int NBITS = 16;
int n;

class Tnode
{
public:
    int cnt;//记录当前节点一共有多少个数“经过”。假如这一位表示0,则代表有多少个数这一位是0;假如这一位表示1,则代表有多少个数这一位是1.
    Tnode *left, *right;
    Tnode(int v): cnt(v), left(NULL), right(NULL){}
};

class LTrie//链表式的Trie树
{
public:
    Tnode* root;

    LTrie()
    {
        root = new Tnode(0);
    }

    void insert(int x)
    {
        Tnode *iter = root;
        for (int i = NBITS-1; i >= 0; --i)
        {
            if ((x & (1 << i)))// 当前这一位是1
            {
                if (iter->right == NULL)
                    iter->right = new Tnode(0);//新建节点,这一位经过的数=1


                iter = iter->right;
            }
            else// 当前这一位是0
            {
                if (iter->left == NULL)
                    iter->left = new Tnode(0);
                iter = iter->left;
            }
            iter->cnt ++;
        }
    }

    int search(int x, int m)// 查找与x异或大于m的数有几个
    {
        Tnode *iter = root;
        int res = 0;
        for (int i = NBITS-1; i >= 0; --i)
        {
            int mbit = (m & (1 << i));
            int xbit = (x & (1 << i));
            if (mbit) // m的当前这一位是1,那么要找的数的当前位与x的当前位必须不同,才能保证比m大
            {
                if (xbit) // x的当前这一位是1,那么要求找的数当前这一位要是0才行。
                {
                    if (iter->left != NULL) // 的确存在当前这一位是0的数,那就继续往下比较
                        iter = iter->left;
                    else // 不存在当前这一位是0的数,说明找不到与x异或比m大的数,返回0
                        return res;
                }
                else // x的当前这一位是0,那么要求找的数当前这一位要是1才行。
                {
                    if (iter->right != NULL) // 的确存在当前这一位是1的数,那就继续往下比较
                        iter = iter->right;
                    else // 不存在当前这一位是1的数,说明找不到与x异或比m大的数,返回0
                        return res;
                }
            }
            else // m的当前这一位是0,那么要找的数的当前位与x的当前位可以相同也可以不同,都可以保证比m大
            {
                if (xbit) // x的当前这一位是1
                {
                    if (iter->left != NULL) // 的确存在当前这一位是0的数,所有“经过”这一位的数都将比m大,返回这些数字的数量
                        res += iter->left->cnt;
                    // iter当前这一位是1也可能可以,继续往下比较
                    if (iter->right == NULL) // 走到头了
                    {
                        return res;
                    }
                    iter = iter->right; // 继续往下
                }
                else // x的当前这一位是0
                {
                    if (iter->right != NULL) // 的确存在当前这一位是1的数
                        res += iter->right->cnt;
                    // iter当前这一位是0也可能可以,继续往下比较
                    if (iter->left == NULL) // 走到头了
                    {
                        return res;
                    }
                    iter = iter->left; // 继续往下
                }
            }
        }
        return res;
    }

};

int bruce(vector<int> v, int m)
{
    int cnt = 0;
    for (int i = 0; i < v.size(); ++i)
    {
        int tmp = 0;
// printf("for %d: ", v[i]);
        for (int j = 0; j < v.size(); ++j)
        {
            if ((v[i] ^ v[j]) > m)
            {
// printf("%d ", v[j]);
                tmp++;
            }
        }
// printf("\nfor %d, we have %d\n", v[i], tmp);
        cnt += tmp;
    }
    return cnt/2;
}

int main()
{
    int n, m;
    while (cin >> n >> m)
    {
        long begint, endt;
        vector<int> v(n);
        for (int i = 0; i < n; ++i)
        {
            cin >> v[i];
// cin.clear();
// cin.sync();
            cout << i << ": " << v[i] << " " << cin.good() << " \n";
        }
        // ltrie
        cout << "\nlt begin\n";
        begint = clock();
        LTrie lt;
        for (int i = 0; i < n; ++i)
        {
// cout << i << " ";
            lt.insert(v[i]);
        }
        cout << "lt insert finish\n";
        int cnt = 0;
        for (int i = 0; i < n; ++i)
        {
            int tmp = lt.search(v[i], m);
// printf("for %d, we have %d\n", v[i], tmp);
            cnt += tmp;
        }
        endt = clock();
        cout << "Trie result: " << cnt/2 << " time: " << endt - begint << endl;

        // bruce
        begint = clock();
        int res2 = bruce(v, m);
        endt = clock();
        cout << "Bruce result: " << res2 << " time: " << endt - begint << endl << endl;
    }
    return 0;
}

输出结果:

lt begin lt insert finish Trie result: 255347 time: 819 Bruce result: 255347 time: 10138

可见肯定比暴力的方法好。

Xcode无法接收大量输入。只能使用命令行。有大神知道原因的话求指导~

点赞