“最小表示法”思想的提出
首先来看一个引例:
[引例]有两列数,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