将字符串翻转到单调递增(非DP的解法)

53 阅读2分钟

简介

926. 将字符串翻转到单调递增 的算法问题大多使用动态规划(DP)来求解。本文介绍了一种利用数学推导的来解决这个问题的方法,简单有趣。

问题描述

如果一个二进制字符串,是以一些 0(可能没有 0)后面跟着一些 1(也可能没有 1)的形式组成的,那么该字符串是 单调递增 的。
给你一个二进制字符串 s,你可以将任何 0 翻转为 1 或者将 1 翻转为 0 。
返回使 s 单调递增的最小翻转次数。

示例 1:
输入: s = "00110"
输出: 1
解释: 翻转最后一位得到 00111.

示例 2:
输入: s = "010110"
输出: 2
解释: 翻转得到 011111,或者是 000111。

示例 3:
输入: s = "00011000"
输出: 2
解释: 翻转得到 00000000。

问题分析

首先,我们换个角度来重新描述这个问题:

给你一个长度为 nn 二进制字符串 s。
请你选取前 xx 个字符,将前 xx 个字符中的所有 1 翻转为 0,将之后的( nxn - x 个)字符的所有 0 翻转为 1。

求:当 xx为多少时翻转次数 f(x)f(x) 最小?

通过上面的描述我们不难看出:

翻转次数 = 前 xx 个字符中 1 的个数 + 后面的 nxn - x 个字符中 0 的个数。

假设:前 xx 个字符中有 a(x)a(x) 个 0
:前 xx 个字符中有 xa(x)x - a(x) 个 1

又因为:长度为 nn 二进制字符串 s 中总共有 a(n)a(n)个 0
:后面的 nxn - x 个字符中有 a(n)a(x)a(n) - a(x) 个 0

那么我们可以得到: f(x)=(xa(x))+(a(n)a(x))f(x) = (x - a(x)) + (a(n) - a(x))

f(x)=a(n)+(x2a(x))f(x) = a(n) + (x - 2a(x))

再次回到我们的问题:

求解 fmin(x){f}_{min}(x) ?

答案呼之欲出,我们只需要遍历一遍根据上述公式计算并记录下函数的最小值即可。

代码实现

这里有一个细节:由于 a(n)a(n) 是固定的常量,所以可以在遍历的过程中忽略,在遍历结束后加上。


/**
 * 将字符串翻转到单调递增
 * @param {String} s  e.g. "AABBAAB"
 */
function minFlipsMonoIncr(s) {
  const n = s.length

  // partial 的最小值
  let partial_min = 0

  // 记录 0 的个数
  let a = 0

  for (let i = 0; i < n; i++) {
    // 计数 `a(x)`
    if (s[i] === '0') {
      a++
    }

    const x = i + 1

    // 公式中 `x - 2 * a(x)` 的部分
    const partial = x - 2 * a

    partial_min = Math.min(partial_min, partial)
  }

  // 追加上 a(n)
  return a + partial_min
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

写在最后

如果你有其他有意思的方法,欢迎留言分享。
如果你想了解动态规划(DP)版的求解,可以参看官方的解题