字符串DP总结


前言:
字符串DP大多数题型为用最小或最大的操作步数,使原串变为新串,因此大多数使用区间DP来解决,部分会进行区间DP优化,下边是一些常见题型。

一、回文字符串多维度变形

1.求字符串中最长回文子序列长度

这是一个区间DP模板题了,代码很容易看懂,这里不在赘述

1.最长回文子序列—区间DP

题目链接
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n=s.size();
        s=" "+s;
        vector<vector<int>>f(n+1,vector<int>(n+1));
        for(int len=1;len<=n;len++)
            for(int i=1;i+len-1<=n;i++)
            {
                int j=i+len-1;
                if(i==j)f[i][i]=1;
                else 
                {
                    f[i][j]=max(f[i+1][j],f[i][j-1]);
                    if(s[i]==s[j])f[i][j]=max(f[i+1][j-1]+2,f[i][j]);
                }
            }
        return f[1][n];
    }
};

2.求字符串中最长回文子串长度

1.最长回文子串—区间DP

题目链接

这题易错点在于不能像大多数区间dp一样直接 l e n = 1 len=1 len=1时使 f [ i ] [ j ] = 1 f[i][j]=1 f[i][j]=1
注意要特判 l e n = 2 且 s [ i ] = s [ j ] len=2且s[i]=s[j] len=2s[i]=s[j]的情况

class Solution {
public:
    int getLongestPalindrome(string s) {
        int n=s.size();
        s=" "+s;
        int res=0;
        vector<vector<int>>f(n+1,vector<int>(n+1));
        for(int len=1;len<=n;len++)
            for(int i=1;i+len-1<=n;i++)
            {
                int j=i+len-1;
                if(s[i]==s[j])
                {
                    if(len<=3)f[i][j]=true;
                    else f[i][j]=f[i+1][j-1];
                }
                else f[i][j]=false;
                if(f[i][j])res=max(res,j-i+1);
            }
        return res;
    }
};

2.小A的回文串—区间DP

题目链接

升级版,加了一个常见的循环处理

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
const int N=5005;
bool f[2*N][2*N];
int main()
{
    string s;
    cin>>s;
    int n=s.size();
    s+=s;
    s=" "+s;
    int res=0;
    for(int len=1;len<=n;len++)
    {
        for(int i=1;i+len-1<=2*n;i++)
        {
            int j=i+len-1;
            if(s[i]==s[j])
            {
                if(j-i<3)f[i][j]=true;
                else f[i][j]=f[i+1][j-1];
            }
            else f[i][j]=false;
            if(f[i][j])res=max(res,j-i+1);
        }
    }
    cout<<res<<endl;
}

3.无权值回文字符串最值问题

1.Palindrome—LCS优化

题目链接

MLE写法:(区间DP)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=5010;
int f[N][N];
int main()
{
    int n;
    string s;
    cin>>n;
    cin>>s;
    s=" "+s;
    for(int len=1;len<=n;len++)
    {
        for(int i=1;i+len-1<=n;i++)
        {
            int j=i+len-1;
            if(len==1)f[i][j]=0;
            else if(s[i]==s[j])
            {
                if(len==2)f[i][j]=0;
                else f[i][j]=f[i+1][j-1];
            }
            else
            {
                f[i][j]=min(f[i+1][j],f[i][j-1])+1;
            }
        }
    }
    cout<<f[1][n]<<endl;
}

滚动数组优化AC写法(这里需要用上一个数学公式)
r e s = n − L C S res=n-LCS res=nLCS

因为字符串和它的反向字符串的最大公共子序列的长度LCS,就等同于在原串中有LCS个字符是对应相等的。答案就是那些对应不相等的在对应位置添加相同的字符。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=5010;
int f[2][N]={0};
int main()
{
	int n;
	cin>>n;
    string a,b;
    cin>>b;a=b;
    reverse(b.begin(),b.end());
    a=" "+a;
    b=" "+b;
    for(int i=1;i<=n;i++)
    {
    	for(int j=1;j<=n;j++)
    	{
    		f[i&1][j]=max(f[i-1&1][j],f[i&1][j-1]);
    		if(a[i]==b[j])f[i&1][j]=max(f[i&1][j],f[i-1&1][j-1]+1);
		}
	}
	cout<<n-f[n&1][n]<<endl;
}

