Write by Zhuangli(zjfc3) 浙江林学院 ACM集训队阶段总结 Write by Zhuangli(zjfc3)
图论 What is that?
什么是图论? 图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若 干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系 。 图论本身是应用数学的一部份,因此,历史上图论曾经被好多位数学家各自独立地建立过。关于图论的文字记载最早出现在欧拉1736年的论着中,他所考虑的原始问题有很强的实际背景。
并查集及其拓展 并查集是一种信息内聚很强的数据结构,它在判定图的连通性以及等价类划分的时空效率上有着不可替代的优势。但并查集的特殊应用也应该有所了解. 以下两类是并查集在实际问题中的特殊拓展,希望大家能够进行快速掌握. 1.集合计数问题 2.二分图识别
集合计数问题 HDU 1856 More is better 题意: 若A与B认识,B与C认识,则A与C认识,现给你一组人员的关系表,求在这样的不同群组内,拥有最多人数群组的人数。
集合计数问题 有什么想法? 并查后统计并查数组? 不可行 数据范围10000000 如果逐个统计必定TLE 不能如此暴力…. 思路如何……想3分钟
集合计数问题 联想到并查集的构造原理 构造rank数组,在并操作中入手!! 好,问题解决!!
二分图识别 HDU 1829 A Bug Of Life 题意: 假定两只飞虫(Bug)关系表,如A B表示A仰慕B,问是否存在同性的飞虫互相仰慕(也就是同性恋问题).
二分图识别 难点:A与B的集合归属不定 如何解决?思考!!!
二分图识别 非此即彼思想的运用 构造数组opp,初始化为本身编号,若A与B有关,首先进行find(A),find(B)的操作,若根相同,则说明A与B属于同一集合,即同性恋,否则执行下面的操作,如果A的opp等于本身,说明A的opp未初始化,使之等于B,否则将A的opp与B进行Unition操作,类似的如果B的opp等于本身,说明B的opp未初始化,使之等于A,否则将B的opp与A进行Unition操作
二分图识别 想想为什么? 二分图的性质决定 更深入的二分识别……(如统计最小单元集,如何进行 >_< 课后思考!)
最短路径问题 在一个网络图中求解一点到另一点间最短距离及其路径的算法称之为最短路径问题。 1、单源正权最短路径 2、单源带负权最短路径 3、多源最短路径
单源正权最短路径 求解单源最短路径的Dijkstra算法,状态转移与贪心准则的完美结合。 思想:动态规划 策略:贪心( O(Ve) ) 步骤: 1.构造辅助数组Dis[],Vist[],Dis[i]表示从源点出发到达i点的最短距离,Vist[i]表示i点是否已被访问,算法开始执行时令所有Dis[i]=w(start,i)[不可达为MAX],Vist[start]=true. 2.每次得到Dis[]数组中最小且未被访问过的点i,标记Vist[i]=true,遍历所有与i相关的边,若得到Dis[i]+w(i,j)<Dis[j],则更新Dis[j]=Dis[i]+w(i ,j). 3.如此循环直到所有点均被标记.
单源正权最短路径 Dijkstra的致命弱点:不能处理带负权的边 思考:Why? 问题出自贪心策略!!若存在负权,则算法将不断更新负权边相关顶点的Dis值,从而导致答案错误!
单源带负权最短路径 如何处理Dijkstra的遗留问题? 摈弃贪心策略 执行松弛技术-----Bellman-ford算法
单源带负权最短路径 什么是松弛技术? 日常生活中的例子~~(1+1猜想) 松弛技术的本质是首先给予一个物体很高的估价,在每次的迭代中对估价进行修正,在有限次的迭代后使估价与实际值吻合的技术。
单源带负权最短路径 思想:若存在N个点的网络,则对于起点到终点最多经过N-1条边 策略:有限迭代下的松弛技术
单源带负权最短路径 步骤: 1、开辟辅助数组Dis[],记录源点到点i的最小距离,初始时设所有Dis[]值为MAX,Dis[start]=0. 2、进行n-1次迭代,对于所有边,若满足Dis[i]<MAX&&Dis[i]+w(i,j)<Dis[j],更新Dis[j]= Dis[i]+w(i,j). 3、执行完成后,再执行1次迭代,若出现Dis[i]+w(i,j)<Dis[j]的情况,则表明出现了负环,此时不存在最短路径,否则Dis[end]即所求单源最短路径.
单源带负权最短路径 算法分析: 1、迭代v-1次,每次遍历所有边,复杂度O(VE),E为全部边,Dijkstra的复杂度O(Ve),e为部分边…..效率差别很大!! 2、如何优化?思考!!
单源带负权最短路径 优化1:使用bool值Y 标记此次迭代是否进行松弛,若没进行松弛表明已经得到最短路径,因此跳出循环。(常系数优化)--YEN氏优化 优化2:使用标记数组以及队列辅助,初始化时推入start点,标记start在队内,每次执行时,将队首推出,标记其不在队内,遍历其邻边,进行松弛操作,将所有不在队内且进行过松弛操作的边相关的点入队直到队列为空(即不再进行松弛操作)--SPFA!!
单源带负权最短路径 由优化2得到的正是传说中的SPFA,拥有神一般的效率O(KE),K一般取值3-5。算法如其名Shortest Path Fast Algorithm! 瓶颈:如何判负环?思考!!! 当一个点入队次数超过边的次数!需要辅助数组统计各点入队次数,此时的空间与时间效率都极低!!因此此算法在稠密带负环图中的表现不如bellman-ford的YEN氏优化!!请大家慎重选择使用。
多源最短路径 当题目要求大量的查询工作时,需要一种算法能在多项式时间内得到所有顶点间的最短距离并保存结果备查。 Floyed算法应运而生!!
多源最短路径 思想:传递关系闭包 策略:动态规划O (V*V*V) 步骤: 1、对于点k,若存在w(i,k)+w(k,j)<w(i,j),则更新w(i,j)=w(i,k)+w(k,j). 2、迭代k直到结束
多源最短路径 算法分析: 1、不论正负权,大小通吃 2、一次计算,次次查询 3、可扩展性强!(关系传递,最长路径)
图的遍历 对于网络图G,如何不失一般性的搜索各结点—图的遍历. DFS(深度优先) BFS(广度优先) --不再一一评述
图的表示 邻接阵:对于拥有稠密边的图是个很好表示方法 隆重推出~_~邻接表:在图的边数有限,点数过多时是一个很好的表示,主要的效率消耗在于结点的动态生成,然而有一种方法……使得动态的表达静态化…效率神一样的提高……
静态邻接表 ZJFC-1236 环球旅行 题意: ~~中文题自己看!!!
静态邻接表 演示Sample的代码 优点1、使用辅助数组p[],p[i]为p点邻接的边号,若为-1则表示无边相关,否则可据此访问边数组,通过next值遍历该点邻接的所有边 优点2、空间是边相关的O(E),而不是邻接阵的O(V*V),想想吧,V为10W对于邻接阵的恐怖概念… 优点3、插入操作时,执行三步曲,使表结构成链状,p[]数组得到更新,具有很高的技巧性,对于每次操作只需对p数组的初始化,而不需要对边表进行改动,从动态向静态转变!
静态邻接表 邻接表下对Dijkstra的优化 上面讲过其时间效率O(Ve)是基于邻接表,而在邻接阵中是O(V*V*V),使用静态邻接表本身就是场轰轰烈烈的优化! 使用优先队列(二叉堆)!由于使用到贪心策略,我们可以很好想象,优化每次GetMin的操作,即将Dis数组撤消,转换做优先队列,每次取与更新Dis值从原先的O(E+V) 转换为O(logV),算法总效率提高到O(VlogV)
静态邻接表 对于环球旅行题目的求解步骤 1、使用map(红黑树)进行键值关联 2、构造静态邻接表 3、二叉堆优化下的Dijkstra求解
最小生成树 对于连通下的带权网络图G,存在一个经过所有点而不成回路的连通,使其权和为本网络最小,称为最小生成树。 应用:最小代价网络。 方法1:Prim算法 方法2:Kruskal算法
Prim算法 对Dijkstra算法的推广,主算法几乎一模一样。 思想:贪心 步骤1:第一次首先选择最小的边,并标记边的2个端点 步骤2:以后的每次操作都是在被标记点为起点,未被标记点为末点中取最小边,执行连接,并标记末点,直到所有的点被标记
Prim算法 复杂度为O(VE),想想还有什么更好优化? 对!贪心策略的优化一般使用优先队列实现,使GetMin操作为O(logE)! 于是整体复杂度为O(VlogE)效率大大提高
Kruskal算法 一种无视顶点的操作 进行该操作需要边结构与并查集 思想:贪心 优先队列优化! 步骤: 1、得到边序列推入优先队列 2、每次得到一条边,使用并查集判断是否连通,若连通则执行并操作。 3、迭代直到执行|V|-1次操作
Kruskal算法 算法分析 1、点无关的操作,只需要边结构,适合多点图,防止邻接阵暴内存。 2、实现方便,代码清晰。 3、O(VlogE)复杂度,稀疏图的良方!
差分约束初步 什么是差分约束? 对于一组未知解[x1 x2 x3…xn-1 xn]任意两个不同解xi,xj存在xi-xj>=(或<=)C(常数),以此为模型构成的约束系统,称之为差分约束系统。
差分约束初步 首先我们回顾下松弛技术,在Bellman-Ford算法中,有一个十分关键的三角不等式Dis[i]+w(i,j)<Dis[j]使得迭代结束后所有的Dis[j]<= Dis[i]+w(i,j)!! 再结合差分约束系统的概念,任意未知数间存在xi-xj>=(或<=)C,我们取<=情况研究,则要求xi-xj<=C,即xi<=xj+C!! 看出什么了么? 对!这就是以j为始点,i为末点,C为权,构造出带约束边的图,并以此求得最短路径的算法啊!数与图得到了完美的结合!!
最大流问题 在带源点与汇点的带权连通网络G中,求得自源点出发,受各边容量限制最后到达汇点的流量的问题,称之为最大流问题。 最大流网络满足3大性质: 流守恒性 网络内流不增加不减少 容量限制 流量受边负载限制 反对称性 任意结点 流进多少 流出多少 解决方案:FF方法
Ford-Fulkerson方法 这是种方法而非算法,在此种方法指导下的算法最坏上界完全相同,但最好下界却各有千秋。 增广路径:在残留网络中,从源点出发,可以通过该路径使得所经边残余流量减少,而通向汇点的流量增大。 最小割-最大流定理:网络流中,存在的最大流受限制于该网络的最小割。 E-K算法,此算法是最常用的最大流算法,以BFS为基础,每次选择残余网络中的结点数最少的可增广路径进行增广,直到无法进行下去,此算法的最大特点是最后一次保存的路径是该网络流的最小截集。
二分匹配 对于图G,可以将顶点重构为两个集合,每个集合内的点不自交,则该图称之为二部图,对其求解最大匹配的过程称之为二分匹配。在普通二分匹配中,其最小路径覆盖为点数-最大匹配数。 不带权二分匹配(匈牙利算法) 带权二分匹配(KM算法)
匈牙利算法 由匈牙利数学家提出的解决二分图匹配的算法。 本质是找增广路径。这里的增广路径定义为交错轨,即一端为已匹配点,另一端为未匹配点,如果通过任意顶点的遍历,能够找到这样的路径,则对其取反,必会使匹配数+1,如此迭代直到无法找到这样的路径为止。 对最大匹配的题目一般以最小路径覆盖的形式出现。
KM算法 KM算法是通过给每个顶点一个标号(叫做顶标)来把求最大权匹配的问题转化为求完备匹配的问题的。设顶点Xi的顶标为A[i],顶点Yi的顶标为B [i],顶点Xi与Yj之间的边权为w[i,j]。在算法执行过程中的任一时刻,对于任一条边(i,j),A[i]+B[j]>=w[i,j]始终 成立。KM算法的正确性基于以下定理: 若由二分图中所有满足A[i]+B[j]=w[i,j]的边(i,j)构成的子图(称做相等子图)有完备匹配,那么这个完备匹配就是二分图的最大权匹配。 这个定理是显然的。因为对于二分图的任意一个匹配,如果它包含于相等子图,那么它的边权和等于所有顶点的顶标和;如果它有的边不包含于相等子图,那么它的边权和小于所有顶点的顶标和。所以相等子图的完备匹配一定是二分图的最大权匹配。 算法的本质是对匈牙利算法的改进,但实际上却比匈牙利算法早发表几年,其核心仍然是寻找增广路径的过程。
KM算法 步骤 初始时为了使A[i]+B[j]>=w[i,j]恒成立,令A[i]为所有与顶点Xi关联的边的最大权,B[j]=0。如果当前的相等子图没有完备匹配,就按下面的方法修改顶标以使扩大相等子图,直到相等子图具有完备匹配为止。 我们求当前相等子图的完备匹配失败了,是因为对于某个X顶点,我们找不到一条从它出发的交错路。这时我们获得了一棵交错树,它的叶子结点全部是X顶点。现在我们把交错树中X顶点的顶标全都减小某个值d,Y顶点的顶标全都增加同一个值d,那么我们会发现: 两端都在交错树中的边(i,j),A[i]+B[j]的值没有变化。也就是说,它原来属于相等子图,现在仍属于相等子图。 两端都不在交错树中的边(i,j),A[i]和B[j]都没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。 X端不在交错树中,Y端在交错树中的边(i,j),它的A[i]+B[j]的值有所增大。它原来不属于相等子图,现在仍不属于相等子图。 X端在交错树中,Y端不在交错树中的边(i,j),它的A[i]+B[j]的值有所减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大。 现在的问题就是求d值了。为了使A[i]+B[j]>=w[i,j]始终成立,且至少有一条边进入相等子图,d应该等于min{A[i]+B[j]-w[i,j]|Xi在交错树中,Yi不在交错树中}。 理解原理就行,在一般情况下KM算法的代码不会调整,因此只要会使用模版,一切不在话下,当然图论本身就是充满了建模的思想,只有好的模型才能有真正的效率!
KM算法 效率 O(V*V*V) KM算法的本质,对于N*N的矩阵每行每列只能取一个数,使其和为最大!!
强连通分支 什么是强连通分支? 强连通分支就是任意两点均可达的有向子图。 理论:白色路径定理 求解:Tarjan算法
强连通分支 什么是时间戳? DFS进行时,访问到节点所花的时间 算法理论依据:DFS时,如果所达的下一个点i已经被访问,则从该点j出发,寻找父节点到i,其间所经过的所有点必为以i为代表的强连通分支上的点(并查实现) 如何判断是否是该次被访问?时间戳!
强连通分支 步骤 对于每个顶点,设立Num、Low、Used、Alive四个属性,一个Stack保存当前正在被遍历的顶点; 每访问一个顶点,将它的Num和Low设立为当前时间戳,Used、Alive设为True,并将顶点入栈; 对于它的每条边,若边的终点未Used,则访问,而后用终点的Low值更新它的Low值,对于每个Used且Alive的终点,用它的Num值更新当前值; 访问完毕当前顶点后,若Low与Num相等,说明找到了一个强连通分量,它包括栈中所有在当前顶点上方的顶点,将它们出栈并记下Belong值,出栈的顶点的Alive要置为false。 扫描各点Belong值,其代表的点的个数便为强连通分支数!
强连通分支 应用 1、求解单纯问题 HDU 1269 2、缩图技术 (适用于多点多边的关系闭包求解)
待研究的问题 次小生成树 最优比例生成树 最小树型图 弦图 稳定婚姻问题
数论 It’s easy to learn!
数论 什么是数论? 数论是研究整数性质的一门很古老的数学分支, 其初等部分是以整数的整除性为中心的,包括整除性、不定方程、同余式、连分数、素数(即整数)分布 以及数论函数等内容,统称初等数论(elementary number theory)。 初等数论的大部份内容早在古希腊欧几里德的《 几何原本》中就已出现。欧几里得证明了素数有无穷多个,他还给出求两个自然数的最大公约数的方法, 即所谓欧几里得算法。我国古代在数论方面亦有杰出之贡献,现在一般数论书中的「中国剩余定理」正是我国古代《孙子算经》中的下卷第26题,我国称之为 「孙子定理」。 近代初等数论的发展得益于费马、欧拉、拉格朗日、勒让德和高斯等人的工作。1801年,高斯的《算术探究》是数论的划时代杰作。高斯还提出:「数学是科学的皇后,数论是数学中的皇冠」。可见高斯对数论的高度评价。 由于自20世纪以来引进了抽象数学和高等分析的 巧妙工具,数论得到进一步的发展,从而开阔了新的研究领域,出现了代数数论、解析数论、几何数论等 新分支。而且近年来初等数论在计算器科学、组合数学、密码学、代数编码、计算方法等领域内更得到了广泛的应用,无疑同时间促进着数论的发展。
同余定理 对于正整数a,b,c (a%c+b%c)%c=(a+b)%c (a*b)%c=(a%c*b%c)%c 注意对于除法与减法%运算为非封闭运算 需要进行数论变换才能继续进行下去
同余定理 大数求余 设大数R=a1*10^0+a2*10^1+….+an*10^n-1 则R%C=(a1*10^0%C+a2*10^1%C+….+an*10^n-1%C)%C 可以在O(logn)时间内解决大数求余问题 步骤: 以字符读入大数后,设置计数器sum=0 sum=(10*sum+key[i]-’0’)%C; 迭代后令sum=sum%C;
GCD GCD(最大公约数)的一般求解 欧几里德辗转相除法(必须掌握) If(a%b==0) return b; Else return gcd(b,a%b); 本过程具有收敛性质……
扩展欧几里德 在一些具体的题目中,我们还需要的到一组满足ax+by=gcd(a,b)的最小解,用以判断模方程是否有解,此时就要使用扩展欧几里德. 相比一般欧几里德方法,扩展欧几里德有一个对于X,Y求解的递推过程。使用递归实现,递归条件为b==0时,X=1,Y=0;否则t=X; X=Y; Y=t-(a/b)*X;(递推求得)可以证明最后出来的X,Y必然是最小解. 应用:模线性方程的求解
循环群生成元 对于mod n域中的任意数a,若有gcd(m,n)=1,则m为该域的生成元,使得a+km可以为域中任意数. 证明十分简单,若gcd(m,n)=1,则lcm(m,n)=m*n,则对于a的mod n运算,需要n次的计算才能回到a,期间必遍历该域中所有数!
因子分解 对于筛选大量数的因子分解工作,可以与筛选质数同时进行。 令len[i]记录数i的因子个数,则对于每个素数p的倍数及本身,插入因子p,并使len值增长,筛选完所有素数后就完成了因子分解
素数判定 对于一千万内素数的判定: 筛选法最优 对于一千万外素数的判定: 米勒测试
筛选法 给定辅助bool数组prime,首先全置true,使prime[0]=prime[1]=false; 遍历,当遇到prime[i]=true,将之小于n的所有倍数置false; 最后一次遍历,所有值为true的数即为素数! 时空效率 均摊相关伪线性复杂度
米勒测试 理论基础——费马定理 实践工具——二分求幂
米勒测试 费马定理: 若p为素数----a^(p-1)%p==1 注意此定理只为充分 不为必要 米勒测试以概率的形式避免了误判的发生 从严格意义上来说米勒测试的意义并不科学 但是实际证明在可数范围内的伪素数十分之少 而且同时满足a=2,a=3,a=7的底的伪素数更是少得可怜,因此该测试从概率上满足了快速判定素数的需求。
米勒测试 二分快速求幂模原理 对于res=a^b%c的求解 若b%2==0则res=((a^(b/2))%c*(a^(b/2))%c)%c 否则res=(a*((a^(b/2))%c*(a^(b/2))%c))%c
欧拉函数 设E(n)为n以内(不包括n)与n互质数的个数,则E(n)称为关于n的欧拉函数。 欧拉函数的求法,对于n=p1*p2*p3…pn E(n)=n*(1-1/p1)*(1-1/p2)…*(1-1/pn) 可以以容斥原理证明其正确性! 欧拉定理: a^(E(n))%n==1
模线性方程求解 ax≡b(mod n)有解,当且仅当b%gcd(a,n)==0 使用扩展欧几里德求得d=gcd(a,n),则aX+nY=d,x=(X*(b/d))%n Why? b/d=m a(X*m)+nY*m=b=>x=(X*m)%n
中国剩余定理 设 n=n1*n2...nk, 其中因子两两互质.有: a-----(a1,a2,...,ak), 其中ai = a mod ni, 则 a和(a1,a2,...,ak),关系是一一对应的.就是说可以由 a求出(a1,a2,...,ak), 也可以由(a1,a2,...,ak)求出a 求解: 中国古代演算法 模线性方程代入
中国剩余定理 应用 大整数表示 快速运算
连分数逼近 在数学中,连分数或繁分数即如上表达式. 这里的 a0 是某个整数而所有其他的数 an 都是正整数。可依样定义出更长的表达式。如果部分分子(partial numerator)和部分分母(partial denominator)允许假定任意的值,在某些上下文中可以包含函数,则最终的表达式是广义连分数。在需要把上述标准形式与广义连分数相区别的时候,可称它为简单或正规连分数,或称为是规范形式的 一个数的连分数表示是有限的,当且仅当这个数是有理数。 “简单”有理数的连分数表示是简短的。 任何有理数的连分数表示是唯一的,如果它没有尾随的 1。(但是 [a0; a1, ... an, 1] = [a0; a1, ... an + 1]。) 无理数的连分数表示是唯一的。 连分数的项将会重复,当且仅当它是一个二次无理数(即整数系数的二次方程的实数解)的连分数表示。 数 x 的截断连分数表示很早产生 x 的在特定意义上“最佳可能”的有理数逼近。
连分数逼近 意义 1、精度保留 2、避免浮点运算 3、无理数的表示唯一 4、研究连分数的动机源于想要有实数在“数学上纯粹”的表示。 求解 欧几里德变体!!
连分数逼近
数据结构 Soul !!!
数据结构 什么是数据结构? 数据结构是计算机存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率的算法。数据结构往往同高效的检索算法和索引技术有关。 数据结构在计算机科学界至今没有标准的定义。个人根据各自的理解而有不同的表述方法: Sartaj Sahni 在他的《数据结构、算法与应用》一书中称:“数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这些联系可以通过定义相关的函数来给出。”他将数据对象(data object)定义为“一个数据对象是实例或值的集合”。 Clifford A.Shaffer 在《数据结构与算法分析》一书中的定义是:“数据结构是 ADT(抽象数据类型 Abstract Data Type) 的物理实现。” Lobert L.Kruse 在《数据结构与程序设计》一书中,将一个数据结构的设计过程分成抽象层、数据结构层和实现层。其中,抽象层是指抽象数据类型层,它讨论数据的逻辑结构及其运算,数据结构层和实现层讨论一个数据结构的表示和在计算机内的存储细节以及运算的实现。
数据结构 为什么要使用数据结构? 1、便于理清结构,易于表示出一个实体的内部属性与外部联系。 2、更好利用实体特性,从而为算法高效铺路。 3、强有力的数据结构,能够取代算法的优势。
数据结构 数据结构的基本类型: 1、顺序表 2、链表 3、广义表 4、树 5、图 6、串
顺序表 顺序表是在内存上开辟的连续片段,每个元素单元拥有固定大小,以此组织形成的数据结构。 优点: 1、随机存取 2、索引唯一 3、结构简单
顺序表 一般应用: 1、寄存数组(包括栈和队列) 2、哈希数组 3、树状数组
寄存数组 这是我们最常见的顺序表表现方式,一般的大数据量程序,如果不能有边读边处理的方法,就需要首先保留所有的输入,从而能够在而后的过程中进行处理。 主要应用: 1、各类算法的线性预处理 2、保留线性逻辑关系 3、记忆化辅助
寄存数组 优点: 1、静态 2、随机 3、高效 缺点: 1、插入与删除操作效率极度低下 2、内存分配不灵活 技巧: 1、满足递推与DP内存需求 滚动数组 2、满足图的快速判重 化行为数 状态压缩
哈希数组 什么是哈希? 从广义上说哈希是一种键值对应的操作,是从数域到值域的一一映射,也就是我们通常称其为哈希函数的由来。 为什么哈希表可以用数组实现? 数组满足哈希的3个必要条件: 1、键——index 2、值——value 3、键值映射(index->value)O(1)
哈希数组 散列函数的选用 一般情况下我们使用哈希数组,采用的是开放散列策略,也就是说内存与键是一一对应,即hash[key]=value,在对于key值要求不大的情况下是很好的选择。 然而当要求的key很大时该怎么办,此时就应该选用一个好的映射函数使hash[f(key)]=value,且f(key)的值必定均匀分布在映射区间内。当然要找到这样的函数是十分困难的,我们无法了解到数据的具体信息,因此一般的f(key)为key%p,p为一个质数,结合数论知识,我们可以知道当gcd(key,p)=1时,key是一个循环群生成元,使用这个性质,我们可以少了许多因大多数冲突而引发避免策略造成的效率问题。
哈希数组 HDU 2192 题意 : 递增的序列组成一个集合 请问至少有几个? 思路?
哈希数组 出现次数最多的数,决定了能分成的最少群落! 实现方式?? 哈希!!! 散列函数——如何刻画?思考! 根据问题的输入规模,最大10^4个数,因此选取大于10^4的质数作为散列函数的参数,令f(key)=key%p 还有什么问题?思考 出现冲突!!!!
哈希数组 解决方案: 1、线性扫描法 2、二次线性扫描 3、桶链 问题完美解决,注意各个结点在不同解决方案下添加的相关变量。
树状数组 首先我们得知道一个问题,那就是线段树的作用并不只是用来存储线段的,也可以存储点的值等等. 对于静态的线段树,空间上需要的数组有:当前结点的数据值,左儿子编号,右儿子编号.至少这么三个数组. 而在时间上虽然是NlogN的复杂度,但是系数很大. 实现起来的时候编程复杂度大,空间复杂度大,时间效率也不是很理想.
那么是否有更好的解决方法呢? 有! 那就是树状数组!
树状数组 先看一个例题: 数列操作: 给定一个初始值都为0的序列,动态地修改一些位置上的数字,加上一个数,减去一个数,或者乘上一个数,然后动态地提出问题,问题的形式是求出一段数字的和.
树状数组 如果直接做的话,修改的复杂度是O(1),询问的复杂度是O(N),M次询问的复杂度是M*N.M,N的范围可以有100000以上
用线段树可以这样解: 若要维护的序列范围是0..5,先构造下面的一棵线段树:
树状数组 可以看出,这棵树的构造用二分便可以实现.复杂度是2*N. 每个结点用数组a来表示该结点所表示范围内的数据之和. 对于询问的回答,可以直接查找i..j范围内的值,遇到分叉时就兵分两路,最后在合起来.也可以先找出0..i-1的值和0..j的值,两个值减一减就行了.后者的实际操作次数比前者小一些. 这样修改与维护的复杂度是logN.询问的复杂度也是logN,对于M次询问,复杂度是MlogN.
树状数组 然而不难发现,线段树的编程复杂度大,空间复杂度大,时间效率也不高. 怎么办 用树状数组!!!
树状数组 下图中的C数组就是树状数组,a数组是原数组 先自己研究一下这个东西
树状数组 可以发现这些规律 C1=a1 C2=a1+a2 C3=a3 C4=a1+a2+a3+a4 C5=a5 …… C8=a1+a2+a3+a4+a5+a6+a7+a8 C2^n=a1+a2+….+a2^n
树状数组 对于序列a,我们设一个数组C定义C[i] = a[i – 2^k + 1] + … + a[i],k为i在二进制下末尾0的个数。 2^k=x and (x xor (x-1)) 以6为例 (6)10=(0110)2 xor 6-1=(5)10=(0101)2 (0011)2 and (6)10=(0110)2 (0010)2
树状数组 function Lowbit(x: Integer): Integer; begin Lowbit := x and (x xor (x – 1)); end; 若要修改a[i]的值,则C数组的修改 是: Procedure change(k,delta:integer); Begin while k<=n do C[k]:=C[k]+delta; k:=k+lowbit(k); End;
树状数组 若要求I..j的元素和的值,则求出 1..I-1的值和1..j的值. Function getsum(k:integer):integer; Var t:integer; Begin t:=0; while k>0 do begin t:=t+c[k]; k:=k-lowbit(k); end; getsum:=t; End;
树状数组 对于刚才的一题,每次修改与询问都是对C数组做处理.空间复杂度有3N降为N,时间效率也有所提高.编程复杂度更是降了不少. 在很多的情况下,线段树都可以用树状数组实现.凡是能用树状数组的一定能用线段树.当题目不满足减法原则的时候,就只能用线段树,不能用树状数组.例如数列操作如果让我们求出一段数字中最大或者最小的数字,就不能用树状数组了.
链表 链表是内存中不连续块链接而成的数据结构,本着使用多少分配多少的原则,极大地节约和利用了内存,是一种十分基础的数据结构。 优势: 1、动态分配 2、快速删除与插入 3、节省空间
链表 缺点: 1、查找效率低下 2、动态分配结点调用操作系统实现,导致空间分配效率消耗 一种对空间的优化: 内存静态化!! 一般应用 1、桶 2、跳跃表
桶 一般用做哈希的冲突避免策略,使用桶结构存储在同一冲突域下的数据,作为键值的扩展。 基数数排序的辅助结构。
跳跃表 可以看做是链表结构最优秀的应用,使用跳跃表有着令人震撼的效率,对查询、删除和添加的操作均为O(logn)的复杂度。 利用了链表自身的特点,以较小的空间代价提升了自身的性能。使用了概率算法,使效率堪与AVL、RB-Tree等BST相媲美。 相比而言,极低的编程复杂度!!!有什么理由可以不去掌握呢?
跳跃表 跳跃表由多条链构成(S0,S1,S2 ……,Sh),且满足如下三个条件: 每条链必须包含两个特殊元素:+∞ 和 -∞ 每条链中的元素集合必须包含于序数较小的链的元素集合,即: 53 45 37 30 29 15 11 -∞ +∞ S0 S1 S2 S3 跳跃表的实例
跳跃表之查找 查找成功! 目的:在跳跃表中查找一个元素 x 在跳跃表中查找一个元素x,按照如下几个步骤进行: 从最上层的链(Sh)的开头开始 假设当前位置为p,它向右指向的节点为q(p与q不一定相邻),且q的值为y。将y与x作比较 x=y 输出查询成功,输出相关信息 x>y 从p向右移动到q的位置 x<y 从p向下移动一格 如果当前位置在最底层的链中(S0),且还要往下移动的话,则输出查询失败 查找成功! S3 -∞ 11 +∞ S2 -∞ 30 53 +∞ S1 -∞ 45 +∞ S0 -∞ 15 29 37 +∞ 查询元素53的全过程
跳跃表之插入 目的:在跳跃表中插入一个元素 x 插入操作由两部分组成: 查找插入的位置和插入对应元素。 为了确定插入的“列高”,我们引入一个随机决策模块: 产生一个0到1的随机数r r ← random() 如果r小于一个概率因子P,则执行方案A, if r<p then do A 否则,执行方案B else do B 列的初始高度为1。插入元素时,不停地执行随机决策模块。如果要求执行的是A操作,则将列的高度加1,并且继续反复执行随机决策模块。直到第i次,模块要求执行的是B操作,我们结束决策,并向跳跃表中插入一个高度为i的列。
跳跃表之插入 假设我们现在要插入一个元素40到已有的跳跃表中。 随机化模块运行状况: S0 S1 S2 S3 高度+1 高度+1 高度+1 插入的位置 37 30 29 15 -∞ S0 S1 S2 S3 40 53 45 +∞ 53 随机化模块运行状况: 高度+1 高度+1 高度+1 结束 高度=4
跳跃表之删除 目的:从跳跃表中删除一个元素 x 删除操作分为以下三个步骤: 在跳跃表中查找到这个元素的位置,如果未找到,则退出 将该元素所在整列从表中删除 将多余的“空链”删除 -∞ S0 S1 S2 S3 53 45 37 30 29 15 -∞ +∞ S0 S1 S2 11 53 45 37 30 29 15 +∞
跳跃表之概率因子影响 在插入操作中,我们引入了一个概率因子P,它决定了跳跃表的高度,并影响到了整个数据结构的效率。 平均操作时间 平均列高 总结点数 每次查找跳跃次数(平均值) 每次插入跳跃次数(平均值) 每次删除跳跃次数(平均值) 2/3 0.0024690 ms 3.004 91233 39.878 41.604 41.566 1/2 0.0020180 ms 1.995 60683 27.807 29.947 29.072 1/e 0.0019870 ms 1.584 47570 27.332 28.238 28.452 1/4 0.0021720 ms 1.330 40478 28.726 29.472 29.664 1/8 0.0026880 ms 1.144 34420 35.147 35.821 36.007
跳跃表 高效率的相关操作和较低的编程复杂度使得跳跃表在实际应用中的范围十分广泛。尤其在那些编程时间特别紧张的情况下,高性价比的跳跃表很可能会成为你的得力助手。 在实际编程中,使用指针而非构造多级链表以实现跳跃。
广义表 广义表是对表概念的扩充,使链表到树之间的概念缓冲,其所有的表元素并非原子集合,在概念上是对自身的递归定义。 实际应用: Trie
Trie 什么是Trie? Trie是一种十分清晰的广义表结构,它存储字母,并实现了前缀字母路径唯一,因此被称作前缀字典树,或者字母树。 它是如何实现的呢?
Trie 前缀共享 每个结点分叉26条支路实现跳转 以标记变量表示一个单词的结束 其中前缀共享是一个非常重要的性质!!
Trie HDU 1251 题意: 中文题!!自己理解!! 思考?。。。
Trie 根据题意,需要统计单词前缀,此时,首先应将所有字典单词加入Trie。 在加入的过程中,在各结点设置计数器,每个字母走过后,进行计数累加。 得到查询串,在字典树上走完后,返回计数器值。 是不是很简单?
树 树的实质就是对广义表的约束,它只有一个入度为0的结点,作为一切遍历的开始,这也是对非线性数据结构的一种形象表示。 优点: 1、快速查找 2、快速删除与插入 3、内存分配的灵活 总结:可见树是对链表与数组优势的互补,缺憾的折衷,可以说是一种优秀的数据结构。
树 树结构的一些应用: 1、二叉排序树 2、二叉平衡树 3、堆 4、可并堆 5、AC自动机
二叉排序树 所谓二叉排序树是这样的一种结构,对于它的每个结点,左儿子的值小于根的值,右儿子的值大于根值,对其进行中序遍历后,将得到一个递增的数列。 优点: 1、适用于快速查找 2、实现简单
二叉排序树 缺点只有一个: 树的退化(致命的) 怎么样才算是一种退化?思考! 以降序输入数据后,得到的将是一张链表,查找效率退化为O(n)
二叉排序树 如何解决? 引入平衡概念!!BST从此诞生!!
二叉平衡树 什么是二叉平衡树? 任意结点左子树深度与右子树深度之差的绝对值不大于1 如何保持这一性质? AVL:记录深度,违反平衡必引起旋转。 RB-Tree:进行染色,违反染色原则,进行重染,不超过3次的旋转染色。 实际上STL都已经封装了这2个数据结构AVL(BinarySearchTree),RB-Tree(Set or Map)!
堆 以树为概念,以树型数组实现的数据结构 满足对于任意结点,其值必然小于左儿子与右儿子的值 理论证明,从堆中得到最小元素的复杂度是log(n),因此为n个序列进行排序的复杂为nlog(n)。其构造复杂度不超过O(4n),在小数据量下甚至超过了主算法,因此在数据较小情况下不考虑使用堆排序。 STL的封装priority_queue!!
堆 性质 对于堆来说,堆顶永远为堆中最小值。 每次pop,将牺牲O(logn)的时间进行调整 每次push,将牺牲O(logn)的时间进行调整 致命缺陷: 堆的归并!!!思考,为什么? 2个大小为n的堆进行归并,必须逐个弹入,即O(nlogn)的复杂度,不可进行快速归并,否则将影响堆的平衡性! 如何解决?
可并堆 为了解决堆间的合并问题,必须使用一个更好的数据结构适应这种频繁的操作。 目前有2种编程复杂度比较实际的应用 1、左偏树 2、斜堆
左偏树 一个左偏树满足如下性质: 1、左偏树(Leftist Tree)是一种可并堆(Mergeable Heap) ,它除了支持优先队列的三个基本操作(插入,删除,取最小节点),还支持一个很特殊的操作——合并操作 2、左偏树是一棵堆有序(Heap Ordered)二叉树。 3、左偏树满足左偏性质(Leftist Property)。
左偏树的性质 定义一棵左偏树中的外节点(External Node) 为左子树或右子树为空的节点。 定义节点 i 的距离(dist(i)) 为节点 i 到它的后代中,最近的外节点所经过的边数。 任意节点的左子节点的距离不小于右子节点的距离(左偏性质) 左偏树满足下面两条基本性质: [性质1] 节点的键值小于或等于它的左右子节点的键值。 即key(i)≤key(parent(i)) 这条性质又叫堆性质。符合该性质的树是堆有序的(Heap-Ordered)。有了性质1,我们可以知道左偏树的根节点是整棵树的最小节点,于是我们可以在O(1) 的时间内完成取最小节点操作。 [性质2] 节点的左子节点的距离不小于右子节点的距离。 即dist(left(i))≥dist(right(i)) 这条性质称为左偏性质。性质2是为了使我们可以以更小的代价在优先队列的其它两个基本操作(插入节点、删除最小节点)进行后维持堆性质。 。 由左偏性质可知,一个节点的距离等于以该节点为根的子树最右路径的长度。 定理:若一棵左偏树有N个节点,则该左偏树的距离不超过 log(N+1) -1。
左偏树 C ← Merge(A,B) Merge( ) 把A,B两棵左偏树合并,返回一棵新的左偏树C,包含A和B中的所有元素。在本文中,一棵左偏树用它的根节点的指针表示。 在合并操作中,最简单的情况是其中一棵树为空(也就是,该树根节点指针为NULL)。这时我们只须要返回另一棵树。 若A和B都非空,我们假设A的根节点小于等于B的根节点(否则交换A,B),把A的根节点作为新树C的根节点,剩下的事就是合并A的右子树right(A) 和B了。 right(A) ← Merge(right(A), B) 合并了right(A) 和B之后,right(A) 的距离可能会变大,当right(A) 的距离大于left(A) 的距离时,左偏树的性质2会被破坏。在这种情况下,我们只须要交换left(A) 和right(A)。 若dist(left(A)) > dist(right(A)),交换left(A) 和right(A) 最后,由于right(A) 的距离可能发生改变,我们必须更新A的距离: dist(A) ← dist(right(A)) + 1 不难验证,经这样合并后的树C符合性质1和性质2,因此是一棵左偏树。至此左偏树的合并就完成了。
左偏树 合并操作的代码如下: 合并操作都是一直沿着两棵左偏树的最右路径进行的。 一棵N个节点的左偏树,最右路径上最多有 log(N+1) 个节点。 因此,合并操作的时间复杂度为: O(log N1 + log N2) = O(log N) 合并操作的代码如下: Function Merge(A, B) If A = NULL Then return B If B = NULL Then return A If key(B) < key(A) Then swap(A, B) right(A) ← Merge(right(A), B) If dist(right(A)) > dist(left(A)) Then swap(left(A), right(A)) If right(A) = NULL Then dist(A) ← 0 Else dist(A) ← dist(right(A)) + 1 return A End Function
左偏树 左偏树的特点: 时空效率高 编程复杂度低 左偏树的应用: 可并堆 优先队列
左偏树 HDU 1512 题意: 刚开始所有的猴子自成一群且互相都不认识,他们互相认识当且仅当他们各自群中最强的猴子互相打了一场,体力各减为一半。问最后这群猴子中体力最强猴子的体力是多少 思路如何?
左偏树 并查? 可行,但只能作为辅助手段。 优先队列+并查? O(nlogn)的合并效率实在不敢恭维 很好。。。左偏树开始发挥了效力了!
左偏树 步骤: 使用并查集(这里其实准确来说应该是一种记录父结点编号的数组)记录各结点在各自堆中的父结点 查询两只猴子所在堆的根结点,如果两只猴子根结点相同,则不必打架,否则,取各自堆中最大元素(也就是根结点)打一场打架,此时先让各自堆中根的左右儿子指向本身,再对左右儿子进行合并生成新树(即pop操作)并适当修改父结点数组,令根结点值减半后与新树再次合并,并适时修改父结点数组(即push操作),再将两个堆中的根进行合并(即unition操作)。 是不是很简单的实现?
斜堆 如何对左偏树进行优化? 思考!!! 我们注意到在左偏树中必须要有距离的概念,但我们可以发现一个性质,如果每次合并都向右进行,每次完成后将右边的结点甩到左边,不正好符合了左偏的性质么? 很好去掉距离,提高效率,优化的王道! 不足(单次合并可能退化到O(n),但实际效果上,两者都差不多)
AC自动机 AC自动机是由Aho和Corasick提出的数据结构,是对Trie树的一种改造,是对find操作的修正,也是多模式串匹配的基本实现! HDU 2222 题意 给你N个关键字,以及一句描述,问有多少个关键字在这句描述中出现? 思路???
AC自动机 采用普通方法? O(Nnm)… m=50,n=1000000,N=10000 黄花菜都凉了…… KMP? O(N(m+n))? 就这点优化?塞牙缝都不够 那怎么办!!! AC自动机!! O(Nm+n)…强大!!!
AC自动机 步骤 将所有关键字塞入Trie 好了,现在我们已经有一棵Trie了,但这还不够,我们还要在Trie上引入一个很强大的东西:失败指针或者说shift数组或者说Next函数 …..你爱怎么叫怎么叫吧,反正就是KMP的精华所在,这也是我为什么叫你看KMP的原因。 KMP中我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符,当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。 Trie树上的失败指针与此类似。
AC自动机 假设有一个节点k,他的失败指针指向j。那么k,j满足这个性质:设root到j的距离为n,则从k之上的第n个节点到k所组成的长度为n的单词,与从root到j所组成的单词相同。 那么我们要怎样构建这个东西呢?其实我们可以用一个简单的BFS搞定这一切。 对于每个节点,我们可以这样处理:设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个节点,他的儿子中也有字母为C的节点。然后把当前节点的失败指针指向那个字目也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root 最开始,我们把root加入队列(root的失败指针显然指向自己),这以后我们每处理一个点,就把它的所有儿子加入队列,直到搞完。 好了,现在我们有了一棵带失败指针的Trie了 !!!可以开始进行AC自动机了!
AC自动机 一开始,Trie中有一个指针t1指向root,待匹配串(也就是“文章”)中有一个指针t2指向串头。 接下来的操作和KMP很相似:如果t2指向的字母,是Trie树中,t1指向的节点的儿子,那么t2+1,t1改为那个儿子的编号,否则t1顺这当前节点的失败指针向上找,直到t2是t1的一个儿子,或者t1指向根。如果t1路过了一个绿色的点,那么以这个点结尾的单词就算出现过了。或者如果t1所在的点可以顺着失败指针走到一个绿色点,那么以那个绿点结尾的单词就算出现过了。 本题要注意的是必须在描述串后面添加特殊符号,否则将忽略对完全匹配关键字的统计。 注意:BFS寻找失败指针的过程,是一个自我匹配的过程。
AC自动机 特点: 多模式匹配的良方,一次自我适配后,对任意文章的匹配均是线性复杂度。 编程复杂度较低。 使用方便,居家旅行之必备!
图 图的结构是树结构的拓展,但在实现基本以数组的形式进行,在本质上来说所有的数据结构都是互相渗透交融的,所谓大道行简,只有掌握了本质,才能以不变应万变!!! 基本表示: 邻接阵 邻接表 由于上部分内容已经有所涉猎,这里就不再提过了
串 什么是串? 串是由字符所组成的有限序列,我们只关心串的字符属性,而不关心其数值属性。 串有着十分明显的逻辑结构,前驱后继都是固定的,不得随意更改! 关于字符串的处理一般而言都是ICPC的中档题目,如果使用模拟或者暴力方法,一般来说都是无效的。 应用结构: 后缀数组
后缀数组 什么是后缀数组? 对于长度为len的字符串str,从位置i开始至len的所有字符序列称为i的后缀 后缀数组(SA)保存的就是这些后缀序列的排序后的开头位置. Rank数组是SA的反函数,也就是说Rank[i]保存的是从位置i开始的后缀在所有后缀中的名次.
后缀数组 如何利用这种结构性质? 我们还需要计算一个辅助的工具——最长公共前缀(Longest Common Prefix)!! 对两个字符串u,v 定义函数lcp(u,v)=max{i|u=iv},也就是从头开始顺次比较u 和v 的对应字符,对应字符持续相等的最大位置,称为这两个字符串的最长公共前缀。 对正整数i,j 定义LCP(i,j)=lcp(Suffix(SA[i]),Suffix(SA[j]),其中i,j 均为1 至n 的整数。LCP(i,j)也就是后缀数组中第i 个和第j 个后缀的最长公共前缀的长度。 关于LCP 有两个显而易见的性质: 性质2.1 LCP(i,j)=LCP(j,i) 性质2.2 LCP(i,i)=len(Suffix(SA[i]))=n-SA[i]+1 这两个性质的用处在于,我们计算LCP(i,j)时只需要考虑i<j 的情况,因为 i>j时可交换i,j,i=j时可以直接输出结果n-SA[i]+1。
后缀数组 直接根据定义,用顺次比较对应字符的方法来计算LCP(i,j)显然是很低效的,时间复杂度为O(n),所以我们必须进行适当的预处理以降低每次计算LCP的复杂度。 经过仔细分析,我们发现LCP 函数有一个非常好的性质: 设i<j,则LCP(i,j)=min{LCP(k-1,k)|i+1≤k≤j} (LCP Theorem) 根据LCP Theorem得出必然的一个推论: LCP Corollary 对i≤j<k,LCP(j,k)≥LCP(i,k)。
后缀数组 定义一维数组height,令height[i]=LCP(i-1,i),1<i≤n,并设height[1]=0。 由LCP Theorem,LCP(i,j)=min{height[k]|i+1≤k≤j},也就是说,计算LCP(i,j)等同于询问一维数组height 中下标在i+1 到j 范围内的所有元素的最小值。如果height 数组是固定的,这就是非常经典的RMQ(Range Minimum Query)问题。 RMQ 问题可以用线段树或静态排序树在O(nlogn)时间内进行预处理,之后每次询问花费时间O(logn),更好的方法是RMQ 标准算法,可以在O(n)时间内进行预处理,每次询问可以在常数时间内完成。 对于一个固定的字符串S,其height 数组显然是固定的,只要我们能高效地求出height 数组,那么运用RMQ 方法进行预处理之后,每次计算LCP(i,j)的时间复杂度就是常数级了。于是只有一个问题——如何尽量高效地算出height 数组。
后缀数组 性质3 对于i>1 且Rank[i]>1,一定有h[i]≥h[i-1]-1。 根据性质3,可以令i从1 循环到n按照如下方法依次算出h[i]: 若Rank[i]=1,则h[i]=0。字符比较次数为0。 若i=1 或者h[i-1]≤1,则直接将Suffix(i)和Suffix(Rank[i]-1)从第一个字符开始依次比较直到有字符不相同,由此计算出h[i]。字符比较次数为h[i]+1,不超过h[i]-h[i-1]+2。 否则,说明i>1,Rank[i]>1,h[i-1]>1,根据性质3,Suffix(i)和Suffix(Rank[i]-1)至少有前h[i-1]-1 个字符是相同的,于是字符比较可以从h[i-1]开始,直到某个字符不相同,由此计算出h[i]。字符比较次数为h[i]-h[i-1]+2。 设SA[1]=p,那么不难看出总的字符比较次数不超过4N.也就是说,整个算法的复杂度为O(n)。 求出了h 数组,根据关系式height[i]=h[SA[i]]可以在O(n)时间内求出height数组,于是可以在O(n)时间内求出height 数组。
后缀数组 结合RMQ 方法,在O(n)时间和空间进行预处理之后就能做到在常数时间内计算出对任意(i,j)计算出LCP(i,j)。 因为lcp(Suffix(i),Suffix(j))=LCP(Rank[i],Rank[j]),所以我们也就可以在常数时间内求出S 的任何两个后缀之间的最长公共前缀。这正是后缀数组能强有力地处理很多字符串问题的重要原因之一。
后缀数组 如何构造后缀数组? 普通方法?将所有后缀提炼出来后进行快排! O(n*n)太慢了,根本没有考虑字符串间的联系!!! 使用倍增法!!
后缀数组 步骤: 首先对字符串所有字母进行排序,并记录入Rank与SA数组中(意义同前)。 倍增比较字符的长度,使用求出的Rank迅速求得新的SA,再重新求得新的Rank 以此直到倍增长度大于等于总长度 复杂度O(nlogn)
后缀数组构造示意 设u=Suffix(i),v=Suffix(j) 对u、v在2k-前缀意义下进行比较 i+k j+k 设u=Suffix(i),v=Suffix(j) 后缀u,以i开头 后缀v,以j开头` 比较红色字符相当于在k-前缀意义下比较Suffix(i) 和 Suffix(j) 对u、v在2k-前缀意义下进行比较 比较绿色字符相当于在k-前缀意义下比较Suffix(i+k) 和 Suffix(j+k)
后缀数组构造示意 SAm Rankm …… O(nlogn) 总复杂度为 直接根据首字符排序 SA1 Rank1 O(nlogn) SA2 m=2t且m≥n …… O(n) 总复杂度为 O(nlogn) O(nlogn)的操作一次 O(n)的操作[logn]次
后缀数组的应用 O(nlogn)求解最长回文串问题 O(nlogn)求解最长公共子串问题 HDU 1403 题意: 给予2个字符串,求它们之间的最长公共子串长度! 思考!!
后缀数组的应用 普通方法行不行? 求解所有子串O(n*n) 进行逐个比较O(n*n*n*n)…… n=100000 ……要死人的大哥!! 使用后缀数组!!!O(nlogn) 还有更好的构造方法DC-3线性三分构造法 O(n)!!!
后缀数组的应用 步骤 连接2个字符串,中间部分以字典序最小的符号前一位中断(使之能在SA中总在最前,不进行比较,并使lcp得到修正,避免串的跨越) 构造后缀数组,求得lcp数组 设len为第一个字符串的长度,遍历当(SA[i-1]<len&&SA[i]>len)||( SA[i-1]>len&&SA[i]<len)时的lcp[i-1],并更新最大值max 最后打印出最大值
数据结构 程序=数据结构+算法 掌握常用的数据结构非常重要 希望大家能够在课后认真总结与复习,这里大部分的模版,本人已经自己写过或者借鉴过别人的东西,但仍然希望大家以了解构造原理为主,毕竟这样想问题的思想是我们的最终目的,参加ICPC的主要动机也是为了训练自己的思维。
感谢各位集训队队员的旁听!! Zhuangli (zjfc3) Start : 2008.8.25 Finish : 2008.8.29