字符串的最小表示法

“最小表示法”思想的提出

首先来看一个引例:

[引例]有两列数,a1,a2,a3 .....an 和b1,b2,b3..... bn ,不记顺序,判断它们是否相同。

[分析]由于题目要求“不记顺序”,因此每一列数的不同形式高达n!种之多!如果要一一枚举,显然是不科学的。

于是一种新的思想提出了:如果两列数是相同的,那么将它们排序之后得到的数列一定也是相同的。算法复杂度迅速降低为O(Nlog2N)。

这道题虽然简单,却给了我们一个重要的启示:

当某两个对象有多种表达形式,且需要判断它们在某种变化规则下是否能够达到一个相同的形式时,可以将它们都按一定规则变化成其所有表达形式中的最小者,然后只需要比较两个“最小者”是否相等即可!

 

最小表示法是求与某个字符串循环同构的所有字符串中,字典序最小的串是哪个

 

循环同构
字符串S = “bacda”,它的循环同构"acdab",“cdaba”,“dabac”,“abacd”.

最小表示
字符串和它的所有循环同构中字典序最小的字符串。S = “bacda"的最小表示"abacd”

这几个串在原串上的开始位置分别是0,1,2,3,4。

默认从0开始比较方便,这一点之后也会再提到。

暴力方法很简单,把所有的都列出来再排个序就行了。

暴力的时间复杂度是很高的,然而我们可以做到O(n)求出字典序最小的串的开始位置。

 

大致思路如下:

设i、j是两个“怀疑是最小的位置”,比如说如果你比较到了bacda的两个a,你目前还不知道从哪个i开始的字符串是最小的。

设k表示,从i往后数和从j往后数,有多少是相同的。

开始时先设i=0,j=1,k=0。

每次都对i+k、j+k进行一次比较。

发现i+k有可能大于字符串长度n啊,怎么办呢?

首先想到将字符串倍长:bacdabacda。

但是这样做很麻烦,而且倍长之后,前后两段都是完全一样的。

所以我们只需要取模n就好了:(i+k)%n。

这么做就要求字符串从0开始,如果从1开始的话,就有点麻烦了,还得+1-1什么的,不如从0开始简单明了。

比较完i+k和j+k,如果两个字符相等,那么显然k++。

如果不相等,那么哪边比较大,哪边就肯定不是最小的了,同时把k重置为0。

如果出现了i、j重合的情况,把j往后移动一位。

最后输出i、j较小的那个就好了。

 

什么,想看具体点的,那接着往下看吧

 

定义三个指针,i,j,k

初始i=0;  j=1;  k=0;

首先,如果s[i]<s[j]那么很明显j++

如果s[i]>s[j]那么也很明显i=j++

剩下的就是如果s[i]==s[j]的时候。

这时候有一个性质就是在i和j之间的所有的字符一定是大于等于s[i]的

令k=0,循环寻找第一个s[i+k]!=s[j+k]的位置

如果s[i+k]<s[j+k]那么j+=k+1

为什么呢?

首先s[i]到s[i+k-1]一定是大于等于s[i],因为如果其中有一个数小于s[i],那么这个数一定在s[j]到s[j+k-1]中存在,又因为必定有一个会在后面,所以如果s[j]先碰到了,那么一定不会继续到k的位置的,所以一定不存在比s[i]小的字符。

所以从其中的任意一个字符开始当作起始点,都不会比现在更小,所以只有从选出来的序列的后面那一个字符开始才有可能会是最小。

所以j+=k+1

如果序列中某个数和s[i]相等的话,那么一定会有之前或者以后再这个位置起始过,所以不需要再从这个位置进行起始。

因为在这里i和j是等价的,i在前和j在前的结果是一样的,所以i和j的处理是相同的,下面就不仔细的进行讲解了。

还有就是如果i==j那么让j++就可以回到原先的状态了

最后的时候,肯定是小的不会动,而大的会不停的向后移动,所以最后只需要输出i和j最小的一个即可

 

再从头过一遍思路:

假设有两个下标i,j,表示如果从i和从j出发的字符串,有一个k表示字符串的长度,如果长度达到len,就表示找到最小的串。

s[i+k] == s[j+k]:  k++

