题目难度: 中等
今天继续更新 Leetcode 的剑指 Offer(专项突击版)系列, 大家在公众号 算法精选 里回复
剑指offer2
就能看到该系列当前连载的所有文章了, 记得关注哦~
题目描述
如果一个由 '0' 和 '1' 组成的字符串,是以一些 '0'(可能没有 '0')后面跟着一些 '1'(也可能没有 '1')的形式组成的,那么该字符串是 单调递增 的。
我们给出一个由字符 '0' 和 '1' 组成的字符串 s,我们可以将任何 '0' 翻转为 '1' 或者将 '1' 翻转为 '0'。
返回使 s 单调递增 的最小翻转次数。
示例 1:
- 输入:s = "00110"
- 输出:1
- 解释:我们翻转最后一位得到 00111.
示例 2:
- 输入:s = "010110"
- 输出:2
- 解释:我们翻转得到 011111,或者是 000111。
示例 3:
- 输入:s = "00011000"
- 输出:2
- 解释:我们翻转得到 00000000。
提示:
- 1 <= s.length <= 20000
- s 中只包含字符 '0' 和 '1'
题目思考
- 如何尽可能优化时间和空间复杂度?
解决方案
- 分析题目, 要想使得字符串单调递增, 肯定需要在某个位置有一个虚拟的分隔符, 它左边全是 0, 右边全是 1, 分隔符在开头或末尾时则代表全 1 或全 0 的特殊情况
- 这就引出了一个很直接的想法: 我们可以遍历每个分隔符位置, 然后统计它左边 1 的数目和右边 0 的数目, 它们的和就是需要翻转的次数, 而所有分隔符的翻转次数的最小值就是最终结果
- 不过这样时间复杂度达到了 O(N^2), 如何优化呢?
- 我们可以先遍历一遍字符串, 预处理统计每个下标的左边 1 和右边 0 的数目, 并记录在列表中; 然后再次遍历整个字符串, 累加两个计数, 就直接得到了每个分隔符位置的最小翻转次数, 这样就把时间复杂度优化到了 O(N), 不过需要额外的 O(N) 空间来存储两种计数, 有没有空间更优的做法呢?
- 我们可以换个思路, 分别记录每个下标字符设为 0 或 1 时, 以它结尾的最小翻转次数, 这样我们可以根据前一下标和原始字符递推得到当前最小次数, 这就是动态规划的思想, 有以下四种情况:
- 当前字符是 0 且需要保持为 0 时, 则只能从上一字符设为 0 的状态转移而来, 否则就违反了单调递增条件
- 当前字符是 0 且需要翻转成 1 时, 则可以从上一字符设为 0 或 1 的任意状态转移而来, 并加上当前的翻转次数 1
- 当前字符是 1 且需要翻转成 0 时, 则只能从上一字符设为 0 的状态转移而来, 否则就违反了单调递增条件, 当然转移时也需要加上当前的翻转次数 1
- 当前字符是 1 且需要保持为 1 时, 则可以从上一字符设为 0 或 1 的任意状态转移而来, 无需增加翻转次数
- 用数学语言来表示, 假设
dp[i][0]
/dp[i][1]
分别代表将第 i 个字符设为 0/1 时的最小翻转次数, 那么就有:if s[i] == '0':
dp[i][0]=dp[i-1][0]
dp[i][1]=min(dp[i-1][0],dp[i-1][1])+1
if s[i] == '1':
dp[i][0]=dp[i-1][0]+1
dp[i][1]=min(dp[i-1][0],dp[i-1][1])
- 由于当前 dp 值只和前面一个 dp 值有关, 所以我们可以只使用三个变量
dp0
和dp1
, 分别代表当前字符设为 0/1 时的最小翻转次数, 从而进一步优化空间复杂度 - 然后根据上面转移方程滚动计算新的
dp0
和dp1
, 最终结果就是两个 dp 值的较小值 - 下面的代码中有详细的注释, 方便大家理解
复杂度
- 时间复杂度
O(N)
: 需要遍历整个数组一遍 - 空间复杂度
O(1)
: 只需要几个常数空间的变量
代码
class Solution:
def minFlipsMonoIncr(self, s: str) -> int:
# 双变量滚动DP+01结尾
# dp[i][x]表示当前以x结尾的翻转次数
# 若s[i]==0: dp[i][0] = dp[i-1][0], dp[i][1] = min(dp[i-1][0],dp[i-1][1])+1
# 若s[i]==1: dp[i][0] = dp[i-1][0]+1, dp[i][1] = min(dp[i-1][0],dp[i-1][1])
# 最终结果就是min(dp[n-1])
# dp0和dp1分别表示当前以0/1结尾时的最小翻转次数
dp0, dp1 = 0, 0
for c in s:
if c == "0":
# dp[i][0] = dp[i-1][0], 所以dp0不变
# dp[i][1] = min(dp[i-1][0],dp[i-1][1])+1
dp1 = min(dp0, dp1) + 1
else:
# dp[i][1] = min(dp[i-1][0],dp[i-1][1])
dp1 = min(dp0, dp1)
# dp[i][0] = dp[i-1][0]+1
# 注意顺序, 不能先更新dp0, 否则dp1就不是从前一个dp0转移过来, 结果就不对了
dp0 += 1
return min(dp0, dp1)
大家可以在下面这些地方找到我~😊
我的公众号: 算法精选, 欢迎大家扫码关注~😊