浅谈竞赛中哈希表的应用(四)
4.5 需要微小变化的例子
下面,我们来分析一道 NOI 的试题:
方程的解数
问题描述
已知一个n元高次方程:
其中:是未知数,是系数,是指数。且方程中的所有数均为整数。
假设未知数1≤ ≤M, i=1,,,n,求这个方程的整数解的个数。
约束条件
1≤n≤6;1≤M≤150;
方程的整数解的个数小于。
本题中,指数Pi(i=1,2,……,n)均为正整数。
这个是 NOI 2001 的第二试中的《方程的解数》。
分析:初看此题,题目要求出给定的方程解的个数,这个方程在最坏的情况下可以有6个未知数,而且次数由输入决定。这样就不能利用数学方法直接求出解的个数,而且注意到解的范围最多150个数,因此恐怕只能使用枚举法了。最简单的思路是穷举所有未知数的取值,这样时间复杂度是 O(M^6) ,无法承受。因此我们需要寻找更好的方法,自然想到能否缩小枚举的范围呢?但是发现这样也有很大的困难。我们再次注意到M 的范围,若想不超时,似乎算法的复杂度上限应该是 O(M^3) 左右,这是因为 150^3 < 10000000 。这就启示我们能否仅仅通过枚举3个未知数的值来找到答案呢?如果这样,前一半式子的值 S 可以确定,这时只要枚举后3 个数的值,检查他们的和是否等于 -S 即可。这样只相当于在 O(M^3) 前面加了一个系数,当然还需要预先算出 1 到 150 的各个幂次的值。想到了这里,问题就是如何迅速的找到某个 S 是否曾经出现过,以及出现过了多少次,于是又变成了"某个元素是否在给定集合中"这个问题。所以,我们还是使用哈希表解决这个问题。至于哈希函数不是问题,还是把 S 的值作为关键字使用除余法即可。然而有一点需要注意,这个例子我们不仅需要纪录某个 S 是否出现,出现的次数也很重要,所以可以用一个2维数组,仅仅是加了一个存储出现次数的域而已。
Var
e:array[0..max-1,1..2]of longint; {e[x,1] 记录哈希函数值为 x 的 S 值, e[x,2] 记录这个 S 值出现了几次}
因此 insert 过程也需要一些变化:
procedure ins(x:longint);
var posi:longint;
begin
posi:=locate(x);
e[posi,1]:=x;
inc(e[posi,2]); {仅仅这一条语句,就可以记录下来 S 出现了几次}
end;
4.6 最后一个例子
下面我们来仔细分析下面这个问题:
迷宫的墙
题意描述:
神话中byte山边有一个井之迷宫。迷宫的入口在山顶。迷宫中有许多房间,每个的颜色是以下之一:红、绿、蓝。两个相同颜色的房间看起来相似而不可区分。每个房间里有三口井标以1,2,3。从一个房间到另一间只有一种方式:从上面房间的井里跳到(不一定竖直地)井底的房间。可以从入口房间到达任何其他房间。迷宫中的所有通路走向坐落在最底部的龙宫。所有的迷宫之旅对应了一系列在相继访问的房间里选择的井的标号。这一列数称为一个旅行计划。一个走过好几次迷宫的英雄bytezar画好了图,然而有的房间重复出现了多次。
输入:
第一行有一个整数n,2<=n<=6000,房间数(包括龙宫)。房间从1到n标号,较大编号的房间再较低处(入口房间编号1,龙宫编号n)。接下的n-1行描述迷宫的房间(除了龙宫)和井。每行有一个字母,一个空格,和三个由空格分隔的整数。字母代表了房间的颜色(C--红,Z--绿,N--蓝),第i(i=1,2,3)个数是第i个井通往的房间号。
输出:
迷宫最少的房间数目
这是 IOI 2003 中国国家集训队难题讨论活动的 0020 题。
分析:题目的意思是给出这个迷宫的地图,去掉重复出现的房间,找出这个迷宫的最少房间数目。于是关键就是确定什么样的房间是重复的。通过对样例的分析,可以看出这样的房间是重复的:如果两个房间 i 和 j (1<=i,j <=n-1),他们的颜色相同,而且第 k (k=1,2,3) 堵墙通向的房间或者相同、或者重复。因为这样从 i 和 j 可到达的房间是完全相同的。
所以,我们只需要记录下每个房间的情况和已经被确定相同的房间,然后挨个比较即可。于是又需要用到高效的数据存储与查找,自然想到哈希表。然而,这里面需要对哈希表作更大的改进:首先每个房间只能是3种颜色之一,因此针对每种颜色分别建立哈希表,可以使哈希函数的自变量减少一个;其次还需要纪录每个不重复的房间每堵墙都通向哪个房间,还有哪些房间是重复的。
具体这样实现:
var
e:array[0..2,0..p-1,1..4]of longint; {0..2 代表共有3种颜色,0..p-1 是哈希函数的值域,而 1..4 中的 1..3 表示三堵墙连接到那个房间,4 表示这个单元存储的是哪个节点}
r:array[1..maxn]of longint; {r 表示与 i 相同的节点。如果有多个节点都是相同的,择取其中最大的(这一点不需要特殊的操作,只要在处理节点的时候注意就行了)}
至于哈希函数,最开始我是随意的写了一个(因为越是随意的,就越是随机的!),定位函数是这样的:
function locate(var a,b,c,d:longint):longint;
var t:longint;
i:integer;
begin
t:=r*10037+r[c]*5953+r[d]*2999; {用3堵墙的值任意乘大素数相加再取余数,使得结果分布比较随机,也就比较均匀}
t:=t mod p;
i:=0;
while (e[a,(t+i)mod p,1]<>0)and(e[a,(t+i)mod p,1]<>r) do
if (e[a,(t+i)mod p,2]<>r[c])or(e[a,(t+i)mod p,3]<>r[d]) then inc(i); {线性重新散列}
locate:=(t+i)mod p;
end;
但是后来发现完全没有必要这样做,这样的哈希函数在计算 t 的时候浪费了很多时间(不过数据规模不是很大,所以这点不十分明显),而且素数起到的作用也不应当是这样的。其实让 r,r[c],r[d] 组成 n 进制数就完全能够达到目的了,加入了素数不仅是小规模数据计算浪费时间,对大数据最后结果的分布平均也没有起到比 n 进制数更多的作用。因此改为
t:=r*sqr(n)+r[c]*n+r[d];
当然肯定会有更好的哈希函数的。
4.7 小结
第一个例子,乍一看与哈希表毫无关系;第二个例子叙述比较复杂,但是经过仔细分析,发现问题的本质都是确定"某个元素是否在给定集合中",这正是哈希表的特点。所以,不论题目的表面看起来如何,只要本质是需要高效的数据检索,哈希表通常就是最好的选择!
另外,这两个例子都在原来哈希表的基础上作了一些变化。第一个例子加入了纪录某个值出现次数的域,第二个例子加入了纪录所有墙的情况以及原节点编号的域。虽然都只是很小的变化,但是却给问题的解决带来了不小的方便。因此我们得到提示:哈希表虽然有标准的操作,但也不是一成不变的,需要具体问题具体分析,根据题目的要求和特点作出相应变化。