2

国庆七天乐——第五天

                      20171005

【【数据结构】】

【st表】

定义:ST表能够快速高效地支持对静态序列的区间最小值的查询,它由该序列所有长度为2的幂的子序列的最小元素组成,换言之,ST表包含长度为1,2,4,8, .. , 2k的子序列的元素的最小值。

而计算任意长度L(2j<L≤2j+1)的子序列时,只需要取出两个长度为2j的重叠的子序列的最小值,就能计算其最小值。

我们采用倍增的方法,通过不断合并首尾相接的区间,得到更大长度的区间的答案。

令f(i,j)表示从下标i开始长度为2j的区间的最小元素,则我们有

 

𝑓(𝑖,𝑗)=𝑎𝑖   j=0                                  min⁡{𝑓(𝑖,𝑗−1),𝑓(𝑖+2^(𝑗−1),𝑗−1)}     ,𝑗>0

这样计算每一个答案的时间都是常数级别的,总的预处理时间复杂度为O(nlogn)。

求区间a[x] .. a[y-1](设2i<y-x≤2i+1)的最小值

min{ a[x] , a[x+1] , ... , a[y-1] } = min{ f(x,i) , f(y-2i,i) }

st表的代码实现也很短,下用st[i][j]来存储前面的f(j,i)。

已知数组a[0..n-1],求解st[ ][ ],其中st[i][j]=f(j,i)。

st表的建立

void create_rmq()

{

       for (i=0;i<n;i++) st[0][i]=a[i];

       for (i=1,j=1;j<=n;i++,j=jn)

              for (jn=j<<1,k=0;k<=n-jn;k++)

                     st[i][k]=min(st[i-1][k],st[i-1][k+j]);

}

为了方便st表的查询,我们预先计算所有可能长度的对数:

       log[1]=0; for (i=2;i<=n;i++) log[i]=log[i>>1]+1;

建立对数数组后,st表的查询操作很简单:

int query_rmq ( int l , int r )  //查询区间a[l]..a[r-1],该区间长度为r-l

{

       g=log[r-l];

       return min(st[g][l],st[g][r-(1<<g)]);

}

可以看到,st表的单次询问只要拿出两个序列取最小值,拥有O(1)的时间复杂度。

  1. Rmq问题

给出一个包含数字的序列,现在需要对这个序列进行多次询问,每次询问指定一个子序列,求子序列中元素的最小值。

序列长度为n

方法0

暴力求解,对序列不做任何处理,每次询问直接枚举子序列内所有元素找最小值。时间复杂度O(n)-O(n),询问效率太低。

前一个O(n)表示预处理的时间复杂度,后一个O(n)表示单次询问的时间复杂度。

方法2

用st表

这方法的时间复杂度为O(nlogn)-O(1)

  1. lca

a)  从根出发遍历这棵有根树,记录下欧拉回路并对其做st表。

b)  (得到欧拉回路253835752,对应的高度为012321210)

c)   对于每次询问(询问8和7的最近公共祖先),我们从欧拉回路中找到两个结点第一次出现的位置(位置4和位置7)

d)  然后通过st表求以这两个位置为端点的区间的最小高度(利用st表从序列8357中得到高度最小的5),这个点就是最近公共祖先。

  1. rmq&&lca
  2. LCA问题可以转化为±1RMQ问题(也就是相邻两项差的绝对值正好是1的RMQ问题),RMQ问题也能够转化为LCA问题?

【线段树】

  1. 序列维护问题

增加/修改一个元素的值

询问某个子序列的和

方法0

a)    用数组直接存储元素,通过枚举处理询问。

b)    单次询问时间复杂度O(n),单次修改时间复杂度O(1)

方法1

c)     用数组直接存储前缀和,通过枚举修改每个被影响的前缀和。

d)    单次询问时间复杂度O(1),单次修改时间复杂度O(n)

方法2(分块)

e)    将序列分成k块,每块包含n/k个元素,用一个数组统计每一块所有元素的和,用另一个数组存储每一个元素,修改的时间复杂度O(1)

f)      询问的时候,将较长的子区间里面完整的块的和直接取用,剩余两端的元素直接枚举,时间复杂度O(n/k+k),取k≈n1/2得最小值O(n1/2)。

  1. 方法2.1(分块2)

在方法2的基础上作改进,将序列分成k大块,每大块包含r小块,每小块包含n/(kr)个元素,按照上面的分析方法,询问的时间复杂度可以降到O(k+r+n/(kr)),同样取k=r=n1/3可以得到最小值O(n1/3)。

