又见神奇的异或。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无法接收大量输入。只能使用命令行。有大神知道原因的话求指导~