如何在二进制字符串中查找余额的最长子字符串,即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的修改后的变体。不同之处在于,在链接问题中,只允许这样的子串,其中余额永远不会低于零(在向前或向后方向上查看字符串)。在给定的问题中,如果余额在稍后阶段恢复,则允许余额低于零。
答案 0 :(得分:5)
我的解决方案需要O(n)
额外的内存和O(n)
时间。
让我们将索引h(i)
的“高度”表示为
h(i) = <number of 1s in the substring 1..i> - <number of 0s in the same substring>
现在可以将问题重新表述为:查找i
和j
,例如h(i) <= h(j)
和j-i -> max
。
显然,h(0) = 0
,如果是h(n) = 0
,那么解决方案就是整个字符串。
现在让我们计算数组B
,以便B[x] = min{i: h(i) = -x
}。换句话说,让B[x]
成为i
所在的最左侧索引h(i)= -x
。
数组B[x]
的长度最多为n
,并在一次线性传递中计算。
现在我们可以迭代原始字符串,并为每个索引i
计算最长序列的长度,其中非负余额以i
结尾,如下所示:
Lmax(i) = i - B[MIN{0, h(i)}]
所有Lmax(i)
中最大的i
将为您提供所需的长度。
我将证据留作练习:)如果你无法弄明白,请联系我。
此外,我的算法需要2次传递原始字符串,但您可以将它们合并为一个。
答案 1 :(得分:3)
这可以在O(n)
中使用“高度数组”轻松回答,表示相对于0的数字的1的数量。与链接问题中的my answer一样。
现在,我们不再专注于原始数组,而是专注于两个索引高度的数组,一个将包含最小的索引,如找到高度,另一个将包含最大的索引这样的高度被找到。由于我们不想要负指数,我们可以将所有内容都移开,这样最小高度为0。
因此对于示例案例(我在最后添加了两个1以表明我的观点):
1110000011010000011111 Array height visualization /\ / \ / \ \ /\/\ / \/ \ / \ / \ / \/ (lowest height = -5) Shifted height array: [5, 6, 7, 8, 7, 6, 5, 4, 3, 4, 5, 4, 5, 4, 3, 2, 1, 0, 1, 2, 3] Height: 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个空格和填充数字
我们可以在first_view
中构建last_view
和O(n)
数组。
现在,对于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]
因此,找到答案只是从first_view
,
last_view_max
first_view = [17,16,15, 8, 7, 0, 1, 2, 3] last_view_max = [22,22,22,22,22,22, 5, 4, 3] result = [ 5, 6, 7,14,15,22, 4, 2, 0]
并且从起始索引0到结束索引22(即整个字符串)获得最大值(再次在O(n)
中),即22。 = d
正确性证明:
假设最大子字符串从索引i
开始,以索引j
结束。
如果索引i
的高度与索引k<i
的高度相同,那么k..j
将是一个更长的子字符串,仍然满足要求。因此,考虑每个高度的第一个指数就足够了。类似于最后一个指数。
答案 2 :(得分:0)
我们将从头开始寻找(本地)最长的子串,余额为零。我们将忽略零的字符串。 (转角情况:全零 - &gt;空字符串,余额永远不会再达到零 - &gt;整个字符串。)在这些余额为零的子字符串中,将删除所有尾随零。
由B表示具有余额的子串&gt; 0和Z是一个只有零的子串。每个输入字符串可以如下分解(伪正则表达式):
乙? (Z B)* Z?
每个B都是最大可行解决方案,这意味着它不能在不减少平衡的情况下向任一方向扩展。但是,如果在折叠后余额仍然大于零,则可能会折叠BZB或ZBZ的序列。
请注意,如果ZBZ部分具有平衡&gt; = 0,则总是可以将BZBZB的序列折叠为单个B.(可以在线性时间内一次完成。)一旦所有这些序列都被折叠,每个ZBZ部分的平衡低于零。尽管如此,仍有可能存在平衡度大于零的BZB部分 - 即使在平衡值低于零的BZBZB序列中,前导和尾随BZB部分的平衡均为零。在这一点上,似乎很难决定哪个BZB崩溃。
无论如何,通过这种简化的数据结构,人们可以尝试所有的Bs作为起点(如果还有余额,可能会向左延伸)。运行时间仍然是二次方,但(实际上)的n值要小得多。
答案 3 :(得分:0)
另一个经典。应该在O(n log n)中运行,但很难实现。
最长的可行子串位于左半部分,右半部分,或者经过边界。为两半调用算法。对于边界:
假设问题大小为n。对于跨越边界的最长可行子字符串,我们将计算子字符串左半部分的余额。
确定,每个可能的余额在-n / 2和n / 2之间,在左半部分,最长字符串的长度在边界处结束并具有此(或更大)平衡。 (线性时间!)对于从边界开始的右半部分和最长的字符串执行相同的操作。结果是两个大小为n + 1的数组;我们反转其中一个,按元素添加它们并找到最大值。 (再次,线性。)
跨越边界的余额> gt = 0的子字符串可以具有余额&lt;如果另一部分对此进行补偿,则左侧或右侧部分为0。 (“借款”余额。)关键问题是借款多少;我们迭代所有潜在的“平衡信用”并找到最佳权衡。
因为合并(查看边界交叉串)只需要线性时间。
练习留给读者。
答案 4 :(得分:0)
受到this blog post的启发。简单而有效,一次通过online algorithm,但需要一些时间来解释。
上面的链接显示了一个不同的问题:最大子序列总和。它不能以1:1的方式映射到给定的问题,这里需要O(n)的“状态”,与原始问题的O(1)相反。不过,状态可以在O(1)中更新。
让我们重新解释一下这个问题。我们正在寻找输入中最长的子字符串,其中余额,即0
和1
之间的差异大于零。
状态类似于我的另一个分而治之的解决方案:我们针对每个可能的余额 {{1}计算每个职位i
和最长字符串的起始位置b
,余额s(i, b)
或更高,结束位置b
。也就是说,从索引i
开始到s(i, b) + 1
结束的字符串的余额为i
或更高,并且不再有以b
结尾的字符串。
我们通过最大化i
来找到结果。
当然,我们不会将所有i - s(i, 0)
保留在内存中,只保留当前s(i, b)
(我们对输入进行迭代)的内容。我们从i
的{{1}}和s(0, b) := 0
的{{1}}开始。对于每个b <= 0
,我们会使用以下规则进行更新:
:= undefined
:b > 0
。i
:1
如果已定义,则s(i, b) := s(i - 1, b - 1)
如果0
未定义。函数s(i, b) := s(i - 1, b + 1)
(对于当前s(i, 0) := i
)可以实现为指向长度为s(i - 1, 1)
的数组的指针;根据输入,此指针向前或向后移动。在每次迭代中,我们都会注意到s
的价值。
状态函数i
生效,尤其是从开始到2n + 1
的余额为负数时。对于尚未读取的所有可能s(i, 0)
个数,它记录了达到零余额的最早起点。
因为状态函数的递归定义等同于其直接定义 - 最长字符串的起始位置,其余为s
或更大,以位置i
结束。
通过归纳证明。