那么再接着增加层次是不是可以优化算法?

  1. 方法3

是的,我们把能切开的序列都对半切开

如右图,我们得到了一个深度为logn的二叉树

每个叶子结点表示一个元素

每个非叶子结点表示一个子序列

修改和询问的时间复杂度均变为O(logn)

我们得到了一个线段树

 

****************************线段树生成*********************

那么,我们怎么从一个序列产生一个线段树呢?

利用分治的思想

如果序列长度大于1,就先把它对半分成两个子序列

然后对两个子序列各自产生线段树

建立一个根结点,整合来自两棵子树根结点的信息(需要区间可加性)

 

这样,只要关于序列的信息具有区间可加性,就能用线段树维护

*********************存储方式******************************

线段树的实现从存储方式来说有两种方式,一种是顺序存储,一种是链式存储。

其中对于不需要共享结点的很多静态情况,顺序存储是更好的选择。代码框架如下:

struct SegTree{ Datatype data[max_size]; int size; }

而对于一些需要动态申请空间甚至共享结点的情况,链式存储才能应付。代码框架如下:

typedef struct Snode{ Datatype data; Snode *Lchild,*Rchild; }Snode,*Stree;

2.区间修改

我们发现线段树修改的很多结点,不一定要立刻用上,我们可以延迟执行这个区间的修改,把修改操作的影响停留在高度最小的那些结点(最多2logn个)并在结点上记录未进行完全的修改操作,到了需要细分那些结点的时候再继续进行修改。

我们发现线段树修改的很多结点,不一定要立刻用上,我们可以延迟执行这个区间的修改,把修改操作的影响停留在高度最小的那些结点(最多2logn个)并在结点上记录未进行完全的修改操作,到了需要细分那些结点的时候再继续进行修改。

所记录的被延迟的修改操作,我们称为“延迟标记”,有了延迟标记,我们就可以在没必要往下访问的时候节省工夫。

与对维护的信息的要求类似,我们需要延迟标记所承载的修改操作满足可加性:

对于操作f,g,存在同类型操作h=f·g,使得f(g(x))=h(x)总是成立。

对于区间修改为d的操作,改为每个元素乘以0再加上d就行了
*******************应用**********************************

  1. count color

a)  可以直接开30个线段树各维护一种颜色,但是这样就有30的常数,很难通过。可以将区间所包含的颜色的集合通过位压缩压缩成一个int,区间的合并就相当于对应的int做或运算,时间复杂度O(OlogL)

  1. mayor’s poster(poj 2528)

a)  用线段树可以维护每一块都能看见哪张海报,然后对线段树做一次遍历,就可以得到答案。

b)  问题是直接建一个长度为10000000的线段树,常数会很大,而且需要动态建点。我们可以预先读取有用的端点,然后以这些端点重新划分成长度最多为2n的线段树,这样离散化的时间复杂度O(nlogn)

3. 一个序列A,你要维护2个操作:

给区间[L,R]内元素依次加上1,2,3... 具体地,A[L]+=1,A[L+1]+=2 ... A[R]+=R-L+1

求区间[L,R]内元素的和。

序列长度最多100000,操作个数最多100000。

一看到区间修改,延迟标记在所难免,但是如何设计延迟标记呢?

我们已经知道真实的修改操作摊派到每个结点上一定是给区间里的元素加上t,t+1,t+2...的形式,看起来延迟标记只需要记录t,但是这样的话延迟标记就没法合并(123…+123…=246…,没有包含在记录范围内)

但是仔细观察,等差数列的和只可能是等差数列,也就是说,只需要再记录一个公差d,就能完全维护所有修改操作(以及它们之间的结合)

  1. 三维偏序

我们将问题逐步化简。首先,语文最好的一定不会郁闷,其他同学也只可能因为一个语文比自己好的同学而郁闷。

所以我们可以按照语文排名一个个将学生加入一个序列,序列的第i位表示已有的数学排名i的学生的英语排名,加入学生就直接修改序列的对应位置。查询数学排名b英语排名c的同学的时候,就检查序列前b-1位是否有小于c的元素。时间复杂度O(nlogn),后面介绍的树状数组也能做。

  1. 四维偏序

