LeetCode 1531. 压缩字符串 II【动态规划】2575

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

行程长度编码 是一种常用的字符串压缩方法,它将连续的相同字符(重复 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 s s 为「原串」,压缩后字符串 t t t 为「压串」。目标是从 s s s 中删除至多 k k k 个字符,使其对应的 t t t 长度最小。

压串 t t t 由字母和数字间隔组成, c x c_x cx 表示字母, d x d_x dx 表示次数, ( c x , d x ) (c_x, d_x) (cx,dx) 表示原串 s s s 中连续出现了 d x d_x dx 次字母 c x c_x cx 。当出现次数为 1 1 1 时,我们省略 d x d_x dx
t = c 1 d 1 c 2 d 2 … c m d m t = c_1 d_1 c_2 d_2 \dots c_m d_m t=c1d1c2d2cmdm
我们可以根据 t t t 的形式进行动态规划。在状态转移的过程中,每一次只处理 t t t 中一个二元组 ( c x , d x ) (c_x, d_x) (cx,dx)

我们用 f [ i ] [ j ] f[i][j] f[i][j] 表示对于原串 s s s 的前 i i i 个字符,通过删除其中的 j j j 个字符,剩余的 i − j i - j ij 个字符可以得到的最小的压串的长度。为了对边界条件进行处理,这里的 i i i j j j 都从 1 1 1 开始编号。 i i i 的最大值为 ∣ s ∣ |s| s(原串的长度), j j j 的最大值为 k k k

  • 动态规划的边界条件为 f [ 0 ] [ 0 ] = 0 f[0][0] = 0 f[0][0]=0 ,表示空串对应的压串长度为 0 0 0由于要求的是 f [ i ] [ j ] f[i][j] f[i][j] 的最小值,所以先把它们的值初始化为一个很大的正整数
  • 最终答案即为 f [ n ] [ 0 … k ] f[n][0\dots k] f[n][0k] 中的最小值,即我们可以从 s s s 中删除至多 k k k 个字符。由于删除一个字符(得到的压串的长度)永远不会劣于保留该字符,因此实际上最终的答案就是 f [ n ] [ k ] f[n][k] f[n][k] ,即我们恰好删除 k k k 个字符
  • 注意:这里的状态表示中,我们不关心到底删除了哪 j j j 个字符。

如何进行状态转移呢?我们可以考虑是否删除第 i i i 个字符。

  • 如果第 i i i 个字符被删除,则前 i − 1 i-1 i1 个字符中就要删除 j − 1 j-1 j1 个字符,状态转移方程为: f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] f[i][j] = f[i - 1][j - 1] f[i][j]=f[i1][j1]

  • 如果第 i i i 个字符没有被删除,那么考虑以该字符 s [ i ] s[i] s[i] 结尾的一个 ( c x , d x ) (c_x, d_x) (cx,dx) 二元组,其中 c x = s [ i ] c_x = s[i] cx=s[i] 。我们要在 [ 1 , i ) [1, i) [1,i) 的范围内选择若干个(包括零个)与 s [ i ] s[i] s[i] 相同的字符(即保留一个、两个…… d x d_x dx c x c_x cx),一起进行压缩。在选择范围内与 s [ i ] s[i] s[i] 不同的字符,全部被删除。通俗来说,如果保留字符 c x c_x cx ,则此时往前尽量选择保留与字符 c x c_x cx 相同的字符。这个状态转移确实不好理解,但仔细思考一下,最优解应该是包含在里面的。

    形式化地说,我们选择了位置: p 1 < p 2 < ⋯ < p d x − 1 < i p_1 < p_2 < \dots < p_{d_x - 1} < i p1<p2<<pdx1<i 其中 s [ p 1 ] = s [ p 2 ] = ⋯ = s [ p d x − 1 ] = s [ i ] = c x s[p_1] = s[p_2] = \dots = s[p_{d_x - 1}] = s[i] = c_x s[p1]=s[p2]==s[pdx1]=s[i]=cx
    那么我们选择的范围为 [ p 1 , i ] [p_1, i] [p1,i] ,在其中选择了 d x d_x dx 个字符 c x c_x cx剩余的 i − p 1 + 1 − d x i - p_1 + 1 - d_x ip1+1dx 个字符(无论是否为 c x c_x cx)都必须被删除。那么 p 1 − 1 p_1 - 1 p11 个字符中就有 j − ( i − p 1 + 1 − d x ) j - (i - p_1 + 1 - d_x) j(ip1+1dx) 个字符被删除,状态转移方程为: f [ i ] [ j ] = min ⁡ X d { f [ p 1 − 1 ] [ j − ( i − p 1 + 1 − d x ) ] + c o s t ( d x ) } f[i][j]= \min_{ X_d } \{ f[p_1 - 1][j - (i - p_1 + 1 - d_x)] + cost(d_x) \} f[i][j]=Xdmin{f[p11][j(ip1+1dx)]+cost(dx)}
    其中 X d X_d Xd 表示「包含了所有选择 p 1 , … , p d x − 1 p_1, \dots, p_{d_x - 1} p1,,pdx1 的方案集合​」, c o s t ( d x ) cost(d_x) cost(dx) 表示压缩 d x d_x dx 个字符得到的长度: c o s t ( d x ) = { 1 , d x = 1 2 , 2 ≤ d x ≤ 9 3 , 10 ≤ d x ≤ 99 4 , d x = 100 cost(d_x) = \begin{cases} 1,& d_x = 1 \\ 2,& 2 \le d_x \le 9 \\ 3,& 10 \le d_x \le 99 \\ 4,& d_x = 100\end{cases} cost(dx)= 1,2,3,4,dx=12dx910dx99dx=100
    实际上,上述状态转移方程的复杂度非常高,因为 X d X_d Xd 是一个非常大的集合,我们枚举每一种方案是不现实的

事实上,如果 p 1 , … , p d x − 1 , i p_1, \dots, p_{d_x - 1}, i p1,,pdx1,i 是原串 s s s连续的出现字符 c x c_x cx 的位置(不看其他字符),那么方案数就没有这么多了。我们只需要枚举 d x d_x dx ,就可以从 i i i 开始向左依次选取出现字符 c x c_x cx 的位置,当选取了 d x − 1 d_x - 1 dx1 次后(即保留 d x d_x dx c x c_x cx),就对应着唯一的 p 1 , … , p d x − 1 , i p_1,\ \dots,\ p_{d_x - 1},\ i p1, , pdx1, i 。那么这样只考虑选择连续的 c x c_x cx 的做法是否正确呢?

例如 s [ 1... i ] = a a b c a s[1...i] = aabca s[1...i]=aabca 时,我们要选择 2 2 2 a a a ,其中一个 a a a s [ i ] s[i] s[i] ,我们如何保证只考虑选择连续的 a a a ,即选择 a a ‾ b c a ‾ a \underline{a} bc\underline{a} aabca ,而不考虑 a ‾ a b c a ‾ \underline{a} a b c \underline{a} aabca 这种情况(中间隔了一个 a a a )呢?

可以发现,在 a ‾ a b c a ‾ \underline{a} a b c \underline{a} aabca 这种情况中,我们保留了 2 2 2 a a a 而删除了 3 3 3 个字符。由于删除任意一个字符的代价都是一样的,因此我们不如从左往右连续地保留 2 2 2 个出现的 a a a ,而删除剩余的 3 3 3 个字符,即 a ‾ a ‾ b c a \underline{a}\underline{a}bca aabca ,这两种情况是等价的。而后者在 s [ 1... i ′ ] = s [ 1...2 ] = a a s[1...i'] =s[1...2] = aa s[1...i]=s[1...2]=aa 时就已经被考虑到,因此我们不用考虑前者。

更直观地说,如果我们选择的 d x d_x dx c x c_x cx 不是连续的,那么我们可以在对应的选择范围内从左到右连续地重新选择 d x d_x dx c x c_x cx ,二者是等价的。而后者由于连续性,会在之前的状态转移中被考虑到

因此,我们可对状态转移方程进行优化,只考虑选择连续的 c x c_x cx
f [ i ] [ j ] = min ⁡ s [ i 0 ] = s [ i ] { f [ i 0 − 1 ] [ j − d i f f ( i 0 , i ) ] + c o s t ( s a m e ( i 0 , i ) ) } f[i][j] = \min_{ s[i_0] = s[i] } \{ f[i_0 - 1][j - diff(i_0, i) ] + cost(same(i_0, i))\} f[i][j]=s[i0]=s[i]min{f[i01][jdiff(i0,i)]+cost(same(i0,i))}
其中 d i f f ( i 0 , i ) diff(i_0, i) diff(i0,i) 表示 s [ i 0 … i ] s[i_0 \dots i] s[i0i] 中与 s [ i ] s[i] s[i] 不同字符数目, s a m e ( i 0 , i ) same(i_0, i) same(i0,i) 表示 s [ i 0 … i ] s[i_0 \dots i] s[i0i] 中与 s [ i ] s[i] s[i] 相同的字符数目,有:
d i f f ( i 0 , i ) + s a m e ( i 0 , i ) = i − i 0 + 1 diff(i_0, i) + same(i_0, i) = i - i_0 + 1 diff(i0,i)+same(i0,i)=ii0+1
也就是说,我们枚举满足 s [ i 0 ] = s [ i ] = c x s[i_0] = s[i] = c_x s[i0]=s[i]=cx i 0 i_0 i0 ,选择所有在 [ i 0 , i ] [i_0, i] [i0,i] 范围内的 c x c_x cx ,删除剩余的字符(此时剩余的字符均不会是 c x c_x cx)。

// cpp
class Solution {
public:int getLengthOfOptimalCompression(string s, int k) {auto calc = [](int x) {if (x == 1) return 1;if (x < 10) return 2;if (x < 100) return 3;return 4;};int n = s.size();vector<vector<int>> f(n + 1, vector<int>(k + 1, INT_MAX >> 1));f[0][0] = 0;for (int i = 1; i <= n; ++i) {for (int j = 0; j <= k && j <= i; ++j) { // 删除j个字符// 删除0个字符时要保留s[i-1]// 删除多个字符时考虑删除第i个字符和不删除第i个字符的情况if (j > 0) f[i][j] = f[i - 1][j - 1]; // 删除s[i-1]这个字符int same = 0, diff = 0; // diff统计不同的字符,都是要删除的for (int i0 = i; i0 >= 1 && diff <= j; --i0) { // diff<=jif (s[i0 - 1] == s[i - 1]) {++same; // 删除[i0-1,i-1]中间的不同字符f[i][j] = min(f[i][j], f[i0 - 1][j - diff] + calc(same));} else {++diff;}}}}return f[n][k];}
};// java
class Solution {private int calc(int x) {if (x == 1) return 1;if (x < 10) return 2;if (x < 100) return 3;return 4;}public int getLengthOfOptimalCompression(String s, int k) {int n = s.length();int[][] f = new int[n + 1][k + 1];for (int i = 0; i <= n; ++i) {Arrays.fill(f[i], Integer.MAX_VALUE >> 1);}f[0][0] = 0;for (int i = 1; i <= n; ++i) {for (int j = 0; j <= k && j <= i; ++j) {if (j > 0) f[i][j] = f[i - 1][j - 1];int same = 0, diff = 0;for (int i0 = i; i0 >= 1 && diff <= j; --i0) {if (s.charAt(i0 - 1) == s.charAt(i - 1)) {++same;f[i][j] = Math.min(f[i][j], f[i0 - 1][j - diff] + calc(same));} else ++diff;}}}return f[n][k];}
}// python
class Solution:def getLengthOfOptimalCompression(self, s: str, k: int) -> int:calc = lambda x : 1 if x == 1 else (2 if x < 10 else (3 if x < 100 else 4))n = len(s)f = [ [10**9] * (k + 1) for _ in range(n + 1)]f[0][0] = 0for i in range(1, n + 1):for j in range(min(k, i) + 1):if j > 0:f[i][j] = f[i - 1][j - 1]same = diff = 0for i0 in range(i, 0, -1):if s[i0 - 1] == s[i - 1]:same += 1f[i][j] = min(f[i][j], f[i0 - 1][j - diff] + calc(same))else:diff += 1if diff > j:breakreturn f[n][k]

复杂度分析:

  • 时间复杂度: O ( ∣ s ∣ 2 k ) O(|s|^2k) O(s2k) ,其中 ∣ s ∣ |s| s 是原串 s s s 的长度。动态规划中状态的数目为 O ( ∣ s ∣ k ) O(|s|k) O(sk) ,每一个状态需要 O ( ∣ s ∣ ) O(|s|) O(s) 的时间进行转移,相乘即可得到总时间复杂度。
  • 空间复杂度: O ( ∣ s ∣ k ) O(|s|k) O(sk) ,即为动态规划中状态的数目。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/597277.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java SE 三个基本注解(JDK内置)+四个元注解

使用注解(Annotation)时要在前面加符号&#xff0c;注解可以当作一个修饰符来修饰他支持的修饰的元素 Override - 重写&#xff0c;该注解只能用于方法 Deprecated - 已弃用&#xff0c;用在程序元素上&#xff0c;如某个类上或者某个方法上 SuppressWarnings - 抑制编译器警告…

【KingbaseES】实现MySql函数Median

本方法只支持在聚合函数窗口中调用 不支持在GROUP BY中使用&#xff0c;使用plsql写的玩意新能都会稍微差一些 建议使用原生方法修改 CREATE OR REPLACE FUNCTION _final_median(numeric[])RETURNS numeric AS $$SELECT AVG(val)FROM (SELECT valFROM unnest($1) valORDER BY …

CSS的特性与简便写法

<!DOCTYPE html> <html> <head> <meta charset"UTF-8" /> <title>CSS的三大特性</title> <style> /* 子级继承父级的 继承性*/ body{ font-size: 50px; color: red; } /* 层叠性 */ /* 前面的红色会被后面的蓝色覆盖&a…

服务器感染了.kann勒索病毒,如何确保数据文件完整恢复?

导言&#xff1a; 勒索病毒成为当前网络安全领域的一大威胁。.kann勒索病毒是其中的一种变种&#xff0c;对用户的数据造成了极大的威胁。本文91数据恢复将介绍.kann勒索病毒的特征、应对策略以及预防措施&#xff0c;以帮助用户更好地保护个人和组织的数据安全。当面对被勒索…

JavaScript常用事件的类型演示

目录 (一).表单事件(二).UI事件(四).鼠标事件(五).键盘事件 (一).表单事件 1).submit:提交 2).reset&#xff1a;重置 3).change&#xff1a;内容改变且失去焦点时触发 4).input&#xff1a;内容改变时触发&#xff08;兼容性不好&#xff09; 注&#xff1a; submit和res…

vue中动态出来返回的时间秒数,在多少范围显示多少秒,多少范围显示分,小时等等

在Vue中&#xff0c;你可以使用计算属性&#xff08;computed property&#xff09;或过滤器&#xff08;filter&#xff09;来根据动态返回的时间秒数来显示不同的时间单位&#xff0c;比如秒、分、小时等等。 下面是一个使用计算属性的示例&#xff1a; <template>&l…

【Python小游戏】消消乐丨喜羊羊与灰太狼(完整代码)

文章目录 写在前面喜羊羊与灰太狼PyGame入门消消乐注意事项写在后面写在前面 本期内容:基于pygame实现喜羊羊与灰太狼版消消乐小游戏 实验环境 python3.11及以上pycharmpygame安装pygame的命令: pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pygame喜羊羊与灰…

毛虫目标检测数据集VOC格式550张

毛虫&#xff0c;一种令人惊叹的生物&#xff0c;以其独特的外貌和习性&#xff0c;成为了自然界中的一道亮丽风景。 毛虫的外观非常特别&#xff0c;身体呈圆柱形&#xff0c;表面覆盖着许多细小的毛发&#xff0c;这使得它们在叶子上伪装得非常好。它们的头部有一对坚硬的颚…

力扣第一百道题,记录一下——x 的平方根

给你一个非负整数 x &#xff0c;计算并返回 x 的 算术平方根 。 由于返回类型是整数&#xff0c;结果只保留 整数部分 &#xff0c;小数部分将被 舍去 。 注意&#xff1a;不允许使用任何内置指数函数和算符&#xff0c;例如 pow(x, 0.5) 或者 x ** 0.5 。 很容易想到用二分…

自然语言转SQL,一个微调ChatGPT3.5的实例(上)--训练数据准备

背景 让非技术人员可以从数据库中提问&#xff0c;这是学术界和工业界多年来感兴趣的问题。最近&#xff0c;大型语言模型&#xff08;LLM&#xff09;技术&#xff08;如GPT-4&#xff09;的进展提高了所提出解决方案的准确性。然而&#xff0c;由于最先进的LLM尚未开放进行微…

select for update会锁表还是行锁还是其它

select for update含义 select查询语句是不会加锁的&#xff0c;但是 select for update除了有查询的作用外&#xff0c;还会加锁呢&#xff0c;而且它是悲观锁哦。至于加了是行锁还是表锁&#xff0c;这就要看是不是用了索引/主键啦。 没用索引/主键的话就是表锁&#xff0c…

双曲正弦函数(*) 优化麦克劳林公式

#include<stdio.h> #include<math.h> int main() {double x,eps,i3,y,item;scanf("%lf%lf",&x,&eps);yx;itemx;while(fabs(item)>eps){itemitem*x*x/i/(i-1);i2;yitem;}printf("%.6f\n",y);return 0; }

SpringCloud-高级篇(十)

&#xff08;1&#xff09;单节点Redis问题 缓存大家都不陌生&#xff0c;在企业里面只要做缓存都会用到Redis&#xff0c;我们在使用的时候都是做的单节点部署&#xff0c;单节点部署是存在一些问题的&#xff0c;分布式缓存正是Redis的集群&#xff0c;正是为了解决单节点部署…

Vue实现模糊查询

在Vue中实现模糊查询&#xff0c;你可以使用JavaScript的filter和includes方法&#xff0c;结合Vue的v-for指令。下面是一个简单的例子&#xff1a; 首先&#xff0c;你需要在你的Vue实例中定义一个数据数组和一个查询字符串。 data() { return { items: [Apple, Banana, Che…

Windows下多个JDK版本快速切换

Windows下多个JDK版本快速切换 本文以windows11为例&#xff0c;安装了jdk1.8\jdk11\jdk17三个版本。 1 安装junction工具 需要借助junction工具创建软链接 下载地址&#xff1a;https://docs.microsoft.com/zh-cn/sysinternals/downloads/junction 下载后&#xff0c;一般…

大数据毕业设计:租房推荐系统 python 租房大数据 爬虫+可视化大屏 计算机毕业设计(附源码+文档)✅

毕业设计&#xff1a;2023-2024年计算机专业毕业设计选题汇总&#xff08;建议收藏&#xff09; 毕业设计&#xff1a;2023-2024年最新最全计算机专业毕设选题推荐汇总 &#x1f345;感兴趣的可以先收藏起来&#xff0c;点赞、关注不迷路&#xff0c;大家在毕设选题&#xff…

广播及代码实现

广播&#xff08;Broadcast&#xff09;是一种网络通信方式&#xff0c;它允许一台设备向网络中的所有其他设备发送消息。广播通常用于在网络上传递一些信息&#xff0c;让所有设备都能接收并处理。在广播中&#xff0c;通信的目标是整个网络而不是特定的单个设备。 向子网中…

【JAVA】Java 中 Set集合常用方法

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a; JAVA ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 常用方法 代码示例 结语 我的其他博客 前言 Java中的Set接口提供了一种不允许包含重复元素的集合。常用的实现类有HashS…

把苹果手机上的备忘录转为长图片,分享给别人方法教程

在这个信息爆炸的时代&#xff0c;手机备忘录几乎成了我随身携带的“记忆宝库”。每当我脑海中闪现出一个想法、灵感或是需要记住的重要事项&#xff0c;我都会第一时间打开苹果手机的备忘录&#xff0c;将它们一一记录下来。备忘录的简洁界面和高效操作总能让我在忙碌的生活中…

正负样本分配策略simOTA

simOTA是YOLOX中提出的 正负样本分配策略&#xff08;OTA, SimOTA&#xff0c;TAS&#xff09; OTA源于2021年cvpr的论文&#xff0c;使训练和验证的标签有着更好的对应关系。 yolov5没有用到&#xff0c;只有一种loss&#xff1a; from utils.loss import ComputeLoss comput…