括號問題¶
解法一覽¶
-
一、卡特蘭數
-
二、stack 解法
-
三、區間 dp(適用於有很多種括號類型時)
-
四、前綴 dp(適用於只有一種括號類型)
-
五、左括號想成 +1,右括號想成 -1,min prefix sum >= 0,sum = 0
-
greedy 修改
-
資料結構優化
-
左括號+1,右括號-1¶
2023 全國賽模擬賽 pF. 關卡設計 (level)
給一個括號序列,一次操作可將一個括號改變方向,問是否能恰好做 k 次操作使括號序列合法,若可以的話輸出一組解
\(n, k \le 2 \times 10^6\)
思路
【一、轉換問題】
如果將左括號想成 +1,右括號想成 -1, 那麼合法的條件是
- 每個 prefix sum 都大於等於 0
- 總和為 0
【二、修改的 greedy 策略】
也就是我們需要將一些 -1 要變成 +1,一些 +1 要變成 -1。因為要盡量讓每個 prefix sum 都大於等於 0,我們得到了一個 greedy 的策略:
-
-1 改成 +1 的,優先改愈左邊的愈好
-
+1 改成 -1 的,優先改愈右邊的愈好
【三、求得修改個別的數量】
我們假設一開始有 x 個 +1, y 個 -1,-1 改成 +1 的有 u 個,+1 改成 -1 的有 v 個。那麼我們是否可以直接透過 x, y, k 得到 u, v ?
因為總共就只能改 k 個,所以 u + v = k,一開始的 sum = x - y,之後的每個 u 會讓 sum += 2,每個 v 會讓 sum -= 2。而最後的 sum 要是 0,所以 sum = (x-y) + 2u - 2v = 0,我們就可以解一元二次方程式得到 u, v,再套用步驟二的 greedy 策略即可。無解的話可以在解 u, v 或是做 greedy 完後得到。
另一種想法:
為了簡化題目,我們將原本的括號序列每一項都替換成左括號。接著,我們令原本的括號序列為 a,依照上一句話替換後的序列為 b,則對於修改的貢獻,我們可以這麼想: 假設有一個陣列 v,若 a[i] != b[i],則 v[i] = -1,代表反悔操作,反則若 a[i] = b[i],則 v[i] = 1,代表花費一單位的貢獻改變第 i 項。我們的目標就是要從 b 選出 n / 2 項,讓他們重新變回右括號,而且要滿足: 括號序列須合法、最後的序列只能是更動 k 項,那其實這就等價於在 v 內選 n/2 項,讓他們的總和是 k'(k' 就是 a 改成 b 後還能修改的次數)。我們將式子列出來,令要選的 1 的總和為 x,-1 的總合為 y,則可以列出
x + y = k'
舉個實際的例子,例如說 n = 10,k = 6,a = )))))(((((
,則 b = ((((((((((
,所以 v = [-1, -1, -1, -1, -1, 1, 1, 1, 1, 1]
。此時 k' = 6 - 5 = 1。我們來解二元一次方程式
x + y = 1
得到 x = 3, y = -2。
x, y 若會不合法,此時可輸出無解,否則在得到 x, y 之後,我們就可以 greedy 的由後往前,將 b 變成我們最終的答案,具體來說,我們從後往前考慮,假如此時看到第 i 項,我們就看 v[i] 是對應到 x 或 y,若對應到的(假如是 x)還有剩,則就將 b[i] 改動,否則,我們就繼續往前面一項考慮。注意,最後改完的時候,也要記得檢查是否為不合法的括號序列。例如說 n = 10, k = 2, a = )))))(((((
,則 b = ((((((((((
,所以 v = [-1, -1, -1, -1, -1, 1, 1, 1, 1, 1]
。此時 k' = 2 - 5 = -3。我們解二元一次方程式可得到 x = 1, y = -4,但這樣改完後續列為 ())))(((()
,不合法。
最少修改次數
給一個長度為 \(n\) 的括號序列,一次操作可將一個括號改變方向,問至少幾次操作才可以使括號序列合法
\(n\le 2\times 10^6\)
思路
一樣使用上面分析的觀點,假設需要 u 個 -1 變成 +1,v 個 +1 變成 -1,我們的目標是最小化 u + v。我們分成兩個步驟:
- 使 sum 變成 0: 依照 (x-y) + 2u - 2v = 0,我們可求得 u - v,我們先將 u, v 其中一個設成 0,使 u + v 最小
- 使 min prefix sum >= 0: 若此時 min prefix sum < 0,我們就必須將一些-1 變成 +1,也就是 u 要增加到讓 min prefix sum = 0,而 u 增加 v 也要跟著增加,因此我們就可以確定 u + v 要是多少了
舉例來說,假設此時序列為 - + - - + + + - + +
,算出 x = 6, y = 4,所以可以由 (x-y) + 2u - 2v = 0 得到 u - v = -1,我們將 u 設為 0,這時 u = 0, v = 1,目前的 prefix sum:
可以看到 min prefix sum 為 -2,則我們需要 u = 1 來使 min prefix sum >= 0,用 greedy 的策略從前面修改 u,從後面修改 v,得:
所以最後是 u = 1, v = 2
另外一種方法是先使 min prefix sum >= 0,再調整讓 sum = 0。我們從第一項開始往後依序考慮,若當前右括號 - 左括號的數量 < 0 了,則將目前的右括號改成左括號,否則就往下一項看,這樣做到最後可以保證 min prefix sum >= 0,而且可能會多放了幾個左括號,也就是我們需要去增加 v 的數量,所以我們從後面往前將看到的右括號改成左括號直到 sum = 0 即可
合法判斷/最大匹配深度 CF 1263 E. Editor
現在有一個打字機,有以下操作 :
-
L
: 將 pointer 往左移 1 格 -
R
: 將 pointer 往右移 1 格 -
一個小寫字母或者
(
,)
: 將 pointer 上的字元替換為給定字元
在每次操作後,判斷這一行是否是合法括號序列。如果是的話,輸出最大匹配深度
思路
對於一個區間而言,括號能否成功匹配有兩個判斷標準:
- 左右括號數量要相同
- 任意前綴中,右括號的數目不能大於左括號的數目.
如果我們把左括號看為 +1,右括號看為 -1,則上述標準等價於:
- 區間和為 0
- 區間最小前綴和也應等於 0
此時最大匹配深度應是區間最大連續子段和。假設區間可以正確匹配,則前綴和不會出現負數情況,因此最大連續子段和等價於最大前綴和,我們只需要維護最大前綴和即可。
因此對於這類問題,我們只需要維護 :
-
最大前綴和(最大深度)
-
最小前綴和(判斷合法)
-
區間和(判斷合法)
參考 : https://blog.csdn.net/weixin_45799835/article/details/120182104
合法長度/方法數¶
Leetcode 32. Longest Valid Parentheses
給一個長度為 n 的括號序列,問最長合法子字串(連續)長度
\(0\le n\le 3\times 10^4\)
code
合法子字串方法數 51Nod 1791 合法括号子段
給一個長度為 n 的括號序列,求合法子字串個數
\(n\le 1.1\times 10^6\)
思路
考慮 dp 求解,令 dp(i) 表示以 i 結尾的合法括號子字串個數,答案就會是 \(\sum dp(i)\)
使用 stack 對於每一個左括號開始記錄它的位置,當遇到一個右括號,它可以由和自己匹配的位置減去 1 的位置轉移而來,即
要 + 1 是因為可以選擇要不要繼續往前接,或是用跟自己匹配的就好。以這個例子來說 ( ) ( ( ) ( ) ),跟最後一個右括號匹配的左括號以藍色標註: ( ) ( ( ) ( ) ),由於子字串一定要連續,所以只能從匹配的左括號之前繼續接
參考自: https://www.luogu.com.cn/blog/corleonefamily/kuo-hao-xu-lie-wen-ti
code
AcWing 4207. 最长合法括号子序列
給一個長度為 \(n\) 的括號序列,問最長合法子序列(不一定連續)長度
\(1\le n\le 10^6\)
思路
我們從第一項開始往後依序考慮,若當前右括號 - 左括號的數量 < 0 了,就不取當前這一項的右括號,否則就將 ans++
code
最長合法括號序列 CF 380 C. Sereja and Brackets
給定一個長度 \(n\) 的括號序列 \(s\),有 \(q\) 個詢問 :
- \(\text{query}(l, r):\) 輸出 \(s_l,\ldots ,s_r\) 的最長合法括號序列長度
\(1\le n\le 10^6, 1\le m\le 10^5\)
思路
考慮一個括號序列,我們把能匹配的括號全都刪掉,剩下的括號一定形如 ))))))(((((((((
我們考慮用線段樹去維護。考慮記錄每個區間
-
未匹配的左括號數量
-
未匹配的右括號數量
-
當前區間已經產生的貢獻和
在合併兩個區間時: 新區間的貢獻 = 左區間原先的貢獻 + 右區間原先的貢獻 + 合併後新產生的貢獻。其中,合併後新產生的貢獻 = 2 * min(左區間未匹配的左括號數, 右區間未匹配的右括號數)。
參考 : https://blog.csdn.net/weixin_45799835/article/details/120037468
合法括號子序列方法數
給一個長度為 n 的序列,求括號序列的合法「子字串」個數
\(n\le 5000\)
其他¶
最少交換次數
給一個長度為 \(n\) 的括號序列,每次可以 swap 相鄰的兩項,問至少幾次才能變合法
\(n\le 2\times 10^6\)
思路
利用 stack 處理括號的方式,令 cnt 為當前左括號 - 右括號的數量,若當前 cnt < 0,則要將當前最近的左括號給 swap 過來,具體來說,假設當前的 index 是 i,在右側離 i 最近的左括號在 j,則需要花 j - i 的 cost 將這個左括號給搬過來,然後我們再將 i, j swap 即可。實作上,可以用一個 queue 存所有左括號個位置,然後用一個指針在裡面單調往右移動即可,詳見代碼。
code
AcWing 3420. 括号序列
給一個長度為 n 的括號序列,盡可能少地添加若干括號使序列合法,問有幾種添加的方法數
\(n\le 5000\)
思路
【分析,簡化問題】
左括號與右括號所新增的位置方案是相互獨立的,不會相互影響,即使左、右括號添加在同一個間隙,因為不能存在 "()" 的形式,此處只能為類似 "))((" 的一種形式,故總的方案數等於左括號的方案數 * 右括號的方案數。
單獨考慮添加左括號,若以右括號為分割點,將整個序列進行分割,因為分割後的子字串中均為左括號,添加任意數目的左括號方案數均為一種,那麼此時,我們僅需考慮添加不同數量的左括號的方案數即可。右括號同理。
【前綴 dp】 dp(i, j) 表示只考慮前 i 部分,左括號比右括號多 j 個的所有方案數
轉移如下:
-
若 s[i] == '(',轉移式為 dp(i, j) = dp(i - 1, j - 1)
-
若 s[i] == ')',轉移式為 dp(i, j) = dp(i - 1, j + 1) + dp(i - 1, j) + ... + dp(i - 1, 0)。可以透過前綴優化得到 dp(i, j) = dp(i - 1, j + 1) + dp(i, j - 1)。為了防止越界,dp(i, 0) 需要特判。
code
2022 全國賽 pD. 文字編輯器 (editor)
有一個由 \(\texttt{+}, \texttt{[}, \texttt{]}, \texttt{x}\) 組成合法序列,此時將其中一個 \(\texttt{+}\) 改成 \(\texttt{|}\),並將所有 \(\texttt{[}, \texttt{]}\) 換成 \(\texttt{|}\)。給你這個改完的序列 \(s\),輸出任意一個原來的合法序列。
\(|s| \le 10^6\)
思路
兩個 \(\texttt{x}\) 中一定要有 \(\texttt{+}\),看哪兩個 \(\texttt{x}\) 之間沒有 \(\texttt{+}\),Greedy 的放即可
2023 IOIC 305 . 括號國
給一個長度為 \(n\) 的括號字串,問所有 substring 的「最長合法括號子序列長度」總和為何?
\(1\le n\le 2\times 10^5\)
思路
我們使用一般用 stack 括號序列的方式,若我們現在遇到了一個 closing,那前一個 opening 與目前這個 closing 的貢獻就是 opening 往前的長度 \(\times\) closing 往後的長度,這些 l, r 在這些範圍內的都會算到我們
code
UVA 1626. Brackets Sequence
給一個長度 n 的括號序列 s,可任意增加括號,問最少增加幾次才能使 s 成為合法括號序列 ?
\(n\le 100\)
思路
dp(l, r) = s[l..r] 最少要加入幾個括號可以變成合法的
轉移的話我們分成看看能不能拆成前後兩段,不能的話代表一定是前後配對
dp(l, r) = min{
-
dp(l, k) + dp(k + 1, r) // 可以分成前後兩段
-
dp(l + 1, r - 1) if ok(s[l], s[r]) // 前後兩個剛好可以配對
-
dp(l, r - 1) + 1 // 例如說 [ ( [ ( ]
-
dp(l + 1, r) + 1 // 例如說 ( [ ( ] )
TOI 2021 二模 pC. 配對問題(Pairing)
給定一個序列 \(a_1, a_2, ..., a_n\),一個點只能被匹配最多一次,當兩個點 \(i\) 與 \(j\) 配對時,就會獲得 \(a_i + a_{i+1} + \cdots + a_j\) 的分數。任何配對都不能出現部份相交的情形。匹配結束後,所有沒有被匹配到的點 \(i\) ,如果 \(a_i > 0\),可以獲得 \(a_i\) 分。問分數最大是多少
\(1 \leq n \leq 10^5, -10^9 \leq a_i \leq 10^9\)
思路
【區間 dp: O(n^3)】
dp(l, r) : l ~ r 的最多 cost
-
dp(l+1, r) + a[l]
-
dp(l, r-1) + a[r]
-
切兩半 dp(l, k) + dp(k+1, r)
-
l 和 r 配對 dp(l+1, r-1) + (a[l]+...+a[r])
【前綴 dp: O(n^2)】
dp(i, k) : 1~i 的 (#左括號 - #右括號) = k
ans = dp(n, 0)
轉移
-
i 是 "(" : dp(i-1, k-1) - pre[i-1]
-
i 是 ")" : dp(i-1, k+1) + pre[i]
-
i 是 "X" : dp(i-1, k) + pre[i] - pre[i-1]
【Greedy 觀察性質, 後悔法: O(n log n)】
如果有辦法匹配(與 pq.top 的貢獻是正的),就將目前這項選為右括弧,並 push 到 heap 中。若沒辦法匹配,也 push 到 heap 中。
那麼要如何處理「不選」的貢獻呢,我們其實可以將有選的貢獻中加入 -a[i],這樣就可以用扣得算了
這邊提供一組範例
USACO 2017 OPEN Modern Art 2 G
有一個長度為 n 的序列,一開始全部都是為未塗色,一次操作中可將若干個區間覆蓋上指定的顏色,且一種顏色只能塗一次。給一個目標序列 a,問最少做幾次操作才能原始序列變成 a
\(n\le 10^5\)
思路
對於一種顏色 i,因為他只能塗一次,所以我們可以找出這種顏色第一次出現的位置 l[i],與最後一次出現的位置 r[i],[l[i], r[i]] 就是他當初所覆蓋的區間。那如何區分塗色的先後關係?我們發現如果要合法的話,求出來的每個值的 l[i] 和 r[i] 只會是互不重疊,或是完全重疊,不會有部分重疊的情況,這就有點像我們括號問題的感覺了,同時,題目要問最少操作次數,對應到括號就是問最大匹配深度。具體來說,我們先預處理好每個顏色的 l[i], r[i],然後從左到右掃過去,每遇到一個 l[i] 就加上一個嵌套層數,每遇到一個 r[i] 就減去一個嵌套層數,那麼答案就是當前位置嵌套層數的最大值。
不合法的情況就是有部分重疊的時候,例如 1 2 1 2,其實就相當於在掃描的過程中,若加入的點不是左端點,也不與當前在塗的顏色(stack 的頂端)相同。
至於無色的情況怎麼處理? 假設我們的序列是 1-base,我們可以把無色想成是一個從 [0, n + 1] 的塗色。