【问题标题】:Find longest substring in binary string with not less ones than zeros在二进制字符串中查找最长的子字符串,其中不少于零
【发布时间】:2013-10-29 12:29:56
【问题描述】:

如何在二进制字符串中找到余额的最长子字符串,即1和0的数量之差>= 0?

例子:

01110000010 -> 6: 011100

1110000011110000111 -> 19:整个字符串

虽然这个问题看起来与Maximum Value Contiguous Subsequence (Maximum Contiguous Sum) 问题非常相似,但动态规划解决方案似乎并不明显。在分而治之的方法中,如何进行合并?毕竟“高效”的算法是可能的吗? (一个简单的 O(n^2) 算法只会遍历所有可能的起点的所有子字符串。)

这是Finding a substring, with some additional conditions 的修改变体。不同之处在于,在链接的问题中,仅允许平衡永远不会低于零的此类子字符串(以向前或向后的方向查看字符串)。在给定的问题中,允许余额低于零,只要它在稍后的某个阶段恢复。

【问题讨论】:

  • 您在其他问题中的回答不是在O(n) 中使用您的线性时间动态规划回答了这个问题吗?
  • 你的第二个例子有20个字符,不是19个,整个字符串的余额是-2,不是>=0
  • @Zruty:修正了第二个例子。
  • @justhalf:稍后会发布。只是在寻找一些竞争:-)
  • @krlmlr 这个问题与您链接的上一个问题有何不同?

标签: algorithm language-agnostic


【解决方案1】:

我有一个需要O(n) 额外内存和O(n) 时间的解决方案。

让我们将索引h(i) 的“高度”表示为

h(i) = <number of 1s in the substring 1..i> - <number of 0s in the same substring>

现在可以将问题重新表述为:查找ij,例如h(i) &lt;= h(j)j-i -&gt; max

显然,h(0) = 0,如果是h(n) = 0,那么解决方案就是整个字符串。

现在让我们计算数组B,这样B[x] = min{i: h(i) = -x}。换句话说,让B[x] 成为最左边的索引ih(i)= -x

数组B[x] 的长度最多为n,并在一次线性传递中计算。

现在我们可以遍历原始字符串,并为每个索引i 计算以i 结尾的非负余额最长序列的长度,如下所示:

Lmax(i) = i - B[MIN{0, h(i)}]

所有i 中最大的Lmax(i) 将为您提供所需的长度。

我将证明留作练习 :) 如果您无法弄清楚,请与我联系。

另外,我的算法需要对原始字符串进行 2 次传递,但您可以将它们合并为一次。

【讨论】:

  • 我还在写我的答案,看起来完全一样。稍后我会发布它。你打败了我。 =D
  • 猜猜我已经获得了“快速打字机”徽章 :)
  • 没有,因为我的回答比较全面=p
  • 您为每个索引执行min() 函数,看起来像O(N^2)
  • @Ron Teller - 不,它不是 O(N^2)。它是即时计算的。当他检查索引i 时,他还会更新B[h(i)] = i(如果尚未定义),并且h(i) 是预先计算的。
【解决方案2】:

这可以在O(n) 中使用“高度数组”很容易地回答,表示 1 相对于 0 的数量。喜欢链接问题中的my answer

现在,我们不再关注原始数组,而是关注两个数组以高度为索引,一个将包含找到的最小索引,另一个将包含最大的索引找到这样的高度的索引。由于我们不想要负索引,我们可以将所有内容向上移动,使得最小高度为 0。

所以对于示例案例(我在末尾添加了两个 1 以表明我的观点):

1110000011010000011111 数组高度可视化 /\ / \ / \ \ /\/\ / \/ \ / \ / \ / \/ (最低高度 = -5) 移位高度数组: [5, 6, 7, 8, 7, 6, 5, 4, 3, 4, 5, 4, 5, 4, 3, 2, 1, 0, 1, 2, 3] 身高:0 1 2 3 4 5 6 7 8 first_view = [17,16,15, 8, 7, 0, 1, 2, 3] last_view = [17,18,19,20,21,22, 5, 4, 3]

请注意,我们有 22 个数字和 23 个不同的索引,0-22,代表数字之间的 23 个空格并填充数字

我们可以在O(n) 中构建first_viewlast_view 数组。

现在,对于first_view 中的每个高度,我们只需要检查last_view 中每个较大的高度,并取与first_view 索引差异最大的索引。例如,从高度 0 开始,较大高度的索引最大值为 22。因此从索引 17+1 开始的最长子串将在索引 22 处结束。

要查找last_view数组中的最大索引,可以将其转换为O(n)右侧的最大值:

last_view_max = [22,22,22,22,22,22, 5, 4, 3]

所以找到答案只需从last_view_max 中减去first_view

first_view = [17,16,15, 8, 7, 0, 1, 2, 3] last_view_max = [22,22,22,22,22,22, 5, 4, 3] 结果 = [ 5, 6, 7,14,15,22, 4, 2, 0]

