O(n) 的解决方案实际上非常简单,通过构建“高度数组”,表示 1 相对于 0 的数量。所以高度为 2 意味着 1 比 0 多 2 个。一旦在某些条件下执行某些极大值检查,我们就会遍历高度数组。
关键观察
请注意,满足条件的子数组的高度必须在开始时最小,在结束时最大(相对于子数组,而不是整个数组)。
问题中样本的高度数组可以这样绘制,并标注答案:
v
/\/\/\
/\/ \
^
证明:
假设开始时高度不是最小值,这意味着子数组内部有一个点高度低于开始。此时,0的个数应该大于1的个数。矛盾。
假设最后的高度不是最大值,这意味着子数组中有一个点的高度大于结尾,例如索引j。然后在索引j 到最后,0 比 1 多(因为高度减小),所以当我们从右到左“扫描”子数组时,我们会在索引 j 处发现 0 多于 1。矛盾。
算法
现在问题可以解释为找到以子数组中最高高度结束的最长子数组,同时保持最小值不超过开始时的高度。这与 klrmlr 提到的maximum subarray problem 非常相似(“数组的连续子序列”更好地称为“子数组”)。这个想法不是保持O(n) 状态,而是保持“迄今为止的最大值”和“此时的最大值”。
按照该算法,下面是伪代码,遍历数组一次:
程序平衡_左_右
- 记录目前为止的最低点和最高点
- 如果此时的高度低于目前的最低点,则将起点改为该点之后的索引
- 如果此时的高度高于或等于目前为止的最高点,那么这是一个有效的子数组,记录长度(以及开始和结束索引,如果你愿意的话)
但是我们很快就会看到这个测试用例的问题(正如 Adam Jackson 通过个人交流指出的那样):1100101,可视化如下:
/\
/ \/\/
正确答案是 3(最后 101 个),但上面的算法会得到 2(前 11 个)。这是因为我们的答案显然隐藏在一座“高山”后面(即答案的最低点不低于山,而答案的最高点不高于山)。
因此我们需要确保在运行过程 Balance_Left_Right(上图)时,没有隐藏答案的“高山”。因此解决方案是从右侧遍历数组一次,尝试将数组划分为多个部分,在每个部分中,此属性保持:“1 的数量始终 >= 0 的数量,从右侧遍历",而且对于每个部分,它都不能再向左扩展了。
然后,在每个部分中,当从左侧遍历时,在该部分的末尾将具有最大高度,这就是最大值。并且可以证明,有了这个属性, balance_left_right 方法就可以找到本节的正确答案。因此,我们只需在每个部分上调用 balance_left_right 方法,然后从中取最大答案。
现在,您可能会问,为什么在每个部分上运行 Balance_Left_Right 就足够了?这是因为答案要求属性从左到右保持,因此它必须位于其中一个部分内,因为每个部分都满足一半的属性。
算法仍然是O(n),因为我们只访问每个元素两次,一次从右侧,一次从左侧。
最后一个测试用例会被划分如下:
/|\ |
/ | \|/\/
** ***
仅使用标有星号 (*) 的部分。
所以新算法如下:
程序 Max_Balance_Left_Right
- 对输入进行分区,其中 1 的数量 >= 右侧的 0 的数量(使用右侧的 Balance_Left,或者可以称之为 Balance_right)
- 在每个分区上运行 Balance_Left_Right
- 取最大值
这是 Python 中的代码:
def balance_left_right(arr):
lower = 0
upper = -2**32
lower_idx = 0 # Optional
upper_idx = -1 # Optional
result = (0,0,0)
height = 0
length = 0
for idx, num in enumerate(arr):
length += 1
height += 1 if num==1 else -1
if height<lower:
lower = height # Reset the lowest
upper = height # Reset the highest
lower_idx = idx+1 # Optional, record the starting point
length = 0 # Reset the answer
if height>=upper:
upper = height
upper_idx = idx # Optional, record the end point
if length > result[0]: # Take maximum length
result = (length, lower_idx, upper_idx)
return result
def max_balance_left_right(arr):
all_partitions = []
start = 0
end = len(arr)
right_partitions = balance_left(reversed(arr[start:end]))
for right_start, right_end in right_partitions:
all_partitions.append((end-right_end, end-right_start))
result = (0,0,0)
for start, end in all_partitions:
candidate = balance_left_right(arr[start:end])
if result[0] < candidate[0]:
result = (candidate[0], candidate[1]+start, candidate[2]+start)
return result
def balance_left(arr):
lower = 0
start_idx = 0
end_idx = -1
height = 0
result = []
for idx, num in enumerate(arr):
height += 1 if num==1 else -1
if height < lower:
if end_idx != -1:
result.append((start_idx,end_idx))
lower = height
start_idx = idx+1
end_idx = -1
else:
end_idx = idx+1
if end_idx != -1:
result.append((start_idx, end_idx))
return result
test_cases = [
[1,0,1,1,0,1,0,1,0,0],
[0,0,1,0,1,0,0,1,0,1,0,0,1,1,0,1,0,1,0,0,1],
[1,1,1,0,0,0,1,0,0,1,1,0,1,1,0,1,1,0],
[1,1,0,0,1,0,1,0,1,1,0,0,1,0,0],
[1,1,0,0,1,0,1],
[1,1,1,1,1,0,0,0,1,0,1,0,1,1,0,0,1,0,0,1,0,1,1]
]
for test_case in test_cases:
print 'Balance left right:'
print test_case
print balance_left_right(test_case)
print 'Max balance left right:'
print test_case
print max_balance_left_right(test_case)
print
将打印:
左右平衡:
[1, 0, 1, 1, 0, 1, 0, 1, 0, 0]
(8, 0, 7)
左右最大平衡:
[1, 0, 1, 1, 0, 1, 0, 1, 0, 0]
(8, 0, 7)
左右平衡:
[0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1]
(6, 12, 17)
左右最大平衡:
[0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1]
(6, 12, 17)
左右平衡:
[1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0]
(8, 9, 16)
左右最大平衡:
[1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0]
(8, 9, 16)
左右平衡:
[1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0]
(10, 0, 9)
左右最大平衡:
[1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0]
(10, 0, 9)
左右平衡:
[1, 1, 0, 0, 1, 0, 1]
(2, 0, 1)
左右最大平衡:
[1, 1, 0, 0, 1, 0, 1]
(3, 4, 6)
左右平衡:
[1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1]
(5, 0, 4)
左右最大平衡:
[1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1]
(6, 8, 13)
为了您的眼睛享受,测试用例的高度数组:
第一的:
v
/\/\/\
/\/ \
^
第二:
\
\/\/\ v
\/\/\ /\/\/\
\/ \/
^
第三:
v
/\ /\
/ \ /\/
/ \/\ /\/
\/
^
第四:
v
/\ /\
/ \/\/\/ \/\
^ \
第五:
/\ v
/ \/\/
^
第六:
/\ v
/ \ /\
/ \/\/\/ \/\ /
/ ^ \/\/
/
关于问题的说明
由于一些读者对OP到底想要什么感到困惑,尽管问题中已经明确说明了,让我通过一些例子来解释这个问题。
首先,来自问题的任务:
我的任务是找到一个子字符串的长度,其中空值的数量总是
这指的是“Catalan Number Ballot Problem”或“Available Change Problem”。在 Wiki 中您可以检查“单调路径”问题,您可以将“向右移动”映射为“1”,将“向上移动”映射为“0”。
问题是找到原始数组的一个子数组,这样,当从左到右和从右到左遍历子数组时,这个属性成立:
到目前为止看到的 0 的数量不应超过迄今为止看到的 1 的数量。
例如,字符串1010 包含从左到右 的属性,因为如果我们从左到右扫描数组,1 总是比0 多。但该属性从右到左不成立,因为从右边遇到的第一个字符是 0,所以一开始我们的 0(有一个)比 1(没有)多。
以OP给出的例子,我们看到字符串1011010100的答案是前八个字符,即:10110101。为什么?
好的,所以当我们从左到右遍历子数组时,我们看到 1 总是比 0 多。让我们在从左到右遍历数组时检查 1 和 0 的数量:
1:数(0)= 0,数(1)= 1
0:数(0)= 1,数(1)= 1
1:数(0)= 1,数(1)= 2
1:数(0)= 1,数(1)= 3
0:数(0)= 2,数(1)= 3
1:数(0)= 2,数(1)= 4
0:数(0)= 3,数(1)= 4
1:数(0)= 3,数(1)= 5
我们可以看到,在任何时间点,0 的数量总是小于或等于 1 的数量。这就是该属性从左到右的原因。并且可以从右到左进行相同的检查。
那么为什么不1011010100 和回答呢?
看看我们什么时候从右到左遍历字符串:
0:数(0)= 1,数(1)= 0
0:数(0)= 2,数(1)= 0
1:数(0)= 2,数(1)= 1
...
我没有放置完整的遍历,因为从第一步开始就已经违反了该属性,因为我们有num(0) > num(1)。这就是字符串1011010100 不满足问题约束的原因。
你也可以看到我的“高度数组”实际上是1的个数和0的个数之差,即:num(1) - num(0)。因此,为了拥有该属性,我们必须将 [相对] 高度设为正数。这可以通过不小于初始高度的高度来可视化。