Skip to content

Commit 24a478c

Browse files
committed
Add: Add 2025/10/19
1 parent 88327d2 commit 24a478c

File tree

3 files changed

+323
-0
lines changed

3 files changed

+323
-0
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# 1625. Lexicographically Smallest String After Applying Operations
2+
3+
You are given a string `s` of even length consisting of digits from `0` to `9`, and two integers `a` and `b`.
4+
5+
You can apply either of the following two operations any number of times and in any order on `s`:
6+
7+
- Add `a` to all odd indices of `s` (0-indexed).
8+
Digits post 9 are cycled back to `0`.
9+
For example, if `s = "3456"` and `a = 5`, `s` becomes `"3951"`.
10+
- Rotate `s` to the right by `b` positions.
11+
For example, if `s = "3456"` and `b = 1`, `s` becomes `"6345"`.
12+
13+
Return the lexicographically smallest string you can obtain by applying the above operations any number of times on `s`.
14+
15+
A string `a` is lexicographically smaller than a string `b` (of the same length) if in the first position where `a` and `b` differ,
16+
string `a` has a letter that appears earlier in the alphabet than the corresponding letter in `b`.
17+
For example, `"0158"` is lexicographically smaller than `"0190"` because the first position they differ is at the third letter,
18+
and `'5'` comes before `'9'`
19+
20+
**Constraints:**
21+
22+
- `2 <= s.length <= 100`
23+
- `s.length` is even.
24+
- `s` consists of digits from `0` to `9` only.
25+
- `1 <= a <= 9`
26+
- `1 <= b <= s.length - 1`.
27+
28+
## 基礎思路
29+
30+
本題要我們對一個由數字組成、長度為偶數的字串 `s`,重複執行兩種操作,使最終結果在字典序上最小:
31+
32+
1. **加法操作**:對所有奇數索引的位數加上 `a`,若超過 9 則循環回 0;
33+
2. **旋轉操作**:將整個字串向右旋轉 `b` 位。
34+
35+
可任意次數與順序地應用這兩個操作,最後需找出能得到的最小字典序字串。
36+
37+
在思考解法時,我們需要注意以下幾點:
38+
39+
* **操作具週期性**
40+
加法操作對某位數而言,每 10 次會回到原值;旋轉操作則會在經過 $\text{lcm}(b, n)$ 次後回到初始狀態。
41+
* **可達狀態有限**
42+
雖然可重複操作無限次,但實際不同的結果只有有限個,因此可透過數學週期找出所有獨特狀態。
43+
* **索引奇偶的影響**
44+
若旋轉步長 `b` 為奇數,則「奇偶索引」會互換,使得原本只能改變奇數位的操作,經過某些旋轉後也能影響偶數位。
45+
* **字典序判斷**
46+
在生成所有可達狀態後,需有效比較候選字串的字典序以找出最小者。
47+
48+
為此,我們採用以下策略:
49+
50+
* **使用數學週期化簡操作空間**
51+
利用最大公因數 (`gcd`) 找出旋轉與加法的最小循環週期,僅需在有限次內嘗試所有可能組合。
52+
* **分別優化奇偶索引**
53+
根據 `b` 的奇偶性決定是否同時調整偶數索引。
54+
* **直接操作字元陣列**
55+
避免多餘字串拼接的開銷,以陣列形式比較字典序,提升效能。
56+
57+
## 解題步驟
58+
59+
### Step 1:輔助函數 `greaterCommonDivisor` — 計算最大公因數
60+
61+
利用歐幾里得演算法計算兩數的最大公因數,協助求出旋轉與加法的循環週期。
62+
63+
```typescript
64+
/**
65+
* 使用歐幾里得演算法計算兩數的最大公因數。
66+
*
67+
* @param valueA 第一個正整數
68+
* @param valueB 第二個正整數
69+
* @returns 最大公因數
70+
*/
71+
function greaterCommonDivisor(valueA: number, valueB: number): number {
72+
while (valueB !== 0) {
73+
const remainder = valueA % valueB;
74+
valueA = valueB;
75+
valueB = remainder;
76+
}
77+
return valueA;
78+
}
79+
```
80+
81+
### Step 2:初始化主要變數
82+
83+
建立初始狀態,包括原字串、雙倍字串(方便旋轉切片)、旋轉步長與加法循環長度。
84+
85+
```typescript
86+
// 字串長度
87+
const stringLength = s.length;
88+
let smallestString = s;
89+
90+
// 建立雙倍字串,方便無模運算旋轉切片
91+
const doubledString = s + s;
92+
93+
// 計算旋轉步長(由 gcd 決定能形成的唯一旋轉組合數)
94+
const rotationStep = greaterCommonDivisor(b, stringLength);
95+
96+
// 計算加法循環長度(每 cycleLength 次回到原狀)
97+
const cycleLength = 10 / greaterCommonDivisor(a, 10);
98+
```
99+
100+
### Step 3:輔助函數 `applyBestAddition` — 最佳化指定奇偶索引的加法操作
101+
102+
針對奇數或偶數索引,嘗試所有可能的加法次數,找出使該位數最小的增量,並將其應用到所有相同奇偶的位上。
103+
104+
```typescript
105+
/**
106+
* 對指定奇偶性的索引套用最佳加法操作。
107+
*
108+
* @param digits 可變的字元陣列
109+
* @param startIndex 起始索引(0 表偶數,1 表奇數)
110+
*/
111+
function applyBestAddition(digits: string[], startIndex: number) {
112+
// 取得此奇偶性下的首個數字值
113+
const originalDigit = digits[startIndex].charCodeAt(0) - 48;
114+
let minimumDigit = 10;
115+
let bestTimes = 0;
116+
117+
// 嘗試所有加法循環次數,找出能讓結果最小的次數
118+
for (let times = 0; times < cycleLength; times++) {
119+
const addedDigit = (originalDigit + (times * a) % 10) % 10;
120+
if (addedDigit < minimumDigit) {
121+
minimumDigit = addedDigit;
122+
bestTimes = times;
123+
}
124+
}
125+
126+
// 套用最佳增量至所有同奇偶索引位置
127+
const increment = (bestTimes * a) % 10;
128+
for (let index = startIndex; index < stringLength; index += 2) {
129+
const baseDigit = digits[index].charCodeAt(0) - 48;
130+
digits[index] = String.fromCharCode(48 + ((baseDigit + increment) % 10));
131+
}
132+
}
133+
```
134+
135+
### Step 4:輔助函數 `isLexicographicallySmaller` — 比較字典序
136+
137+
逐字比較兩組字元陣列,判斷候選字串是否比當前最佳解更小。
138+
139+
```typescript
140+
/**
141+
* 判斷候選字元陣列是否字典序更小。
142+
*
143+
* @param candidateDigits 候選字元陣列
144+
* @param currentBestString 當前最小字串
145+
* @returns 若候選字串更小則回傳 true
146+
*/
147+
function isLexicographicallySmaller(candidateDigits: string[], currentBestString: string): boolean {
148+
for (let index = 0; index < stringLength; index++) {
149+
const candidateChar = candidateDigits[index].charCodeAt(0);
150+
const bestChar = currentBestString.charCodeAt(index);
151+
152+
if (candidateChar < bestChar) {
153+
return true;
154+
}
155+
if (candidateChar > bestChar) {
156+
return false;
157+
}
158+
}
159+
return false;
160+
}
161+
```
162+
163+
### Step 5:嘗試所有有效旋轉組合
164+
165+
根據旋轉步長 `rotationStep`,嘗試所有唯一旋轉狀態,對每一種旋轉:
166+
167+
1. 取出旋轉後字元陣列;
168+
2. 對奇數索引執行最佳加法;
169+
3.`b` 為奇數,偶數索引經旋轉後也可能成為奇數,需再執行一次最佳化;
170+
4. 與當前最小字串比較更新。
171+
172+
```typescript
173+
// 嘗試所有唯一旋轉組合
174+
for (let rotationIndex = 0; rotationIndex < stringLength; rotationIndex += rotationStep) {
175+
// 擷取當前旋轉後的子字串
176+
const rotatedDigits = doubledString.slice(rotationIndex, rotationIndex + stringLength).split("");
177+
178+
// 永遠可對奇數位進行加法操作
179+
applyBestAddition(rotatedDigits, 1);
180+
181+
// 若旋轉步長為奇數,偶數位也可能被影響,需再執行一次
182+
if ((b & 1) === 1) {
183+
applyBestAddition(rotatedDigits, 0);
184+
}
185+
186+
// 比較並更新目前的最小字串
187+
if (isLexicographicallySmaller(rotatedDigits, smallestString)) {
188+
smallestString = rotatedDigits.join("");
189+
}
190+
}
191+
```
192+
193+
### Step 6:返回最小字典序結果
194+
195+
完成所有旋轉與加法嘗試後,回傳最小結果。
196+
197+
```typescript
198+
// 返回最終的最小字典序字串
199+
return smallestString;
200+
```
201+
202+
## 時間複雜度
203+
204+
- `applyBestAddition()`:每次最多嘗試 10 次加法循環,需遍歷 $\frac{n}{2}$ 位數,為 $O(n)$。
205+
- 旋轉嘗試次數最多為 $\frac{n}{\gcd(b, n)}$。
206+
- 整體複雜度為 $O(\frac{n^2}{\gcd(b, n)})$,在 $n \le 100$ 時可接受。
207+
- 總時間複雜度為 $O(n^2)$。
208+
209+
> $O(n^2)$
210+
211+
## 空間複雜度
212+
213+
- 使用少量臨時陣列(旋轉後字串、副本)與常數映射表。
214+
- 額外空間與輸入長度成正比。
215+
- 總空間複雜度為 $O(n)$。
216+
217+
> $O(n)$
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Compute the greatest common divisor using Euclidean algorithm.
3+
*
4+
* @param valueA First positive integer
5+
* @param valueB Second positive integer
6+
* @returns Greatest common divisor of valueA and valueB
7+
*/
8+
function greaterCommonDivisor(valueA: number, valueB: number): number {
9+
while (valueB !== 0) {
10+
const remainder = valueA % valueB;
11+
valueA = valueB;
12+
valueB = remainder;
13+
}
14+
return valueA;
15+
}
16+
17+
function findLexSmallestString(s: string, a: number, b: number): string {
18+
const stringLength = s.length;
19+
let smallestString = s;
20+
21+
// Double the string for rotation without modulo overhead
22+
const doubledString = s + s;
23+
24+
// Compute the greatest common divisor of rotation step and length
25+
const rotationStep = greaterCommonDivisor(b, stringLength);
26+
27+
// Addition operation cycles every cycleLength times
28+
const cycleLength = 10 / greaterCommonDivisor(a, 10);
29+
30+
/**
31+
* Apply the best possible addition operation to indices of given parity.
32+
* Finds the minimum digit achievable by trying all valid add cycles,
33+
* then applies that increment to all indices of the same parity.
34+
*
35+
* @param digits Mutable array of digits as characters
36+
* @param startIndex Starting index (0 for even, 1 for odd)
37+
*/
38+
function applyBestAddition(digits: string[], startIndex: number) {
39+
const originalDigit = digits[startIndex].charCodeAt(0) - 48; // '0' -> 48
40+
let minimumDigit = 10;
41+
let bestTimes = 0;
42+
43+
// Determine how many times to add 'a' for minimal result
44+
for (let times = 0; times < cycleLength; times++) {
45+
const addedDigit = (originalDigit + (times * a) % 10) % 10;
46+
if (addedDigit < minimumDigit) {
47+
minimumDigit = addedDigit;
48+
bestTimes = times;
49+
}
50+
}
51+
52+
// Apply the best increment to all same-parity indices
53+
const increment = (bestTimes * a) % 10;
54+
for (let index = startIndex; index < stringLength; index += 2) {
55+
const baseDigit = digits[index].charCodeAt(0) - 48;
56+
digits[index] = String.fromCharCode(48 + ((baseDigit + increment) % 10));
57+
}
58+
}
59+
60+
/**
61+
* Compare a candidate digits array to the current best string.
62+
* Performs character-by-character comparison without joining strings.
63+
*
64+
* @param candidateDigits Candidate array of digits as characters
65+
* @param currentBestString Current smallest string
66+
* @returns True if the candidate is lexicographically smaller
67+
*/
68+
function isLexicographicallySmaller(candidateDigits: string[], currentBestString: string): boolean {
69+
for (let index = 0; index < stringLength; index++) {
70+
const candidateChar = candidateDigits[index].charCodeAt(0);
71+
const bestChar = currentBestString.charCodeAt(index);
72+
73+
if (candidateChar < bestChar) {
74+
return true;
75+
}
76+
if (candidateChar > bestChar) {
77+
return false;
78+
}
79+
}
80+
return false;
81+
}
82+
83+
// Explore valid rotations spaced by rotationStep
84+
for (let rotationIndex = 0; rotationIndex < stringLength; rotationIndex += rotationStep) {
85+
// Extract substring representing this rotation
86+
const rotatedDigits = doubledString.slice(rotationIndex, rotationIndex + stringLength).split("");
87+
88+
// Always can modify odd indices
89+
applyBestAddition(rotatedDigits, 1);
90+
91+
// If rotation step is odd, even indices can also become odd after rotations
92+
if ((b & 1) === 1) {
93+
applyBestAddition(rotatedDigits, 0);
94+
}
95+
96+
// Compare to current best string before joining
97+
if (isLexicographicallySmaller(rotatedDigits, smallestString)) {
98+
smallestString = rotatedDigits.join("");
99+
}
100+
}
101+
102+
return smallestString;
103+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function findLexSmallestString(s: string, a: number, b: number): string {
2+
3+
}

0 commit comments

Comments
 (0)