【动态规划】字符串类型动态规划

这里总结一下字符串类型的动态规划问题,一般涉及最值问题,方案数问题都可以使用动态规划来解决

最短编辑距离

这个问题显然是一个最优解问题,对于最优解问题我们可以考虑动态规划解题,因此我们得先考虑子问题,既从a[1...i]到b[1..j]的最短编辑距离。

我们设状态变量:

        f [i][j] = 表示从a[1...i]到b[1...j]的编辑距离

        那么我们每一次的修改,删除,添加都会使编辑距离增长,同时也会影响的字符的状态,那么我们就要选择一个最优的操作这样才能保证最后的结果最优,那么如何选择操作呢?

        当a[i] = b[j] 时,既当两个字符匹配相同的时候,说明我们这步不需要任何操作,那么我们只要从上一次状态既f[i-1][j-1]这个操作次数转移过来即可

        当a[i] != b[j]时,既当两个字符不相等,那么我们就应该从修改,插入,删除里面选一个最优状态来作为当前状态,那我们如何用状态方程来表示修改,插入,删除呢?

        我们以 L O V T ----> L O V E 为例

修改操作:

                        比如,当a[i](T) != b[j](E) 的时候,我们知道的是LOV其实已经拼凑出来了,也就是说我们要拿到拼出LOV的编辑次数再加上将T--->E的修改操作次数就等于最后一步为修改操作时的编辑距离,用状态方程既为 f[i][j] = f[i-1][j-1]+1

删除操作 :

                如果是通过删除操作得到的LOVE,那也就是说我们前一个状态LOV已经将LOVE拼凑出来了,现在只要将T删除就得到了LOVE,用状态方程可以表示为:f[i][j] = f[i-1][j]+1

插入操作:

        如果最后一步操作为插入操作,也就是我们只要将LOVT--->LOV的操作次数再加一步插入E即可得到LOVE,用状态方程可以表示为:f[i][j] = f[i][j-1]+1

    因此我们将状态方程分为两大类:

        当 a[i] == b[j] 时,f[i][j] = f[i-1][j-1]

        当a[i] !=b[j]时:

                               修改:f[i][j] = f[i-1][j-1]+1

                               删除:f[i][j] = f[i-1][j]+1

                               插入:f[i][j] = f[i][j-1]+1

         然后在这三个状态里面选取一个最小值作为当前的最优状态

public class Main {
	public static void main(String[] args) {
		Scanner sr = new Scanner(System.in);
		String s1 = sr.next();
		String s2 = sr.next();
		int n1 = s1.length();
		int n2 = s2.length();
		String[] t1 = s1.split("");//将字符转化为字符数组
		String[] t2 = s2.split("");
		int f[][] = new int[n1+1][n2+1];//开设状态方程数组 f[i][j] 代表将前i个字符编辑为前j个字符
		for(int i=1;i<=n2;++i) f[0][i] = i;//边界条件初始化
		for(int i=1;i<=n1;++i)f[i][0] = i;
		for(int i=1;i<=n1;++i) {//从第一个字符开始遍历
			for(int j=1;j<=n2;++j) {
				if(t1[i-1].equals(t2[j-1])) {//比较两个字符是否相等
					f[i][j] = f[i-1][j-1];//如果相等直接从上一个字符转移过来
				}else {
					f[i][j]=Math.min(f[i-1][j]+1, Math.min(f[i][j-1]+1, f[i-1][j-1]+1));//如果不相等则从修改删除插入中选一个最优解
				}
			}
		}
		System.out.print(f[n1][n2]);//打印出最后结果
	}
}

从状态方程我们可以发现,第i个状态只与i-1的状态相关,因此我们可以通过滚动数组优化空间复杂度,将空间复杂度从mn降为n

但要注意的是边界条件的初始化要有所调整,其他情况都是一样的

public class Main {
	public static void main(String[] args) {
		Scanner sr = new Scanner(System.in);
		String s1 = sr.next();
		String s2 = sr.next();
		int n1 = s1.length();
		int n2 = s2.length();
		String[] t1 = s1.split("");//将字符转化为字符数组
		String[] t2 = s2.split("");
		int f[][] = new int[2][n2+1];//开设状态方程数组 f[i][j] 代表将前i个字符编辑为前j个字符
		for(int i=1;i<=n2;++i) f[0][i] = i;//边界条件初始化
		for(int i=1;i<=n1;++i) {//从第一个字符开始遍历
			for(int j=0;j<=n2;++j) {
				int now = i%2;//通过将i%2进行滚动
				if(j==0) {
					f[now][j] = f[1-now][j]+1;
					continue;
				} 
				if(t1[i-1].equals(t2[j-1])) {//比较两个字符是否相等
					f[now][j] = f[1-now][j-1];//如果相等直接从上一个字符转移过来
				}else {
					f[now][j]=Math.min(f[1-now][j]+1, Math.min(f[now][j-1]+1, f[1-now][j-1]+1));//如果不相等则从修改删除插入中选一个最优解
				}
			}
		}
		System.out.print(f[n1%2][n2]);//打印出最后结果
	}
}

