題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。
rating: 1969
Problem Statement
題目簡述
這是一個互動題。
Dilhan 有兩個初始為 [ 2 k ] [2^k] [ 2 k ] 的陣列 a a a 和 b b b 。他可以任意次數執行以下操作:
Flatten :選定一個陣列中的最大偶數元素 x x x ,將其替換為兩個 x / 2 x/2 x / 2 。
Concatenate :將 a a a 和 b b b 都更新為 a + b a+b a + b (陣列串接)。
最終 b b b 被丟棄,a a a 被隱藏,長度為 n n n 。你可以進行最多 300 300 3 0 0 次詢問,每次詢問區間 [ l , r ] [l, r] [ l , r ] 的和。
目標是求出隱藏陣列 a a a 中的最大值 。
Constraints
約束條件
n n n (1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1 ≤ n ≤ 1 0 5 )。
隱藏陣列中的元素 1 ≤ a i ≤ 2 30 1 \le a_i \le 2^{30} 1 ≤ a i ≤ 2 3 0 。
詢問次數限制:300 300 3 0 0 。
思路:二分搜尋
核心觀察
Flatten 操作:將 x x x 替換為 x 2 , x 2 \dfrac{x}{2}, \dfrac{x}{2} 2 x , 2 x ,總和不變 。
Concatenate 操作:將 a , b a, b a , b 都變為 a + b a + b a + b ,總和翻倍 。
因此,最終陣列的總和一定是 2 k 2^k 2 k 。
分割點的意義
我們可以透過二分搜尋找到前綴和等於 2 k − 1 2^{k-1} 2 k − 1 的位置 x x x 。這個分割點可能有兩種含義:
來自 Concatenate 操作 :
陣列是由 [ 1 , x ] [1, x] [ 1 , x ] 和 [ x + 1 , n ] [x+1, n] [ x + 1 , n ] 兩個子陣列拼接而成。
由於較短的區間使用 Flatten 操作的次數更少(因為 Flatten 只增加長度、不減少),所以最大值必定落在較短的區間 。
來自 Flatten 操作 :
這個區間是由某個陣列經過 Flatten 操作分裂而成。
同樣地,較短的區間使用 Flatten 的次數更少,最大值也落在較短區間 。
無論是哪種情況,最大值一定位於長度較短的那一半區間中 。
二分搜尋策略
基於上述觀察,我們可以採用以下策略:
查詢或從前一輪的總和 2 S 2S 2 S ,得到當前區間 [ l , r ] [l, r] [ l , r ] 的總和 S S S 。
利用二分搜尋找到分割點 m m m ,使得 [ l , m ] [l, m] [ l , m ] 的和為 S 2 \dfrac{S}{2} 2 S 。
比較左右兩部分的長度,選擇較短 的區間繼續處理。
當區間長度為 1 1 1 時,當前的總和 S S S 即為該元素的值,也是最大值。
經過一輪二分後,最大值所在的區間長度至少縮小一半。因此最多經過 O ( log n ) O(\log n) O ( log n ) 輪二分,就可以將區間縮小至 1 1 1 。
複雜度分析
時間複雜度 :O ( log 2 n ) \mathcal{O}(\log^2 n) O ( log 2 n )
每輪需要 O ( log n ) O(\log n) O ( log n ) 次詢問來二分找分割點,共 O ( log n ) O(\log n) O ( log n ) 輪,總計 O ( log 2 n ) ≤ 300 O(\log^2 n) \le 300 O ( log 2 n ) ≤ 3 0 0 。
空間複雜度 :O ( 1 ) \mathcal{O}(1) O ( 1 )
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 def query (l, r ): print (f"? {l} {r} " , flush=True ) return int (input ()) def answer (x ): print (f"! {x} " , flush=True ) def solve (): n = int (input ()) l, r = 1 , n s = query(1 , n) while l < r: s //= 2 left, right = l, r - 1 while left <= right: mid = (left + right) // 2 if query(l, mid) < s: left = mid + 1 else : right = mid - 1 if (left - l + 1 ) <= (r - left): r = left else : l = left + 1 answer(s) if __name__ == "__main__" : t = int (input ()) for _ in range (t): solve()
寫在最後
對於這類互動題,我們可以在本地撰寫一個 Judge 類別來模擬評測系統的行為。我們可以「劫持」原本用於與評測機通訊的 query() 和 answer() 函數,將它們重新指向本地 Judge 的對應方法。這樣一來,我們的程式碼可以在實際提交到評測系統之前,先在本地進行一定的測試和驗證。
Code with Judge
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 from random import randint, choicefrom itertools import accumulateDEBUG = False class Judge : def __init__ (self, n: int ): self.n = n self.query_count = 0 self.max_queries = 300 self.array = self._generate_array(n) self.s = list (accumulate(self.array, initial=0 )) def _generate_array (self, n: int ): k = randint(1 , 30 ) A, B = [1 << k], [1 << k] def flatten (arr ): max_val = max (arr) idx = choice([i for i, x in enumerate (arr) if x == max_val]) return arr[:idx] + [max_val // 2 , max_val // 2 ] + arr[idx+1 :] while len (A) < n: ops = [] if max (A) > 1 : ops.append('flatten_a' ) if max (B) > 1 and len (A) + len (B) + 1 <= n: ops.append('flatten_b' ) if len (A) + len (B) <= n: ops.append('concat' ) if not ops: k = randint(1 , 30 ) A, B = [1 << k], [1 << k] continue op = choice(ops) if op == 'concat' : new = A + B A = new[:] B = new[:] elif op == 'flatten_a' : A = flatten(A) else : B = flatten(B) assert len (A) == n return A def query (self, l: int , r: int ) -> int : assert 1 <= l <= r <= self.n self.query_count += 1 assert self.query_count <= self.max_queries return self.s[r] - self.s[l - 1 ] def answer (self, x: int ): print (f"[DEBUG] Answer: ! {x} " ) print (f"[DEBUG] Total queries used: {self.query_count} " ) if x == (ans := max (self.array)): print ("[RESULT] Correct!" ) else : print (f"[DEBUG] Generated array: {self.array} " ) raise ValueError(f"Wrong answer: {x} , Correct answer: {ans} " ) def query (l, r ): print (f"? {l} {r} " , flush=True ) return int (input ()) def answer (x ): print (f"! {x} " , flush=True ) def solve (): n = int (input ()) if not DEBUG else randint(1 , 1500 ) if DEBUG: global query, answer judge = Judge(n) query = judge.query answer = judge.answer l, r = 1 , n s = query(1 , n) while l < r: s //= 2 left, right = l, r - 1 while left <= right: mid = (left + right) // 2 if query(l, mid) < s: left = mid + 1 else : right = mid - 1 if (left - l + 1 ) <= (r - left): r = left else : l = left + 1 answer(s) if __name__ == "__main__" : t = int (input ()) if not DEBUG else 10 for _ in range (t): solve()