Hard
题目描述
给你一个只包含小写英文字母的字符串 s。
在一次移动中,你可以选择 s 中任意两个相邻的字符并交换它们。
返回使 s 变成回文串所需的最少移动次数。
注意:输入保证 s 总是可以转换为回文串。
示例 1:
输入:s = "aabb"
输出:2
解释:
我们可以从 s 得到两个回文串:"abba" 和 "baab"。
- 我们可以用 2 次移动从 s 得到 "abba":"aabb" -> "abab" -> "abba"。
- 我们可以用 2 次移动从 s 得到 "baab":"aabb" -> "abab" -> "baab"。
因此,使 s 变成回文串所需的最少移动次数是 2。
示例 2:
输入:s = "letelt"
输出:2
解释:
我们可以用 2 次移动从 s 得到回文串 "lettel"。
其中一种方法是:"letelt" -> "letetl" -> "lettel"。
诸如 "tleelt" 的其他回文串也可以用 2 次移动得到。
可以证明少于 2 次移动无法得到回文串。
约束条件:
1 <= s.length <= 2000s只包含小写英文字母s可以在有限次移动内转换为回文串
解题思路
这是一个经典的贪心问题。核心思路是从外向内逐层构造回文串。
基本思路:
- 对于当前的左端点字符,我们需要在右半部分找到与它匹配的字符
- 将找到的匹配字符通过相邻交换移动到对应的右端点位置
- 删除已配对的字符,递归处理内部子串
特殊情况处理:
- 如果字符串长度为奇数,中间的字符可以单独存在,不需要配对
- 当某个字符在字符串中只出现一次时,它必须放在中间位置
算法步骤:
- 使用递归或迭代的方式,每次处理最外层的字符对
- 对于当前左端点字符
s[left],从右端点开始向左查找相同字符 - 找到匹配字符后,通过相邻交换将其移动到
right位置 - 累计交换次数,然后缩小搜索范围继续处理
优化考虑: 可以使用树状数组或其他数据结构来优化查找和移动过程,但对于题目给定的数据规模,简单的贪心算法已经足够高效。
代码实现
class Solution {
public:
int minMovesToMakePalindrome(string s) {
int moves = 0;
int n = s.length();
for (int left = 0; left < n / 2; left++) {
int right = n - 1 - left;
// 找到与 s[left] 匹配的字符
int match = right;
while (match > left && s[match] != s[left]) {
match--;
}
// 如果是奇数长度且找到中间位置,将其移到中心
if (match == left) {
moves += n / 2 - left;
} else {
// 将匹配的字符移动到 right 位置
while (match < right) {
swap(s[match], s[match + 1]);
match++;
moves++;
}
}
}
return moves;
}
};
class Solution:
def minMovesToMakePalindrome(self, s: str) -> int:
s = list(s) # 转换为列表便于修改
moves = 0
n = len(s)
left = 0
while left < n // 2:
right = n - 1 - left
# 找到与 s[left] 匹配的字符
match = right
while match > left and s[match] != s[left]:
match -= 1
# 如果是奇数长度且找到中间位置,将其移到中心
if match == left:
moves += n // 2 - left
else:
# 将匹配的字符移动到 right 位置
while match < right:
s[match], s[match + 1] = s[match + 1], s[match]
match += 1
moves += 1
left += 1
return moves
public class Solution {
public int MinMovesToMakePalindrome(string s) {
char[] chars = s.ToCharArray();
int moves = 0;
int n = chars.Length;
for (int left = 0; left < n / 2; left++) {
int right = n - 1 - left;
// 找到与 chars[left] 匹配的字符
int match = right;
while (match > left && chars[match] != chars[left]) {
match--;
}
// 如果是奇数长度且找到中间位置,将其移到中心
if (match == left) {
moves += n / 2 - left;
} else {
// 将匹配的字符移动到 right 位置
while (match < right) {
(chars[match], chars[match + 1]) = (chars[match + 1], chars[match]);
match++;
moves++;
}
}
}
return moves;
}
}
var minMovesToMakePalindrome = function(s) {
let arr = s.split('');
let moves = 0;
let left = 0;
let right = arr.length - 1;
while (left < right) {
if (arr[left] === arr[right]) {
left++;
right--;
continue;
}
let k = right - 1;
while (k > left && arr[k] !== arr[left]) {
k--;
}
if (k === left) {
[arr[left], arr[left + 1]] = [arr[left + 1], arr[left]];
moves++;
} else {
while (k < right) {
[arr[k], arr[k + 1]] = [arr[k + 1], arr[k]];
k++;
moves++;
}
left++;
right--;
}
}
return moves;
};
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间复杂度 | O(n²) |
| 空间复杂度 | O(1) 或 O(n) |
说明:
- 时间复杂度:外层循环 O(n/2),内层查找和移动最坏情况下都是 O(n),总体为 O(n²)
- 空间复杂度:C++ 版本为 O(1)(原地修改),Python/JavaScript 版本为 O(n)(需要额外数组存储)