这里我们可以考虑维护一个矩阵A[1..N][1..N],意义类似前一题,查询(a,b,c,d)时,检查A[1..b][1..c]子矩阵里面是否有小于d的元素。这个可以用线段树套线段树的方法做,其中外层线段树维护的就是内层线段树,时间复杂度O(nlog2n),在这里就不详细介绍了。

由于查询的性质,在介绍了树状数组之后,这题可以将外层线段树换成树状数组。

5.100维偏序(?)

学生都被你们吓跑了,直接枚举就行了。时间复杂度O(n2)。

N<50

【树状数组】

一个序列,你要维护如下操作:

增加/修改一个元素的值

询问某个前缀的和

仔细想一下,这可是前缀和啊,线段树的所有结点都有用吗?

并不。每个结点的右子结点,在询问前缀和的时候,永远用不到。

那就删了它们吧~

为了问题叙述的方便,现在我们假定n=2k

删了右子结点以后,我们需要重新组织这个树

我们看到,删掉右子树后,所有元素只剩一个以自己为右边界的结点了。

这样的话,要想计算前k个元素的前缀和,就必须把以第k个元素为右边界的结点统计在内,剩下的元素又变成了新的前缀和……

令Ftree[i]表示以第i个元素为右边界的结点的区间和,那么我们有:

sum[1]=Ftree[1]

sum[2]=Ftree[2]

sum[3]=Ftree[3]+sum[2] , 2=3-lowbit(3)

sum[4]=Ftree[4]

sum[5]=Ftree[5]+sum[4] , 4=5-lowbit(5)

sum[6]=Ftree[6]+sum[4] , 4=6-lowbit(6)

sum[7]=Ftree[7]+sum[6] , 6=7-lowbit(7)

sum[8]=Ftree[8]

令Ftree[i]表示以第i个元素为右边界的结点的区间和,那么我们有:

sum[x]=Ftree[x]+sum[x-lowbit(x)]

这样的原因也很简单:Ftree[x]包含的元素个数恰好就是lowbit(x),而前x个元素的和,就等于Ftree[x]的区间和与前x-lowbit(x)个元素的和相加。

这样我们就解决了查询问题,并且证明了,修改前缀没有右子结点也是可以的。

 

我们称这阉割了右子结点的线段树为树状数组(Fenwick tree)。

它不仅能用数组存储,而且它的下标是和对应下标的元素有直接关联的。

对于修改问题,由线段树的特性可知,如果两个结点有公共元素,那么这两个结点必定相互包含。

树状数组在从单个元素往上修改的时候,和线段树是一样的,只是会跳过其中的右子结点而已。

线段树的结点向上走的时候,如果自己是左子结点,就会从右边合并一个区间,如果自己是右子结点,就会从左边合并一个。

树状数组的结点x往上走的时候

首先由于自己是左子结点,它必然会从右边合并一个与自己等长(lowbit(x))的区间

这时候其右边界右移了lowbit(x)

如果它到达了一个右子结点,它还会往上走

但是直到碰到一个左子结点为止,它的右边界不再右移

这样当它最终到达下一个左子结点的时候,它的右边界就是x+lowbit(x)

可以看到,只要利用高效的位运算跳转到下一个结点,树状数组在处理前缀和问题的时候,虽然时间复杂度和线段树一样,但是比线段树更有效率。

 

而且,很多用线段树解决的问题都能转化成前缀和问题。

 

前面我们假设过,n是2的幂,如果n不是2的幂,我

们该怎么做呢?

把n改成8

**************实现************************************

修改单个元素(a[x]+=d)

for ( t=x ; t<=n ; t+= (t&-t) ) Ftree[t]+=d;

询问前缀和(求a[1..x]的和)

ans=0; for ( t=x ; t>0 ; t-=(t&-t) ) ans+=Ftree[t];

**********************应用*****************************

例1

维护区间和,支持区间加。

解答

首先区间和可以转化为前缀和的差,问题转化为维护前缀和。

然后我们可以拆开区间加,令其变成两个后缀加的差:

比如说[5,8]内元素各加1,可以看作从第5个开始各加3,然后从第9个开始各减去3

这有什么用呢?研究后缀加对前缀和的影响更为容易。

比如说,从第5个开始各加3的话,就意味着:

对于每个不小于5的x,都有sum[x]+=3*(x-4)

可以进一步拆成,每个不小于5的x,sum[x]先加上3x再减去12

如果开两个数组b[ ]和c[ ]使得sum[x]=b[1..x]+xc[1..x],并给它们维护前缀和。

