題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。

🔗 ABC381E 11/22 Subsequence

Problem Statement

題目簡述

給定一個只包含 12/ 的字串,每次查詢一段子字串,要求在這段範圍內選出一個最長子序列,使它形如若干個 1、一個 /、再接上相同數量的 2。若無法選出任何合法的 11/22 字串,輸出 0

Constraints

約束條件

  • 1N1051 \leq N \leq 10^5
  • 1Q1051 \leq Q \leq 10^5
  • SS 是長度為 NN 的字串,且只包含 12/
  • 1LRN1 \leq L \leq R \leq N
  • 輸入皆為整數或合法字元

思路:二分答案

一個合法的 11/22 子序列長度一定是奇數,並且可以寫成:

k 個 1+1 個 /+k 個 2k \text{ 個 } 1 + 1 \text{ 個 } / + k \text{ 個 } 2

因此,問題轉化為:對每個查詢區間,找出最大的 kk,使得區間內能依序選出 kk1、一個 /、再 kk2。答案即為 2k+12k+1

為什麼不能直接統計數量?

因為子序列仍然要維持原本順序。即使區間內有足夠多的 12/,也必須存在一個 /,讓前面的 1 都能放在它左側,後面的 2 都能放在它右側。

為什麼可以二分答案

kk 可行,則任何更小的 k<kk' < k 也必然可行——只需捨棄多餘的配對,因此可行性具有單調性,故每個查詢可以對 kk 進行二分搜尋,找出最大的可行值。

檢查給定的 kk 是否可行

要檢查 kk 是否可行,最寬容的貪心策略是讓 1 盡量靠左、2 盡量靠右,以最大化中間能放 / 的範圍:

  • 在查詢區間內,取左側第 kk1 的位置。
  • 在查詢區間內,取右側倒數第 kk2 的位置。

若這兩個位置之間至少存在一個 /,則 kk 可行——選這兩個位置之間的任一個 / 即可配對。

反之,若這樣最寬容的安排都找不到中間的 /,任何其他選法只會讓 / 的可用範圍更窄,因此 kk 必然不可行。

找到區間內左側第 kk1 和右側倒數第 kk2 的位置,可以用二分搜尋在預先記錄的 12 的位置列表中找到。檢查兩個位置之間是否有 /,則可以用前綴和在 O(1)\mathcal{O}(1) 時間內完成。

結論

固定 kk 時只需要關心「第 kk 個可用的 1」與「倒數第 kk 個可用的 2」之間是否有 /,因此每次可行性檢查只需兩次位置二分搜尋搭配一次前綴和查詢,可在 O(logN)\mathcal{O}(\log N) 內完成。

複雜度分析

  • 時間複雜度:O(N+Qlog2N)\mathcal{O}(N + Q \log^2 N)
    • 預處理 NN 個字元的位置信息和前綴和需要 O(N)\mathcal{O}(N)
    • 對於每個查詢,二分答案 kk 需要 O(logN)\mathcal{O}(\log N),每次檢查 kk 的可行性又需要兩次二分搜尋(各 O(logN)\mathcal{O}(\log N))和一次前綴和查詢(O(1)\mathcal{O}(1)),總計 O(log2N)\mathcal{O}(\log^2 N)
  • 空間複雜度:O(N)\mathcal{O}(N)

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from bisect import bisect_left, bisect_right
from collections import defaultdict
from itertools import accumulate


def solve():
n, q = map(int, input().split())
s = input()
assert len(s) == n

# 記錄每個字元出現的所有位置 (1-indexed)
pos = defaultdict(list)
for i, ch in enumerate(s, start=1):
pos[ch].append(i)

# '/' 的前綴和:ss[i] = s[1..i] 中 '/' 的個數
ss = list(accumulate(map(lambda ch: ch == "/", s), initial=0))

for _ in range(q):
l, r = map(int, input().split())

# 區間內沒有 '/' 則不可能形成 11/22
if ss[r] - ss[l - 1] == 0:
print(0)
continue

def check(mid: int) -> bool:
# 在 [l, r] 內找第 mid 個 '1' 的位置
idx1 = bisect_left(pos["1"], l) + mid - 1
# 在 [l, r] 內找倒數第 mid 個 '2' 的位置
idx2 = bisect_right(pos["2"], r) - mid
if idx1 >= len(pos["1"]) or idx2 < 0:
return False
ll, rr = pos["1"][idx1], pos["2"][idx2]
# [ll, rr] 之間是否有至少一個 '/'
return ss[rr] - ss[ll - 1] > 0

# 二分搜尋最大的可行 k
left, right = 1, r - l + 1
while left <= right:
mid = (left + right) >> 1
if check(mid):
left = mid + 1
else:
right = mid - 1
# 答案 = 2k + 1
print(1 + (right << 1))


if __name__ == "__main__":
solve()