作者推荐
本文涉及知识点
LeetCode 1531. 压缩字符串 II
行程长度编码 是一种常用的字符串压缩方法,它将连续的相同字符(重复 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 。
提示:
1 <= s.length <= 100
0 <= k <= s.length
s 仅包含小写英文字母
动态规划
预处理
将s转成arr,每个元素是{字符,长度}。
比如:aabbaa变成{{‘a’,2},{'b",2},{‘a’,2}}
长度0,表示0个字符。长度1,表示1个字符。长度2,表示2到9.长度3,表示10到99,长度4,表示100及以上。
动态规划的状态表示
pre[j] 表示处理完arr[0,i)后, 用去j个字符的最短行程码。
dp[j] 表示处理完arr[0,i]后, 用去j个字符的最短行程码。
pre2[ch][j][m] 表示处理完arr[0,i)后,,以ch+'a’结尾,用去j个字符,最后有m个ch的最短行程码。
dp2表示处理完arr[0,i]…
动态规划的转移方程
arr[i]没有和前面的元素合并:
枚举j,枚举减少长度:0、1、2、3、4
arr[j]和前面的合并:
枚举j,m 再枚举减少长度:0、1、2、3 、4
合并示例:aad d ‾ \underline{dd}ddaa 删除dd后,就是4个aa了。
动态规划的初始状态
pre[0]=0,其它100。
pre2全部100。
动态规划的填表顺序
i从小到大。
动态规划的返回值
pre.back().back()
代码
核心代码
class Solution { public: int getLengthOfOptimalCompression(string s, int k) { const int lenArr = s.length(); vector<pair<char, int>> arr; for (int left = 0, i = 0; i <= s.length(); i++) { if ((i >= s.length()) || (s[left] != s[i])) { arr.emplace_back(s[left], i - left); left = i; } } vector<int> vLen = { 0,1,2,10,100 }; auto GetCodeLen = [&vLen](int len) { int i = vLen.size() - 1; for (; (i >= 0) && (len < vLen[i]); i--); return i; }; auto MaxLen = [&vLen](int len) { return vLen[len + 1] - 1; }; vector<int> pre(lenArr + 1, 100); pre[0] = 0; vector<vector<vector<int>>> dp3(26, vector<vector<int>>(lenArr+1, vector<int>(lenArr + 1, 100))); for (const auto& [ch, cnt] : arr) { vector<int> dp(lenArr + 1, 100); auto& dp2 = dp3[ch - 'a']; auto pre2 = dp2; auto Update = [&lenArr,&dp,&dp2](int j, int iCodeLen,const char& chEnd,int iEndLen) { if (j > lenArr) { return; } dp[j] = min(dp[j], iCodeLen); if (iEndLen <= lenArr) { dp2[j][iEndLen] = min(dp2[j][iEndLen], iCodeLen); } }; //处理没合并 for (int j = 0; j <= lenArr; j++) { const int curCodeLen = GetCodeLen(cnt); Update(j + cnt, pre[j] + curCodeLen,ch,cnt); for (int curCodeLen2 = curCodeLen - 1; curCodeLen2 >= 0; curCodeLen2--) {//处理 行程妈缩短1,2... Update(j + MaxLen(curCodeLen2), pre[j] + curCodeLen2,ch, MaxLen(curCodeLen2)); } } for (int j = 0; j <= lenArr; j++) { for (int m = 0; m <= j; m++) { const int curCodeLen = GetCodeLen(cnt+m ); Update(j + cnt, pre2[j][m] - GetCodeLen(m) + GetCodeLen(m + cnt), ch, m + cnt); for (int curCodeLen2 = curCodeLen - 1; curCodeLen2 >= 0; curCodeLen2--) {//处理 行程妈缩短1,2... Update(j -m + MaxLen(curCodeLen2), pre2[j][m] - GetCodeLen(m) + curCodeLen2,ch, MaxLen(curCodeLen2)); } } } pre.swap(dp); } return *std::min_element(pre.begin() + pre.size() - k-1, pre.end()); } };
测试用例
template<class T> void Assert(const T& t1, const T& t2) { assert(t1 == t2); } template<class T> void Assert(const vector<T>& v1, const vector<T>& v2) { if (v1.size() != v2.size()) { assert(false); return; } for (int i = 0; i < v1.size(); i++) { Assert(v1[i], v2[i]); } } int main() { string s; int k; { Solution sln; s = "aaa", k = 2; auto res = sln.getLengthOfOptimalCompression(s, k); Assert(1, res); } { Solution sln; s = "aaab", k = 2; auto res = sln.getLengthOfOptimalCompression(s, k); Assert(2, res); } { Solution sln; s = "aaabcccd", k = 2; auto res = sln.getLengthOfOptimalCompression(s, k); Assert(4, res); } { Solution sln; s = "aabbaa", k = 2; auto res = sln.getLengthOfOptimalCompression(s, k); Assert(2, res); } { Solution sln; s = "aaaaaaaaaaa", k = 0; auto res = sln.getLengthOfOptimalCompression(s, k); Assert(3, res); } { Solution sln; s = "spnskpulpsiqagreoajsltdrdlnpsdqapmsdlnlirasgfijafeoqjnddpaifsqpghshclqummgootsmkcgneofrkboirkplqijoi", k = 25; auto res = sln.getLengthOfOptimalCompression(s, k); Assert(3, res); } }
动态规划优化
前一个解法的空间复杂度在过与不过的边缘。
动态规划的状态表示
dp[i][j] 表示处理了arr[0,i),选择了j个字符的最短行程码。
动态规划的转移方程
分两种情况: 和前面的项目合并,和前面的项不合并。细节同上。
动态规划的初始值
dp[0][0]=0,其它100。
动态规划的填表顺序
i从小到大,j从小到大。
动态规划的返回值
dp.back的后k+1个元素的最小值。
优化后的代码
class Solution { public: int getLengthOfOptimalCompression(string s, int k) { const int lenArr = s.length(); vector<pair<char, int>> arr; for (int left = 0, i = 0; i <= s.length(); i++) { if ((i >= s.length()) || (s[left] != s[i])) { arr.emplace_back(s[left], i - left); left = i; } } vector<int> vLen = { 0,1,2,10,100 }; auto GetCodeLen = [&vLen](int len) { int i = vLen.size() - 1; for (; (i >= 0) && (len < vLen[i]); i--); return i; }; auto MaxLen = [&vLen](int len) { return vLen[len + 1] - 1; }; vector<vector<int>> dp(arr.size() + 1, vector<int>(lenArr + 1, 100)); dp[0][0] = 0; int i = -1; for (const auto& [ch, cnt] : arr) { i++; auto& pre = dp[i]; auto& cur = dp[i + 1]; auto Update = [&lenArr, &cur](int j, int iCodeLen) { if (j > lenArr) { return; } cur[j] = min(cur[j], iCodeLen); }; //处理没合并 for (int j = 0; j <= lenArr; j++) { const int curCodeLen = GetCodeLen(cnt); Update(j + cnt, pre[j] + curCodeLen); for (int curCodeLen2 = curCodeLen - 1; curCodeLen2 >= 0; curCodeLen2--) {//处理 行程妈缩短1,2... Update(j + MaxLen(curCodeLen2), pre[j] + curCodeLen2); } } int cnt2 = 0; for (int m = i ; m >= 0; m--) { if (arr[m].first != ch) { continue; } cnt2 += arr[m].second;//合并后的字符数 const int curCodeLen = GetCodeLen(cnt2); for (int j = 0; j <= lenArr; j++) { Update(j + cnt2, dp[m][j] + curCodeLen); for (int curCodeLen2 = curCodeLen - 1; curCodeLen2 >= 0; curCodeLen2--) {//处理 行程妈缩短1,2... Update(j + MaxLen(curCodeLen2), dp[m][j] + curCodeLen2); } } } } return *std::min_element(dp.back().begin() + dp.back().size() - k - 1, dp.back().end()); } };
动态规划三
arr数组,少许提升性能,但增加了复杂度,不采用。
动态规划的状态
dp[i][j]表示 从s[0,i)中删除j个字符 最短的行程码。
动态规划的转移方程
令x = dp[i+1][j]
情况一:删除s[i+1]
那x等于dp[i][j-1] 公式一
情况二:不删除,且可能和前面的字符结合后,删除。
不市一般性,令s[i]=‘a’,且它的前面只有三个’a’,小标分别为i1,i2,i3。
情况a:
s[i]没有和其它’a’结合,则x= dp[i][j]+GetCodeLen (1)。 公式二
情况b:
s[i]和s[i3]结合,s(i3,i)之间非’a’的数量为diff,全部删除。
b1: i和i3 都没删除。 x = dp[i3][j-diff] + GetCodeLen(2) → \rightarrow→ dp[i-diff-1][j-diff] + GetCodeLen(2) 公式三
b2: i3删除。x = dp[i3][j-diff-1] + GetCodeLen(1) → \rightarrow→ dp[i-diff-1][j-diff-1] + GetCodeLen(1) 就是公式二和公式一结合。
情况c:
s[i]和s[i2] s[i3]结合: s(i2,i)之间非’a’的数量为diff2,全部删除。
c1,不删除’a’。 dp[i2][j-diff2] + GetCodeLen(3) ** 公式四**
c2,删除一个’a’ dp[i2][j-diff2-1] + GetCodeLen(2) → \rightarrow→ dp[i-diff2-2][j-diff2-1]+GetCodeLen(2) 就是公式三和公式的结合,不需要枚举。
c3 删除两个’a’。dp[i-diff2-2][j-diff2-2] + GetCodeLen(1) 就是公式二和公式一结合,不用枚举。
总结:
无论多少个字符结合,全删除就是公式一。
保留一个就是公式二。
保留三个就是公式三。
…
m个字符结合,只需要枚举m个字符,mm个字符(mm < m )枚举mm个字符结合的时候考虑。
可以这样理解:
m个字符合并后,删除m-mm个,保留mm个。 保留任意mm个都一样,那保留后mm个。所以只需要枚举:保留后mm个。
动态规划的初始值
dp[0][0] = 0,其它100。
动态规划的填表顺序
i从小到大。
动态规划的返回值
dp.back()的最小值。
代码
class Solution { public: int getLengthOfOptimalCompression(string s, int k) { const int n = s.length(); vector<int> vLen = { 0,1,2,10,100 }; auto GetCodeLen = [&vLen](int len) { int i = vLen.size() - 1; for (; (i >= 0) && (len < vLen[i]); i--); return i; }; vector<vector<int>> dp(n + 1, vector<int>(k + 1, 100)); dp[0][0] = 0; for (int i = 0; i < n; i++) { //处理删除s[i] for (int j1 = 1; j1 <= min(i+1,k); j1++) { dp[i+1][j1] = dp[i][j1-1]; } //处理不删除s[i] for (int same = 0, diff = 0, preLen = i;preLen>=0; preLen--) { if (s[preLen] == s[i]) { same++; for (int j1 = diff; j1 <= min(i + 1, k); j1++) { dp[i + 1][j1] = min(dp[i + 1][j1], dp[i + 1 - same - diff][j1 - diff] + GetCodeLen(same)); } } else { diff++; } } } return *std::min_element(dp.back().begin() , dp.back().end()); } };
2023年2月 第一版
class Solution {
public:
int getLengthOfOptimalCompression(const string s, const int k) {
int pre[100 + 1][27][101];
memset(pre, 101, sizeof(pre));
pre[0][26][1] = 0;
for (const auto& ch : s)
{
int dp[100 + 1][27][101];
memset(dp, 101, sizeof(dp));
for (int iK = 0; iK <= k; iK++)
{
for (int j = 0; j < 27; j++)
{
for (int iNew = 0; iNew < 101; iNew++)
{
const int& iLen = pre[iK][j][iNew];
if (iLen > 100)
{
continue;
}
if (iK < k)
{//删除
dp[iK + 1][j][iNew] = min(dp[iK + 1][j][iNew], iLen);
}
if (j + ‘a’ != ch)
{
dp[iK][ch - ‘a’][1] = min(dp[iK][ch - ‘a’][1], iLen + 1);
}
else
{
const int iNewNum = min(100, iNew + 1);
dp[iK][ch - ‘a’][iNewNum] = min(dp[iK][ch - ‘a’][iNewNum], iLen + ((1 == iNew) || (9 == iNew) || (99 == iNew)));
}
}
}
}
memcpy(pre,dp, sizeof(pre));
}
int iMin = INT_MAX;
if (100 == s.length())
{
const char chMin = *std::min_element(s.begin(), s.end());
const char chMax = *std::max_element(s.begin(), s.end());
if (chMin == chMax)
{
iMin = 4;
}
}
for (int iK = 0; iK <= k; iK++)
{
for (int j = 0; j < 27; j++)
{
for (int iNew = 0; iNew < 101; iNew++)
{
if (pre[iK][j][iNew] < iMin)
{
iMin = pre[iK][j][iNew];
}
}
}
}
return iMin;
}
};
2023年2月 第二版
class Solution {
public:
int getLengthOfOptimalCompression(const string s, const int k) {
if (100 == s.length())
{
const char chMin = *std::min_element(s.begin(), s.end());
const char chMax = *std::max_element(s.begin(), s.end());
if (chMin == chMax)
{
const int iRemain = s.length() - k;
if (iRemain >= 100)
{
return 4;
}
if (iRemain >= 10)
{
return 3;
}
if (iRemain >= 2 )
{
return 2;
}
return iRemain;
}
}
int pre[100 + 1][27][11];
memset(pre, 101, sizeof(pre));
pre[0][26][1] = 0;
for (const auto& ch : s)
{
int dp[100 + 1][27][11];
memset(dp, 101, sizeof(dp));
for (int iK = 0; iK <= k; iK++)
{
for (int j = 0; j < 27; j++)
{
for (int iNew = 0; iNew < 11; iNew++)
{
const int& iLen = pre[iK][j][iNew];
if (iLen > 100)
{
continue;
}
if (iK < k)
{//删除
dp[iK + 1][j][iNew] = min(dp[iK + 1][j][iNew], iLen);
}
if (j + ‘a’ != ch)
{
dp[iK][ch - ‘a’][1] = min(dp[iK][ch - ‘a’][1], iLen + 1);
}
else
{
const int iNewNum = min(10, iNew + 1);
dp[iK][ch - ‘a’][iNewNum] = min(dp[iK][ch - ‘a’][iNewNum], iLen + ((1 == iNew) || (9 == iNew) || (99 == iNew)));
}
}
}
}
memcpy(pre, dp, sizeof(pre));
}
int iMin = INT_MAX;
for (int iK = 0; iK <= k; iK++)
{
for (int j = 0; j < 27; j++)
{
for (int iNew = 0; iNew < 11; iNew++)
{
if (pre[iK][j][iNew] < iMin)
{
iMin = pre[iK][j][iNew];
}
}
}
}
return iMin;
}
};
2023年2月版
class Solution {
public:
int getLengthOfOptimalCompression(const string s, const int k) {
if (100 == s.length())
{
const char chMin = *std::min_element(s.begin(), s.end());
const char chMax = *std::max_element(s.begin(), s.end());
if (chMin == chMax)
{
const int iRemain = s.length() - k;
if (iRemain >= 100)
{
return 4;
}
if (iRemain >= 10)
{
return 3;
}
if (iRemain >= 2 )
{
return 2;
}
return iRemain;
}
}
int pre[100 + 1][27][11];
memset(pre, 101, sizeof(pre));
pre[0][26][1] = 0;
for (const auto& ch : s)
{
int dp[100 + 1][27][11];
memset(dp, 101, sizeof(dp));
for (int iK = 0; iK <= k; iK++)
{
for (int j = 0; j < 27; j++)
{
for (int iNew = 1; iNew < 11; iNew++)
{
const int& iLen = pre[iK][j][iNew];
if (iLen > 100)
{
continue;
}
if (iK < k)
{//删除
dp[iK + 1][j][iNew] = min(dp[iK + 1][j][iNew], iLen);
}
if (j + ‘a’ != ch)
{
dp[iK][ch - ‘a’][1] = min(dp[iK][ch - ‘a’][1], iLen + 1);
}
else
{
const int iNewNum = min(10, iNew + 1);
dp[iK][ch - ‘a’][iNewNum] = min(dp[iK][ch - ‘a’][iNewNum], iLen + ((1 == iNew) || (9 == iNew) || (99 == iNew)));
}
}
}
}
memcpy(pre, dp, sizeof(pre));
}
int iMin = INT_MAX;
for (int iK = 0; iK <= k; iK++)
{
for (int j = 0; j < 27; j++)
{
for (int iNew = 1; iNew < 11; iNew++)
{
if (pre[iK][j][iNew] < iMin)
{
iMin = pre[iK][j][iNew];
}
}
}
}
return iMin;
}
};