4、有权值回文字符串最值问题

1.Cheapest Palindrome—区间dp

题目链接

d[i][j]:从ij为回文串花费的最小
状态转移方程:
s [ i ] = s [ j ] s[i]=s[j] s[i]=s[j]时:

d [ i ] [ j ] = d [ i + 1 ] [ j − 1 ] d[i][j]=d[i+1][j-1] d[i][j]=d[i+1][j1]

否则

d [ i ] [ j ] = m i n ( d [ i ] [ j − 1 ] + a d d [ s [ j ] ] , d [ i + 1 ] [ j ] + a d d [ s [ i ] ] , d e l [ s [ i ] ] + d [ i + 1 ] [ j ] , d e l [ s [ j ] ] + d [ i ] [ j − 1 ] ) d[i][j]=min(d[i][j-1]+add[s[j]],d[i+1][j]+add[s[i]],del[s[i]]+d[i+1][j],del[s[j]]+d[i][j-1]) d[i][j]=min(d[i][j1]+add[s[j]],d[i+1][j]+add[s[i]],del[s[i]]+d[i+1][j],del[s[j]]+d[i][j1])

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 2010;
int f[N][N];
int n,m;
int add[N],del[N];
int main()
{
    cin>>n>>m;
    string s;
    cin>>s;s=" "+s;
    char op[2];
    int x,y;
    for(int i=0;i<n;i++)
    {
    	cin>>op;
    	cin>>x>>y;
    	add[op[0]-'a']=x;
    	del[op[0]-'a']=y;
	}
	memset(f,0x3f,sizeof f);
	f[0][0]=0;
	for(int len=1;len<=m;len++)
	{
		for(int i=1;i+len-1<=m;i++)
		{
			int j=i+len-1;
			if(s[i]==s[j])
			{
				if(len<=2)f[i][j]=0;
				else f[i][j]=f[i+1][j-1];
			}
			else
			{
				int x=min(f[i+1][j]+add[s[i]-'a'],f[i][j-1]+add[s[j]-'a']);
				int y=min(f[i+1][j]+del[s[i]-'a'],f[i][j-1]+del[s[j]-'a']);
				f[i][j]=min(x,y);
			}
		}
	}
	cout<<f[1][m]<<endl;
}

5.合并回文子串

1.合并回文子串—区间DP

题目链接

相当于在最长回文子串的基础上扩展二维

详细题解入口:传送门

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=110;
void solve()
{
    bool f[N][N][N][N];
    int res=0;
    string a,b;
    cin>>a>>b;
    int n=a.size(),m=b.size();
    a=" "+a,b=" "+b;
    for(int len1=0;len1<=n;len1++)
    {
        for(int len2=0;len2<=m;len2++)
        {
            for(int i=1;i+len1-1<=n;i++)
            {
                for(int k=1;k+len2-1<=m;k++)
                {
                    int j=i+len1-1,l=k+len2-1;
                    if(len1+len2<=1)f[i][j][k][l]=1;
                    else
                    {
                        bool &v=f[i][j][k][l];
                        v=0;
                        if(len1>1)v|=(f[i+1][j-1][k][l]&&(a[i]==a[j]));
                        if(len1&&len2)v|=(f[i+1][j][k][l-1]&&(a[i]==b[l]));
                        if(len1&&len2)v|=(f[i][j-1][k+1][l]&&(a[j]==b[k]));
                        if(len2>1)v|=(f[i][j][k+1][l-1]&&(b[k]==b[l]));
                    }
                    if(f[i][j][k][l])res=max(len1+len2,res);
                }
            }
        }
    }
    cout<<res<<endl;
    
}
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        solve();
    }
}

二、从原串中找子序列

1.不同的子序列—DP

题目链接

y总题解:
在这里插入图片描述

class Solution {
public:
    int numDistinct(string s, string t) {
        int n=s.size(),m=t.size();
        s=" "+s;
        t=" "+t;
        vector<vector<unsigned long long>>f(n+1,vector<unsigned long long>(m+1));
        for(int i=0;i<=n;i++)f[i][0]=1;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
            {
                f[i][j]=f[i-1][j];
                if(s[i]==t[j])f[i][j]+=f[i-1][j-1];
            }
        return f[n][m];
    }
};

