zoj2744 Palindromes 字符串的题

题意是求给一个字符串算出其中有多少个回文子串(每个回文子串必须是连续的),关于这题由于数据范围才5000,所以可以用O(N*N)的动态规划去做或者是用暴力的搜索,每次枚举回文串的对称中点复杂度不超过O(N*N),但是用dp做就很蛋疼了,因为内存限制存不了所有的状态,所以得用滚动数组去存,下面将给出几种不同实现的代码。如果暴力枚举中点方法确实很简单,但我还是建议,写一下dp,/可以锻炼一下代码能了,或者说是扩展一下思维吧  这题有成型的算法是MANACHER算法。。

***********************************************************

强烈建议看一下这题,也是回文串不过就只能dp了点击打开链接,dp的状态转移方程十分相似,但难度不一样

*************************************************************

我的动态规划代码,因为这题和我前不久做过的一题很像,所以就套用了一下思路,写了一个dp式 dp[i][j]=dp[i][j-1]+dp[i+1][j]-dp[i+1][j-1] ,另外如果从i到j的子串也是回文串则dp[i][j]+=1;dp[i][j] 表示从i到j内有多少个满足的回文子串。举例,如ch[10000]=asa则dp[0][1] 的值是2,有a和s 2个回文子串 又dp[1][1]=1,dp[1][2]=2且 ch[0]=ch[2]同时有ch[1]到ch[1]也是回文子串     所以 d[0][2]=dp[0][1]+dp[1][2]-dp[1][1]+1=4

强烈建议看一下这题,也是算回文串不过就只能dp了点击打开链接

有了这个递推式要是写记忆化搜索是很快的,于是我就有了如下代码(别急着ctrl+c 代码不会AC的)

#include <cstdio>
#include<cstring>
#include <bitset>
using namespace std;
int dp[5010][5010],len;
char ch[5010];
bitset<5010> vis[5010];//用这个bitset做标记实在是太好了,很省内存
int memdp(int i,int j)//这个是记忆化搜索的代码
{
	if(i>j) return 0;
	if(i==j) return vis[i][j]=dp[i][j]=1;
	if(dp[i][j])return dp[i][j];
	dp[i][j]=memdp(i+1,j)+memdp(i,j-1)-memdp(i+1,j-1);
	if(ch[i]==ch[j])
	{
		if(i+1==j){vis[i][j]=1;return dp[i][j]=3;}//当i和j相邻时可以直接返回结果
		if(vis[i+1][j-1])//若从i+1到j-1 是回文串则说明从i到j也是一个回文串了,因为已经判了ch[i]==ch[j]
		{
			vis[i][j]=1;//标记从i到j是回文串
			return ++dp[i][j];//返回
		}
	}
	return dp[i][j];
}
int main()
{
	while(~scanf("%s",ch))
	{
		memset(dp,0,sizeof(dp));
		memset(vis,0,sizeof(vis));
		len=strlen(ch);
		printf("%d\n",memdp(0,len-1));
	}
    return 0;
}



虽说上面的代码,还算简洁好懂,写起来也不难,但是没办法那个dp数组太大一下就MLE了,悲剧啊,于是我又想到了,用滚动数组。好吧说写就写用滚动数组优化内存

于是就有了以下的代码,实际上我还做了另一个优化,没有了标记数组,就是上面的代码中的vis数组,我把dp数组的个位数字给用成标记了,把dp数组的值整除10,才是答案,

想想这下总该能A了吧,其中的j%2就是滚动数组了,因为这样后dp[i][j]的实际位置就不断在dp[i][0]和dp[i][1]之间滚来滚去,滚来滚去了。。。

#include<cstdio>
#include<cstring>
char ch[5012];
int dp[5010][2];
int memdp(int i,int j)//记忆化搜索运用了滚动数组,就是把j每次用2取模,就可以了
{
	if(i>j) return 0;
	if(i==j) return dp[i][j%2]=11;
	dp[i][j%2]=memdp(i+1,j)/10+memdp(i,j-1)/10-memdp(i+1,j-1)/10;//因为个位是标记故不能直接相加,得除以10才行
	dp[i][j%2]*=10;//乘以10,即把个位标记变成0,恢复dp这个数的意义
	if(ch[i]==ch[j])
	{
		if(i+1==j){return dp[i][j%2]=31;}
		if(dp[i+1][((j-1)+2)%2]%10)//取出标记进行判定
		{
			dp[i][j%2]++;
			return dp[i][j%2]+=10;
		}
	}
	return dp[i][j%2];
}
int main()
{
	while(~scanf("%s",ch))
	{
		memset(dp,0,sizeof(dp));
		int len=strlen(ch);
		printf("%d\n",memdp(0,len-1)/10);
	}
    return 0;
}

