力扣 664. 奇怪的打印机

力扣 \(664\). 奇怪的打印机

题目描述

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

打印机每次只能打印由 同一个字符 组成的序列。

每次可以在任意起始和结束位置打印新字符,并且会覆盖掉原来已有的字符。

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

示例 1

输入:\(s = "aaabbb"\)
输出:\(2\)
解释:首先打印 \("aaa"\) 然后打印 \("bbb"\)

示例 2:

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

提示:

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

解决方案 : 动态规划

我们可以使用 动态规划 解决本题

一、状态表示

\(f[i][j]\) 表示打印完成区间 \([i,j]\)最少操作数

\(Q:\)为什么这样设计状态?

\(A:\) 状态的设计一般是根据 尝试经验

经验:以前做过类似的题,知道状态该怎么设计。

尝试:才用最简单的一维状态表示来描述,如果发现行不通,再转为二维状态表示,实在不行再三维状态表示。

  • 尝试用一维状态表示它:\(f[i]\):结束位置为\(i\)时的最少操作数
    这样表示行不行呢?不行呗!为啥呢?因为人家题目说了,可以从任意位置 开始、结束,你这里只记录了结束,开始的概念丢失了。

  • 尝试用二维状态表示它:\(f[i][j]:\)\(i\)开始到\(j\)结束的最少操作次数

状态设计原则

  • 状态设计是不是最终能包含答案
  • 通过简单枚举可以找出正确答案

二、状态转移方程

当我们尝试计算出 \(f[i][j]\) 时,需要考虑两种情况:

  • \(\large s[i] = s[j]\)
    区间两端的字符相同时,那么当我们打印左侧字符 \(s[i]\) 时,可以顺便打印右侧字符 \(s[j]\),这样我们即可忽略右侧字符对该区间的影响,只需要考虑如何尽快打印完区间 \([i,j - 1]\) 即可,即此时有 \(f[i][j] = f[i][j-1]\)

  • \(\large s[i] \neq s[j]\)
    即区间两端的字符不同,那么我们需要分别完成该区间的左右两部分的打印。我们记两部分分别为区间 \([i,k]\) 和区间 \([k+1,j]\) (其中\(\large i<=k<j\)),此时

\[\large f[i][j]=\min_{k=i}^{j-1}(f[i][k]+f[k+1][j]) \]

注意:这里\(k\)的取值范围值得仔细思考,因为是划分成两段,设第一段结束点为\(k\),第二段的开始点为\(k+1\),则有\(k>=i,k+1<=j\),即:\(i<=k<j\)


\(i==k\)时是成立的,表示只有\(i\)点打印一次,从\(k+1\)开始至\(j\)打印其它的(不一定是同一种噢~)

总结状态转移方程为:

\[\large \displaystyle f[i][j]= \left\{\begin{matrix} f[i][j-1] & s[i]=s[j] \\ \displaystyle \min_{k=i}^{j-1}f[i][k]+f[k+1][j] & s[i] \neq s[j] \end{matrix}\right. \]

三、边界与答案

边界条件为 \(f[i][i] = 1\),对于长度为 \(1\) 的区间,最少打印 \(1\) 次。最后的答案为\(f[0][n - 1]\)

四、枚举的顺序

注意到 \(f[i][j]\) 的计算需要用到 \(f[i][k]\)\(f[k + 1][j]\) (其中

\[\large \large i<=k<j \]

)。 为了保证动态规划的计算过程满足无后效性,在实际代码中,我们需要 改变动态规划的计算顺序,从大到小地枚举 \(i\),并从小到大地枚举 \(j\)这样可以保证当计算 \(f[i][j]\) 时, \(f[i][k]\)\(f[k + 1][j]\) 都已经被计算过。

复杂度分析

时间复杂度:\(O(n^3)\),其中 \(n\) 是字符串的长度。
空间复杂度:\(O(n^2)\),其中 \(n\) 是字符串的长度。我们需要保存所有 \(n^2\) 个状态。

实现代码

class Solution {
    const int INF = 0x3f3f3f3f;

public:
    int f[110][110];
    int strangePrinter(string s) {
        int n = s.size();
        //预求最小,先设最大
        memset(f, 0x3f, sizeof f);
        //1.因为f[i][j]依赖于f[i][k],f[k+1][j],其中k+1>i的,为了保证无后效性,需要在计算f[i]之前准备好f[k+1],所以,倒序枚举i

        //2.因为f[i][j]依赖于f[i][k],f[k+1][j],f[i][j-1],k<j的,所以在计算f[i][j]之前,f[i][k],f[i][j-1]需要提前准备好,正序枚举j
        
        //倒序枚举i,从字符串最后一位出发,向0前进
        for (int i = n - 1; i >= 0; i--) {
            //初始状态,每个起点出发,到自己结束,只包含一个字符的情况就是最简单的基本情况,此时打印次数为1,可以确定,其它无法确定的,靠状态转移方程进行转移即可填充数据
            f[i][i] = 1;

            //正序枚举j,由于i==j,也就是一个子串中只有一个字符的情况上面已经处理过了,这里只需要处理j>i的即可,并且,j的极限值是n-1, 因为j的字符串的长度上限
            for (int j = i + 1; j < n; j++) {

                if (s[i] == s[j])
                    f[i][j] = f[i][j - 1];
                else {
                    for (int k = i; k < j; k++)//注意思考与理解k的范围,尤其是带等号和不带等号的地方
                        f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j]);
                }
            }
        }
        //范围i=0,j=n-1的最小打印次数就是答案
        return f[0][n - 1];
    }
};
posted @ 2022-11-08 15:05  糖豆爸爸  阅读(47)  评论(0编辑  收藏  举报
Live2D