【区间 DP】运用区间 DP 解决古老原题

题目描述

这是 LeetCode 上的 664. 奇怪的打印机 ,难度为 困难

Tag : 「区间 DP」

有台奇怪的打印机有以下两个特殊要求:

  • 打印机每次只能打印由 同一个字符 组成的序列。
  • 每次可以在任意起始和结束位置打印新字符,并且会覆盖掉原来已有的字符。

给你一个字符串 s ,你的任务是计算这个打印机打印它需要的最少打印次数。

示例 1:

arduino 复制代码
输入:s = "aaabbb"

输出:2

解释:首先打印 "aaa" 然后打印 "bbb"。

示例 2:

arduino 复制代码
输入:s = "aba"

输出:2

解释:首先打印 "aaa" 然后在第二个位置打印 "b" 覆盖掉原来的字符 'a'。

提示:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = s . l e n g t h < = 100 1 <= s.length <= 100 </math>1<=s.length<=100
  • s 由小写英文字母组成

基本分析

首先,根据题意我们可以分析出一个重要推论:连续相同的一段字符,必然可以归到同一次打印中,而不会让打印次数变多。注意,这里说的是「归到一次」,而不是说「单独作为一次」

怎么理解这句话呢?

举个 🌰,对于诸如 ...bbaaabb... 的样例数据,其中多个连续的 a 必然可以归到同一次打印中,但这一次打印可能只是将 aaa 作为整体进行打印;也有可能是 aaa 与前面或者后面的 a 作为整体被打印(然后中间的 b 被后来的打印所覆盖)。但无论是何种情况连续一段的 aaa 必然是可以「归到同一次打印」中。

我们可以不失一般性证明「连续相同的一段字符,必然可以归到同一次打印中,而不会让打印次数变多」这个推理是否正确:

假设有目标序列 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ . . . , a i , . . . , a j , . . . ] [...,ai,...,aj,...] </math>[...,ai,...,aj,...] 其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i, j] </math>[i,j] 连续一段字符相同,假如这一段的打印被最后完成(注意最后完成不代表这一段要保留空白,这一段可以此前被打印多次),除了这一段以外所消耗的打印次数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x,那么根据 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i, j] </math>[i,j] 不同的打印方案有:

  1. 将 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i, j] </math>[i,j] 单纯划分为多段:总共打印的次数大于 <math xmlns="http://www.w3.org/1998/Math/MathML"> x + 1 x + 1 </math>x+1(此方案不会取到打印最小值 <math xmlns="http://www.w3.org/1998/Math/MathML"> x + 1 x + 1 </math>x+1,可忽略)
  2. 将 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i, j] </math>[i,j] 归到同一次打印:总共打印的次数等于 <math xmlns="http://www.w3.org/1998/Math/MathML"> x + 1 x + 1 </math>x+1
  3. 将 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i, j] </math>[i,j] 结合之前的打印划分为多段,即 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i, j] </math>[i,j] 一段的两段本身就是「目标字符」,我们本次只需要打印 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i, j] </math>[i,j] 中间的部分。总共打印的次数等于 <math xmlns="http://www.w3.org/1998/Math/MathML"> x + 1 x + 1 </math>x+1

由于同样的地方可以被重复打印,因此我们可以将情况 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3 中打印边缘扩展到 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> j j </math>j 处,这样最终打印结果不变,而且总的打印次数没有增加。

到这一步,我们其实已经证明出「连续相同的一段字符,必然可以归到同一次打印中,而不会让打印次数变多」的推论成立了。

但可能会有同学提出疑问:怎么保证 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i, j] </math>[i,j] 是被最后涂的?怎么保证 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i, j] </math>[i,j] 不是和其他「不相邻的同样字符」一起打印的?

答案是不用保证,因为不同状态(打印结果)之间相互独立,而有明确的最小转移成本。即从当前打印结果 a 变成打印结果 b,是具有明确的最小打印次数的(否则本题无解)。因此我们上述的分析可以看做任意两个中间状态转移的"最后一步",而且不会整体的结果。

对应到本题,题目给定的起始状态是空白字符串 a,目标状态是入参字符串 s。那么真实最优解中,从 a 状态到 s 状态中间可能会经过任意个中间状态,假设有两个中间状态 pq,那么我们上述的分析就可以应用到中间状态 pq 的转移中,可以令得 pq 转移所花费的转移成本最低(最优),同时这个转移不会影响「ap 的转移」和「qs 的转移」,是相互独立的。

因此这个分析可以推广到真实最优转移路径中的任意一步,是一个具有一般性的结论。

