原帖地址:http://blog.leanote.com/post/dawnmagnet/lc664
题目
有台奇怪的打印机有以下两个特殊要求:
打印机每次只能打印由 同一个字符 组成的序列。
每次可以在任意起始和结束位置打印新字符,并且会覆盖掉原来已有的字符。
给你一个字符串 s ,你的任务是计算这个打印机打印它需要的最少打印次数。
示例 1:
输入:s = "aaabbb"
输出:2
解释:首先打印 "aaa" 然后打印 "bbb"。
示例 2:
输入:s = "aba"
输出:2
解释:首先打印 "aaa" 然后在第二个位置打印 "b" 覆盖掉原来的字符 'a'。
提示:
1 <= s.length <= 100
s 由小写英文字母组成
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/strange-printer
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路分析
这道题是一个困难题。所以我们得多想
这个打印机到底有什么规律可循呢
我们先来列几个示例看看,因为题目给我们的这个示例实在是太寒碜了。
示例|a|ab|abc
–|
最小打印次数|1|2|3
这个还是比较好理解的,因为之前的数组中没有出现过b和c,每当一个新字母出现时,必定是加一
示例|a|aa|aaa
–|
最小打印次数|1|1|1
这个似乎引发了我们的一些思考。难道所有重复的项都可以合并吗。
就比如"aaabbb"和"ab"是等价的吗。当然,因为这个要求的是打印机的最小打印次数,而一次打印多少个字母其实都是可以的,所以连续的a和一个a是等价的,都得花费打印次数。
示例|ab|aba|abb|abc
–|
最小打印次数|2|2|2|3
这些就比较难找规律了。我们试图在"ab"之后加一个新的字母,观察是否会增加打印次数,可以令人疑惑的是,不管加的是"a"还是"b"都不会增加打印次数,当然,加入c的这种情况我们已经在上面讨论过了,因为c没有在之前的任何一点出现过。
那看了上面的案例,可能有人会认为,只要是在前面出现过的字母加到后方都不会增加最小打印次数。这种事情不能急,好好找一找有没有反例。
示例|abcb|abcbc
–|
最小打印次数|3|4
反例还是很好找的
但是反例是找到了,但是我们小小的脑袋充满了大大的疑惑,为什么呢?究竟怎么才能获得加入新的一位的最小打印次数呢?
遇到这种情况不能急,回头再看看题目。
我们设f就是题目的函数,例如f(“abcb”)=3,f(“abcbc”)=4
那么我们可以观察得出,就算退1万步,如果新加入一个c,这个c不占用新打印次数的情况,只有可能出现在我们在之前的数组出现过c的位置,打印机将之前出现过的某个或者数个c和最后一个一起打印了,这样才能不占用次数。
那么不难发现,f(“abcbc”)=f(“ab”)+f(“cb”)=f(“ab”)+f(“cbc”)这样才能出现我加了个c且没有让打印机再跑一遍。
那么我们通过上式发现,f(“abcbc”)虽然等于f(“ab”)+f(“cb”),但其实影响它的因素有很多,假如加入的字母c之前出现过许多次c,我们都需要对这么许多次c进行遍历。最后取一个最小值。
写一个表达式就是
f
(
T
+
字
符
c
)
=
m
i
n
(
所
有
的
f
(
T
的
子
串
)
+
f
(
T
的
以
字
符
c
开
头
的
子
串
)
与
f
(
T
)
+
1
)
f(T+字符c)=min(所有的 f(T的子串)+f(T的以字符c开头的子串)与f(T)+1)
f(T+字符c)=min(所有的f(T的子串)+f(T的以字符c开头的子串)与f(T)+1)
这样写计算机肯定看不懂,因为这里T以c开头的子串可以是任意子串,所以我们就设一个二维数组
d
p
dp
dp,其中dp[i][j]=f(s中区间[i,j](包含i,j)的元素构成的子串)
这样我们再用计算机的语言重写上面的推到表达式
d
p
[
i
]
[
j
]
=
d
p
[
i
]
[
j
−
1
]
+
1
(
如
果
区
间
[
i
,
j
]
内
没
有
c
h
=
=
s
[
j
]
)
d
p
[
i
]
[
j
]
=
M
I
N
(
d
p
[
i
]
[
m
−
1
]
+
d
p
[
m
]
[
j
−
1
]
(
满
足
s
[
m
]
=
=
s
[
j
]
且
m
>
i
且
m
<
j
的
所
有
m
)
)
dp[i][j] = dp[i][j - 1] + 1 (如果区间[i,j]内没有ch == s[j]) \\ dp[i][j] = MIN(dp[i][m - 1] + dp[m][j - 1] (满足s[m] == s[j]且m > i 且m < j的所有m))
dp[i][j]=dp[i][j−1]+1(如果区间[i,j]内没有ch==s[j])dp[i][j]=MIN(dp[i][m−1]+dp[m][j−1](满足s[m]==s[j]且m>i且m<j的所有m))
当然,作为一个合格的dp数组,它的边界条件就是每一个字符都需要1次的最小打印次数,也就是说
d
p
[
i
]
[
i
]
=
1
dp[i][i] = 1
dp[i][i]=1
最后我们需要的答案其实就是
d
p
[
0
]
[
n
−
1
]
dp[0][n-1]
dp[0][n−1],也就是包含所有元素的最小打印次数。
有了这些我们就可以进行dp工作了。
但是这个dp和平常的dp还有些许不同。普通的dp都是按顺序来,也就是说第一个下标从小向大推,第二个下标从小向大推,但我们这个不行,dp[m][j-1]中的m比i要大,就决定了我们没法用普通的方法推。
通过观察递推式可以得知,我们是间隔从小向大推,也就是我们首先遍历的元素是j-i,因为不管是dp[i][m-1]还是dp[m][j-1]还是dp[i][j-1]都是间隔要小于j-i的,我们通过遍历间隔的方式,就能将间隔由小向大推出。而且我们给的边界条件就是间隔最小的(dp[i][i]=1),我们所求的答案就是间隔最大的(dp[0][n-1]),也符合dp递推的关系。
然后就比较简单了。
C++代码
#define REP(i, j) for (int i = 0; i < j; ++i)
class Solution {
public:
int strangePrinter(string s) {
string st;
for (auto & ch : s) {
if (!(st.size() && ch == st.back()))
st.push_back(ch);
}
int n = st.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
cout << st << endl;
REP(i, n) dp[i][i] = 1;
for (int interval = 1; interval < n; ++interval)
REP(i, n - interval) {
int j = i + interval;
dp[i][j] = dp[i][j - 1] + 1;
if (st[i] == st[j]) dp[i][j] -= 1;
for (int m = i + 1; m < j; ++m)
if (st[m] == st[j]) {
dp[i][j] = min(dp[i][j], dp[i][m - 1] + dp[m][j - 1]);
m += 1;
}
}
return dp[0][n - 1];
}
};
Rust代码
impl Solution {
pub fn strange_printer(s: String) -> i32 {
let n = s.len();
let mut dp = vec![vec![0; n]; n];
let mut st = s.into_bytes();
for i in 0..n {
dp[i][i] = 1;
}
for interval in 1..n {
for i in 0..(n - interval) {
let j = i + interval;
dp[i][j] = dp[i][j - 1] + 1;
if st[i] == st[j] {
dp[i][j] -= 1;
}
for m in (i + 1)..j {
if st[m] == st[j] {
dp[i][j] = dp[i][j].min(dp[i][m - 1] + dp[m][j - 1]);
}
}
}
}
dp[0][n - 1]
}
}