2.Nun Heh Heh Aaaaaaaaaaa—动态规划+快速幂+后缀和

题目链接

详细内容请看代码注解

先不考虑a,先算芳香字符串非a个数,最后个数乘以2的a个数次方-1,相加取模。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+5;
typedef long long LL;
int dp[10][N];
int a[N];//a的后缀数量
const int mod = 998244353;


//预处理后缀中a的个数
void init(string s)
{
    memset(a,0,sizeof a);
    for(int i=s.size();i>=1;i--)
    {
        a[i]=a[i+1]+(s[i]=='a');
    }
}
// 这里别不小心用了龟速乘,我刚才不小心用了龟速乘一直TLE
//后面有几个a,每个a可以选也可以不选所以是2的a次方,但a个数不能为0所以-1
LL qmi(int b)
{
    LL res=1%mod;
    LL a=2;
    while(b){
        if(b&1)res=res*a%mod;
        a=a*a%mod;
        b>>=1;

    }
    return (res-1)%mod;
}
int main()
{
    std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
    int T;
    cin>>T;
    while(T--)
    {
        string s;
        cin>>s;
        //使下标从1开始
        s=" "+s;
        init(s);
        
        //string ss=" nunhehheh";
        
        //dp过程 nunhehheh
        //dp[j][i]表示s前i个字符中ss[1~j]字符串出现次数
        LL res=0;
        for(int i=1;i<=s.size();i++)
        {
            dp[1][i]=(dp[1][i-1]+(s[i]=='n'))%mod;
            dp[2][i]=(dp[2][i-1]+dp[1][i-1]*(s[i]=='u'))%mod;
            dp[3][i]=(dp[3][i-1]+dp[2][i-1]*(s[i]=='n'))%mod;
            dp[4][i]=(dp[4][i-1]+dp[3][i-1]*(s[i]=='h'))%mod;
            dp[5][i]=(dp[5][i-1]+dp[4][i-1]*(s[i]=='e'))%mod;
            dp[6][i]=(dp[6][i-1]+dp[5][i-1]*(s[i]=='h'))%mod;
            dp[7][i]=(dp[7][i-1]+dp[6][i-1]*(s[i]=='h'))%mod;
            dp[8][i]=(dp[8][i-1]+dp[7][i-1]*(s[i]=='e'))%mod;
            dp[9][i]=(dp[8][i-1]*(s[i]=='h'))%mod;
            if(dp[9][i])res=res%mod+(dp[9][i]*qmi(a[i+1]))%mod;
            res%=mod;
        }
        cout<<res<<endl;
    }
}

三、字符串划分问题

1.[NOIP2001]统计单词个数—双重DP+字符串处理

题目链接

双重DP
第一个sum[i][j]预处理i到j中所含有的字符串数量
从后往前判断,便于判断第i个字符是否用上写一个check函数
如果第i个字符用上了x.find(a[i])==0返回true

第二个dp算最大值
f[i][j]表示前i个字符分为j段的最大值
f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ k ] [ j − 1 ] + s u m [ k + 1 ] [ i ] ) ( j − 1 < k < i ) f[i][j]=max(f[i][j],f[k][j-1]+sum[k+1][i]) (j-1<k<i) f[i][j]=max(f[i][j],f[k][j1]+sum[k+1][i])(j1<k<i)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210;
int sum[N][N];
int f[N][45];
string a[10];
int n,m,K;
string s;
bool check(int l,int r)
{
    string ss=s.substr(l,r-l+1);
    for(int i=1;i<=n;i++)
        if(ss.find(a[i])==0)return true;
    return false;
}
int main()
{
    cin>>n>>K;
    s=" ";
    while(n--)
    {
        string _;
        cin>>_;
        s+=_;
    }
    m=s.size()-1;
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int r=m;r>=1;r--)
    {
         for(int l=r;l>=1;l--)
         {
             sum[l][r]=sum[l+1][r];
             if(check(l,r))sum[l][r]++;
         }
        f[r][1]=sum[1][r];
    }
    for(int i=2;i<=m;i++)
        for(int j=2;j<=K;j++)
            for(int k=j-1;k<i;k++)
                f[i][j]=max(f[i][j],f[k][j-1]+sum[k+1][i]);
    cout<<f[m][K]<<endl;
}

2.[SCOI2009]粉刷匠