最长公共子序列

         由于是求最长公共子串,那么我们可以通过先求出较短串的最长公共子串,然后自底向上求出原问题的最长公共子串,比如要求解abccd---aecd的长度,那么我们可以先求出abc--aec的最长公共子串进而推到abccd---aecd的公共子串的长度,也就是说这题是可以用动态规划来求解的,因此我们得写出状态转移方程,我们用f[i][j]表示a[1..i]到b[1..j]的最长公共子串,此时存在两种情况:

        1、当a[i] == b[j]时:

        也就是此时字符匹配上了,那么就应该增加公共子串的长度,将前一个字符的最长公共子串的长度+1,既f[i][j] = f[i-1][j-1]+1

        2、当a[i] != b[j]时:

          此时字符并没有匹配上,我们应该把前一个字符匹配的结果直接转移过来就行了,但这里存在两种情况:

                既当a[i]在公共子串中时:f[i][j]=f[i][j-1]

                当b[j]在公共字串中:f[i][j]=f[i-1][j]

因此我们应该在这两种情况里面取一个最大值作为当前状态的最优值:

                f[i][j]=Math.max(f[i][j-1],f[i-1][j]) 

所以状态方程应为:

             当a[i] != b[j] :f[i][j] = f[i-1][j-1]+1

             当a[i] != b[j]时:  f[i][j]=Math.max(f[i][j-1],f[i-1][j]) 

由于我们每一次的状态都由上一步的状态转移过来,因此我们可以通过滚动数组来优化代码,将时间复杂度由O(MN)优化为O(N)

import java.util.Scanner;

public class 最长公共子序列 {
	/*
	 * 思路:
	 * 		定义两个字符串,我们要分别比较两字符是否相等:
	 * 			1、如果相等则从上一个字符转移并且加1
	 * 			2、如果不相等那么直接从上一个字符转移	
	 */
	public static void main(String[] args) {
		Scanner sr = new Scanner(System.in);
		String s1 = sr.next();
		String s2 = sr.next();
		int m1 = s1.length();
		int m2 = s2.length();
		String[] t1 = s1.split("");
		String[] t2 = s2.split("");
		int [][] f = new int[2][m2+1];//定义状态方程 f[i][j] 代表a[1..i]到b[1..j]的最长公共子序列
		for(int i=1;i<=m1;++i) {
			int now = i%2;//定义滚动数组
			for(int j=1;j<=m2;++j) {
				if(t1[i-1].equals(t2[j-1])) {//第i个字符和第j个字符相等
					f[now][j] = f[1-now][j-1]+1;
				}else {//不相等时两种情况取最大值
					f[now][j] = Math.max(f[1-now][j],f[now][j-1]);
				}
			}
		}
		System.out.print(f[m1%2][m2]);
	}
}

数组切分--JavaB G

        我们这题求的是一段数组能被切分的方案数,假设我们求解的是前i个数的方案数,这里就要分为两种情况来看:

        1、如果我们把第i个字符看成单独一份,那么我们就要将前i-1个数的方案数转移过来

        2、如果我们将第i个字符与前i-1,i-2,i-3看成一段,那么我们就要判断[i..i-1,i-2,i-3]这个区间是否连续,如果连续的话,说明这一段是可以单独看成一段,那么我们就应该把这种情况加上,这样我们就可以往前一直枚举并判断枚举的[i...i-t..1]这段是否连续,如果连续的话就将该情况加上,否则就pass

        因此我们就可以通过动态规划来解决这个问题,我们假设f[i]为第i位数切分的方案数,假设原问题数组长度为m,那么我们可以通过从f[1]一直递推到f[m],至底向上求解问题

状态方程:

        f[i] = 第i位数切分数组的方案总数

        但这里要注意一个问题,我们怎么才能判断[i..i-t]这段是否连续呢,假设我们现在要求解前i个数的方案数,当我们判断[i..i-1]这段是否连续时,我们只需要判断这段的max-min是否等于整个区间的长度-1如果等于则代表这段是连续的,比如4 5 6 这段的max=6,min=4 则6-4 = 3-1说明这段是连续的,因此我们在往前枚举的时候得记录这个区间的最大值和最小值,这样我们才能判断这个区间是否是连续的。

