详解如何分析 区间 DP 转移思路 |Java 刷题打卡

简介: 详解如何分析 区间 DP 转移思路 |Java 刷题打卡

网络异常,图片无法展示
|


题目描述



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


Tag : 「区间 DP」


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


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


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


示例 1:


输入:s = "aaabbb"
输出:2
解释:首先打印 "aaa" 然后打印 "bbb"。
复制代码


示例 2:


输入:s = "aba"
输出:2
解释:首先打印 "aaa" 然后在第二个位置打印 "b" 覆盖掉原来的字符 'a'。
复制代码


提示:


  • 1 <= s.length <= 100
  • s 由小写英文字母组成


基本分析



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


怎么理解这句话呢?


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


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


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


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


由于同样的地方可以被重复打印,因此我们可以将情况 33 中打印边缘扩展到 iijj 处,这样最终打印结果不变,而且总的打印次数没有增加。


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


但可能会有同学提出疑问:怎么保证 [i, j][i,j] 是被最后涂的?怎么保证 [i, j][i,j] 不是和其他「不相邻的同样字符」一起打印的?


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


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


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

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


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


动态规划


.

定义 f[l][r]f[l][r] 为将 [l, r][l,r] 这一段打印成目标结果所消耗的最小打印次数。


不失一般性考虑 f[l][r]f[l][r] 该如何转移:


  • 只染 ll 这个位置,此时 f[l][r] = f[l + 1][r] + 1f[l][r]=f[l+1][r]+1
  • 不只染 ll 这个位置,而是从 ll 染到 kk(需要确保首位相同 s[l] = s[k]s[l]=s[k]):f[l][r] = f[l][k - 1] + f[k + 1][r], l < k <= rf[l][r]=f[l][k1]+f[k+1][r],l<k<=r


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


最终的 f[l][r]f[l][r] 为上述所有方案中取 minmin


代码:


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];
    }
}
复制代码


  • 时间复杂度:O(n^3)O(n3)
  • 空间复杂度:O(n^2)O(n2)


总结



这道题的原型应该出自 String painter


如果只是为了把题做出来,难度不算特别大,根据数据范围 10^2102,可以猜到是 O(n^3)O(n3) 做法,通常就是区间 DP 的「枚举长度 + 枚举左端点 + 枚举分割点」的三重循环。


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


最后



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


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


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


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

相关文章
|
1月前
|
存储 Java
【编程基础知识】 分析学生成绩:用Java二维数组存储与输出
本文介绍如何使用Java二维数组存储和处理多个学生的各科成绩,包括成绩的输入、存储及格式化输出,适合初学者实践Java基础知识。
67 1
|
14天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
32 2
|
15天前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
10 2
|
1月前
|
Java
让星星⭐月亮告诉你,Java synchronized(*.class) synchronized 方法 synchronized(this)分析
本文通过Java代码示例,介绍了`synchronized`关键字在类和实例方法上的使用。总结了三种情况:1) 类级别的锁,多个实例对象在同一时刻只能有一个获取锁;2) 实例方法级别的锁,多个实例对象可以同时执行;3) 同一实例对象的多个线程,同一时刻只能有一个线程执行同步方法。
18 1
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
38 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
1月前
|
Java
如何从Java字节码角度分析问题|8月更文挑战
如何从Java字节码角度分析问题|8月更文挑战
|
20天前
|
存储 Java 编译器
[Java]基本数据类型与引用类型赋值的底层分析
本文详细分析了Java中不同类型引用的存储方式,包括int、Integer、int[]、Integer[]等,并探讨了byte与其他类型间的转换及String的相关特性。文章通过多个示例解释了引用和对象的存储位置,以及字符串常量池的使用。此外,还对比了String和StringBuilder的性能差异,帮助读者深入理解Java内存管理机制。
18 0
|
6天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
2天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
16 9
|
6天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####