题目
给你一个字符串 s 以及两个整数 a 和 b 。其中,字符串 s 的长度为偶数,且仅由数字 0 到 9 组成。
你可以在 s 上按任意顺序多次执行下面两个操作之一:
- 累加:将
a加到s中所有下标为奇数的元素上(下标从 0 开始)。数字一旦超过9就会变成0,如此循环往复。例如,s = "3456"且a = 5,则执行此操作后s变成"3951"。 - 轮转:将
s向右轮转b位。例如,s = "3456"且b = 1,则执行此操作后s变成"6345"。
请你返回在 s 上执行上述操作任意次后可以得到的 字典序最小 的字符串。
如果两个字符串长度相同,那么字符串 a 字典序比字符串 b 小可以这样定义:在 a 和 b 出现不同的第一个位置上,字符串 a 中的字符出现在字母表中的时间早于 b 中的对应字符。例如,"0158” 字典序比 "0190" 小,因为不同的第一个位置是在第三个字符,显然 '5' 出现在 '9' 之前。
示例 1:
输入:s = "5525", a = 9, b = 2 输出:"2050" 解释:执行操作如下: 初态:"5525" 轮转:"2555" 累加:"2454" 累加:"2353" 轮转:"5323" 累加:"5222" 累加:"5121" 轮转:"2151" 累加:"2050" 无法获得字典序小于 "2050" 的字符串。
示例 2:
输入:s = "74", a = 5, b = 1 输出:"24" 解释:执行操作如下: 初态:"74" 轮转:"47" 累加:"42" 轮转:"24" 无法获得字典序小于 "24" 的字符串。
示例 3:
输入:s = "0011", a = 4, b = 2 输出:"0011" 解释:无法获得字典序小于 "0011" 的字符串。
提示:
2 <= s.length <= 100s.length是偶数s仅由数字0到9组成1 <= a <= 91 <= b <= s.length - 1
解题思路
这是一个典型的状态搜索问题,目标是找到从初始状态(原始字符串 s)出发,通过两种操作(累加、轮转)所能达到的所有状态中,字典序最小的那个。
解决这类问题的核心思路是图搜索,最常用的是广度优先搜索 (BFS)。
- 状态 (State): 图中的每一个节点 (node) 就是一个我们通过操作可以得到的字符串。
- 边 (Edge): 图中的边代表一次操作。从任何一个字符串 si 出发,都有两条边(两种操作)指向下一个状态: a.
累加操作,得到 sj=add(si,a) b.轮转操作,得到 sk=rotate(si,b) - 目标 (Goal): 我们要遍历这个图,找出所有从初始字符串 s 出发可达 (reachable) 的节点(字符串),并返回其中字典序最小的一个。
- 算法流程 (BFS): BFS 是完成这个任务的完美工具,因为它能系统地探索所有可达状态。为了防止无限循环(例如,连续轮转 n/b 次又回到原点),我们需要一个集合 (Set) 来记录已经访问过的状态。
- 初始化:
- 创建一个队列
q,并将初始字符串 s 加入队列。 - 创建一个集合
visited,并将 s 加入集合,标记为已访问。 - 创建一个变量
min_s,初始化为 s,用来记录搜索过程中遇到的最小字典序字符串。
- 创建一个队列
- 搜索循环:
- 当
q不为空时,从队列中取出一个字符串current_s。 - 比较:将
current_s与min_s进行字典序比较。如果current_s < min_s,则更新min_s = current_s。 - 生成新状态 (操作): a. 累加操作:计算 sadd=add(current_s,a)。 * 如果 sadd 不在
visited集合中: * 将其加入visited。 * 将其加入q队列。 b. 轮转操作:计算 srot=rotate(current_s,b)。 * 如果 srot 不在visited集合中: * 将其加入visited。 * 将其加入q队列。
- 当
- 返回结果:
- 当队列为空时,表示所有可达的状态都已访问过。
- 返回
min_s。
- 初始化:
具体代码
/**
* 累加操作:将 'a' 加到所有奇数下标的数字上
*/
func applyAdd(s string, a int) string {
// 将字符串转为字节切片,方便修改
sBytes := []byte(s)
n := len(sBytes)
for i := 1; i < n; i += 2 {
// 1. 将 ASCII 字符 ('0'-'9') 转为整数 (0-9)
digit := int(sBytes[i] - '0')
// 2. 执行 (digit + a) % 10
newDigit := (digit + a) % 10
// 3. 将整数 (0-9) 转回 ASCII 字符 ('0'-'9')
sBytes[i] = byte(newDigit + '0')
}
// 将修改后的字节切片转回字符串
return string(sBytes)
}
/**
* 轮转操作:将字符串向右轮转 'b' 位
*/
func applyRotate(s string, b int) string {
n := len(s)
// 确保 b 在 [0, n-1] 范围内 (虽然题目保证了 b < n,但取模是个好习惯)
b = b % n
// s[n-b:] 是最后 b 个字符
// s[:n-b] 是前面 (n-b) 个字符
return s[n-b:] + s[:n-b]
}
/**
* 主函数:使用 BFS 查找字典序最小的字符串
*/
func findLexSmallestString(s string, a int, b int) string {
// 1. 初始化
// 队列,用于 BFS
queue := []string{s}
// visited 集合,用于记录已访问过的字符串,防止重复搜索和死循环
// Go 中使用 map[string]struct{} 作为 Set
visited := make(map[string]struct{})
visited[s] = struct{}{}
// 记录目前找到的字典序最小的字符串
minS := s
// 2. BFS 循环
for len(queue) > 0 {
// 2a. 出队
currentS := queue[0]
queue = queue[1:]
// 2b. 检查并更新最小值
if currentS < minS {
minS = currentS
}
// --- 2c. 生成下一个状态 ---
// 操作 1: 累加
sAdd := applyAdd(currentS, a)
if _, ok := visited[sAdd]; !ok {
// 如果这个新字符串没被访问过
visited[sAdd] = struct{}{}
queue = append(queue, sAdd) // 入队
}
// 操作 2: 轮转
sRot := applyRotate(currentS, b)
if _, ok := visited[sRot]; !ok {
// 如果这个新字符串没被访问过
visited[sRot] = struct{}{}
queue = append(queue, sRot) // 入队
}
}
// 3. 返回结果
return minS
}