前言
在使用动态规划解决问题时,最关键的一步就是列出状态转移方程,完善代码一般就是水磨工夫了。但是状态转移方程并不好列,我们需要将问题拆分成子问题,并找到如何用子问题的解递推我们当前问题的解。
要找到状态转移方程,首先我们要找到一种最合适的方法去表示状态,或者说中间结果,然后找到这样表示状态时状态间是如何递推的。这两步都很关键。
简单的DP问题其状态转移方程往往是很容易看出的,当前的结果一般是之前结果的加减乘除或者逻辑运算。而复杂一些的DP问题就会转几个弯,一般是无法直接从中间结果得到,需要根据具体的问题做一些问题的转化。
分情况讨论就是一种常见的思路。
例题
行程长度编码 是一种常用的字符串压缩方法,它将连续的相同字符(重复 2 次或更多次)替换为字符和表示字符计数的数字(行程长度)。例如,用此方法压缩字符串 “aabccc” ,将 “aa” 替换为 “a2” ,“ccc” 替换为` “c3” 。因此压缩后的字符串变为 “a2bc3” 。
注意,本问题中,压缩时没有在单个字符后附加计数 ‘1’ 。
给你一个字符串 s 和一个整数 k 。你需要从字符串 s 中删除最多 k 个字符,以使 s 的行程长度编码长度最小。
请你返回删除最多 k 个字符后,s 行程长度编码的最小长度 。
示例 1:
输入:s = "aaabcccd", k = 2
输出:4
解释:在不删除任何内容的情况下,压缩后的字符串是 "a3bc3d" ,长度为 6 。最优的方案是删除 'b' 和 'd',这样一来,压缩后的字符串为 "a3c3" ,长度是 4 。
示例 2:
输入:s = "aabbaa", k = 2
输出:2
解释:如果删去两个 'b' 字符,那么压缩后的字符串是长度为 2 的 "a4" 。
示例 3:
输入:s = "aaaaaaaaaaa", k = 0
输出:3
解释:由于 k 等于 0 ,不能删去任何字符。压缩后的字符串是 "a11" ,长度为 3 。
分析
这类问题,如果动态规划接触的比较多的话,很容易就会想到用dp[i][j]来保存中间结果,dp[i][j]表示前i个字符中删除j个字符得到的行程长度编码的最小长度。
那么如何写出状态转移方程呢?
我们考虑当前的状态,dp[i][j]是指删除j个字符得到的行程长度编码的最小长度,显然j满足j <= i
,对于这i个字符,每个字符都有两种可能,删除或者保留,那么是否有一个特殊位置的字符能被用来进行求解当前问题呢?
我们考虑当前要处理的字符串的最后一个字符,即第i个字符。对于这个字符,有两种处理方式,删除或者保留。让我们尝试一下这两种情况下能否通过之前的结果递推出当前所求的dp[i][j]。
- 当我们要删除第i个字符时,那么前i-1个字符中我们要删除j-1个字符,同时最终得到的字符串是与前i-1个字符中删除j-1个字符的结果相同,即若我们删除第i个字符,则dp[i][j]=dp[i-1][j-1];
- 当我们保留第i个字符时,显然最终结果中的最后一组字符数字组合,字符一定是第i个字符。那么我们所需要关注的,就是此时有哪些可能能得到dp[i][j],很容易想到,所有的可能为最后一组字符数字组合中数字的个数不同,随着个数的不同,我们会删除掉从第i个字符往前遍历的不同个数的第i个字符中间夹杂的其他字符,每一种情况的dp[i][j] = dp[i-l][j-del] + len(最后一组字符数字组合的长度)
- 举个例子:对于字符串
asdasfasdfasfaaaadfsafaa
删除j个,对于不保留最后一个字符的情况,显然等价于在asdasfasdfasfaaaadfsafa
删除j-1个,对于保留最后一个字符的情况,编码后最后的一组字符数字组合显然是ax
,x为a连续出现的次数,那么x能等于多少呢?显然就需要向前遍历,若x=2,则需要删除后两个a中间的所有字符,若x=3,则需要删除后三个a中间的所有字符…找出所有结果中的最小结果,即完成了这次DP。
代码
class Solution {
public int getLengthOfOptimalCompression(String s, int k) {
int l = s.length();
int[][] dp = new int[l+1][k+1];
//初始化dp数组
for(int[] i : dp) {//初始化为最大值
Arrays.fill(i, l);
}
//初始化第一列,当删除0个字符时的行程长度编码长度
dp[0][0] = 0;
int t1 = 1;
int t2 = 0;
for(int i = 1; i <= l; i++) {
dp[i][0] = t2 + len(t1);
if(i < l && s.charAt(i-1) == s.charAt(i)) {
t1++;
} else {
t2 += len(t1);
t1 = 1;
}
}
//DP
for(int i = 1; i <= l; i++) {
for(int j = 1; j <= i && j <= k; j++) {
dp[i][j] = dp[i-1][j-1]; //删除第i个字符
//不删除第i个字符
int del = 0;
int cnt = 0;
for(int m = i; m > 0; m--) { //枚举最后一对字符数字组合的所有可能
if(s.charAt(m-1) == s.charAt(i-1)) {
cnt++;
dp[i][j] = Math.min(dp[i][j], dp[m-1][j-del] + len(cnt));
} else {
del++;
if(del > j) {
break;
}
}
}
}
}
return dp[l][k];
}
private int len(int x) {
return x == 1 ? 1 : (x < 10 ? 2 : x < 100 ? 3 : 4);
}
}