问题描述
小C手中有一个由小写字母组成的字符串 s。她希望构造另一个字符串 t,并且这个字符串需要满足以下几个条件:
t由小写字母组成,且长度与s相同。t是回文字符串,即从左到右与从右到左读取相同。t的字典序要小于s,并且在所有符合条件的字符串中字典序尽可能大。
小C想知道是否能构造出这样的字符串 t,如果能,输出这样的 t;如果无法构造满足条件的字符串 t,则输出 -1。
1背景知识
1.1 什么是字典序?怎么操作一个字符串的字典序?
| 字符 | 字典序 |
|---|---|
| a | 1 |
| b | 2 |
| c | 3 |
让我们根据这些规则,给出从 "abca" 向前和向后走 5 步的操作。
向前走 5 步(比 "abca" 小的字典序字符串):
"abca"(原始字符串)"abbz"(从"abca"向前走一步)"abby"(继续减小a为y,得到下一个比"abca"小的字符串)"abbx"(继续减小a为x)"abbw"(继续减小a为w)
向后走 5 步(比 "abca" 大的字典序字符串):
"abca"(原始字符串)"abcb"(从"abca"向后走一步)"abcc"(继续增大a为c,得到下一个比"abca"大的字符串)"abcd"(继续增大a为d)"abce"(继续增大a为e)
1.2 什么是回文字符串?
回文字符串是指正着读和反着读都一样的字符串。换句话说,如果一个字符串的前半部分和后半部分对称,那么这个字符串就是回文字符串。 例如:
"racecar":正着读是"racecar",反着读也是"racecar",因此它是一个回文字符串。"madam":正着读和反着读都一样,所以它是回文字符串。"abcba":前后字符对称,反向读取仍然是"abcba",因此也是回文字符串。
2 解题思路
2.1 初始回文字符串的构造
为了获取一个长度相同且字典序尽可能大的回文字符串,我们可以直接取原字符串的前半部分构造一个回文字符串,这样得到的字符串可以保证前半部分的字典序完全相同,也就是前半部分字典序尽可能大的回文字符串。
举例来说,假设 s = "abc",那么我们可以得到初始的回文字符串 t = "aba"。
tip:奇数个数
tip: 偶数个数
2.2 初步检查字典序
在构造回文字符串后,我们可以首先检查这个回文字符串是否已经小于原始字符串 s。如果初步构造的回文字符串 t 已经小于 s,那么我们可以直接返回这个回文字符串。
接下来我会尝试从逻辑上证明此时已经是最优解。
2.2.1 怎么对比回文字符串的字典序?
普通字符串的字典序对比就是遍历整个字符串逐位对比,直到找到不相同的字母,对其对比。
abcd abcc,一路对比到d和c可以分出字典序。
又或者abc acc到第二位就对比出。
当字符串为回文字符串时,
abba abba,我们发现如果前半部分相等时,整个字符串就相同。
也就是说,回文字符串只需要对比前半部分的字典序。
为什么?当前半部分不同时,已经可以分出字典序大小,就如同之前普通字符串的例子一样,abc acc,已经分出大小,无需看后半部分。而当前半部分相等时,不用像普通字符串一样往下遍历,因为此时两个字符串相等。
我们得到结论:在回文字符串领域,前半部分字典序就等于回文字符串的字典序。
回到原题,第一次构造完回文字符串,我们得到的是,长度相同,且前半部分字典序完全等于原字符串的字符串,例如 原字符串“wxbkybcz” 构造后“wxbkkbxw” 。在回文字符串领域里,我们已经无法再找到前半部分字典序更大的回文字符串了,因为题目要求处理后的字符串字典序小于原字符串,前半部分再变大字典序就会超过原字符串,不符合要求。又因为在回文字符串之间,前半部分字典序就等于回文字符串的字典序,前半部分我们取到了符合题意的上限,已经是最大字典序回文字符串,而后半部分字典序小于原字符串,刚好符合整体字典序小于原字符串条件的,所以为最优解。
我们刚刚说到,第一步取到了字典序最大的回文字符串,也就是上限,当这个上限的后续部分字典序大于原字符串,我们就需要对其进行字典序降低,以使得字典序变得更小,并且尽可能接近原始字符串的字典序。
2.3 从中间开始逐步调整
因为在回文字符串之间,前半部分字典序就等于回文字符串的字典序,所以我们只需要关心前半部分即可,这下调整的思路就很简单了。用之前给出的例子:
还记得怎么操作一个字符串的字典序吗?
这里关注边界条件a和z。比如上图中abca减低一个字典序长度就为abbz。具体代码实现里,我们要从中间往前寻找到第一个不为a的字符,对其降级,比如说c变为b,再把从这个位置到中间的字符变为z(因为他们都是a)。并且操作前半部分的字符还要同步到后半部分,维护他的回文特性。具体修改的例子如下图:
tip:整体来说,这个回文字符串的字典序等级由abca降低到了abbz
2.4 恢复字符并尝试其他调整
有时候,调整后得到的回文字符串仍然不符合字典序小于 s 的要求。在这种情况下,我们需要恢复之前的修改,继续调整其他字符,直到找到符合条件的回文字符串为止。如果所有调整都无法得到符合条件的回文字符串,则返回 -1。
这里的例子就是 aaa,根据算法构建出回文字符串aaa(取前半部分aa),整体字典序不小于原字符串,开始调整,从中间字符a开始,向前一直没有找到不为a的字符,所以没有答案,返回-1.
3 解题代码(多版本)
def solution(s: str) -> str:
n = len(s)
t = list(s) # 转换为列表,方便修改
# 先构建一个回文字符串
for i in range(n // 2):
t[n - i - 1] = t[i] # 保持回文性
# 如果初始回文字符串已经小于s,直接返回
if ''.join(t) < s:
return ''.join(t)
# 否则,从中间开始向前调整
for i in range((n - 1) // 2, -1, -1):
if t[i] > 'a': # 如果当前字符大于 'a',可以减小
t[i] = chr(ord(t[i]) - 1)
t[n - i - 1] = t[i] # 保持回文性
# 调整后面的位置为尽可能的小字符 'z',确保字典序最小
for j in range(i + 1, n - i - 1):
t[j] = 'z'
t[n - j - 1] = t[j]
# 生成回文字符串并检查字典序
t_str = ''.join(t)
if t_str < s:
return t_str
else:
# 如果调整后仍然不满足条件,继续尝试下一个字符
t[i] = s[i]
t[n - i - 1] = t[i]
return '-1'
# 测试用例
if __name__ == '__main__':
print(solution("abc") == 'aba') # 输出 'aba'
print(solution("cba") == 'cac') # 输出 'cac'
print(solution("aaa") == '-1') # 输出 '-1'
#include <iostream>
#include <string>
using namespace std;
string solution(string s) {
int n = s.length();
string t = s; // 转换为列表,方便修改
// 先构建一个回文字符串
for (int i = 0; i < n / 2; ++i) {
t[n - i - 1] = t[i]; // 保持回文性
}
// 如果初始回文字符串已经小于s,直接返回
if (t < s) {
return t;
}
// 否则,从中间开始向前调整
for (int i = (n - 1) / 2; i >= 0; --i) {
if (t[i] > 'a') { // 如果当前字符大于 'a',可以减小
t[i] = t[i] - 1;
t[n - i - 1] = t[i]; // 保持回文性
// 调整后面的位置为尽可能的小字符 'z',确保字典序最小
for (int j = i + 1; j < n - i - 1; ++j) {
t[j] = 'z';
t[n - j - 1] = t[j];
}
// 生成回文字符串并检查字典序
if (t < s) {
return t;
} else {
// 如果调整后仍然不满足条件,继续尝试下一个字符
t[i] = s[i];
t[n - i - 1] = s[i];
}
}
}
return "-1";
}
int main() {
cout << (solution("abc") == "aba") << endl; // 输出 1 (true)
cout << (solution("cba") == "cac") << endl; // 输出 1 (true)
cout << (solution("aaa") == "-1") << endl; // 输出 1 (true)
return 0;
}
public class Main {
public static String solution(String s) {
int n = s.length();
char[] t = s.toCharArray(); // 转换为字符数组,方便修改
// 先构建一个回文字符串
for (int i = 0; i < n / 2; ++i) {
t[n - i - 1] = t[i]; // 保持回文性
}
// 如果初始回文字符串已经小于s,直接返回
if (new String(t).compareTo(s) < 0) {
return new String(t);
}
// 否则,从中间开始向前调整
for (int i = (n - 1) / 2; i >= 0; --i) {
if (t[i] > 'a') { // 如果当前字符大于 'a',可以减小
t[i] = (char)(t[i] - 1);
t[n - i - 1] = t[i]; // 保持回文性
// 调整后面的位置为尽可能的小字符 'z',确保字典序最小
for (int j = i + 1; j < n - i - 1; ++j) {
t[j] = 'z';
t[n - j - 1] = t[j];
}
// 生成回文字符串并检查字典序
String tStr = new String(t);
if (tStr.compareTo(s) < 0) {
return tStr;
} else {
// 如果调整后仍然不满足条件,继续尝试下一个字符
t[i] = s.charAt(i);
t[n - i - 1] = s.charAt(i);
}
}
}
return "-1";
}
public static void main(String[] args) {
System.out.println(solution("abc").equals("aba")); // 输出 true
System.out.println(solution("cba").equals("cac")); // 输出 true
System.out.println(solution("aaa").equals("-1")); // 输出 true
}
}
package main
import (
"fmt"
"strings"
)
func solution(s string) string {
n := len(s)
t := []rune(s) // 转换为字符数组,方便修改
// 先构建一个回文字符串
for i := 0; i < n/2; i++ {
t[n-i-1] = t[i] // 保持回文性
}
// 如果初始回文字符串已经小于s,直接返回
if string(t) < s {
return string(t)
}
// 否则,从中间开始向前调整
for i := (n - 1) / 2; i >= 0; i-- {
if t[i] > 'a' { // 如果当前字符大于 'a',可以减小
t[i] = t[i] - 1
t[n-i-1] = t[i] // 保持回文性
// 调整后面的位置为尽可能的小字符 'z',确保字典序最小
for j := i + 1; j < n-i-1; j++ {
t[j] = 'z'
t[n-j-1] = t[j]
}
// 生成回文字符串并检查字典序
if string(t) < s {
return string(t)
} else {
// 如果调整后仍然不满足条件,继续尝试下一个字符
t[i] = rune(s[i])
t[n-i-1] = rune(s[i])
}
}
}
return "-1"
}
func main() {
fmt.Println(solution("abc") == "aba") // 输出 true
fmt.Println(solution("cba") == "cac") // 输出 true
fmt.Println(solution("aaa") == "-1") // 输出 true
}