【C/C++】926. 将字符串翻转到单调递增

154 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情


题目链接:926. 将字符串翻转到单调递增

题目描述

如果一个二进制字符串,是以一些 0(可能没有 0)后面跟着一些 1(也可能没有 1)的形式组成的,那么该字符串是 单调递增 的。

给你一个二进制字符串 s,你可以将任何 0 翻转为 1 或者将 1 翻转为 0

返回使 s 单调递增的最小翻转次数。

提示:

  • 1s.length1051 \leqslant s.length \leqslant 10^5
  • 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,这都是符合题意的答案。

解题思路分析

首先观察题目数据范围,由于数据范围很大,无法通过暴力选取 01 的分界点然后再统计 0 前面 1 的个数和 1 后面 0 的个数来确定最小操作次数。

方法一:前缀和

我们可以考虑前缀和的思想来优化统计 01 的个数问题。我们可以提前预处理出 [1, n]n 为字符串长度)所有的前缀和,比如我们想求区间 [a, b] 中有多少个 0 或者有多少个 1,可以通过 pre[b] - pre[a - 1] 来求得 1 或者 0 的个数。

方法二:动态规划

首先考虑两个状态:

  1. 假设使得字符串当前位置为 0 时,首先考虑当前位置是否为 0,如果不为 0 需要增加一次翻转次数。其次考虑当前这个 0 能由前面一个字符在什么状态下转移过来(核心思想),首先排除是由 1 转移过来的,因为题目要求我们将字符串翻转成 000……01……111 的形式,而 ……10…… 不符合题意,那么只剩下由 0 转移过来,可以确定如果当前位置要确定为 0 时,只能由前一个字符为 0 的情况转移过来。
  2. 假设当前字符为 1 时,首先同样判断当前位置是否为 1,是否需要增加翻转次数。其次考虑是由 0 还是 1 转移而来,由于当前字符为 1 时,前面一个字符既能是 0 也能是 1,这是因为可能是 01 的分界点,也可能是一串 1 中间的一个 1,所以我们需要在二者中取最小值即可(因为题目求最小翻转次数)。
  3. 定义 dp[i][j] 表示为字符 s[i]j 时(j10)前 i 个字符所需的最小翻转次数。
  4. 最后答案就是取最后一个字符为 01 时的二者中翻转次数最小的那个。

具体实现

方法一:前缀和

  1. 预处理字符串 s 字符 1 的前缀和个数。用 pre[i] 表示前 i + 1 个字符中(因为下标从 0 开始)有多少个字符 1
  2. 初始化答案为字符串中 1 的个数和 0 的个数中最小值,因为存在不包含 0 和不包含 1 的情况,将答案初始化为边界中的最小值。
  3. 枚举 [0, n - 1] 中每个位置作为 01 的分界线,这里需要注意的是,由于初始化答案的时候已经将分界线位于下标 0 左边,和下标 n - 1 右边的分界点已经计算过了,所以这里直接从 0 开始枚举。

这里需要注意枚举边界情况,全 1 和全 0 的情况。

  1. 由预处理得到的前缀和可以直接计算出当前分界线左边 1 的个数和右边 0 的个数:
  • 左边 1 的个数:pre[i]
  • 右边 0 的个数:(n - 1 - i) - (pre[n - 1] - pre[i])

(n - 1 - i) 表示右边总的个数,(pre[n - 1] - pre[i]) 表示右边 1 的个数,相减得到 0 的个数。

  1. 由枚举得到的翻转次数取最小值即可。

方法二:动态规划

  1. 定义 dp[i][j] 表示当前字符为 j 时(j = 0j = 1),前 i + 1 个字符(因为下标从 0 开始)所需的最小翻转次数。
  2. 转移方程:
  • 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 需要增加一次翻转次数。
  1. min(dp[n - 1][0], dp[n - 1][1]),最后返回二者中较小的即可。

复杂度分析

方法一:前缀和

  • 时间复杂度:O(n)O(n),其中 n 是字符串 s 的长度。
  • 空间复杂度:O(n)O(n)

方法二:动态规划

  • 时间复杂度:O(n)O(n),其中 n 是字符串 s 的长度。需要遍历字符串一次,对于每个字符计算最小翻转次数的时间都是 O(1)O(1)
  • 空间复杂度:O(1)O(1)。使用空间优化的方法,空间复杂度是 O(1)O(1)。因为 dp[i] 只依赖于 dp[i - 1],因此在计算状态值的过程中只需要维护前一个下标处的状态值,将空间复杂度降低到 O(1)O(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 的情况需要考虑到。
  • 方法一:前缀和测试结果:

微信截图_20220614132205.png

  • 方法二:动态规划测试结果:

926. 将字符串翻转到单调递增.png 可以看到两种方法在复杂度上并没有很大差异,但是我们可以改进动态规划的空间复杂度,因为 dp[i] 只依赖于 dp[i - 1],因此在计算状态值的过程中只需要维护前一个下标处的状态值,将空间复杂度降低到 O(1)O(1)

微信截图_20220614133756.png

结束语

在完成并不熟悉的事情时,我们时常害怕做不好而被人嘲笑。其实,我们不必太过关注他人的目光,与其畏手畏脚,不如大胆向前。给自己制定一些能够达到的小目标,每实现一个,你就会离自信更进一步。