POJ 2185 Milking Grid(KMP:循环节加强版)
http://poj.org/problem?id=2185
题意:
给你一个R*C的字符矩阵,要你求最小的子矩阵,我们能通过不断重复该子矩阵从而得到一个大矩阵把原矩阵包含在内,求这个子矩阵的面积?
分析:
做这题之前首先要知道一个字符串(可能内带数个循环节但是不完整)的最短循环节长度= len - next[len]. 详见HDU3746:
http://blog.csdn.net/u013480600/article/details/22954037
解法一:(推荐解法)
将大矩阵的每一列看成一个字符,然后对该大矩阵的每一列求出next[i],则列最短循环节长度=小矩阵的宽=ans1=c-next[c]。
将大矩阵的每一行看成一个字符,然后对该大矩阵的每一行求出next[j],则行最短循环节长度=小矩阵的高=ans2=r-next[r]。
最后答案:ans1*ans2即为所求矩阵的面积。
AC代码:63ms
<span style="font-size:18px;">#include<cstdio>
#include<cstring>
using namespace std;
char s[10010][100];
int next[10010];
int r,c;
bool str1(int i,int j)//判断第i行和第j行是否相等
{
for(int k=0;k<c;k++)
if(s[i][k]!=s[j][k])
return false;
return true;
}
bool str2(int i,int j)//判断第i列和第j列是否相等
{
for(int k=0;k<r;k++)
if(s[k][i]!=s[k][j])
return false;
return true;
}
int main()
{
while(scanf("%d%d",&r,&c)==2)
{
for(int i=0;i<r;i++)
scanf("%s",s[i]);
next[0]=next[1]=0;
for(int i=1;i<r;i++)//把每行看成一个字符
{
int j=next[i];
while(j && str1(i,j)==false) j=next[j];
next[i+1] = (str1(i,j)==true)? j+1 :0;
}
int ans1=r-next[r];
next[0]=next[1]=0;
for(int i=1;i<c;i++)//把每列看成一个字符
{
int j=next[i];
while(j && str2(i,j)==false) j=next[j];
next[i+1] = (str2(i,j)==true)? j+1 :0;
}
int ans2=c-next[c];
printf("%d\n",ans1*ans2);
}
}
</span>
解法二:
首先需要用穷举法(网上很多没用穷举法做的代码有部分是有缺陷的,见后面牛人的思路)求出对于矩阵的每一行,长度为k(k从1到C)的从0开始的子串是不是可以作为该行的循环节.然后在所有行的可行循环节长度中找出大家都可行(即计数数组f[width]=R)的那个最小宽width.
其次求小矩阵的列,以上面求得的宽width,最为一个单元,把第0行到第r-1行看成是一个字符串,用KMP求出该字符串的next函数,然后r-next[r] 即是它的最小循环节,也是小矩阵的高.例子如下:
abab
acac
abab
acac
上面4*4的矩阵,首先可以求得width=2,然后每行都是2个字母一循环的,现在求列的循环,即可这样看矩阵了:
ab
ac
ab
ac
上面的矩阵就看成了:ab ac ab ac这样的具有4个单元的串.
可以看出小矩阵的列应该为2.
AC代码:
<span style="font-size:18px;">#include<cstdio>
#include<cstring>
using namespace std;
char s[10010][100];
char a[100];
int next[10010],f[100];
int r,c;
int main()
{
while(scanf("%d%d",&r,&c)==2)
{
int i,x,y;
memset(f,0,sizeof(f));
for(i=0;i<r;i++)
{
scanf("%s",s[i]);
strcpy(a,s[i]);
for(int j=c-1;j>=0;j--)//当前检查的字串长度,长度为c不用检查,当所有长度字串不合适,自然就是c了
{
a[j]=0;
for(x=0,y=0;s[i][y];y++,x++)
{
if(!a[x])x=0;
if(a[x]!=s[i][y]) break;
}
if(!s[i][y]) f[j]++;
}
}
for(i=0;i<c;i++)
if(f[i]==r)break;
x=i;
for(i=0;i<r;i++)s[i][x]=0;
next[0]=next[1]=0;
for(int i=1;i<r;i++)
{
int j=next[i];
while(j && strcmp(s[i],s[j])!=0) j=next[j];
next[i+1] = (strcmp(s[i],s[j])==0)? j+1:0;
}
printf("%d\n",x*(r-next[r]));
}
return 0;
}
</span>
下面给出网上牛人的思路(转):
http://blog.sina.com.cn/s/blog_69c3f0410100tyjl.html
在网上找到了好多代码都是能通过部分数据,他们的代码和误点给我以提示。
思路:
支持他们的观点:最小重复矩阵一定靠左上角。
第一步也是找最小重复子矩阵的宽,网上的大部分代码就卡在这里,如实例(poj讨论数据):
实例1:
2 8
ABCDEFAB
AAAABAAA
实例2:
2 8
ABCDEFAB
AAAABABC
误区1:网上的代码找宽的方法(多数代码):用KMP的next求出每行最小重复子串长度,然后求这些长度的公倍数,对于这个实例:第一行为6,第二行为5,6与5的最小公倍数为30,大于8则取8为宽,这种思路对实例2很合适,但明显对于实例1是错误的。(这种思路的AC代码网上有非常多)
误区2:同样用KMP的next求出每行最小重复子串长度,然后取最大的那个长度作为宽,这种对于实例1来说刚好能过,但对于实例2就无能为力了。(这种思路的AC代码网上有比较少)
出现以上误区的博文误倒大家,只能说明POJ数据太弱。
说说我的思路吧:找出每行的重复子串长度的各种可能情况,然后每行都有的并且是最小长度作为宽width。
第二步找最小重复子矩阵的高,这个思路和网上的差不多,取每行的宽为width的前缀作为一个单位,对这0到r-1个单位求出KMP的next函数,找出最小重复子序列的单位数作为高height,最终答案为width*height。
code:
<span style="font-size:18px;">#include<stdio.h>
#include<string.h>
char s[10010][80];
int next[10010];
int main()
{
int i,j,x,y,r,c,f[80];
char a[80];
scanf("%d%d",&r,&c);
for(i=0;i<c;i++)f[i]=0;
for(i=0;i<r;i++){
scanf("%s",s[i]);
strcpy(a,s[i]);
//将每行的每种重复子串长度都求出来
for(j=c-1;j>0;j--){//j表示当前判断长度为j+1的串a[0]--a[j] 是否和s[i]串循环匹配
a[j]=0;//这里令a[j]=0,是表示第j个位置是末尾了
for(x=0,y=0;s[i][y];x++,y++){
if(!a[x])x=0;
if(a[x]!=s[i][y])break;
}
if(!s[i][y])f[j]++;//f[j]=k表示以长度为j的字串作为循环节的串有k个
}
}
for(i=0;i<c;i++)//找出所有行的最小相同的子串长度,为最小重复子矩阵的列数
if(f[i]==r)break;
x=i;//最小重复子矩阵的列数
for(i=0;i<r;i++)s[i][x]=0;
next[0]=next[1]=0;//按纵列求KMP的next函数,以求最小重复子矩阵的行数
for(i=1;i<r;i++){
int j=next[i];
while(j && strcmp(s[i],s[j])!=0) j= next[j];
next[i+1] = (strcmp(s[i],s[j])==0)?j+1:0;
}
printf("%d\n",(r-next[r])*x);//行列相乘即为最终结果
return 0;
}
</span>