@@ -17,64 +17,235 @@ A subsequence of a string is a new string generated from the original string wit
1717
1818## 基礎思路
1919
20- 先找尋所有字母的最起始位置和最結束位置,然後再找尋中間的字母是否存在,如果存在則計數加一。
20+ 本題要求計算字串中所有長度為 3 的獨特迴文子序列,形式必定為 ** x y x** 。
21+ 子序列不需連續,只需保持原本字元出現順序;
22+ 同一種類的迴文(如 ` "aba" ` )無論可形成幾次,只能計算一次。
23+
24+ 在思考解法時,需掌握以下觀察:
25+
26+ - ** 迴文 x y x 的外層 x 必須同時出現在 y 的左邊與右邊。**
27+ 因此若以每個位置作為中心 y,我們只需找出左右皆存在的 x。
28+
29+ - ** 可用 26-bit 的 bitmask 記錄哪些字母已出現在左側或右側。**
30+ 使用 ` leftMask ` 表示左側出現過的字元集合;
31+ ` futureMask ` 表示右側仍會出現的字元集合。
32+
33+ - ** 對於每個中心字元 y,需要避免重複計算同一種迴文 x y x。**
34+ 因此為每個 y 建立 ` visitedOuterMaskForCenter[y] ` ,避免重複加計。
35+
36+ - ** 計算新出現的外層字元集合時,可用位元操作加速。**
37+
38+ 透過上述策略,我們可以在線性時間內找出所有獨特的長度 3 迴文子序列。
2139
2240## 解題步驟
2341
24- ### Step 1: 找尋所有字母的最起始位置和最結束位置
42+ ### Step 1:輔助函式 — 計算位元遮罩中 1 的個數
43+
44+ 實作一個工具函式,用 Kernighan 演算法計算 bitmask 中有多少個 bit 為 1,後面用來統計「新出現的外層字元 x」個數。
45+
46+ ``` typescript
47+ /**
48+ * 計算遮罩中 1 的個數(僅使用最低 26 bits)
49+ *
50+ * @param mask 32 位元整數遮罩
51+ * @returns 遮罩中 1 的數量
52+ */
53+ function countSetBitsInMask(mask : number ): number {
54+ let bitCount = 0 ;
55+
56+ // Kernighan 演算法:反覆移除最低位的 1
57+ while (mask !== 0 ) {
58+ mask &= mask - 1 ;
59+ bitCount ++ ;
60+ }
61+
62+ return bitCount ;
63+ }
64+ ```
65+
66+ ### Step 2:預處理字串長度與索引
67+
68+ 若字串長度小於 3,無法形成長度為 3 的迴文,直接回傳 0;
69+ 否則將每個字元轉成 0–25 的索引,並統計每個字元出現次數。
70+
71+ ``` typescript
72+ const length = s .length ;
73+
74+ // 若長度不足 3,無法形成 x y x 型迴文
75+ if (length < 3 ) {
76+ return 0 ;
77+ }
78+
79+ // 預先計算每個位置的字母索引與右側總出現次數
80+ const characterIndices = new Uint8Array (length );
81+ const rightCharacterCount = new Uint32Array (ALPHABET_SIZE );
82+
83+ for (let positionIndex = 0 ; positionIndex < length ; positionIndex ++ ) {
84+ const characterIndex = s .charCodeAt (positionIndex ) - 97 ; // 'a' 的字元編碼為 97
85+ characterIndices [positionIndex ] = characterIndex ;
86+ rightCharacterCount [characterIndex ]++ ;
87+ }
88+ ```
89+
90+ ### Step 3:初始化未來字元集合 ` futureMask `
91+
92+ ` futureMask ` 用一個 26-bit mask 表示「從當前中心位置往右看,仍然會出現的字元集合」。
93+
94+ ``` typescript
95+ // futureMask:若 bit c = 1 表示字元 c 仍會在當前中心的右側出現
96+ let futureMask = 0 ;
97+ for (let alphabetIndex = 0 ; alphabetIndex < ALPHABET_SIZE ; alphabetIndex ++ ) {
98+ if (rightCharacterCount [alphabetIndex ] > 0 ) {
99+ futureMask |= 1 << alphabetIndex ;
100+ }
101+ }
102+ ```
103+
104+ ### Step 4:初始化每個中心字元的「已用外層集合」
105+
106+ 為每種中心字元 y 建立一個 mask,紀錄已經搭配過的外層字元 x,避免重複計數同一種迴文 ` x y x ` 。
107+
108+ ``` typescript
109+ // visitedOuterMaskForCenter[c]:若 bit o = 1,代表迴文 o c o 已被計數過
110+ const visitedOuterMaskForCenter = new Uint32Array (ALPHABET_SIZE );
111+ ```
112+
113+ ### Step 5:初始化左側集合與結果,並建立主迴圈骨架
114+
115+ ` leftMask ` 記錄「在目前中心左側出現過的字元集合」,
116+ ` uniquePalindromeCount ` 負責統計最終不同迴文個數。
117+ 主迴圈會逐一將每個位置視為中心字元 y,先取得中心字元索引與其對應 bitmask。
118+
119+ ``` typescript
120+ // leftMask:bit c = 1 表示字元 c 已在當前中心左側出現過
121+ let leftMask = 0 ;
122+ // 紀錄獨特長度為 3 的迴文子序列數量
123+ let uniquePalindromeCount = 0 ;
124+
125+ // 將每個位置視為迴文 x y x 的中心字元
126+ for (let positionIndex = 0 ; positionIndex < length ; positionIndex ++ ) {
127+ const centerCharacterIndex = characterIndices [positionIndex ];
128+ const centerCharacterBitMask = 1 << centerCharacterIndex ;
129+
130+ // ...
131+ }
132+ ```
133+
134+ ### Step 6:在主迴圈中更新右側計數與 ` futureMask `
135+
136+ 在同一個主迴圈中,先將當前中心字元從右側計數中扣除,
137+ 若右側已不再出現該字元,則從 ` futureMask ` 中清除此字元。
25138
26139``` typescript
27- // 標記所有字母的最起始位置和最結束位置為 -1
28- const firstIndex = new Array (26 ).fill (- 1 );
29- const lastIndex = new Array (26 ).fill (- 1 );
140+ for (let positionIndex = 0 ; positionIndex < length ; positionIndex ++ ) {
141+ // Step 5:初始化中心字元索引與對應 bitmask
30142
31- for (let i = 0 ; i < n ; i ++ ) {
32- // 利用 ASCII 碼計算字母的 index
33- const charIndex = s .charCodeAt (i ) - ' a' .charCodeAt (0 );
143+ const centerCharacterIndex = characterIndices [positionIndex ];
144+ const centerCharacterBitMask = 1 << centerCharacterIndex ;
34145
35- // 僅在第一次出現時更新最起始位置
36- if (firstIndex [charIndex ] === - 1 ) {
37- firstIndex [charIndex ] = i ;
146+ // 從右側剩餘次數中移除這個中心字元
147+ const updatedRightCount = rightCharacterCount [centerCharacterIndex ] - 1 ;
148+ rightCharacterCount [centerCharacterIndex ] = updatedRightCount ;
149+
150+ // 若右側不再出現此字元,則在 futureMask 中清除此 bit
151+ if (updatedRightCount === 0 ) {
152+ futureMask &= ~ centerCharacterBitMask ;
38153 }
39154
40- // 持續更新最結束位置
41- lastIndex [charIndex ] = i ;
155+ // ...
156+ }
157+ ```
158+
159+ ### Step 7:在主迴圈中取得外層候選字元集合
160+
161+ 外層字元 x 必須同時出現在左側與右側,因此候選集合為 ` leftMask & futureMask ` 。
162+
163+ ``` typescript
164+ for (let positionIndex = 0 ; positionIndex < length ; positionIndex ++ ) {
165+ // Step 5:初始化中心字元索引與對應 bitmask
166+
167+ // Step 6:更新右側計數與 futureMask
168+
169+ // 外層字元必須同時存在於左側與右側
170+ const outerCandidateMask = leftMask & futureMask ;
171+
172+ // ...
42173}
43174```
44175
45- ### Step 2: 找尋中間的字母是否存在
176+ ### Step 8:在主迴圈中排除已使用外層,並累加新迴文數量
177+
178+ 對於當前中心字元 y,從候選集合中去除已使用過的外層 x,
179+ 計算新出現的外層 x 數量並累加,最後更新「已用外層集合」。
46180
47181``` typescript
48- // 依序檢查所有字母
49- for (let i = 0 ; i < 26 ; i ++ ) {
50- const start = firstIndex [i ];
51- const end = lastIndex [i ];
52-
53- // 若字母存在,且中間至少有一個字母時做計數
54- if (start !== - 1 && end !== - 1 && end > start + 1 ) {
55- const uniqueChars = new Set ();
56-
57- // 找尋中間的獨一無二字母
58- for (let j = start + 1 ; j < end ; j ++ ) {
59- uniqueChars .add (s [j ]);
182+ for (let positionIndex = 0 ; positionIndex < length ; positionIndex ++ ) {
183+ // Step 5:初始化中心字元索引與對應 bitmask
184+
185+ // Step 6:更新右側計數與 futureMask
186+
187+ // Step 7:取得外層候選字元集合
188+ const outerCandidateMask = leftMask & futureMask ;
189+
190+ if (outerCandidateMask !== 0 ) {
191+ const alreadyVisitedMask =
192+ visitedOuterMaskForCenter [centerCharacterIndex ];
193+
194+ // 僅保留尚未與此中心字元組成迴文的外層 x
195+ const newOuterMask = outerCandidateMask & ~ alreadyVisitedMask ;
196+
197+ // 若存在新的外層 x,則可以形成新的 x y x 型迴文
198+ if (newOuterMask !== 0 ) {
199+ uniquePalindromeCount += countSetBitsInMask (newOuterMask );
200+
201+ // 將這些外層 x 標記為已使用
202+ visitedOuterMaskForCenter [centerCharacterIndex ] =
203+ alreadyVisitedMask | newOuterMask ;
60204 }
61-
62- // 計數加上獨一無二字母的數量
63- result += uniqueChars .size ;
64205 }
206+
207+ // ...
208+ }
209+ ```
210+
211+ ### Step 9:在主迴圈中將中心字元加入左側集合,並結束迴圈後回傳結果
212+
213+ 處理完當前位置後,將中心字元納入 ` leftMask ` ,
214+ 讓之後的位置可以把它當作「左側可用外層字元」。
215+ 迴圈結束後回傳最後統計的迴文數。
216+
217+ ``` typescript
218+ for (let positionIndex = 0 ; positionIndex < length ; positionIndex ++ ) {
219+ // Step 5:初始化中心字元索引與對應 bitmask
220+
221+ // Step 6:更新右側計數與 futureMask
222+
223+ // Step 7:取得外層候選字元集合
224+
225+ // Step 8:排除已使用外層並累加結果
226+
227+ // 將當前中心字元加入左側集合,供後續位置使用
228+ leftMask |= centerCharacterBitMask ;
65229}
230+
231+ // 回傳獨特長度 3 迴文子序列的總數
232+ return uniquePalindromeCount ;
66233```
67234
68235## 時間複雜度
69236
70- - 由於需要遍歷所有字串內的字母,因此時間複雜度為 $O(n)$。
237+ - 每個位置只進行常數次位元運算(包含 mask 加減、bitwise AND/OR/NOT)。
238+ - ` countSetBitsInMask ` 最多執行 26 次迴圈(固定字母數量)。
239+ - 未使用巢狀迴圈、雙指標或額外掃描。
71240- 總時間複雜度為 $O(n)$。
72241
73242> $O(n)$
74243
75244## 空間複雜度
76245
77- - 不論字串內有多少字母,都僅需要建立兩個長度為 26 的陣列,因此空間複雜度為 $O(1)$。
78- - 總空間複雜度為 $O(1)$。
246+ - ` characterIndices ` 使用 $O(n)$
247+ - ` rightCharacterCount ` 、` visitedOuterMaskForCenter ` 、bitmask 等皆為常數空間
248+ - 無額外動態空間成長
249+ - 總空間複雜度為 $O(n)$。
79250
80- > $O(1 )$
251+ > $O(n )$
0 commit comments