s[i+k]>s[j+k]: i=i+k+1 表示以i,到i+k为起点的字符串,都不是最小字符串的前缀。

s[i+k]<s[j+k]: j=j+k+1 同理

注意:

  • i和j一定是不同的。
  • 每次不等时,需要设置k为0。
  • 循环条件是小于不是小于等于

 

 1 int getmin(char *str)
 2 {
 3     int len=strlen(str);
 4     int i=0,j=1,k=0;
 5     while(i<len&&j<len&&k<len)
 6     {
 7         int t=str[(i+k)%len]-str[(j+k)%len];
 8         if(!t) k++;
 9         else
10         {
11             if(t>0) i=i+k+1;
12             else j=j+k+1;
13             if(i==j) j++;
14             k=0;
15         }
16     }
17     return min(i,j);
18 } 

 

 


 

如果遇到了bcdefgabcdefg这样的字符串,上述的写法就有点不太好用了

 

下面介绍一种改进的算法:(注意理解加粗字体)

初始i = 0, j = 1, k = 0. i 指向第一个字符,j 指向第二个字符,k 表示增量。
每次比较 t = s[(i+k) % len] - s[(j+k) % len]
t == 0 表示从i,j开始长度为(k + 1)的字符串相同, 所以更新k++。
t < 0 表示从i 开始长度为(k + 1)的字符串的字典序小于从 j 开始, 所以更新j = max(j+k+1, i+1)。
t > 0 表示从i 开始长度为(k + 1)的字符串的字典序大于从 j 开始, 所以更新i = max(i+k+1, j+1)。
当k更新到len次或者i,j超出长度。就能确定一个位置min(i, j)
为什么更新i,j 用max?例如移动 i 时,正常更新是向后移动k位,但是如果移动之后还是小于j,完全可以移动到j +1的位置。因为i, j 之间的字符肯定大于j。

 1 int MinRepresent(char *s)
 2 {
 3     int i = 0, j = 1, k = 0;
 4     int len = strlen(s);
 5     while (i < len && j < len && k < len) {
 6         int t = s[(i+k) % len] - s[(j+k) % len];
 7         if (t == 0) k++;
 8         else{
 9             if (t < 0)  j = max(j+k+1, i+1);
10             else i = max(i+k+1, j+1);
11             k = 0;
12         }
13     }
14     return min(i, j);
15 }

 

 

 

 


 

 

一些应用

 

 

求字符串的最小表示法

求最小周期长度

求最大表示法

 

 

总结(摘自周源《浅析“最小表示法”思想在字符串循环同构问题中的应用》):

最小表示法是一个与匹配算法本质不同的线性算法。“最小表示法”思想引导我们从问题的另一方面分析,进而构造出一个全新的算法。

比起匹配算法,它容易理解,便于记忆,实现起来,也不过是短短的几重循环,非常方便,便于应用于扩展问题

 

“最小表示法”是判断两种事物本质是否相同的一种常见思想,它的通用性也是被人们认可的——无论是搜索中判重技术,还是判断图的同构之类复杂的问题,它都有着无可替代的作用。仔细分析可以得出,其思想精华在于引入了“序”这个概念,从而将纷繁的待处理对象化为单一的形式,便于比较。

 

然而值得注意的是,在如今的信息学竞赛中,试题纷繁复杂,使用的算法也不再拘泥于几个经典的算法,改造经典算法或是将多种算法组合是常用的方法之一。单纯的寻求字符串的最小表示显得得不偿失,但利用“最小表示法”的思想,和字符串的最小表示这个客观存在的事物,我们却找到了一个简单、优秀的算法。

因此,在解决实际问题时,只有深入分析,敢于创新,才能将问题化纷繁为简洁,化无序为有序。

 

 


 

我在学习该算法时阅读了许多博客,上文是我借鉴它们总结的一篇文章,以便自己以后查阅用的

下面是原文地址:

https://www.cnblogs.com/eternhope/p/9972846.html

https://www.cnblogs.com/XGHeaven/p/4009210.html

https://blog.csdn.net/henuyh/article/details/85868528

 

posted @ 2019-07-24 11:19  jiamian22  阅读(1622)  评论(0编辑  收藏  举报