• 常见算法笔记


    算法

    刷题过程中学了很多算法,但是都没有做个笔记==,写一下稍微留个印象~

    1.曼彻斯特算法

    首先用一个非常巧妙的方式,将所有可能的奇数/偶数长度的回文子串都转换成了奇数长度:在每个字符的两边都插入一个特殊的符号。比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。 为了进一步减少编码的复杂度,可以在字符串的开始加入另一个特殊字符,这样就不用特殊处理越界问题,比如$#a#b#a#(注意,下面的代码是用C语言写 就,由于C语言规范还要求字符串末尾有一个''所以正好OK,但其他语言可能会导致越界)。

    下面以字符串12212321为例,经过上一步,变成了 S[] = "$#1#2#2#1#2#3#2#1#";

    然后用一个数组 P[i] 来记录以字符S[i]为中心的最长回文子串向左/右扩张的长度(包括S[i],也就是把该回文串“对折”以后的长度),比如S和P的对应关系:

    S  #  1  #  2  #  2  #  1  #  2  #  3  #  2  #  1  #
    P  1  2  1  2  5  2  1  4  1  2  1  6  1  2  1  2  1
    (p.s. 可以看出,P[i]-1正好是原字符串中回文串的总长度)

    那么怎么计算P[i]呢?该算法增加两个辅助变量(其实一个就够了,两个更清晰)id和mx,其中id表示最大回文子串中心的位置,mx则为id+P[id],也就是最大回文子串的边界。

    然后可以得到一个非常神奇的结论,这个算法的关键点就在这里了:如果mx > i,那么P[i] >= MIN(P[2 * id - i], mx - i)。就是这个串卡了我非常久。实际上如果把它写得复杂一点,理解起来会简单很多:

    当 P[j] >= mx - i 的时候,以S[j]为中心的回文子串不一定完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是 说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] >= mx - i。至于mx之后的部分是否对称,就只能老老实实去匹配了。

    对于 mx <= i 的情况,无法对 P[i]做更多的假设,只能P[i] = 1,然后再去匹配了。

    实现代码:

     1 char ch1[1000010];
     2 char ch2[2000010];
     3 int p[2000010];
     4 
     5 void build()
     6 {
     7     ch2[0] = '@';
     8     ch2[1] = '#';
     9     int i, len = strlen(ch1);
    10     for (i = 0; i < len; ++i)
    11     {
    12         ch2[2*i+2] = ch1[i];
    13         ch2[2*i+3] = '#';
    14     }
    15     ch2[2*i+2] = '';
    16 }
    17 
    18 int solve()
    19 {
    20     p[0] = p[1] = 1;
    21     int id = 1, mx = 2, maxlen = 0;
    22     for (int i = 2; ch2[i]; ++i)
    23     {
    24         p[i] = mx > i ? min(p[2*id-i], mx-i) : 1;
    25         while (ch2[i+p[i]] == ch2[i-p[i]]) p[i]++;
    26         if (i+p[i] > mx)
    27         {
    28             mx = i+p[i];
    29             id = i;
    30         }
    31         maxlen = max(maxlen, p[i]-1);
    32     }
    33     return maxlen;
    34 }
    View Code

     2.KMP

    转自matrix67

       解决这类问题,通常我们的方法是枚举从A串的什么位置起开始与B匹配,然后验证是否匹配。假如A串长度为n,B串长度为m,那么这种方法的复杂度是 O (mn)的。虽然很多时候复杂度达不到mn(验证时只看头一两个字母就发现不匹配了),但我们有许多“最坏情况”,比如,A= "aaaaaaaaaaaaaaaaaaaaaaaaaab",B="aaaaaaaab"。我们将介绍的是一种最坏情况下O(n)的算法(这里假设 m<=n),即传说中的KMP算法。
        之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取 了这三个人的名字的头一个字母。这时,或许你突然明白了AVL 树为什么叫AVL,或者Bellman-Ford为什么中间是一杠不是一个点。有时一个东西有七八个人研究过,那怎么命名呢?通常这个东西干脆就不用人名 字命名了,免得发生争议,比如“3x+1问题”。扯远了。
        个人认为KMP是最没有必要讲的东西,因为这个东西网上能找到很多资料。但网上 的讲法基本上都涉及到“移动(shift)”、“Next函数”等概念,这非常容易产生误解(至少一年半前我看这些资料学习KMP时就没搞清楚)。在这 里,我换一种方法来解释KMP算法。

        假如,A="abababaababacb",B="ababacb",我们来看看KMP 是怎么工作的。我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符(j当然越大越好),现在需要检验A[i+1]和B[j+1]的关系。当A[i+1]=B[j+1]时,i和j各加一;什么时候j=m了,我们就 说B是A的子串(B串已经整完了),并且可以根据这时的i值算出匹配的位置。当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能继续增加)。我们看一看当 i=j=5时的情况。

        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b …
        B = a b a b a c b
        j = 1 2 3 4 5 6 7

        此 时,A[6]<>B[6]。这表明,此时j不能等于5了,我们要把j改成比它小的值j'。j'可能是多少呢?仔细想一下,我们发现,j'必须 要使得B[1..j]中的头j'个字母和末j'个字母完全相等(这样j变成了j'后才能继续保持i和j的性质)。这个j'当然要越大越好。在这里,B [1..5]="ababa",头3个字母和末3个字母都是"aba"。而当新的j为3时,A[6]恰好和B[4]相等。于是,i变成了6,而j则变成了 4:

        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b …
        B =     a b a b a c b
        j =     1 2 3 4 5 6 7

        从 上面的这个例子,我们可以看到,新的j可以取多少与i无关,只与B串有关。我们完全可以预处理出这样一个数组P[j],表示当匹配到B数组的第j个字母而 第j+1个字母不能匹配了时,新的j最大是多少。P[j]应该是所有满足B[1..P[j]]=B[j-P[j]+1..j]的最大值。
        再后来,A[7]=B[5],i和j又各增加1。这时,又出现了A[i+1]<>B[j+1]的情况:

        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b …
        B =     a b a b a c b
        j =     1 2 3 4 5 6 7

        由于P[5]=3,因此新的j=3:

        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b …
        B =         a b a b a c b
        j =         1 2 3 4 5 6 7

        这时,新的j=3仍然不能满足A[i+1]=B[j+1],此时我们再次减小j值,将j再次更新为P[3]:

        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b …
        B =             a b a b a c b
        j =             1 2 3 4 5 6 7

        现在,i还是7,j已经变成1了。而此时A[8]居然仍然不等于B[j+1]。这样,j必须减小到P[1],即0:

        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b …
        B =               a b a b a c b
        j =             0 1 2 3 4 5 6 7

        终于,A[8]=B[1],i变为8,j为1。事实上,有可能j到了0仍然不能满足A[i+1]=B[j+1](比如A[8]="d"时)。因此,准确的说法是,当j=0了时,我们增加i值但忽略j直到出现A[i]=B[1]为止。
        这个过程的代码很短(真的很短),我们在这里给出:

    bool judge()
    {
        int j = -1, len1 = strlen(ch1), len2 = strlen(ch2);
        for (int i = 0; i < len1; ++i)
        {
            while (j >= 0 && ch2[j+1] != ch1[i]) j = nxt[j];
            if (ch2[j+1] == ch1[i]) j++;
            if (j+1 == len2) return true;
        }
        return false;
    }
    judge
     1 j:=0;
     2 for i:=1 to n do
     3 begin
     4    while (j>0) and (B[j+1]<>A[i]) do j:=P[j];
     5    if B[j+1]=A[i] then j:=j+1;
     6    if j=m then
     7    begin
     8       writeln('Pattern occurs with shift ',i-m);
     9       j:=P[j];
    10    end;
    11 end;
    View Code

        最后的j:=P[j]是为了让程序继续做下去,因为我们有可能找到多处匹配。
        这个程序或许比想像中的要简单,因为对于i值的不断增加,代码用的是for循环
    。因此,这个代码可以这样形象地理解:扫描字符串A,并更新可以匹配到B的什么位置。
        现在,我们还遗留了两个重要的问题:一,为什么这个程序是线性的;二,如何快速预处理P数组。
        为 什么这个程序是O(n)的?其实,主要的争议在于,while循环使得执行次数出现了不确定因素。我们将用到时间复杂度的摊还分析中的主要策略,简单地说 就是通过观察某一个变量或函数值的变化来对零散的、杂乱的、不规则的执行次数进行累计。KMP的时间复杂度分析可谓摊还分析的典型。我们从上述程序的j 值入手。每一次执行while循环都会使j减小(但不能减成负的),而另外的改变j值的地方只有第五行。每次执行了这一行,j都只能加1;因此,整个过程 中j最多加了n个1。于是,j最多只有n次减小的机会(j值减小的次数当然不能超过n,因为j永远是非负整数)。这告诉我们,while循环总共最多执行 了n次。按照摊还分析的说法,平摊到每次for循环中后,一次for循环的复杂度为O(1)。整个过程显然是O(n)的。这样的分析对于后面P数组预处理 的过程同样有效,同样可以得到预处理过程的复杂度为O(m)。
        预处理不需要按照P的定义写成O(m^2)甚至O(m^3)的。我们可以通 过P[1],P[2],…,P[j-1]的值来获得P[j]的值。对于刚才的B="ababacb",假如我们已经求出了P[1],P[2],P[3]和 P[4],看看我们应该怎么求出P[5]和P[6]。P[4]=2,那么P [5]显然等于P[4]+1,因为由P[4]可以知道,B[1,2]已经和B[3,4]相等了,现在又有B[3]=B[5],所以P[5]可以由P[4] 后面加一个字符得到。P[6]也等于P[5]+1吗?显然不是,因为B[ P[5]+1 ]<>B[6]。那么,我们要考虑“退一步”了。我们考虑P[6]是否有可能由P[5]的情况所包含的子串得到,即是否P[6]=P[ P[5] ]+1。这里想不通的话可以仔细看一下:

            1 2 3 4 5 6 7
        B = a b a b a c b
        P = 0 0 1 2 3 ?

        P[5]=3 是因为B[1..3]和B[3..5]都是"aba";而P[3]=1则告诉我们,B[1]、B[3]和B[5]都是"a"。既然P[6]不能由P[5] 得到,或许可以由P[3]得到(如果B[2]恰好和B[6]相等的话,P[6]就等于P[3]+1了)。显然,P[6]也不能通过P[3]得到,因为 B[2]<>B[6]。事实上,这样一直推到P[1]也不行,最后,我们得到,P[6]=0。
        怎么这个预处理过程跟前面的KMP主程序这么像呢?其实,KMP的预处理本身就是一个B串“自我匹配”的过程。它的代码和上面的代码神似:

     1 void getNext()
     2 {
     3     nxt[0] = -1;
     4     int j = -1, len = strlen(ch2);
     5     for (int i = 1; i < len; ++i)
     6     {
     7         while (j >= 0 && ch2[i] != ch2[j+1]) j = nxt[j];
     8         if (ch2[j+1] == ch2[i]) j++;
     9         nxt[i] = j;
    10     }
    11 }
    getNext
    1 P[1]:=0;
    2 j:=0;
    3 for i:=2 to m do
    4 begin
    5    while (j>0) and (B[j+1]<>B[i]) do j:=P[j];
    6    if B[j+1]=B[i] then j:=j+1;
    7    P[i]:=j;
    8 end;
    View Code


        最后补充一点:由于KMP算法只预处理B串,因此这种算法很适合这样的问题:给定一个B串和一群不同的A串,问B是哪些A串的子串。

    3.后缀数组

     1 int wa[maxn],wb[maxn],wv[maxn],ws[maxn];
     2 int cmp(int *r,int a,int b,int l)
     3 {return r[a]==r[b]&&r[a+l]==r[b+l];}  //就像论文所说,由于末尾填了0,所以如果r[a]==r[b](实际是y[a]==y[b]),说明待合并的两个长为j的字符串,前面那个一定不包含末尾0,因而后面这个的起始位置至多在0的位置,不会再靠后了,因而不会产生数组越界。
     4 //da函数的参数n代表字符串中字符的个数,这里的n里面是包括人为在字符串末尾添加的那个0的,但论文的图示上并没有画出字符串末尾的0。
     5 //da函数的参数m代表字符串中字符的取值范围,是基数排序的一个参数,如果原序列都是字母可以直接取128,如果原序列本身都是整数的话,则m可以取比最大的整数大1的值。
     6 void da(int *r,int *sa,int n,int m)
     7 {
     8     int i,j,p,*x=wa,*y=wb,*t;
     9     //以下四行代码是把各个字符(也即长度为1的字符串)进行基数排序,如果不理解为什么这样可以达到基数排序的效果,不妨自己实际用纸笔模拟一下,我最初也是这样才理解的。
    10     for(i=0;i<m;i++) ws[i]=0;
    11     for(i=0;i<n;i++) ws[x[i]=r[i]]++;  //x[]里面本意是保存各个后缀的rank值的,但是这里并没有去存储rank值,因为后续只是涉及x[]的比较工作,因而这一步可以不用存储真实的rank值,能够反映相对的大小即可。
    12     for(i=1;i<m;i++) ws[i]+=ws[i-1];
    13     for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i;  //i之所以从n-1开始循环,是为了保证在当字符串中有相等的字符串时,默认靠前的字符串更小一些。
    14     //下面这层循环中p代表rank值不用的字符串的数量,如果p达到n,那么各个字符串的大小关系就已经明了了。
    15     //j代表当前待合并的字符串的长度,每次将两个长度为j的字符串合并成一个长度为2*j的字符串,当然如果包含字符串末尾具体则数值应另当别论,但思想是一样的。
    16     //m同样代表基数排序的元素的取值范围
    17     for(j=1,p=1;p<n;j*=2,m=p)
    18     {
    19         //以下两行代码实现了对第二关键字的排序
    20         for(p=0,i=n-j;i<n;i++) y[p++]=i;  //结合论文的插图,我们可以看到位置在第n-j至n的元素的第二关键字都为0,因此如果按第二关键字排序,必然这些元素都是排在前面的。
    21         for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;  //结合论文的插图,我们可以看到,下面一行的第二关键字不为0的部分都是根据上面一行的排序结果得到的,且上一行中只有sa[i]>=j的第sa[i]个字符串(这里以及后面指的“第?个字符串”不是按字典序排名来的,是按照首字符在字符串中的位置来的)的rank才会作为下一行的第sa[i]-j个字符串的第二关键字,而且显然按sa[i]的顺序rank[sa[i]]是递增的,因此完成了对剩余的元素的第二关键字的排序。
    22         //第二关键字基数排序完成后,y[]里存放的是按第二关键字排序的字符串下标
    23         for(i=0;i<n;i++) wv[i]=x[y[i]];  //这里相当于提取出每个字符串的第一关键字(前面说过了x[]是保存rank值的,也就是字符串的第一关键字),放到wv[]里面是方便后面的使用
    24         //以下四行代码是按第一关键字进行的基数排序
    25         for(i=0;i<m;i++) ws[i]=0;
    26         for(i=0;i<n;i++) ws[wv[i]]++;
    27         for(i=1;i<m;i++) ws[i]+=ws[i-1];
    28         for(i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i];  //i之所以从n-1开始循环,含义同上,同时注意这里是y[i],因为y[i]里面才存着字符串的下标
    29         //下面两行就是计算合并之后的rank值了,而合并之后的rank值应该存在x[]里面,但我们计算的时候又必须用到上一层的rank值,也就是现在x[]里面放的东西,如果我既要从x[]里面拿,又要向x[]里面放,怎么办?当然是先把x[]的东西放到另外一个数组里面,省得乱了。这里就是用交换指针的方式,高效实现了将x[]的东西“复制”到了y[]中。
    30         for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
    31         x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; //这里就是用x[]存储计算出的各字符串rank的值了,记得我们前面说过,计算sa[]值的时候如果字符串相同是默认前面的更小的,但这里计算rank的时候必须将相同的字符串看作有相同的rank,要不然p==n之后就不会再循环啦。
    32     }
    33     return;
    34 }
    35 
    36 //能够线性计算height[]的值的关键在于h[](height[rank[]])的性质,即h[i]>=h[i-1]-1,下面具体分析一下这个不等式的由来。
    37 //论文里面证明的部分一开始看得我云里雾里,后来画了一下终于搞明白了,我们先把要证什么放在这:对于第i个后缀,设j=sa[rank[i] - 1],也就是说j是i的按排名来的上一个字符串,按定义来i和j的最长公共前缀就是height[rank[i]],我们现在就是想知道height[rank[i]]至少是多少,而我们要证明的就是至少是height[rank[i-1]]-1。
    38 //好啦,现在开始证吧。
    39 //首先我们不妨设第i-1个字符串(这里以及后面指的“第?个字符串”不是按字典序排名来的,是按照首字符在字符串中的位置来的)按字典序排名来的前面的那个字符串是第k个字符串,注意k不一定是i-2,因为第k个字符串是按字典序排名来的i-1前面那个,并不是指在原字符串中位置在i-1前面的那个第i-2个字符串。
    40 //这时,依据height[]的定义,第k个字符串和第i-1个字符串的公共前缀自然是height[rank[i-1]],现在先讨论一下第k+1个字符串和第i个字符串的关系。
    41 //第一种情况,第k个字符串和第i-1个字符串的首字符不同,那么第k+1个字符串的排名既可能在i的前面,也可能在i的后面,但没有关系,因为height[rank[i-1]]就是0了呀,那么无论height[rank[i]]是多少都会有height[rank[i]]>=height[rank[i-1]]-1,也就是h[i]>=h[i-1]-1。
    42 //第二种情况,第k个字符串和第i-1个字符串的首字符相同,那么由于第k+1个字符串就是第k个字符串去掉首字符得到的,第i个字符串也是第i-1个字符串去掉首字符得到的,那么显然第k+1个字符串要排在第i个字符串前面,要么就产生矛盾了。同时,第k个字符串和第i-1个字符串的最长公共前缀是height[rank[i-1]],那么自然第k+1个字符串和第i个字符串的最长公共前缀就是height[rank[i-1]]-1。
    43 //到此为止,第二种情况的证明还没有完,我们可以试想一下,对于比第i个字符串的字典序排名更靠前的那些字符串,谁和第i个字符串的相似度最高(这里说的相似度是指最长公共前缀的长度)?显然是排名紧邻第i个字符串的那个字符串了呀,即sa[rank[i]-1]。也就是说sa[rank[i]]和sa[rank[i]-1]的最长公共前缀至少是height[rank[i-1]]-1,那么就有height[rank[i]]>=height[rank[i-1]]-1,也即h[i]>=h[i-1]-1。
    44 //证明完这些之后,下面的代码也就比较容易看懂了。
    45 int rank[maxn],height[maxn];
    46 void calheight(int *r,int *sa,int n)
    47 {
    48     int i,j,k=0;
    49     for(i=1;i<=n;i++) rank[sa[i]]=i;  //计算每个字符串的字典序排名
    50     for(i=0;i<n;height[rank[i++]]=k)  //将计算出来的height[rank[i]]的值,也就是k,赋给height[rank[i]]。i是由0循环到n-1,但实际上height[]计算的顺序是由height[rank[0]]计算到height[rank[n-1]]。
    51     for(k?k--:0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++);  //上一次的计算结果是k,首先判断一下如果k是0的话,那么k就不用动了,从首字符开始看第i个字符串和第j个字符串前面有多少是相同的,如果k不为0,按我们前面证明的,最长公共前缀的长度至少是k-1,于是从首字符后面k-1个字符开始检查起即可。
    52     return;
    53 }
    54 
    55 //最后再说明一点,就是关于da和calheight的调用问题,实际上在“小罗”写的源程序里面是如下调用的,这样我们也能清晰的看到da和calheight中的int n不是一个概念,同时height数组的值的有效范围是height[1]~height[n]其中height[1]=0,原因就是sa[0]实际上就是我们补的那个0,所以sa[1]和sa[0]的最长公共前缀自然是0。
    56 da(r,sa,n+1,128);
    57 calheight(r,sa,n);
    View Code
  • 相关阅读:
    struts2基础
    javaEE环境搭建-eclipse
    geth
    redis常用命令
    angular-ui-select 下拉框支持过滤单选多选解决方案(系列一)
    angularjs中向html页面添加内容节点元素代码段的两种方法
    modal
    弹性布局
    自定义鼠标样式
    angularjs指令弹框点击空白处隐藏及常规方法
  • 原文地址:https://www.cnblogs.com/JustForCS/p/4903105.html
Copyright © 2020-2023  润新知