public class Main {
	public static void main(String[] args) {
		Scanner sr = new Scanner(System.in);
		int N = sr.nextInt();
		int mod = 1000000007;//对结果求模,防止结果过大
		int [] num = new int[N+1];
		int [] dp  = new int[N+1];
		dp[0] = 1;
		for(int i=1;i<=N;++i) {
			num[i] = sr.nextInt();
		}
		//1 3 2 4  
		for(int i=1;i<=N;++i) {//往前递推自底向上求解
			boolean flag = true;
			dp[i] =(dp[i]+dp[i-1])%mod;//将第i个字符单独看成一段,此时就将i-1的方案数直接转移过来
			int max = num[i];
			int min = num[i];
			for(int j=i-1;j>=1;--j) {
				if(num[j]>max)max=num[j];
				if(num[j]<min)min=num[j];
				if(i-j==max-min){//表示这一段连续,那么就将[i..i-t]这段当成一段直接加上
					dp[i] =(dp[i]+dp[j-1])%mod;
				}
			}
		}
		System.out.println(dp[N]);//输出结果,表示第N位长度能切分数组的方案数
	}
}

子串---NOIP2015

         子串(原题)

        这题其实跟上面的数组切分思考维度是一致的,这题不同的点在于将一个字符串提取k个字符与另一个字符串相匹配的方案数,我们假设在a[1..i]中选k段与b[1..j]匹配,那么我们有两种可能:

        1、如果a[i] != b[j] 说明最后一个字符不匹配,那么这个字符肯定不会在匹配的字符中出现,我们就应该从上一个字符i-1(a[1..i-1])到j b[1...j]且匹配k次的方案转移过来。

        2、如果a[i]==b[j]说明最后一个字符是匹配的,我们可以将最后一个字符单独看成一段,那么我们只要从a[1..i-1]选出k-1段与b[1...j-1]相匹配就ok了,但是可能不止一个字符是匹配上的,比如 i-2与j-2匹配,i-t与j-t匹配,那么最坏的可能是匹配的字符长度为n,那么这里就得再枚举一遍n,因此我们必须考虑优化,这里我们可以单独拿一个数组res[i][j][k](代表前i个字符取出k个字符与前j个字符匹配并且第i个字符与第j个字符相等)来记录尾部字符匹配的方案数,当a[i] == b[j]时res[i][j][k]=上一个字符取k-1次与第j-1个字符匹配的方案数+res[i-1][j-1][k],如果a[i] != b[j],直接将res记为0

import java.util.Scanner;
/*
 * 思路:
 * 		s1 字符和 s2字符进行匹配
 * 			当每往前枚举一个s1字符的时候都要对s2前面的字符进行匹配,因为要找出的是与b相等的字串,那么如果新更新的字符与b字符的尾部不相等的话
 * 			那么当前的状态一定是由上一个状态转移过来的,还有一种情况是如果相等,那么当前状态应该是前一个字符匹配k次相等的状态+上几个字符匹配
 * 			相等的状态量之和,现在问题就是如何表示上几个字符匹配相等的状态量之和,这里我们不妨开辟一个数组res,记录前几个字符相等情况下状态之和
 * 			通过以上分析 我们不难发现下一层的状态只与上一层的状态有关系,那么此时可以使用滚动数组进行更新
 * 		res :
 * 			i,j,k 表示前i个字符,匹配到j段,选取p次的次数,该数组由两部分更新来:
 * 				1、f[i-1][j-1][k-1] 此时匹配到的字符单独算一段,所以前面应该是p-1段,所以当前的方案数应该是f [i-1][j-1][k-1]
 * 				2、	
 */
public class Main {
	public static void main(String[] args) {
		Scanner sr = new Scanner(System.in);
		int m = sr.nextInt();//A 字符串的长度 abcadb
		int n = sr.nextInt();//B 字符串的长度 ab
		int p = sr.nextInt();//从A 中取出p个互不重叠的字符串
		String s1 = sr.next();//s1字符串
		String s2 = sr.next();//s2字符串		
		int [][][] f = new int[2][n+1][p];//定义一个状态数组,表示前m个字符匹配到了前n个字符,取出p次的状态量
 		int [][][] res = new int[2][n+1][p];//定义一个状态数组,记录m个字符匹配到了前n个字符,取出p次中存在相等字符的情况之和
 		for(int i=1;i<=m;++i) {//第一层枚举m次
 			int now = i%2;//利用now进行滚动更新
 			for(int j=1;j<=n;++j) {//第二层枚举n次
 				for(int k =1;k<=p;++k) {//第三层枚举k
 					if(s1.charAt(i-1)!=s1.charAt(j-1))res[i][j][k] = 0;//不匹配的时候直接把res赋值为0
 					else {
 						res[i][j][k] = res[i-1][j-1][k]+f[i-1][j-1][k-1];//匹配到的时候,之前(i-1,j,k)的方案数+前m个字符匹配相等的方案数的总和
 					}		
 					f[i][j][k] = f[i-1][j][k]+res[i][j][k];
 				}
 			}
 		}
 		System.out.print(f[m][n][p]);
	}	
}

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值