那么从第5个开始各加3的操作对sum[x]的影响

就可以通过b[5]+=-12和c[5]+=3完全模拟。

更一般地,如果是区间[L,R)加d的操作,那么就能通过

c[L]+=d, c[R]-=d, b[L]-=d*(L-1), b[R]+=d*(R-1)

这四个单点加来实现对sum[x]的维护。

例2

火柴排队(noip2013)

给出两个序列A和B,同一个序列中没有相同元素

现在要通过交换序列中相邻元素的操作,使得∑_(𝑖=1)^𝑛▒〖(𝑎_𝑖−𝑏_𝑖)〗^2 最小。

求最小交换次数

为了使这个距离最小,必然使得∑_(𝑖=1)^𝑛▒〖𝑎_𝑖 𝑏_𝑖 〗最大,根据柯西不等式,两个序列对应位置元素在本序列中的大小排名必须一样。

我们把序列中每个元素替换成该元素在序列中的大小排名,然后把序列B中的元素(已经是排名了)进一步替换成在A中出现的位置。

这时候原问题的交换次数,等同于替换后的B序列的逆序对数量。

逆序对可以用树状数组求出数量。

例题3

一个序列A,你要维护2个操作:

给区间[L,R]内元素依次加上1,2,3... 具体地,A[L]+=1,A[L+1]+=2 ... A[R]+=R-L+1

求区间[L,R]内元素的和。

序列长度最多100000,操作个数最多100000。

解答

这个问题,可以通过维护sum[x]=a[1..x]x2+b[1..x]x+c[1..x]转化为若干个单点加/询问前缀和问题,做法类似于例1。

  1. 三维偏序

a)  如果这是静态查询前缀的最小值的话,树状数组完全没有问题。但是这是加入了单点修改的树状数组,单点修改的条件下,如果把某个元素变大了,树状数组完全无法应付……好在这里的修改可以看作是把元素变小,树状数组是可以用的。

b)  和线段树一样,直接插入对应位置,然后询问就是询问前缀的最值。

 

  1. 四维偏序

a)  参考前面说到的线段树的问题,由于外层线段树仅限于前缀查询,所以完全可以用树状数组替代外层线段树。

b)  问题:内层线段树能用树状数组替代吗?不能

7.(听说你会前缀,还会求和)

RMQ问题,只能用树状数组

解法

我们可以保留原数组(相当狡猾的操作)同时像求和一样求最小值Ftree[ ]然后对于每个询问(L,R]从Ftree[R]开始考虑问题我们照常取结点然后R=R-(R&-R),直到这么做会越过L (R-(R&-R)<L) 为止,如果R≠L,我们取单个元素,R--,然后接着做。如此直到R=L,把中途取的区间和单个元素的最小值整合回答询问单次询问时间复杂度O(log2n),术业有专攻,树状数组确实不太适合。

【trie树】

以包含以下词汇的词典为例建立一棵树:

a aye bad bat cc

 

可以发现,这棵树每个存在单词的结点,单词都完全取决于从根结点到这个结点的路径,也就是说对于每个结点来说,只需要记是否有单词结束于此就可以了。

到这里,我们就得到了一个包含a,aye,bad,bat,cc五个单词的字典树了。

查询的时候,只要按照所查询的单词的每一位从根开始走。

如果最终走不下去(不存在这样的结点)就表示没有这单词。

反之如果最终走到了一个标记了的结点,就表示有这单词。

往词典里面添加单词也很简单,从根结点按照这个单词的字母往下走,如果没有对应结点就添加一个。当走完对应的路径时就打上标记,表示这个路径对应的单词被加入了词典。

利用字典树,时间复杂度可以达到O(NL+ML)

由于字典树的度最大可以达到127(虽然一般也就26)而且深度可能会比较大。

我们不能使用顺序存储的方法,必须使用指针。

在现实编程中,我们通常使用数组模拟指针,也就是利用child[node][char]的形式来存储字典树。

***************************实现*************************

所以在本课件中,字典树的描述是这样的:

struct Trie {

       int child[max_tree_size][char_set_size]; //这里令结点1为根,结点0为空

       bool cnt[max_tree_size]; int size; //一开始只有根,size=1

       int parent[max_tree_size]; //cnt表示要在字典树上维护的信息

};

插入单词(以前面的词典查询为例)

