Skip to content

Commit 68a6113

Browse files
committed
Feat: Improve 1930 speed for 20x
1 parent 147edcb commit 68a6113

File tree

2 files changed

+289
-59
lines changed

2 files changed

+289
-59
lines changed

1930-Unique Length-3 Palindromic Subsequences/Note.md

Lines changed: 204 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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)$
Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,99 @@
1+
const ALPHABET_SIZE = 26;
2+
3+
/**
4+
* Count the number of set bits (1s) in a 32-bit mask.
5+
*
6+
* Only the lower 26 bits are used in this problem.
7+
*
8+
* @param mask - 32-bit integer mask.
9+
* @returns Number of bits set to 1 in the mask.
10+
*/
11+
function countSetBitsInMask(mask: number): number {
12+
let bitCount = 0;
13+
14+
// Kernighan's algorithm: repeatedly remove the lowest set bit
15+
while (mask !== 0) {
16+
mask &= mask - 1;
17+
bitCount++;
18+
}
19+
20+
return bitCount;
21+
}
22+
23+
24+
/**
25+
* Count the number of unique palindromic subsequences of length three.
26+
*
27+
* A valid palindrome has the form x y x, where x and y are lowercase letters.
28+
*
29+
* @param s - Input string consisting of lowercase English letters.
30+
* @returns Number of unique palindromes of length three that are subsequences of s.
31+
*/
132
function countPalindromicSubsequence(s: string): number {
2-
const n = s.length;
3-
let result = 0;
33+
const length = s.length;
434

5-
// Mark the appearance of the first and last index of each character
6-
const firstIndex = new Array(26).fill(-1);
7-
const lastIndex = new Array(26).fill(-1);
35+
// If the string is too short, there cannot be any length-3 palindromes
36+
if (length < 3) {
37+
return 0;
38+
}
839

9-
for (let i = 0; i < n; i++) {
10-
// Convert the character to an index (ASCII)
11-
const charIndex = s.charCodeAt(i) - 'a'.charCodeAt(0);
40+
// Precompute each character's alphabet index and its total frequency
41+
const characterIndices = new Uint8Array(length);
42+
const rightCharacterCount = new Uint32Array(ALPHABET_SIZE);
1243

13-
// Update the first only if first appearance
14-
if (firstIndex[charIndex] === -1) {
15-
firstIndex[charIndex] = i;
16-
}
44+
for (let positionIndex = 0; positionIndex < length; positionIndex++) {
45+
const characterIndex = s.charCodeAt(positionIndex) - 97; // 'a' has char code 97
46+
characterIndices[positionIndex] = characterIndex;
47+
rightCharacterCount[characterIndex]++;
48+
}
1749

18-
// Always update the last appearance
19-
lastIndex[charIndex] = i;
50+
// futureMask: bit c is 1 if character c still appears at or to the right of the current center
51+
let futureMask = 0;
52+
for (let alphabetIndex = 0; alphabetIndex < ALPHABET_SIZE; alphabetIndex++) {
53+
if (rightCharacterCount[alphabetIndex] > 0) {
54+
futureMask |= 1 << alphabetIndex;
55+
}
2056
}
2157

22-
// Iterate through all characters
23-
for (let i = 0; i < 26; i++) {
24-
const start = firstIndex[i];
25-
const end = lastIndex[i];
58+
// visitedOuterMaskForCenter[c]: bit o is 1 if palindrome o c o has already been counted
59+
const visitedOuterMaskForCenter = new Uint32Array(ALPHABET_SIZE);
60+
61+
// leftMask: bit c is 1 if character c has appeared strictly to the left of the current center
62+
let leftMask = 0;
63+
let uniquePalindromeCount = 0;
2664

27-
// If the character appears and there is at least one character between the first and last appearance
28-
if (start !== -1 && end !== -1 && end > start + 1) {
29-
const uniqueChars = new Set();
65+
// Treat each position as the middle character of x y x
66+
for (let positionIndex = 0; positionIndex < length; positionIndex++) {
67+
const centerCharacterIndex = characterIndices[positionIndex];
68+
const centerCharacterBitMask = 1 << centerCharacterIndex;
3069

31-
// Count the unique characters between the first and last appearance
32-
for (let j = start + 1; j < end; j++) {
33-
uniqueChars.add(s[j]);
70+
// Remove this occurrence from the right side
71+
const updatedRightCount = rightCharacterCount[centerCharacterIndex] - 1;
72+
rightCharacterCount[centerCharacterIndex] = updatedRightCount;
73+
74+
// If no more of this character on the right, clear its bit in futureMask
75+
if (updatedRightCount === 0) {
76+
futureMask &= ~centerCharacterBitMask;
77+
}
78+
79+
// Outer letters must appear both left and right of the center
80+
const outerCandidateMask = leftMask & futureMask;
81+
82+
if (outerCandidateMask !== 0) {
83+
const alreadyVisitedMask = visitedOuterMaskForCenter[centerCharacterIndex];
84+
const newOuterMask = outerCandidateMask & ~alreadyVisitedMask;
85+
86+
// Only count outer letters we have not used with this center letter yet
87+
if (newOuterMask !== 0) {
88+
uniquePalindromeCount += countSetBitsInMask(newOuterMask);
89+
visitedOuterMaskForCenter[centerCharacterIndex] =
90+
alreadyVisitedMask | newOuterMask;
3491
}
35-
result += uniqueChars.size;
3692
}
93+
94+
// After processing this center, mark it as available on the left side
95+
leftMask |= centerCharacterBitMask;
3796
}
3897

39-
return result;
98+
return uniquePalindromeCount;
4099
}

0 commit comments

Comments
 (0)