题目链接
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 52;
int f[N][N][N*N][2];
char g[N][N];
//dp[i][j][k][0/1]
//前i条j段涂k次,最后一段涂红/蓝的最多正确格子数
//红(0)蓝(1)
int main()
{
    int n,m,K;
    cin>>n>>m>>K;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>g[i][j];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            for(int k=1;k<=K;k++)
            {
                if(j==1)
                {
                    f[i][j][k][0]=max(f[i-1][m][k-1][0],f[i-1][m][k-1][1])+(g[i][j]=='0');
                    f[i][j][k][1]=max(f[i-1][m][k-1][0],f[i-1][m][k-1][1])+(g[i][j]=='1');
                }
                else
                {
                    f[i][j][k][0]=max(f[i][j-1][k][0],f[i][j-1][k-1][1])+(g[i][j]=='0');
                    f[i][j][k][1]=max(f[i][j-1][k][1],f[i][j-1][k-1][0])+(g[i][j]=='1');
                }
            }
    cout<<max(f[n][m][K][0],f[n][m][K][1])<<endl;
}

四、字符串涂色问题

1.[CQOI2007]涂色PAINT—区间DP

题目链接

f[i][j]为左边界为i,右边界为j的最小涂色次数
如果 s[i]==s[j]
f [ i ] [ j ] = m i n ( f [ i + 1 ] [ j ] , f [ i ] [ j − 1 ] ) f[i][j]=min(f[i+1][j],f[i][j-1]) f[i][j]=min(f[i+1][j],f[i][j1])
否则
f [ i ] [ j ] = m i n ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k + 1 ] [ j ] ) ( i = < k < j ) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]) (i=<k<j) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j])(i=<k<j)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 55;
int n;
char s[N];
int f[N][N];
int main()
{
    cin>> s+1;
    n=strlen(s+1);
    for(int len=1;len<=n;len++)
    {
        for(int i=1;i+len-1<=n;i++)
        {
            int j=i+len-1;
            if(len==1)f[i][j]=1;
            else if(s[i]==s[j])f[i][j]=min(f[i+1][j],f[i][j-1]);
            else
            {
                for(int k=i;k<j;k++)f[i][j]=min(f[i][k]+f[k+1][j],f[i][j]);
            }
        }
    }
    cout<<f[1][n]<<endl;
}

2.小小粉刷匠

题目链接

这个同理,只是限制区间长度了,加个判断就行了

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1010;
int f[N][N];
int s[N];
int main()
{
    int n,K;
    cin>>n>>K;
    for(int i=1;i<=n;i++)cin>>s[i];
    memset(f,0x3f,sizeof f);
    for(int len=1;len<=n;len++)
        for(int i=1;i+len-1<=n;i++)
        {
            int j=i+len-1;
            if(i==j)f[i][j]=1;
            else if(len<=K&&s[i]==s[j])f[i][j]=min(f[i+1][j],f[i][j-1]);
            for(int k=i;k<j;k++)f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
        }
    cout<<f[1][n]<<endl;
}

3.Color Stripe —DP/贪心

题目链接

贪心写法

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 500010;
signed main()
{
	int n,k;
	cin>>n>>k;
	string s;cin>>s;
	int res=0;
	if(k>2)
	{
		int res=0;
		for(int i=1;i<s.size();i++)
		{
			if(s[i]==s[i-1])
			{
				res++;
				for(int j=0;j<k;j++){
					if('A'+j!=s[i] && 'A'+j!=s[i+1]){
						s[i]='A'+j;
						break;
					}
				}
			}
		}
		cout<<res<<endl;
		cout<<s<<endl;
	}
	else 
	{
		string ss1="A",ss2="B";
		
		int res1=0,res2=0;
		res1+=ss1[0]!=s[0],res2+=ss2[0]!=s[0]; 
		for(int i=1;i<s.size();i++)
		{
			ss1+=ss1[i-1]=='A'?'B':'A';
			ss2+=ss2[i-1]=='A'?'B':'A';
			if(ss1[i]!=s[i])res1++;
			if(ss2[i]!=s[i])res2++;
		}
		if(res1<res2)
		{
			cout<<res1<<endl;
			cout<<ss1<<endl;
		}
		else 
		{
			cout<<res2<<endl;
			cout<<ss2<<endl;
		}
	}
}

