注:leetcode的要求是第一列中相同的数只算一次。POJ是单独算的。
比如
4
0 0 0 0
0 0 0 0
0 0 0 0
0 0 0 0
这组测试数据,leetcode上面要求返回1,而在POJ上要求返回4*4*4*4 = 256。
好吧。上次写POJ解题报告的解题的时间为“2011-03-18 01:41”。现在时间是“2012-10-20 15:48”。我就不继续说下去了。免得自己太难过。。。
这次写POJ这道题的缘由是在准备笔试面试,在leetcode上见到一道题是是求所以a+b+c+d=0的可能的组数。于是看到了POJ这道题的链接。
当时在leetcode的OJ通过的做法是先排序,然后对两行遍历,另外两行一个指针从上往下走,一个指针从下往上走。时间复杂度是O(n^3)。
但在POJ,这是无法通过的。。。
于是,在尝试把前两组遍历,求出所有可能的和,保存在map中,然后遍历后两组,对每一个和sum,在map中找-sum。这样的时间复杂度是O(n^2*log*(n^2))。
但在POJ,这是无法通过的。。。
于是,开始“参考”网上的做法,使用hash。后来提交的时候还是各种RE,后来我的程序改得和网上的一模一样了。同学们可以直接移步到这里:http://www.cnblogs.com/wuyiqi/archive/2012/06/05/2536785.html
因为程序一模一样了,我也就不贴程序了。
这道题给我长进是:链式Hash
下面的hash函数使用两个大素数取余,
int hash(int num){
return (num+3899971)%3999971;
}
为什么用这种方法可以见http://stackoverflow.com/questions/1145217/why-should-hash-functions-use-a-prime-number-modulus
但是估计看完也只有直观的感受,那就是用素数会让整个hash表尽可能都用到。当然,用这是由于“数学原理”来理解也就差不多了。。。
int tot;
struct edge{
int v, next, cnt;
}edge[3999971];
int head[3999971];
这是链式hash表实现的数据结构,实际上没有使用链表,而是使用数组,用数组索引代替指针(原因是在POJ中一次开很大的数组是允许的,这样可以避免每次动态申请空间的时间开销,算是用空间换时间)。head用做索引,指向hash值为s的第一个num。这个num及所有hash值为s的数保存在edge数组中。edge中的每一个元素是一个结构体,保存这个元素的num(用v表示),下一跳在edge数组中的索引,还有num出现了多少次(用cnt表示)。hash值取0~3999970(见hash函数中的取余素数)。所以两个数组开3999971这么大。
这也能看出取hash值的作用:用一个相对较小的空间保存所有可能出现的和,避免一个特大的空间(3999971 vs 2^30,因为每个数的范围为(+-)2^28)。
void add_edge(int num){
int s=hash(num);
for(int i = head[s]; i != -1; i = edge[i].next){
if(edge[i].v == num){
edge[i].cnt++;
return ;
}
}
edge[tot].v = num;
edge[tot].cnt = 1;
edge[tot].next = head[s];
head[s]=tot++;
}
这是插入hash表的过程。首先对num取hash,根据head找到第一个hash值为s的数在edge中的索引,如果找到,则cnt加1。否则在edge表的末尾(索引为tot,tail of table)保存下这个数的信息(数值num, 计数为1)。注意的是这里让这个新的节点下一跳指向当前的头结点,然后更新头结点为这个新节点。这样实际上将节点插入在了这一系列值的最前列。体会下当head[s]为-1时,即没有值时的情况。这样写要比将新节点报错在链式表的末端要统一。
int find(int num){
int s=hash(num);
for(int i=head[s];i!=-1;i=edge[i].next){
if(edge[i].v == num){
return edge[i].cnt;
}
}
return 0;
}
明白了插入的过程后查找的过程就顺其自然了。如果这条链上都没找到,则该数不存在。