Description
艾利斯顿商学院篮球队要参加一年一度的市篮球比赛了。拉拉队是篮球比赛的一个看点,好的拉拉队往往能帮助球队增加士气,赢得最终的比赛。所以作为拉拉队队长的楚雨荨同学知道,帮助篮球队训练好拉拉队有多么的重要。拉拉队的选拔工作已经结束,在雨荨和校长的挑选下,n位集优秀的身材、舞技于一体的美女从众多报名的女生中脱颖而出。这些女生将随着篮球队的小伙子们一起,和对手抗衡,为艾利斯顿篮球队加油助威。一个阳光明媚的早晨,雨荨带领拉拉队的队员们开始了排练。n个女生从左到右排成一行,每个人手中都举了一个写有26个小写字母中的某一个的牌子,在比赛的时候挥舞,为小伙子们呐喊、加油。雨荨发现,如果连续的一段女生,有奇数个,并且他们手中的牌子所写的字母,从左到右和从右到左读起来一样,那么这一段女生就被称作和谐小群体。现在雨荨想找出所有和谐小群体,并且按照女生的个数降序排序之后,前K个和谐小群体的女生个数的乘积是多少。由于答案可能很大,雨荨只要你告诉她,答案除以19930726的余数是多少就行了。
Input
输入为标准输入。第一行为两个正整数n和K,代表的东西在题目描述中已经叙述。接下来一行为n个字符,代表从左到右女生拿的牌子上写的字母。
Output
输出为标准输出。输出一个整数,代表题目描述中所写的乘积除以19930726的余数,如果总的和谐小群体个数小于K,输出一个整数-1。
Sample Input
5 3
ababa
Sample Output
45
样例说明
和谐小群体女生所拿牌子上写的字母从左到右按照女生个数降序排序后为ababa, aba, aba, bab, a, a, a, b, b,前三个长度的乘积为。
HINT
总共20个测试点,数据范围满足:
题意分析
对于一个长度为n的给定字符串,找出其中所有的回文字符串,按照回文串的长度降序排列,计算出前K个长度值的乘积(对19930726的余数),如果回文串的个数小于K个,则输出-1.
题中给的例子:对于一个长度为n=5的字符串“ababa”,计算出前k=3个回文串长度的乘积。
“ababa”的回文串从左到右共有9个(单独一个字符也算回文):“a”、“b”、“a”、“aba”、“b”、“bab”、“a”、“ababa”、“aba”,如果按降序排列的话是:“ababa”、“aba”、“bab”、“aba”、“a”、“b”、“a”、“b”、“a”,其长度分别是5、3、3、3、1、1、1、1、1,前三个的长度是5、3、3,所以乘积是5×3×3=45
解题思路
首先,这个题需要找出给定字符串的所有回文子串的长度,这个显然可以用马拉车算法,马拉车算法计算的数组(在本文中成为数组f[],在讲解manachar算法的那个帖子中称为数组p[])f[]的值就是每个字符为中心的回文串的半径长度,所以根据数组f[]计算出每种回文串的长度及其回文串的个数,最后按降序把长度最大的K个回文串长度值连乘得到结果即可,连乘可以使用快速幂的方法。
这个过程大体可以分为四步:
第一步:对原串使用manachar算法,计算出数组f[],记录下以每一个字符为中心的回文串半径长度,如“ababa”的f[]={0,1,2,1,0}。
第二步:通过对数组f[]的处理,得出每种回文串的长度 以及该种回文串的个数,比如“ababa”中,长度为5的回文串有1个(“ababa”),长度为3的回文串有3个(“aba”、“bab”、“aba”),长度为1的回文串有5个(“a”、“b”、“a”、“b”、“a”),总共有9个回文串。
第三步:看看回文串总数是不是大于K,如果小于,则输出“-1”,否则,从大到小连乘前k个回文串的长度,按照快速幂的方法连乘,例如“ababa”共有9个回文串,长度为5的有1个,长度为3的有3个,长度为1的有5个,则 ans=5×3×3 ,即 51×32=45
在这个过程中,还有一些细节问题需要注意,下面就分别详细介绍三个步骤
第一步
第一步是对原串用manachar算法求出它的f[]数组。manachar算法详细的介绍写过专门的帖子介绍,这里不赘述。但在这里有一些与标准manachar算法不一样的地方需要注意:manachar算法一般要在原串中插入“#”使得原串的长度都变成奇数再进行处理,插入“#”后的串也叫“双倍串”,长度是原来的2倍+1。而这个题中明确说要统计的和谐小群体的人数是奇数个。所以在这个题中,就不用往原串中插入“#”了,原来的串也可以叫做“单倍串”。
为什么呢,比如“ababa”,他的f[]={0、1、2、1、0},每一个f[i]的值是以第i个字符为中心的回文串的半径,如f[3]=2就是第3个字符“a”为中心的回文“ababa”的半径是2,因为是以某一个字符为中心,所以回文串的长度肯定是奇数的。
再比如“babbab”,左边的“bab”和右边的“bab”是对称相等的,所以“babbab”是一个长度为6的回文串,但由于数组f[]是以某一个字符为中心的回文串半径,所以他的f[]={0、1、0、0、1、0},因为是偶数长度的回文串,所以无论是以第3个字符“b”还是第4个字符“b”为中心,都没有回文串(回文串半径为0),因此偶数长度的回文串就不会统计在内了。
这一段的代码如下:
for (i=1;i<=n;i++) //n为字符串长度
{
if (i<=mx) f[i]=min(mx-i,f[2*id-i]);
else f[i]=0;
while (s[i+f[i]+1]==s[i-f[i]-1]) f[i]++;
if (i+f[i]>mx)
{
mx=i+f[i];
id=i;
}
}
这算是一段标准的manachar算法代码,得到的数组f[]中的值是以每一个字符为中心的回文串的半径,这个半径长度是不包括当前中心字符本身的。也就是说“aba”中“b”为中心的回文半径是1,而不是2。将来计算回文串长度时:2×半径+1。
第二步
接下来,要统计每种回文串的长度及个数,为后续的连乘做准备。因为连乘要用快速幂,所以我们要知道:长度为5个回文串有1个,长度为3的回文串有3个,……,将来连乘时,就是51×33……。这里也有几个小细节需要考虑:
1
因为统计是要基于第一步得到的数组f[],而数组f[]存放的是以某个字符为中心的回文半径长度,因此我们可以先统计每种半径长度的回文串个数,统计的结果放在一个数组a[]中。
字符串的长度为n,所以可以定义一个长度为n的数组a[1…n]来存放每种半径长度的回文串的个数(其实只用a的一半就可以了)
2
统计的时候,可以倒序,从半径长度最长的回文串开始统计。因为是按照回文半径长度,因此对于长度为n的字符串,回文串半径最大就是n/2
我们可以通过一个for循环,把f[]中每种半径长度值的个数计入数组a[]中,代码如下:
for (i=1;i<=n;i++)
a[f[i]]++;
在例子“ababa”中,f[]={0、1、2、1、0},得到的a[]={2、2、1、0、0、0},如下图:
注意:这里的s[]和f[]下标都是从1开始,而a[]是从0开始,因为我们也要统计半径为0的回文串的个数。
这时得到的数组a[]还不是我们要的结果!
3
上面的数组a[]只是对f[]中每种半径的个数做了一个简单的计数,它还不是我们要的结果,比如f[3]=2,表示以第3个字符“a”为中心的回文串半径为2,a[2]=1表示我们从f[]中找到了半径为2的个数是1个。再比如f[2]和f[4]都是1,表示以第2个、第4个字符为中心的回文串半径都是1,a[1]=2表示我们从f[]中找到了2个半径为1的数。实际上半径为1的回文串有3个:“aba”、“bab”、“aba”。为什么少了一个?除了以第2个、第4个字符为中心的回文串半径都是1以外,还有一个以第3个字符为中心的回文串“bab”半径也是1,这个回文串包含在了刚才那个半径为2的更大的回文串“ababa”里了。
因此,我们在统计每种半径长度的回文串个数时,要把“上一个更大半径回文串个数” 累加到“当前半径长度回文串个数”,得到的才是当前半径长度的回文串个数。
还是“ababa”这个例子,最大的半径长度是2,a[2]=1,表示最大半径长度2的回文串只有1个;那么半径为1的回文串个数有几个呢?a[1]原来的值是2,表示在数组f[]中记录了有2个,再加上刚才半径为2的回文串中必然也包含有半径为1的回文串,所以我们把a[2]的值加上a[1],得到的3,就是半径为1的回文串的总个数。
写成代码如下:
for (i=n/2;i>=0;i--) //半径最大n/2,从n/2开始倒序循环
a[i]+=a[i+1],tot+=a[i]; //a[i]是半径长度为i的回文串个数
//tot是找到回文串的总数
到此为止,我们得到了每种半径长度的回文串的个数a[],也得到了回文串的总数tot。
第三步
判断一下回文串总数tot,是否小于K,如果小于,那么输出“-1”后程序结束。
代码如下:
if (tot<k)
{
printf("-1\n");
return 0;
}
如果大于等于k,那么就把前K个回文串的长度连乘一下。
在这里是使用快速幂的方法连乘。
对于某一个a[i] = p,代表回文半径为i的回文串有p个,
首先,回文半径为i,整个回文的长度用x表示,则 x=2*i+1,即回文长度为x的回文串有p个,就是要p个x连乘:xp
在连乘的过程中,需要注意的是要从最大的x开始乘,并且一共乘够K个就停止了,
所以,K是要连乘的总个数,当乘了p个x(即乘了 xp)之后,K要减掉本次乘的p个,就是剩余要乘的个数,每次取K和下一组x的个数p中最小的那个座位下一次要乘的个数。
还有,别忘了连乘的时候要取余
代码如下:
for (i=n/2;i>=0&&k;i--) //回文串半径最大是n/2
{
p=min(k,a[i]); //p是本次要乘的幂次,取k和a[i]中小的那个
k-=p; //k是剩余要乘的个数,每次减掉本次乘的幂次
x=2*i+1; //回文半径是i,回文长度就是 2×i+1
for (;p;p>>=1,x=x*x%mod) // 计算 x的p次幂
if (p&1) ans=ans*x%mod;
}
最后的这个for循环,写的也比较特殊,在这里解释一下。
首先说“>>=”操作,这是右移的操作,如果是“>>=1”就是右移1位,对于一个二进制数,右移一位相当于除以2。
再说for循环,在C语言的语法中,for循环是这样的:
for ( 语句1; 语句2; 语句3 )
循环体;
语句1是循环体的初始条件,也就是循环体第一次执行时要执行语句1;语句2是循环条件,每次执行循环体之前,都要执行语句2,语句2一般是个条件语句,如果条件成立就执行循环体,如果条件不成立,就退出循环。
语句3是在每次执行完循环体之后执行,一般是对循环变量的自增或自减等。
语句1和语句3可以是单条语句,也可以逗号隔开的多条语句。
在本题中,语句1省略,表示循环开始时,并不关心p的值是多少。
语句2循环条件语句是 p,表示只要p的值不为0,就可以执行循环体。
语句3是逗号隔开的两条语句:p>>=1和 x=x*x%mod,也就是说每次执行完循环体之后,要执行这两条语句,把p值右移1为,把x乘以x的值再赋值给x(取余)
循环体是一条if语句: if (p&1) ans=ans*x%mod;
p &1 就是p的值和1做二进制“与”的操作,如果p是奇数,“与”之后的结果是1,如果p是偶数,“与”之后的结果就是0,所以这条语句的意思是,如果p是奇数,就把ans和x相乘的结果赋值给ans。
在这道题中,如果 a[1]=2,也就是回文串的半径为1(回文串长度为3)的回文串有2个,ans要乘以32。这是p=2,x=3,在这个for循环中,第一次循环时,p=2,循环条件成立,所以执行循环体,循环体是if语句,p是偶数,所以并不执行ans*x%mod。循环体执行之后,执行语句3,p>>=1,p右移1位为1,x=32。第一次循环执行完。
第二次循环时,p=1,条件仍成立,执行循环体,这是p=1是奇数,所以执行ans=ans*x%mod,ans是32。执行完循环体之后,执行语句3的两条语句,p再右移1位变为0,x再相乘变为x4
第三次循环时,p=0,循环条件不成立了,for循环结束。
如果p的值一开始是3,那么在第一次循环时,ans会乘以一个x,在第二次循环时,再乘以一个x2,一共乘的是x3。具体过程就不赘述了。
程序完整代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define LL long long
const int mod=19930726; //取余
char s[1000010]; //原字符串
int f[1000010],n; //manachar算法得到的数组f[],n是字符串长度
LL k,a[1000010]; //k是要连乘的个数,a是每种半径长度的回文串个数
int main()
{
int i,j,mx,id;
LL tot=0,ans=1,p,x; //tot是回文串总数,ans为结果
scanf("%d%lld%s",&n,&k,s+1);
mx=0;
s[0]='$';
for (i=1;i<=n;i++)
{
if (i<=mx) f[i]=min(mx-i,f[2*id-i]);
else f[i]=0;
while (s[i+f[i]+1]==s[i-f[i]-1]) f[i]++;
if (i+f[i]>mx)
{
mx=i+f[i];
id=i;
}
}
for (i=1;i<=n;i++)
a[f[i]]++;
for (i=n/2;i>=0;i--)
a[i]+=a[i+1],tot+=a[i];
if (tot<k)
{
printf("-1\n");
return 0;
}
for (i=n/2;i>=0&&k;i--)
{
p=min(k,a[i]);
k-=p;
x=2*i+1;
for (;p;p>>=1,x=x*x%mod)
if (p&1) ans=ans*x%mod;
}
printf("%lld\n",ans);
}
以上代码参考了sdfzyhx的代码