簡體   English   中英

在二進制字符串中查找最長子字符串,其中的子字符串不小於零

[英]Find longest substring in binary string with not less ones than zeros

如何在二進制字符串中找到最長的子字符串,其中余額 (即1和0的數量之差)> = 0?

例:

01110000010 - > 6:011100

1110000011110000111 - > 19:整個字符串

雖然這個問題看起來非常類似於最大值連續子序列(最大連續和)問題,但動態編程解決方案似乎並不明顯。 在分而治之的方法中,如何進行合並? 畢竟是一種“高效”的算法嗎? (一個簡單的O(n ^ 2)算法將迭代所有可能起點的所有子串。)

這是查找子字符串的修改變體,帶有一些附加條件 不同之處在於,在鏈接問題中,只允許這樣的子串,其中余額永遠不會低於零(在向前或向后方向上查看字符串)。 在給定的問題中,如果平衡在稍后階段恢復,則允許平衡降至零以下。

我有一個需要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) <= h(j)ji -> 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)}]

所有i最大的Lmax(i)將為您提供所需的長度。

我把證明留作練習:)如果你想不通,請聯系我。

此外,我的算法需要2次傳遞原始字符串,但您可以將它們合並為一個。

這可以在O(n)使用“高度數組”很容易地回答,表示1的數量相對於0的數量。 就像在鏈接問題中的答案一樣。

現在,我們不再關注原始數組,而是關注兩個由高度索引的數組,一個將包含找到高度的最小索引,另一個將包含找到這樣高度的最大索引。 由於我們不想要負指數,我們可以將所有內容都移動,這樣最小高度為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個空格和填充數字

我們可以在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]

所以尋找的答案是簡單地減去first_viewlast_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將是仍然滿足要求的更k..j串。 因此,考慮每個高度的第一個指數就足夠了。 類似於最后一個指數。

壓縮二次運行時

我們將從頭開始尋找具有平衡零的(本地)最長子串。 我們將忽略零的字符串。 (拐角情況:全零 - >空字符串,余額永遠不會再達到零 - >整個字符串。)在這些余額為零的子字符串中,將刪除所有尾隨零。

B表示平衡> 0的子字符串,Z表示僅帶零的子字符串。 每個輸入字符串可以如下分解(偽正則表達式):

B' (ZB)* Z?

每個B都是最大可行解決方案,這意味着它不能在任何方向上擴展而不會減少平衡。 但是,如果在折疊后余額仍大於零,則可能會折疊BZB或ZBZ的序列。

注意,如果ZBZ部分具有平衡> = 0,則總是可以將BZBZB的序列折疊成單個B。(可以在線性時間內一次完成。)一旦所有這樣的序列折疊,每個ZBZ的平衡部分低於零。 盡管如此,仍有可能存在平衡度大於零的BZB部分 - 即使在平衡值低於零的BZBZB序列中,前導和尾隨BZB部分的平衡均為零。 在這一點上,似乎很難確定哪個BZB崩潰。

二次方......

無論如何,通過這種簡化的數據結構,可以嘗試將所有B作為起點(如果還有余額則可能向左延伸)。 運行時間仍然是二次方,但(實際上)n小得多。

分而治之

另一個經典。 應該在O(n log n)中運行,但很難實現。

理念

最長的可行子串位於左半部分,右半部分,或者經過邊界。 為兩半調用算法。 對於邊界:

假設問題大小為n。 對於跨越邊界的最長可行子串,我們將計算子串的左半部分的平衡。

對於左半部分中-n / 2和n / 2之間的每個可能的平衡 ,確定在邊界處結束並且具有該(或更大)平衡的最長字符串的長度。 (線性時間!)對於從邊界開始的右半部分和最長的字符串執行相同的操作。 結果是兩個大小為n + 1的數組; 我們反轉其中一個,按元素添加它們並找到最大值。 (再次,線性。)

它為什么有效?

如果另一部分對此進行補償,則具有平衡> = 0且跨越邊界的子字符串可以在左側或右側部分中具有平衡<0。 (“借款”余額。)關鍵問題是借款多少; 我們迭代所有潛在的“平衡信用”並找到最佳權衡。

為什么這個O(n log n)?

因為合並(查看邊界交叉串)只需要線性時間。

為什么合並O(n)?

練習留給讀者。

動態編程 - 線性運行時間(最后!)

靈感來自這篇博文 簡單高效,一次通過在線算法 ,但需要一些時間來解釋。

理念

上面的鏈接顯示了一個不同的問題:最大子序列和。 它不能以1:1的方式映射到給定的問題,這里需要O(n)的“狀態”,與原始問題的O(1)相反。 仍然可以在O(1)中更新狀態。

讓我們重新解釋一下這個問題。 我們正在尋找輸入中最長的子串,其中平衡 ,即01之間的差值大於零。

狀態類似於我的其他分而治之的解決方案:我們計算每個位置i 每個可能的余額 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 <= 0:= undefined表示b > 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)

它是如何工作的?

如果從開始到i的平衡為負,則狀態函數s變得有效。 它記錄了達到零平衡的最早起始點,對於尚未讀取的所有可能的1秒數。

它為什么有效?

因為狀態函數的遞歸定義等同於其直接定義 - 最長字符串的起始位置,其余為b或更大,以位置i結束。

為什么遞歸定義是正確的?

通過歸納證明。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM