这道题有两种比较优秀的O(n)做法
前者是函数逆用循环节法,抓住了字符串最小表示法的所有性质
后者是反转字符串哈希法,使用了字符串哈希。
【HDU5442 2015长春网络赛F】字符串最小表示法+函数逆用循环节法——
#include<stdio.h> #include<iostream> #include<string.h> #include<ctype.h> #include<math.h> #include<map> #include<set> #include<vector> #include<queue> #include<functional> #include<string> #include<algorithm> #include<time.h> #include<bitset> void fre(){freopen("c://test//input.in","r",stdin);freopen("c://test//output.out","w",stdout);} #define MS(x,y) memset(x,y,sizeof(x)) #define MC(x,y) memcpy(x,y,sizeof(x)) #define MP(x,y) make_pair(x,y) #define ls o<<1 #define rs o<<1|1 typedef long long LL; typedef unsigned int UI; typedef int Int; template <class T> inline void gmax(T &a,T b){if(b>a)a=b;} template <class T> inline void gmin(T &a,T b){if(b<a)a=b;} using namespace std; const int N=20000+10; int casenum,casei; int n; char s[N]; int getmax0() { //i是前起点,j是后起点,k是匹配长度 int i=0,j=1,k=0; while(i<n&&j<n&&k<n) { int t=s[(i+k)%n]-s[(j+k)%n]; if(t==0)k++; else { t>0?j+=k+1:i+=k+1; if(i==j)j++; k=0; } } return min(i,j); } int getmax1() { //i是后起点,j是前起点,k是匹配长度 int i=n-1,j=n-2,k=0; while(i>=0&&j>=0&&k<n) { int t=s[(i-k+n)%n]-s[(j-k+n)%n]; if(t==0)k++; else { t>0?j-=k+1:i-=k+1; if(i==j)j--; k=0; } } if(k==n) { int cir=abs(i-j); return i%cir; } if(i>=0&&j>=0)return min(i,j); return i>=0?i:j; } int cmp(int p0,int p1) { for(int i=0;i<n;i++) { int t=s[(p0+i)%n]-s[(p1-i+n)%n]; if(t>0)return 1; if(t<0)return -1; } return 0; } int main() { scanf("%d",&casenum); for(casei=1;casei<=casenum;casei++) { scanf("%d",&n); scanf("%s",s); int p0=getmax0(); int p1=getmax1(); int v=cmp(p0,p1); if(v==1)printf("%d %d\n",p0+1,0);//正着字典序大 else if(v==-1)printf("%d %d\n",p1+1,1);//逆着字典序大 else p0<=p1?printf("%d %d\n",p0+1,0):printf("%d %d\n",p1+1,1);//一样大看位置 } return 0; } /* 【题意】 T(20)组数据。 对于每组数据,给你一个长度为n(2e4)的字符串,1base,即位置分别是1,2,3,4,……,n 这个字符串是环状,而且可以正着来或者反正来读。这样一共就存在2n种串,长度都为n。 我们想要知道——从哪个位置以什么方向开始读,读出的字符串的字典序是最大的。 输出: 位置(1~n)和方向(0表示正着,1表示反着) 输出要求: 1,如果存在多个位置,我们输出起点编号最小的位置。 2,如果从该位置正反读都可以得到最大字典序的串,那么我们正着读。 【类型】 字符串最小表示法 (+字符串哈希) 【分析】 这道题因为涉及到串的旋转和最大字典序。 所以一眼就想到其可以用字符串最小表示法做。 正着来的话,用字符串最小表示法就已经可以得到字典序最大的位置中编号最小的那个位置了。 不过,反着来的话,要怎么搞? 方法一:把串反转 方法二:把字符串最小表示法的模板反一下。 我们正着使用字符串最小表示法的时候,首先得到的字典序最大的串一定是编号最小的那个。 而如果倒着来,因为编号的顺序是与我们扫描的顺序相逆的。我们得到的是倒着出现的第一个。 为了解决这个问题,我们有两种策略—— 方法一: 既然我们现在已经得到了字典序最大的串。 那么我们用字符串哈希的方式,从1到n扫描,求出以所有位置为开头,方向是逆着来,哪个的字典序一样是最大。 方法二: 我们看到这个可能是倒着出现的第一个,而不是最后一个。 什么时候会出现多个字典序相同的串呢? 当这个串出现循环节的时候。即——最小表示法终止的条件是k==len。 这时,直接mod循环节,就可以得到第一个起点位置。 【时间复杂度&&优化】 O(n) 【数据】 input 9 abcabcabc output 3 1 */
#include<stdio.h> #include<iostream> #include<string.h> #include<ctype.h> #include<math.h> #include<map> #include<set> #include<vector> #include<queue> #include<functional> #include<string> #include<algorithm> #include<time.h> #include<bitset> void fre(){freopen("c://test//input.in","r",stdin);freopen("c://test//output.out","w",stdout);} #define MS(x,y) memset(x,y,sizeof(x)) #define MC(x,y) memcpy(x,y,sizeof(x)) #define MP(x,y) make_pair(x,y) #define ls o<<1 #define rs o<<1|1 typedef long long LL; typedef unsigned int UI; typedef int Int; template <class T> inline void gmax(T &a,T b){if(b>a)a=b;} template <class T> inline void gmin(T &a,T b){if(b<a)a=b;} using namespace std; const int N=20000+10,M=40000+10; int casenum,casei; int n; char a[M]; LL u[N]; LL f[M];//f[i]表示以a[i-1]为结尾字符串的哈希前缀和 int getmax() { //i是前起点,j是后起点,k是匹配长度 int i=0,j=1,k=0; while(i<n&&j<n&&k<n) { int t=a[(i+k)%n]-a[(j+k)%n]; if(t==0)k++; else { t>0?j+=k+1:i+=k+1; if(i==j)j++; k=0; } } return min(i,j); } int cmp(int p0,int p1) { for(int i=0;i<n;i++) { int t=a[(p0+i)%n]-a[(p1-i+n)%n]; if(t>0)return 1; if(t<0)return -1; } return 0; } int hashit(int p) { int m=n+n; for(int i=0;i<n;i++)a[n+i]=a[i];//把字符串二倍化——扩环成链 for(int i=1;i<=m;i++)f[i]=(f[i-1]*26+a[i-1]-'a')%Z;//计算字符串哈希值 int V=(f[p+n]-f[p]*u[n]%Z+Z)%Z;//得到最大字典序串的哈希值 for(int i=0;i<n;i++)if((f[i+n]-f[i]*u[n]%Z+Z)%Z==V)return i;//对于多个最大字典序串,返回最小的位置 } int main() { u[0]=1;for(int i=1;i<=20000;i++)u[i]=u[i-1]*26%Z; scanf("%d",&casenum); for(casei=1;casei<=casenum;casei++) { scanf("%d",&n); scanf("%s",a); int p0=getmax();//正方向字典序最大串的最小点位 strrev(a); int p1=n-1-getmax();//逆方向字典序最大串的最大点位 strrev(a); p1=hashit(p1);//哈希一下就能得到逆方向字典序最大串的最小点位啦。 int v=cmp(p0,p1); if(v==1)printf("%d %d\n",p0+1,0);//正着字典序大 else if(v==-1)printf("%d %d\n",p1+1,1);//逆着字典序大 else p0<=p1?printf("%d %d\n",p0+1,0):printf("%d %d\n",p1+1,1);//一样大看位置 } return 0; } /* 【题意】 T(20)组数据。 对于每组数据,给你一个长度为n(2e4)的字符串,1base,即位置分别是1,2,3,4,……,n 这个字符串是环状,而且可以正着来或者反正来读。这样一共就存在2n种串,长度都为n。 我们想要知道——从哪个位置以什么方向开始读,读出的字符串的字典序是最大的。 输出: 位置(1~n)和方向(0表示正着,1表示反着) 输出要求: 1,如果存在多个位置,我们输出起点编号最小的位置。 2,如果从该位置正反读都可以得到最大字典序的串,那么我们正着读。 【类型】 字符串最小表示法 (+字符串哈希) 【分析】 这道题因为涉及到串的旋转和最大字典序。 所以一眼就想到其可以用字符串最小表示法做。 正着来的话,用字符串最小表示法就已经可以得到字典序最大的位置中编号最小的那个位置了。 不过,反着来的话,要怎么搞? 方法一:把串反转 方法二:把字符串最小表示法的模板反一下。 我们正着使用字符串最小表示法的时候,首先得到的字典序最大的串一定是编号最小的那个。 而如果倒着来,因为编号的顺序是与我们扫描的顺序相逆的。我们得到的是倒着出现的第一个。 为了解决这个问题,我们有两种策略—— 方法一: 既然我们现在已经得到了字典序最大的串。 那么我们用字符串哈希的方式,从1到n扫描,求出以所有位置为开头,方向是逆着来,哪个的字典序一样是最大。 方法二: 我们看到这个可能是倒着出现的第一个,而不是最后一个。 什么时候会出现多个字典序相同的串呢? 当这个串出现循环节的时候。即——最小表示法终止的条件是k==len。 这时,直接mod循环节,就可以得到第一个起点位置。 【时间复杂度&&优化】 O(n) 【数据】 input 9 abcabcabc output 3 1 */