上述分析是第一个切入点,第二个切入点是「重复打印会进行覆盖」,这意味着我们其实不需要确保 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i,j] </math>[i,j] 这一段在目标字符串中完全相同,而只需要 <math xmlns="http://www.w3.org/1998/Math/MathML"> s [ i ] = s [ j ] s[i] = s[j] </math>s[i]=s[j] 相同即可,即后续打印不会从边缘上覆盖 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i,j] </math>[i,j] 区间的原有打印,否则 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i , j ] [i,j] </math>[i,j] 这一段的打印就能用范围更小的区间所代替。

这样就引导出我们状态转移的关键:状态转移之间只需要确保首位字符相同。

动态规划

定义 <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ l ] [ r ] f[l][r] </math>f[l][r] 为将 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ l , r ] [l, r] </math>[l,r] 这一段打印成目标结果所消耗的最小打印次数。

不失一般性考虑 <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ l ] [ r ] f[l][r] </math>f[l][r] 该如何转移:

  • 只染 <math xmlns="http://www.w3.org/1998/Math/MathML"> l l </math>l 这个位置,此时 <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ l ] [ r ] = f [ l + 1 ] [ r ] + 1 f[l][r] = f[l + 1][r] + 1 </math>f[l][r]=f[l+1][r]+1
  • 不只染 <math xmlns="http://www.w3.org/1998/Math/MathML"> l l </math>l 这个位置,而是从 <math xmlns="http://www.w3.org/1998/Math/MathML"> l l </math>l 染到 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k(需要确保首位相同 <math xmlns="http://www.w3.org/1998/Math/MathML"> s [ l ] = s [ k ] s[l] = s[k] </math>s[l]=s[k]): <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ l ] [ r ] = f [ l ] [ k − 1 ] + f [ k + 1 ] [ r ] , l < k < = r f[l][r] = f[l][k - 1] + f[k + 1][r], l < k <= r </math>f[l][r]=f[l][k−1]+f[k+1][r],l<k<=r

其中状态转移方程中的情况 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 需要说明一下:由于我们只确保 <math xmlns="http://www.w3.org/1998/Math/MathML"> s [ l ] = s [ k ] s[l] = s[k] </math>s[l]=s[k],并不确保 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ l , k ] [l, k] </math>[l,k] 之间的字符相同,根据我们基本分析可知, <math xmlns="http://www.w3.org/1998/Math/MathML"> s [ k ] s[k] </math>s[k] 这个点可由打印 <math xmlns="http://www.w3.org/1998/Math/MathML"> s [ l ] s[l] </math>s[l] 的时候一同打印,因此本身 <math xmlns="http://www.w3.org/1998/Math/MathML"> s [ k ] s[k] </math>s[k] 并不独立消耗打印次数,所以这时候 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ l , k ] [l, k] </math>[l,k] 这一段的最小打印次数应该取 <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ l ] [ k − 1 ] f[l][k - 1] </math>f[l][k−1],而不是 <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ l ] [ k ] f[l][k] </math>f[l][k]。

最终的 <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ l ] [ r ] f[l][r] </math>f[l][r] 为上述所有方案中取 <math xmlns="http://www.w3.org/1998/Math/MathML"> m i n min </math>min。

代码:

Java 复制代码
class Solution {
    public int strangePrinter(String s) {
        int n = s.length();
        int[][] f = new int[n + 1][n + 1];
        for (int len = 1; len <= n; len++) {
            for (int l = 0; l + len - 1 < n; l++) {
                int r = l + len - 1;
                f[l][r] = f[l + 1][r] + 1;
                for (int k = l + 1; k <= r; k++) {
                    if (s.charAt(l) == s.charAt(k)) {
                        f[l][r] = Math.min(f[l][r], f[l][k - 1] + f[k + 1][r]);
                    }
                }
            }
        }
        return f[0][n - 1];
    }
}
  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 3 ) O(n^3) </math>O(n3)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2)

总结

这道题的原型应该出自 String painter : acm.hdu.edu.cn/showproblem...

如果只是为了把题做出来,难度不算特别大,根据数据范围 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 2 10^2 </math>102,可以猜到是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 3 ) O(n^3) </math>O(n3) 做法,通常就是区间 DP 的「枚举长度 + 枚举左端点 + 枚举分割点」的三重循环。

但是要搞懂为啥可以这样做,还是挺难,大家感兴趣的话可以好好想想 ~ 🤣

最后

这是我们「刷穿 LeetCode」系列文章的第 No.664 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour...

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关推荐
王哲晓几秒前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4113 分钟前
无网络安装ionic和运行
前端·npm
理想不理想v5 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云15 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:1379712058717 分钟前
web端手机录音
前端
齐 飞23 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod39 分钟前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
神仙别闹40 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
sszmvb12341 小时前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端