构造回文字符串问题(图文加多版本代码) | 豆包MarsCode AI刷题

320 阅读9分钟

image.png

问题描述

小C手中有一个由小写字母组成的字符串 s。她希望构造另一个字符串 t,并且这个字符串需要满足以下几个条件:

  1. t 由小写字母组成,且长度s 相同。
  2. t回文字符串,即从左到右与从右到左读取相同。
  3. t字典序要小于 s,并且在所有符合条件的字符串中字典序尽可能大

小C想知道是否能构造出这样的字符串 t,如果能,输出这样的 t;如果无法构造满足条件的字符串 t,则输出 -1

1背景知识

1.1 什么是字典序?怎么操作一个字符串的字典序?

字符字典序
a1
b2
c3

让我们根据这些规则,给出从 "abca" 向前和向后走 5 步的操作。

向前走 5 步(比 "abca" 小的字典序字符串):
  1. "abca"(原始字符串)
  2. "abbz"(从 "abca" 向前走一步)
  3. "abby"(继续减小 ay,得到下一个比 "abca" 小的字符串)
  4. "abbx"(继续减小 ax
  5. "abbw"(继续减小 aw
向后走 5 步(比 "abca" 大的字典序字符串):
  1. "abca"(原始字符串)
  2. "abcb"(从 "abca" 向后走一步)
  3. "abcc"(继续增大 ac,得到下一个比 "abca" 大的字符串)
  4. "abcd"(继续增大 ad
  5. "abce"(继续增大 ae

1.2 什么是回文字符串?

回文字符串是指正着读和反着读都一样的字符串。换句话说,如果一个字符串的前半部分和后半部分对称,那么这个字符串就是回文字符串。 例如:

  • "racecar":正着读是 "racecar",反着读也是 "racecar",因此它是一个回文字符串。
  • "madam":正着读和反着读都一样,所以它是回文字符串。
  • "abcba":前后字符对称,反向读取仍然是 "abcba",因此也是回文字符串。

2 解题思路

2.1 初始回文字符串的构造

为了获取一个长度相同字典序尽可能大的回文字符串,我们可以直接取原字符串的前半部分构造一个回文字符串,这样得到的字符串可以保证前半部分的字典序完全相同,也就是前半部分字典序尽可能大回文字符串。 举例来说,假设 s = "abc",那么我们可以得到初始的回文字符串 t = "aba"

image.png tip:奇数个数

image.png tip: 偶数个数

2.2 初步检查字典序

在构造回文字符串后,我们可以首先检查这个回文字符串是否已经小于原始字符串 s。如果初步构造的回文字符串 t 已经小于 s,那么我们可以直接返回这个回文字符串。

接下来我会尝试从逻辑上证明此时已经是最优解。

2.2.1 怎么对比回文字符串的字典序?

普通字符串的字典序对比就是遍历整个字符串逐位对比直到找到不相同的字母,对其对比。 abcd abcc,一路对比到d和c可以分出字典序。 又或者abc acc到第二位就对比出。 当字符串为回文字符串时, abba abba,我们发现如果前半部分相等时,整个字符串就相同。 也就是说,回文字符串只需要对比前半部分的字典序为什么?当前半部分不同时,已经可以分出字典序大小,就如同之前普通字符串的例子一样,abc acc,已经分出大小,无需看后半部分。而当前半部分相等时,不用像普通字符串一样往下遍历,因为此时两个字符串相等。

我们得到结论:在回文字符串领域,前半部分字典序就等于回文字符串的字典序

回到原题,第一次构造完回文字符串,我们得到的是,长度相同,且前半部分字典序完全等于原字符串的字符串,例如 原字符串“wxbkybcz” 构造后“wxbkkbxw” 。在回文字符串领域里,我们已经无法再找到前半部分字典序更大的回文字符串了,因为题目要求处理后的字符串字典序小于原字符串,前半部分再变大字典序就会超过原字符串,不符合要求。又因为在回文字符串之间,前半部分字典序就等于回文字符串的字典序,前半部分我们取到了符合题意的上限,已经是最大字典序回文字符串,而后半部分字典序小于原字符串,刚好符合整体字典序小于原字符串条件的,所以为最优解

我们刚刚说到,第一步取到了字典序最大的回文字符串,也就是上限,当这个上限的后续部分字典序大于原字符串,我们就需要对其进行字典序降低,以使得字典序变得更小,并且尽可能接近原始字符串的字典序。

image.png

2.3 从中间开始逐步调整

因为在回文字符串之间,前半部分字典序就等于回文字符串的字典序,所以我们只需要关心前半部分即可,这下调整的思路就很简单了。用之前给出的例子: image.png

还记得怎么操作一个字符串的字典序吗?

image.png

这里关注边界条件a和z。比如上图中abca减低一个字典序长度就为abbz。具体代码实现里,我们要从中间往前寻找第一个不为a字符,对其降级,比如说c变为b,再把从这个位置到中间的字符变为z(因为他们都是a)。并且操作前半部分的字符还要同步到后半部分,维护他的回文特性。具体修改的例子如下图:

image.png 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
}