但是,我又再次悲剧的TLE了,想了想可能是记忆化搜索是递归的太慢了,没办法,改吧,改成非递归的,这样会不会就能A了呢.改啊改就有了一下代码

#include<cstdio>
#include<cstring>
char ch[5012];
int dp[5010][2];
int main()
{
	while(~scanf("%s",ch))
	{
		memset(dp,0,sizeof(dp));
		dp[0][0]=11;
		int len=strlen(ch);
		for(int j=1;j<len;j++)//用记忆化搜索改成的非递归形式
		{
			int jm2=j%2;//这个是为了不TLE用的
			int jj2=!jm2;//这个也是为了省时间的,其实这个是(j-1)%2
			for(int i=j;i>=0;i--)
			{
				if(i==j) dp[i][jm2]=11;
				else
				{
					dp[i][jm2]=dp[i+1][jm2]/10+dp[i][jj2]/10-dp[i+1][jj2]/10;
					dp[i][jm2]*=10;
					if(ch[i]==ch[j])
					{
						if(i+1==j)
							dp[i][jm2]=31;
						else if(dp[i+1][jj2]%10)
						{
							dp[i][jm2]++;
							dp[i][jm2]+=10;
						}
					}
				}

			}
		}
		printf("%d\n",dp[0][(len-1)%2]/10);
	}
    return 0;
}
功夫不负有心人终于AC了,别提心情有多爽啦,哈哈。。。

这个过程可以说是非常地蛋疼啊,但是我感觉确实锻炼了一点代码能力,和优化技巧,如其中的记忆化搜素,写起来很简单,但因为是递归所以会慢一点,所以得会改成非递归的算法,而且滚动数组可以节省大量的内存,这个必须会,另外那个把标记数组和别的数组压缩合在一起的方法其实很好的,有时候很方便,另外stl里面的bitset也是很好用的,十分节省内存,相信你看完这篇博客,还是能有所收获的

下面贴一个我同学的动态规划的代码,他的比我的要快,但比我的要难懂一点,他没有把我的dp数组保存下来,而是直接累加在ans的全局变量里面了,

思路:用f(i,j)代表str[i....j]是不是回文串,容易得出递推式f(i,j)=f(i+1,j-1) && str[i]==str[j],然后将所有的f(i,j)加起来就是答案了。(注意f(i,i)=1和i+1>j-1的情况)

但是这个题目开5000*5000的数组会MLE。于是注意到每次f(i,j)只需要用到f(i+1,j-1),即它的前两层的状态,所以只要开一个5000*3的滚动数组就可以了。

#include<cstdio>
#include<cstring>
#include<iostream>
#define MAXN 5005
using namespace std;
int f[MAXN][3],n,ans;
char s[MAXN];
void getf()
{
	n=strlen(s),ans=0;
	for(int k=0;k<n;++k)
		for(int i=0;i+k<n;++i)
		{
			int j=i+k,now=k%3,pre2=(now-2+3)%3;
			f[i][now]=(k==0 || (k==1 && s[i]==s[j]) || (i+1<=j-1 && s[i]==s[j] && f[i+1][pre2]));
			ans+=f[i][now];
		}
}
int main()
{
	while(~scanf("%s",s))
	{
		getf();
		cout<<ans<<endl;
	}
}


另外附上最简单的枚举对称中心的代码,相信你若能看懂我的代码,那么下面的代码,就是小菜了吧。。

#include<cstdio>
#include<iostream>
#include<cstring>
#define maxn 5009
using namespace std;
char str[maxn];
int main()
{
	int i,j,len,ans,temp;
	while(scanf("%s",&str)!=EOF)
	{
		ans=len=strlen(str);
		for(temp=0;temp<len;temp++)
		{
			i=temp;
			j=temp-1;
			while(str[i]==str[j] && i<len && j>=0)
			{
				ans++;
				i++;j--;
			}
			i=temp+1;
			j=temp-1;
			while(str[i]==str[j]  && i<len && j>=0)
			{
				ans++;
				i++;
				j--;
			}
		}
		cout<<ans<<endl;
	}
	return 0;
}




你可能感兴趣的:(zoj2744 Palindromes 字符串的题)