void add_word(char *wd) { //wd为单词,此处假设其由小写字母组成

       int nd;

       for (nd=1;*wd;wd++) {

              int cd=*wd-’a’;

              if (!child[nd][cd]) { child[nd][cd]=++size; parent[size]=nd; }

              nd=child[nd][cd];

       }

       cnt[nd]=1; //这里可以保证,nd只对应这个单词

}

查询单词存在性

void add_word(char *wd) { //wd为单词,此处假设其由小写字母组成

       int nd;

       for (nd=1;*wd;wd++)

             nd=cd[nd][*wd-’a’];//即便走到结点0也没关系

       return cnt[nd];

}

***********************应用*******************************

例题0

给出一组字符串,求出它们所有的前缀(包括它们自身)

你只需要返回对于所有长度有多少不重复的前缀

解答

这里我们只需要将存储结点的数组从左到右扫描计算一遍就能得出所有结点的高度。

因为在从左到右扫描的过程中,我们可以保证除根结点以外所有结点都会在父结点被扫描到以后才被扫描到。

例题1

给出一组字符串,询问其中有多少个字符串是另一个字符串的前缀。

解答

对于字母树来说,所有的叶子结点必定都被标记了,并且单词的所有前缀对应的结点都会被新建。

所以对这一组字符串新建了结点以后,可以用一个指针记录每个字符串对应的结点。然后检查对应的结点有没有子结点,就知道这个字符串是不是别的字符串的前缀了。

例题2

给出一组字符串,任意两个字符串都能求出最长公共前缀。

求这些公共前缀中最长的一个。

解答

加入一个结点的时候,我们就已经知道哪些结点是某个单词以及它的前缀了,我们令每个结点记录以其为前缀的单词个数,然后扫描筛选出作为至少2个单词前缀的结点,找到其中高度最大的结点,其高度就是最长公共前缀的长度。

例题3

给出一个字符矩阵,以及一些字符串,你的目标是在矩阵中找出以8个方向之一直线排列的这些字符串,如果存在,输出第一个字符的位置以及方向。

解答

我们可以对这些字符串建立字典树,然后枚举矩阵中的第一个字符以及方向,走字典树,在经过的标记点处写下开始点的信息。

最后对于每个标记点,输出其写下的开始点的信息。

需要枚举O(N2)数量的待查找串,每次枚举时间复杂度O(N),故总体时间复杂度O(N3),其中N为矩阵的长/宽。

原题N≤1000,如果数据给力,还是会有超时的风险。

另一种解答

在学习了kmp以后,利用将kmp算法扩展到字母树上得到的ac自动机,可以实现模式串与待查找串子串(不像朴素的字母树只能和前缀)匹配

这样我们可以得到一种时间复杂度为O(N2)的算法。

这里不详细展开。

【heap】

1离散对数问题

求解xb=a(mod p),其中p为素数,b与p-1互质,a非零

并且题目已经给出原根r使得r,r2,r3... rp-1除以p的余数互不相等

p≤1011

首先我们求解a=ry,由于a非零,因此r,r2,r3... rp-1有且只有一个与之相等,设x=rz,则问题转化为zb=y(mod p-1),其中未知数z可以求逆元解决。

问题是如何求解a=ry?这是一个离散对数问题,至今未找到多项式算法(相对于p的长度的多项式算法),其难解性作为椭圆曲线密码算法(ECC)的安全性依靠。

但是这里允许我们用伪多项式算法(相对于p的大小的多项式算法)

算法1

暴力求出r,r2,r3...直到有一个和a相等。

时间复杂度显然为O(p),超时预定

算法2

改迈两步走,小步暴力求出r0,r1,r2,r3...rk-1,然后大步求a,ar-k,ar-2k,ar-3k… 直到有一个和前面小步算出来的相等。

如果我们用数组维护前面的r0,r1,r2,r3...rk-1 ,使得单次检测能够快速返回结果,那么我们将会得到一个时间复杂度O(k+p/k),也就是O(p1/2)【k取最优】的算法,这就是小步大步算法(baby-step giant-step,SGS)的框架。

但是直接开一个大小为p的数组空间不够……我们要怎么办才能在现有空间的条件下维持快速检测的能力?

算法2.0(小步大步算法)

小步暴力求出r0,r1,r2,r3...rk-1,给对应数组下标打上标记,然后大步求a,ar-k,ar-2k,ar-3k… 直到有一个和前面小步算出来的相等。

虽然时间复杂度O(p1/2),但空间复杂度O(p),对于本题来说难以承受。