并取最大值(再次在 O(n) 中),即 22,从起始索引 0 到结束索引 22,即整个字符串。 =D

正确性证明:

假设最大子字符串从索引i 开始,在索引j 结束。 如果索引i 处的高度与索引k&lt;i 处的高度相同,那么k..j 将是一个更长的子字符串,仍然满足要求。因此,考虑每个高度的第一个索引就足够了。最后一个索引类似。

【讨论】:

    【解决方案3】:

    压缩二次运行时

    我们将从头开始寻找(本地)余额为零的最长子串。我们将忽略零字符串。 (极端情况:全零 -> 空字符串,余额永远不会再为零 -> 整个字符串。)在这些余额为零的子字符串中,所有尾随零都将被删除。

    用 B 表示余额 > 0 的子串,用 Z 表示只有零的子串。每个输入字符串可以分解如下(伪正则表达式):

    乙? (Z B)* Z?

    每个 B 都是最大可行解,这意味着它不能在不降低平衡的情况下向任一方向扩展。但是,如果 BZB 或 ZBZ 在折叠后余额仍然大于零,则可能会折叠 BZB 或 ZBZ 序列。

    请注意,如果 ZBZ 部分的余额 >= 0,则始终可以将 BZBZB 的序列折叠为单个 B。(可以在线性时间内一次性完成。)一旦所有此类序列都被折叠,余额每个 ZBZ 部分的值都低于零。尽管如此,仍有可能存在余额高于零的 BZB 部分——即使在余额低于零的 BZBZB 序列中,前导和尾随 BZB 部分的余额都超过零。在这一点上,似乎很难决定要崩溃哪个BZB。

    还是二次方...

    无论如何,使用这种简化的数据结构,可以尝试所有 B 作为起点(如果仍有余额,可能会向左延伸)。运行时间仍然是二次方,但(在实践中)n 小得多。

    【讨论】:

      【解决方案4】:

      分而治之

      另一个经典。应该在 O(n log n) 中运行,但实现起来相当困难。

      想法

      最长的可行子串要么在左半边,要么在右半边,要么越过边界。调用两半的算法。对于边界:

      假设问题大小为 n。对于跨越边界的最长可行子串,我们将计算子串左半部分的余额。

      确定,对于 -n/2 和 n/2 之间的每个可能的平衡,在左半部分,在边界处结束并具有此(或更大)的最长字符串的长度平衡。 (线性时间!)对右半部分和从边界开始的最长字符串执行相同的操作。结果是两个大小为 n + 1 的数组;我们反转其中一个,逐个添加它们并找到最大值。 (再次,线性。)

      为什么会起作用?

      如果另一部分对此进行补偿,则平衡 >= 0 且跨越边界的子串可以在左侧或右侧部分具有平衡

      为什么是 O(n log n)?

      因为合并(查看跨越边界的字符串)只需要线性时间。

      为什么要合并 O(n)?

      练习留给读者。

      【讨论】:

        【解决方案5】:

        动态编程——线性运行时间(终于!)

        灵感来自this blog post。简单高效,一次性online algorithm,但需要一些时间来解释。

        想法

        上面的链接显示了一个不同的问题:最大子序列和。它不能 1:1 映射到给定问题,这里需要 O(n) 的“状态”,与原始问题的 O(1) 形成对比。不过,状态可以在 O(1) 内更新。

        让我们重新表述这个问题。我们正在寻找输入中最长的子字符串,其中 balance,即01 之间的差值大于零。

        该状态类似于我的其他分而治之的解决方案:我们计算每个位置 i 对于每个可能的 balance b位置 s(i, b) 的最长字符串的余额为 b 或更大,结束于位置 i。也就是说,从索引s(i, b) + 1 开始并以i 结束的字符串的余额为b 或更大,并且不再存在以i 结尾的字符串。 我们通过最大化i - s(i, 0)来找到结果。

        算法

        当然,我们不会将所有s(i, b) 都保存在内存中,只保留当前i(我们遍历输入)的那些。我们从s(0, b) := 0 开始b &lt;= 0:= undefined 开始b &gt; 0。对于每个i,我们使用以下规则进行更新:

        1. 如果读取1s(i, b) := s(i - 1, b - 1)
        2. 如果读取0s(i, b) := s(i - 1, b + 1) 如果已定义,s(i, 0) := i 如果s(i - 1, 1) 未定义。

        函数s(用于当前i)可以实现为指向长度为2n + 1的数组的指针;此指针根据输入向前或向后移动。在每次迭代中,我们都会记录 s(i, 0) 的值。

        它是如何工作的?

        状态函数s 变得有效,特别是如果从开始到i 的余额为负数。它记录了所有可能尚未读取的1s 数量达到零余额的最早起点。

        为什么会起作用?

        因为状态函数的递归定义等价于它的直接定义——余额为b或更大且在i位置结束的最长字符串的起始位置。

        为什么递归定义正确?

        归纳证明。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-12-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多