持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情
题目链接:926. 将字符串翻转到单调递增
题目描述
如果一个二进制字符串,是以一些 0(可能没有 0)后面跟着一些 1(也可能没有 1)的形式组成的,那么该字符串是 单调递增 的。
给你一个二进制字符串 s,你可以将任何 0 翻转为 1 或者将 1 翻转为 0 。
返回使 s 单调递增的最小翻转次数。
提示:
s[i]为'0'或'1'
示例 1:
输入: s = "00110"
输出: 1
解释: 翻转最后一位得到 00111.
示例 2:
输入: s = "010110"
输出: 2
解释: 翻转得到 011111,或者是 000111。
示例 3:
输入: s = "00011000"
输出: 2
解释: 翻转得到 00000000。
整理题意
给定一个字符串只包含 '0' 或 '1',我们可以将字符串中任何一个 0 变成 1,任何一个 1 变成 0,题目问将字符串翻转成 000……01……111 的形式至少需要多少步。
需要注意的是
000……01……111的形式可能没有0,也可能没有1,也就可能是00……00,也可能是11……11,这都是符合题意的答案。
解题思路分析
首先观察题目数据范围,由于数据范围很大,无法通过暴力选取 0 和 1 的分界点然后再统计 0 前面 1 的个数和 1 后面 0 的个数来确定最小操作次数。
方法一:前缀和
我们可以考虑前缀和的思想来优化统计 0 和 1 的个数问题。我们可以提前预处理出 [1, n] (n 为字符串长度)所有的前缀和,比如我们想求区间 [a, b] 中有多少个 0 或者有多少个 1,可以通过 pre[b] - pre[a - 1] 来求得 1 或者 0 的个数。
方法二:动态规划
首先考虑两个状态:
- 假设使得字符串当前位置为
0时,首先考虑当前位置是否为0,如果不为0需要增加一次翻转次数。其次考虑当前这个0能由前面一个字符在什么状态下转移过来(核心思想),首先排除是由1转移过来的,因为题目要求我们将字符串翻转成000……01……111的形式,而……10……不符合题意,那么只剩下由0转移过来,可以确定如果当前位置要确定为0时,只能由前一个字符为0的情况转移过来。 - 假设当前字符为
1时,首先同样判断当前位置是否为1,是否需要增加翻转次数。其次考虑是由0还是1转移而来,由于当前字符为1时,前面一个字符既能是0也能是1,这是因为可能是0和1的分界点,也可能是一串1中间的一个1,所以我们需要在二者中取最小值即可(因为题目求最小翻转次数)。 - 定义
dp[i][j]表示为字符s[i]为j时(j为1或0)前i个字符所需的最小翻转次数。 - 最后答案就是取最后一个字符为
0或1时的二者中翻转次数最小的那个。
具体实现
方法一:前缀和
- 预处理字符串
s字符1的前缀和个数。用pre[i]表示前i + 1个字符中(因为下标从0开始)有多少个字符1。 - 初始化答案为字符串中
1的个数和0的个数中最小值,因为存在不包含0和不包含1的情况,将答案初始化为边界中的最小值。 - 枚举
[0, n - 1]中每个位置作为0和1的分界线,这里需要注意的是,由于初始化答案的时候已经将分界线位于下标0左边,和下标n - 1右边的分界点已经计算过了,所以这里直接从0开始枚举。
这里需要注意枚举边界情况,全
1和全0的情况。
- 由预处理得到的前缀和可以直接计算出当前分界线左边
1的个数和右边0的个数:
- 左边
1的个数:pre[i] - 右边
0的个数:(n - 1 - i) - (pre[n - 1] - pre[i])
(n - 1 - i)表示右边总的个数,(pre[n - 1] - pre[i])表示右边1的个数,相减得到0的个数。
- 由枚举得到的翻转次数取最小值即可。
方法二:动态规划
- 定义
dp[i][j]表示当前字符为j时(j = 0或j = 1),前i + 1个字符(因为下标从0开始)所需的最小翻转次数。 - 转移方程:
dp[i][0] = dp[i−1][0] + (s[i] == '1'):表示当前位置为0时,只能由上一个0字符转移过来,且判断当前位置是否为1,如果为1,需要增加一次翻转次数。dp[i][1] = min(dp[i−1][0], dp[i−1][1]) + (s[i] == '0'):表示当前位置为1时,可以由上一个字符0或者1转移过来,并且判断当前字符是否为0,如果为0需要增加一次翻转次数。
min(dp[n - 1][0], dp[n - 1][1]),最后返回二者中较小的即可。
复杂度分析
方法一:前缀和
- 时间复杂度:,其中
n是字符串s的长度。 - 空间复杂度:。
方法二:动态规划
- 时间复杂度:,其中
n是字符串s的长度。需要遍历字符串一次,对于每个字符计算最小翻转次数的时间都是 。 - 空间复杂度:。使用空间优化的方法,空间复杂度是 。因为
dp[i]只依赖于dp[i - 1],因此在计算状态值的过程中只需要维护前一个下标处的状态值,将空间复杂度降低到 。
代码实现
方法一:前缀和
class Solution {
public:
int minFlipsMonoIncr(string s) {
vector<int> pre;
pre.clear();
int n = s.length();
//求前缀和数组
for(int i = 0; i < n; i++){
pre.push_back(s[i] - '0');
if(i > 0) pre[i] += pre[i - 1];
}
//初始化最大反转次数为n
int ans = min(pre[n - 1], n - pre[n - 1]);
for(int i = 0; i < n; i++){
//pre[i]表示i之前1的个数
//(n - 1 - i) - (pre[n - 1] - pre[i])表示后面0的个数
ans = min(ans, pre[i] + (n - 1 - i) - (pre[n - 1] - pre[i]));
}
return ans;
}
};
方法二:动态规划
class Solution {
public:
int minFlipsMonoIncr(string s) {
//字符串长度
int n = s.length();
//动态规划
int dp[n + 1][2];
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++){
//当前 0 只能由 0 转化而来,如果当前为 '1' 需要增加一次操作
dp[i][0] = dp[i - 1][0] + (s[i - 1] - '0');
//当前 1 能由 0 或 1 转化而来,取最小值
dp[i][1] = min(dp[i - 1][0], dp[i - 1][1]);
//如果当前为 '0' 需要增加一次操作
if(s[i - 1] == '0') dp[i][1]++;
}
//返回两种情况的最小值
return min(dp[n][0], dp[n][1]);
}
};
总结
- 该题动态规划做法较难想到,动态规划的做法类似于这题 LCP 19. 秋叶收藏集,都运用了类似的动态规划思想。
- 动态规划的难点在于定义
dp[i][j]和转移方程。 - 该题需要注意边界问题,全为
1和全为0的情况需要考虑到。 - 方法一:前缀和测试结果:
- 方法二:动态规划测试结果:
可以看到两种方法在复杂度上并没有很大差异,但是我们可以改进动态规划的空间复杂度,因为
dp[i] 只依赖于 dp[i - 1],因此在计算状态值的过程中只需要维护前一个下标处的状态值,将空间复杂度降低到 。
结束语
在完成并不熟悉的事情时,我们时常害怕做不好而被人嘲笑。其实,我们不必太过关注他人的目光,与其畏手畏脚,不如大胆向前。给自己制定一些能够达到的小目标,每实现一个,你就会离自信更进一步。