算法2.1(小步大步算法)

我们分析,数组之所以时间上可行,就是因为给出要查询的元素,它能够快速定位到查询该元素的地方。

如果我们建立一个映射,将可能查询的p个元素,分散地映射到长度更小的数组上的话,那么我们能够维持快速查询的能力,同时也能节省空间。

我们称将集合A映射到元素相对较少的集合B以压缩数据的映射称为散列函数(Hash function),其中元素对应的散列函数值,称为该元素的哈希值(Hash value)。我们还将 把对A中元素的查询 映射到对应哈希值的位置进行 的数组称为散列表(Hash table,也称哈希表)

算法2.1 (小步大步算法+散列表)

举例说,我们现在要存7个1..100之间的数字以便后续对相同数字的查询,但是只允许使用20个数字的储存空间,那么我们可以:

建立一个长度为19的数组

将1..100这100个数字通过映射f(x)=x mod 19 映射到[0..18]上

这样当查询/标记一个数字的时候(比如说34)

首先通过f(34)=15把数字34映射到数组下标为15的位置

并把这个位置当成下标34一样进行查询/标记

这样的话,在选择了易于计算并且不易引发冲突的映射的情况下,查询/标记的效率可以很高,时间复杂度可看作O(1),至此我们解决了这题。

虽然我们可以通过合理手段尽可能减少冲突,而且冲突很多情况下并不会对总体的运行效率造成较大影响,但是对于不可能完全避免的冲突,有时候我们需要应对冲突的机制。(有的出题人会故意卡散列)

Heap:

冲突应对

我们还举前面的例子,在插入了34之后,又插入了72,我们发现它们哈希值相同。以它们为例介绍应对冲突的一些方法:

开放寻址法

在插入72时,首先定位了下标15,但是这里已经有34了,所以把下标调整到其后一位16,以后询问72的时候,也这么做,先定位下标15,再调整到下标16进行处理。

按照固定的增量序列{dn},第x次冲突的时候右移dx位试选,直到不再冲突为止。

单独链表法

在插入72时,首先定位了下标15,但是这里已经有34了,所以在下标15处引出一个链表,将72插入链表中。

双散列法

在插入72时,首先定位了下标15,但是这里已经有34了,所以用另一个散列函数f2(x)=(x mod 17)+1算出f2(72)=5,然后以5为跨度像开放寻址法一样调整找到下标1

**************************程序实现********************

开放寻址法的实现

struct HashTable{ int label[p]; data[p]; };

void HashTable::insert(int lb,int dt) { //A[lb]=dt

int r;

for ( r=hash(lb) ; label[r]!=0&&label[r]!=lb ; r=(r+1)%p ); //1可以换成任意序列

if (label[r]==0) label[r]=lb; data[r]=dt;

}

int HashTable::query(int lb) { // 返回A[lb]

int r;

for ( r=hash(lb) ; label[r]!=0&&label[r]!=lb ; r=(r+1)%p );

if (label[r]==0) return 0; return data[r];

}

单独链表法的实现

struct HashTable{ int next[max_size],label[max_size],data[max_size]; int siz;};

void HashTable::insert(int lb,int dt) { //A[lb]=dt   // siz一开始等于p

int r;

for ( r=hash(lb) ; label[r]!=0&&label[r]!=lb ; r=next[r] ) if (!next[r]) next[r]=++siz;

if (label[r]==0) label[r]=lb; data[r]=dt;

}

int HashTable::query(int lb) { // 返回A[lb]

int r;

for ( r=hash(lb) ; label[r]!=0&&label[r]!=lb ; r=next[r] );

if (label[r]==0) return 0; return data[r];

}

双散列法的实现

struct HashTable{ int label[p]; data[p]; };

void HashTable::insert(int lb,int dt) { //A[lb]=dt

int r,d=hash2(lb);

for ( r=hash(lb) ; label[r]!=0&&label[r]!=lb ; r=(r+d)%p );

if (label[r]==0) label[r]=lb; data[r]=dt;

}

int HashTable::query(int lb) { // 返回A[lb]

int r,d=hash2(lb);

for ( r=hash(lb) ; label[r]!=0&&label[r]!=lb ; r=(r+d)%p );

if (label[r]==0) return 0; return data[r];

}

*******************应用********************************

例题1(字符串匹配问题)

给出一个长度为n的模式串,和一个长度为m的待匹配串。(全小写字母)

