HDU 2609 How many(最小表示法)
题目链接:HDU 2609 How many
题目大意:
有n个有01组成的字符串,每个字符串都代表一个项链,那么该字符串就是一个环状的结构,求可以经过循环旋转,最后不同的串有多少个。
算法思想:
将每个字符串转换成最小串,然后放在set里面去重。
最小表示法:
循环字符串的最小表示法的问题可以这样描述:
对于一个字符串S,求S的循环的同构字符串S’中字典序最小的一个。
由于语言能力有限,还是用实际例子来解释比较容易:
设S=bcad,且S’是S的循环同构的串。S’可以是bcad或者cadb,adbc,dbca。而且最小表示的S’是adbc。
对于字符串循环同构的最小表示法,其问题实质是求S串的一个位置,从这个位置开始循环输出S,得到的S’字典序最小。
一种朴素的方法是设计i,j两个指针。其中i指向最小表示的位置,j作为比较指针。
令i=0,j=1
如果S[i] > S[j] i=j, j=i+1
如果S[i] < S[j] j++
如果S[i]==S[j] 设指针k,分别从i和j位置向下比较,直到S[i] != S[j]
如果S[i+k] > S[j+k] i=j,j=i+1
否则j++
返回i
实际上,如果S[i+k] > S[j+k] ,那么S[i + k]就不可能是最小表示的下标,所以此时直接令i = i + k +1;
优化后的算法,也就是说最小表示法,实际上是用 i, j两个指针去找最小的位置。
1) 利用两个指针i, j。初始化时i指向0, j指向1。
(2) k = 0开始,检验s[i+k] 与 s[j+k] 对应的字符是否相等,如果相等则k++,一直下去,直到找到第一个不同,(若k试了一个字符串的长度也没找到不同,则那个位置就是最小表示位置,算法终止并返回)。则该过程中,s[i+k] 与 s[j+k]的大小关系,有三种情况:
证明的时候假设(i<j)的,无伤大雅 ;
(A). s[i+k] > s[j+k],则i滑动到i+k+1处 --- 即s1[i->i+k]不会是该循环字符串的“最小表示”的前缀。
(B). s[i+k] < s[j+k],则j滑动到 j+k+1处,原因同上。(C). s[i+k] = s[j+k],则 k++; if (k == len) 返回结果。
注:这里滑动方式有个小细节,若滑动后i == j,将正在变化的那个指针再+1。直到p1、p2把整个字符串都检验完毕,返回两者中小于 len 的值。
(4) 进一步的优化,例如:i要移到i+k+1时,如果i+k+1 <= p2的话,可以直接把i移到 j+1,因为,j到j+k已经检验过了该前缀比以i到i+k之间任何一个位前缀都小;j时的类似,移动到i+1。
第4步暂时还没有看懂,先给出模板吧
#include<iostream> #include<cstdio> #include<cstring> #include<set> using namespace std; int len; const int maxn = 10000 + 10; char s[maxn]; char t[maxn]; set<string>st; int minRepresentation(char *s){ int i=0,j=1,k=0; while(i<len && j<len && k<len){ int tmp = s[(i + k)%len] - s[(j + k)%len]; if(tmp == 0) k++; //相等 else{ if(tmp > 0) i += k+1; // i = i + k + 1; else j += k+1; if(i == j) j++; k = 0; } } return min(i,j); } void getSet(char *s){ s[len/2] = '\0'; st.insert(s); } int main(){ int n; while(scanf("%d",&n)!=EOF ){ st.clear(); for(int i=0;i<n;i++){ scanf("%s",t); strcpy(s,t); //因为最后还要获得最小表示的串,所以这里将两个串合并,最后在len/2的位置处加上'\0'即可 strcat(s,t); len = strlen(s); int k = minRepresentation(s); getSet(s+k); } cout<<st.size()<<endl; } return 0; }