DP写法

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 500010;
int f[N][28];
int pre[N][28];
signed main()
{
	int n,m;
	cin>>n>>m;
	string s;
	cin>>s;
	s=" "+s;
	memset(f,0x3f,sizeof f);
	for(int i=0;i<m;i++)f[0][i]=0;
	int minv=1e7;
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<m;j++)
		{
			for(int k=0;k<m;k++)
			{
				if(k==j)continue;
				if(j+'A'==s[i])
				{
					if(f[i][j]>f[i-1][k])
					{
						pre[i][j]=k;
						f[i][j]=f[i-1][k];
					}	
				}
				else 
				{
					if(f[i][j]>f[i-1][k]+1)
					{
						pre[i][j]=k;
						f[i][j]=f[i-1][k]+1;	
					}
				}
				
			}
		}
	}
	int res=1e7;
	int t;
	for(int i=0;i<m;i++)
	{
		if(f[n][i]<res)
		{
			res=f[n][i];
			t=i;
		}
	}
	cout<<res<<endl;
	string ss="";
	for(int i=n;i>=1;i--)
	{
		ss+=t+'A';
		t=pre[i][t];
	}
	reverse(ss.begin(),ss.end());
	cout<<ss<<endl;
}
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
### 回答1: 要使一个字符串变成回文串,需要添加的最少字符数取决于字符串本身。如果字符串本身已经是回文串,则不需要添加任何字符。否则,需要添加字符使其变成回文串。 可以使用动态规划来解决这个问题。具体来说,假设字符串为s,长度为n,定义dp[i][j]表示子串s[i...j]需要添加的最少字符数,使得该子串变成回文串。则有以下状态转移方程: 当i = j时,dp[i][j] = 0,因为单个字符本身就是回文串。 当s[i] = s[j]时,dp[i][j] = dp[i+1][j-1],因为如果s[i]和s[j]相等,那么只需要让子串s[i+1...j-1]变成回文串即可。 否则,dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1,因为如果s[i]和s[j]不相等,那么必须要在s[i]和s[j]中间添加一个字符,使得子串s[i...j]变成回文串,那么这个字符可以是s[i],也可以是s[j],所以需要取添加字符后变成回文串所需的最小次数。 最终,dp[0][n-1]就是整个字符串s需要添加的最少字符数,使得它变成回文串。 ### 回答2: 要使字符串str变回文串,需要在字符串的前后添加若干个字符。添加的字符的数量最少是str的长度减去str的回文子串的长度。 首先,我们需要找出str的最长回文子串的长度。可以通过遍历字符串的所有子串,判断是否为回文串,从而得到最长回文子串的长度。 接下来,将str的长度减去最长回文子串的长度,得到需要添加字符的数量。因为添加的字符需要同时出现在str的前后,所以数量是原字符串长度和最长回文子串长度的差值。 举个例子,假设str为"abcbade",该字符串的最长回文子串为"abcba",长度为5。字符串str的长度为7,所以需要添加的字符数量为7-5=2。 所以需要添加至少2个字符才能使字符串str变回文串。 ### 回答3: 已知字符串str,如果它本身就是一个回文串,那么不需要添加任何字符;如果str不是一个回文串,我们需要添加至少的字符使它变成一个回文串。 为了使str变成一个回文串,我们可以在字符串的某个位置添加字符。考虑一个最简单的情况,我们将添加字符在str的最前面,即在str的首字符前添加一个与str首字符相同的字符。这样,str的首字符和最前添加的字符都是相同的,这种情况下,我们只需要添加一个字符。 但是,如果str的首字符和尾字符不相同,我们必须至少添加两个字符。我们可以在str的首字符前添加一个与str首字符相同的字符,再在str的尾字符后添加一个与str尾字符相同的字符。这样,在添加的两个字符的帮助下,str首字符和最前添加的字符相同,str尾字符和最后添加的字符相同,形成了一个回文串。 所以,总结起来,我们需要至少添加的字符数就等于str的首字符和尾字符不相同的情况下需要添加的字符数。即如果str的首字符和尾字符相同,我们只需要添加一个字符;如果不相同,我们需要添加两个字符。 综上所述,已知字符串str,至少需要添加的字符数为1或2,具体取决于str的首字符和尾字符是否相同。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_WAWA鱼_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值