问待匹配串里面是否含有子串和模式串匹配?(n<1000,m<1000000)

解答

我们可以将n位字符串利用散列函数压缩成一个int哈希值,而且我们希望,这个数要在匹配时方便运算,比如说前面去掉一位后面补上一位的过程不是重新计算,而是最大限度地共享信息。

有一种方案:f(s1s2...sn)=s1*26n-1+s2*26n-2+...+sn-1*26+sn(取模)

如果我们需要删掉开头一位,然后从后面补上一位sn+1,那么:

f(s2s3…sn+1)=s2*26n-1+s3*26n-2+…+sn*26+sn+1=26*(s2*26n-2+s3*26n-3+…+sn)+sn+1

=sn+1-s1*26n+26*f(s1s2…sn)

这样,计算模式串以及待匹配串所有长为n的子串的散列函数所需总时间O(m),对于哈希值与模式串相等的子串再逐个比较(或直接输出,勇士)

 

例题1(字符串匹配问题)

给出一个长度为n的模式串,和一个长度为m的待匹配串。(全小写字母)

问待匹配串里面是否含有子串和模式串匹配?(n<1000,m<1000000)

解答

运算的时候需要注意,如果不手动取模,就相当于模232,这称为自然溢出。但是,如果哈希时直接使用26进制和自然溢出,很容易被卡

比如说,出题人只要想到有人会用哈希,就可以首先针对26进制和自然溢出,设计一组数据,令很多子串和模式串哈希值相同却无法匹配

怎么避开这种陷阱呢?换个模或换个进制数都是可以考虑的方法。

虽然对于这道kmp模板题来说,哈希不是最好的方法,但这可以给我们其余的字符串题目提供一种思路。就是如果某道题需要快速匹配,以至于kmp等算法都无法满足要求(或者思路打不开没有想到的时候),哈希至少是一个不算太坏的选择。

例题2

给出一棵二叉树,求这棵树结构不同的子树数量。(结点数n≤100000)

解答

如何判断结构不同?我们知道两棵树结构相同,当且仅当它们同为空树,或者它们的左子树和右子树的结构对应相同。

我们可以通过从叶到根的顺序记录所有的子树,每记录一个子树,就询问前面记录的子树当中有没有一样的。

这时候我们需要一个能够记录下很多子树信息的数据结构,使得后来的子树能够快速判断是否和前面的子树相同。

但是直接把整个子树的结构保存下来太麻烦了,能不能压缩一棵树的信息?

如果能够把一棵二叉树压缩成哈希值的话,我们希望这棵树的哈希值是完全由它的左/右子结点的哈希值决定的,这样对于结构相同的两棵子树而言,它们的哈希值也是相同的。

比如说我们可以设一棵二叉树T(L,R)的计算的哈希值是这样的:

f(empty tree)=17 , f( T(L,R) ) = ( f(L)*23 + f(R)*37 + f(L)*f(R) ) % 9231569

并且,我们希望对于结构不同的两棵树而言,它们的哈希值严格不同,因为这样可以用左右子树的哈希值代表这棵树,为了这样的目的,我们使用开放寻址法,当发生冲突时重新寻址找到新的哈希值。

设计了这样的哈希算法以后,对整棵二叉树进行后序遍历,遍历的时候把计算的每一个哈希值立刻记录下来。时间复杂度O(n)

【stl】

就是一些程序这个库已经帮你实现了,你不用重新编写

*******************分类*************************

STL可分为以下六个部分:

容器(containers)

迭代器(iterators)

空间配置器(allocator)

配接器(adapters)

算法(algorithms)

仿函数(functors)

**************分类讲解******************************

1:算法

FI lower_bound( FI first, FI last, const T& val )

给出有序序列[first,last)求第一个不小于val的元素的出现位置,如果数组元素全部小于val,返回last。

FI upper_bound(FI first, FI last, const T& val )

给出有序序列[first,last)求第一个大于val的元素的出现位置,如果数组元素全部不大于val,返回last。配合上一条可以圈定一组和val大小相等的元素

OI merge (II1 first1, II1 last1, II2 first2, II2 last2, OI result)

给出有序的序列[first1,last1)和[first2,last2),对它们进行二路归并并将归并后的数组写入由result开头的数组/容器中。

bool next_permutation (BI first, BI last)

给出序列[first,last)将这个序列变为字典序下一个排列并返回true,如果已经是最后一个,变为第一个排列并返回false,对应还有prev_permutation()

BI partition(BI first, BI last, UP pred)

将序列[first,last)中元素x按照函数pred(x)的返回值分成两部分,返回true的排前面,返回false的排后面,然后返回后半部分的起点。

void make_heap (RAI first, RAI last)

将序列[first,last)整理成一个最大堆

II find_if (II first, II last, UP pred)

在序列[first,last)中寻找第一个pred(x)返回值为true的元素x,并返回位置或last

 

算法部分还有很多实用的函数,可以实现不同功能,这里只是作为引子介绍,有兴趣的同学可以上这个网站了解更多

http://www.cplusplus.com/reference/algorithm/

2:容器+迭代器

向量<vector>

列表<list>

双端队列<deque>

集合<set>

映射<map>

栈<stack>

队列<queue>

除此之外,迭代器(Iterator)也是使用容器时不可或缺的,它指向容器的特定位置,完成算法所需的修改和查询。

每个头文件都代表着它所包含的的容器的实现

vector,连续存储的元素,装载于头文件<vector>

list,双向链表,每个结点有一个元素,装载于头文件<list>

deque,在向量的基础上加入各种指针,装载于头文件<deque>

set,由结点组成的平衡树,没有两个相同次序的结点,装载于头文件<set>

multiset,由结点组成的平衡树,允许相同次序的结点,装载于头文件<set>

map,由<key,value>对组成的平衡树,允许将key映射到该容器中进行处理,装载与头文件<map>。对应还有multimap,允许同时记录多个key。

stack,维护栈的操作,装载于头文件<stack>

queue,维护队列的操作,装载于头文件<queue>

priority_queue,维护优先队列的操作,装载于头文件<queue>

现在来一一介绍这些容器,以及它支持的操作的接口

************************划重点************************

1.vector

vector<int> a,b; //定义两个包含int型元素的vector

vector<int>::iterator p; //定义指向int型vector的iterator

const c[5]={1,3,4,6,2};

a.assign(c+1,c+5);         //a变为{3,4,6,2}

a.pushback(a[2]);         //a变为{3,4,6,2,6}

b.assign(7,4);          //b变为{4,4,4,4,4,4,4}

a.swap(b);                //a和b指向的数组交换,也就是b变为{3,4,6,2,6}

h=a.insert(a.begin()+3,5); //a变为{4,4,4,5,4,4,4,4},h指向a的下标3处

b.erase(b.begin()+2);   //b变为{3,4,2,6}

2.list

list可以看作是一个双向链表,它支持对链表两端进行查询和修改:

begin(),end()返回链表前端/后端位置的iterator

front(),back()返回链表前端/后端元素

push_front(x),push_back(x)在链表前端/后端插入元素x

pop_front(),pop_back()在链表前端/后端删除一个元素

insert(it,x)在it指向的元素前面插入元素x

erase(it)删除it指向的元素

splice(it,x)将链表x拼接到该链表it指向的位置

merge(it,x)假设该链表和x均为升序,把x归并到该链表并保持升序

list<int> a,b; //定义两个包含int型元素的list

list<int>::iterator p; //定义指向int型list的iterator

const c[5]={1,3,4,6,2};

a.assign(c+1,c+5);         //a变为{3,4,6,2}

a.pushback(a.begin());              //a变为{3,4,6,2,3}

b.assign(4,5);          //b变为{5,5,5,5}

a.sort(); b.unique();      //a变为{2,3,3,4,6} b变为{5}

a.merge(b);                     //a变为{2,3,3,4,5,6} b变为{}

a.insert(a.begin()+3,7)       //a变为{2,3,3,7,4,5,6}

3.pair:二元组

4:map

例:

map <long long,int> a;

map <string,int> b;

a[923402759045]=2352452;

b[“sldnfwejgb”]=7293;

cout<<a[923402759045]+b[“sldnfwejgb”]<<endl;    //输出2359745

cout<<a[516846513546]+b[“sldnfwejgb”]<<endl;    //输出7293

 

注意到上例中,虽然没有为a[516846513546]分配空间,但是在执行完最后一个输出之后, a[516846513546]被分配了空间并且初始化为0。

**********************练习题***************************

poj2945(trie)

poj3764(trie,需要位运算知识)

 

poj2503(hash/ac自动机)

poj3349(hash)

poj2002(hash,O(n2))

posted @ 2017-10-05 16:33  DDYYZZ  阅读(1207)  评论(0编辑  收藏  举报