华为OD机试 - 字符串回文判断 - 枚举(Python/JS/C/C++ 新系统 100分)

在这里插入图片描述

华为OD机试 新系统 统一考试题库清单(持续收录中)以及考点说明(Python/JS/C/C++)

专栏导读

本专栏收录于《华为OD机试真题(Python/JS/C/C++)》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

一、题目描述

给定一个仅由小写英文字母组成的字符串 s,请判断它是否可以在删除一个字符之后,变成一个回文串。
回文串的定义是:从左到右读和从右到左读完全相同。
如果可以,输出一个数组,数组由可删除字符的索引值构成;否则输出一个空数组。

二、输入描述

输入一个字符串 s,2 ≤ s.length ≤ 10⁵

三、输出描述

输出一个数组,内容为:N 种删除方法的字符索引值。如输入为:abca,则输出为:[1, 2],代表可以有两种删除方式:[aba, aca]

  • 1:删除索引值为 1 的字符,即删除字符 b,则变为:aca,属于回文
  • 2:删除索引值为 2 的字符,即删除字符 c,则变为:aba,属于回文

四、测试用例

测试用例1:

1、输入

abca

2、输出

1,2

3、说明

测试用例2:

1、输入

abc

2、输出

3、说明

五、解题思路

最直接的方法是枚举每个下标,删除字符后再判断剩余字符串是否为回文,但字符串长度最大为 10^5,这种方法时间复杂度为 O(n²),会超时。

本题需要找出“所有”能够删除的下标,不能只处理第一对不相等字符。例如字符串 aaba,删除下标 0、1、2 都可以得到回文串,因此仅检查第一处不匹配位置会漏答案。

设准备删除的下标为 i,它关于原字符串中心的镜像位置为 mirror = n - 1 - i

i <= mirror 时,删除左半部分字符 s[i] 后:

  1. 删除位置外面的 i 对字符不会发生错位,所以这些字符在原串中必须已经镜像相等;
  2. 删除字符后,内部剩余区间 s[i+1...mirror] 必须本身是回文串。

i > mirror 时完全对称,需要外面的 mirror 对字符已经匹配,同时内部区间 s[mirror...i-1] 是回文。

因此先从两端向中间统计连续相等的外层字符对数量 matchedOuter,即可 O(1) 判断第一个条件。

第二个条件要求频繁判断某个子串是否为回文。如果每次重新双指针扫描,最坏仍然达到 O(n²)。所以使用 Manacher 算法预处理奇数回文半径数组和偶数回文半径数组。预处理时间 O(n),之后任意连续子串是否为回文均可 O(1) 判断。

最后枚举所有 n 个删除位置,根据上述两个条件判断并加入答案。

数据结构主要使用两个整型数组保存 Manacher 的奇偶回文半径,以及一个 List 保存答案。

六、Python算法源码

def build_manacher(s):
    n = len(s)

    # odd[i]:以 i 为中心的最长奇数回文半径
    odd = [0] * n

    # even[i]:以 i-1 和 i 之间为中心的最长偶数回文半径
    even = [0] * n

    # --------------------
    # 计算奇数长度回文半径
    # --------------------
    left, right = 0, -1

    for i in range(n):
        if i > right:
            k = 1
        else:
            k = min(
                odd[left + right - i],
                right - i + 1
            )

        # 向左右两侧扩展
        while (
            i - k >= 0
            and i + k < n
            and s[i - k] == s[i + k]
        ):
            k += 1

        odd[i] = k

        if i + k - 1 > right:
            left = i - k + 1
            right = i + k - 1

    # --------------------
    # 计算偶数长度回文半径
    # --------------------
    left, right = 0, -1

    for i in range(n):
        if i > right:
            k = 0
        else:
            k = min(
                even[left + right - i + 1],
                right - i + 1
            )

        while (
            i - k - 1 >= 0
            and i + k < n
            and s[i - k - 1] == s[i + k]
        ):
            k += 1

        even[i] = k

        if i + k - 1 > right:
            left = i - k
            right = i + k - 1

    return odd, even


def solve(s):
    n = len(s)

    # Manacher 预处理
    odd, even = build_manacher(s)

    def is_palindrome(left, right):
        """
        O(1) 判断 s[left:right+1] 是否为回文
        """

        # 空串或单字符均为回文
        if left >= right:
            return True

        length = right - left + 1

        if length % 2 == 1:
            center = (left + right) // 2
            return odd[center] >= length // 2 + 1

        center = (left + right + 1) // 2
        return even[center] >= length // 2

    # 统计原字符串从两端开始连续相等的字符对数量
    matched_outer = 0

    while (
        matched_outer < n // 2
        and s[matched_outer]
        == s[n - 1 - matched_outer]
    ):
        matched_outer += 1

    ans = []

    # 枚举每一个删除位置
    for i in range(n):
        mirror = n - 1 - i

        if i <= mirror:
            # 删除左半部分字符:
            # 外层 i 对字符必须已经匹配,
            # 删除后中间部分也必须是回文。
            if (
                i <= matched_outer
                and is_palindrome(i + 1, mirror)
            ):
                ans.append(i)

        else:
            # 删除右半部分字符,对称处理
            if (
                mirror <= matched_outer
                and is_palindrome(mirror, i - 1)
            ):
                ans.append(i)

    return ans


s = input().strip()

ans = solve(s)

print(",".join(map(str, ans)), end="")

七、JavaScript算法源码

const fs = require("fs");

const s = fs.readFileSync(0, "utf8").trim();

function buildManacher(s) {
    const n = s.length;

    // odd[i]:以 i 为中心的最长奇数回文半径
    const odd = new Array(n).fill(0);

    // even[i]:以 i-1 和 i 之间为中心的最长偶数回文半径
    const even = new Array(n).fill(0);

    // --------------------
    // 奇数长度回文
    // --------------------
    let left = 0;
    let right = -1;

    for (let i = 0; i < n; i++) {
        let k;

        if (i > right) {
            k = 1;
        } else {
            k = Math.min(
                odd[left + right - i],
                right - i + 1
            );
        }

        // 向左右扩展
        while (
            i - k >= 0 &&
            i + k < n &&
            s[i - k] === s[i + k]
        ) {
            k++;
        }

        odd[i] = k;

        if (i + k - 1 > right) {
            left = i - k + 1;
            right = i + k - 1;
        }
    }

    // --------------------
    // 偶数长度回文
    // --------------------
    left = 0;
    right = -1;

    for (let i = 0; i < n; i++) {
        let k;

        if (i > right) {
            k = 0;
        } else {
            k = Math.min(
                even[left + right - i + 1],
                right - i + 1
            );
        }

        while (
            i - k - 1 >= 0 &&
            i + k < n &&
            s[i - k - 1] === s[i + k]
        ) {
            k++;
        }

        even[i] = k;

        if (i + k - 1 > right) {
            left = i - k;
            right = i + k - 1;
        }
    }

    return [odd, even];
}

function solve(s) {
    const n = s.length;

    const [odd, even] = buildManacher(s);

    /**
     * 根据 Manacher 数组,
     * O(1) 判断一个连续子串是否为回文。
     */
    function isPalindrome(left, right) {
        // 空串和单字符都是回文
        if (left >= right) {
            return true;
        }

        const len = right - left + 1;

        if (len % 2 === 1) {
            const center = Math.floor((left + right) / 2);

            return odd[center] >= Math.floor(len / 2) + 1;
        }

        const center = Math.floor((left + right + 1) / 2);

        return even[center] >= len / 2;
    }

    /*
     * 计算原字符串从最外侧开始,
     * 连续能够镜像匹配多少对字符。
     */
    let matchedOuter = 0;

    while (
        matchedOuter < Math.floor(n / 2) &&
        s[matchedOuter] === s[n - 1 - matchedOuter]
    ) {
        matchedOuter++;
    }

    const ans = [];

    for (let i = 0; i < n; i++) {
        const mirror = n - 1 - i;

        if (i <= mirror) {
            // 删除左半部分字符
            if (
                i <= matchedOuter &&
                isPalindrome(i + 1, mirror)
            ) {
                ans.push(i);
            }
        } else {
            // 删除右半部分字符,对称判断
            if (
                mirror <= matchedOuter &&
                isPalindrome(mirror, i - 1)
            ) {
                ans.push(i);
            }
        }
    }

    return ans;
}

process.stdout.write(solve(s).join(","));

八、C算法源码

#include <stdio.h>
#include <string.h>

#define MAX_N 100005

static char s[MAX_N];

/* 奇数回文半径 */
static int oddRadius[MAX_N];

/* 偶数回文半径 */
static int evenRadius[MAX_N];

static int min_int(int a, int b) {
    return a < b ? a : b;
}

/*
 * Manacher 算法预处理。
 * 总时间复杂度 O(n)。
 */
static void build_manacher(int n) {
    int left = 0;
    int right = -1;

    /*
     * oddRadius[i] 表示以 i 为中心的
     * 最长奇数长度回文半径。
     */
    for (int i = 0; i < n; i++) {
        int k;

        if (i > right) {
            k = 1;
        } else {
            k = min_int(
                oddRadius[left + right - i],
                right - i + 1
            );
        }

        /* 向两边继续扩展 */
        while (
            i - k >= 0 &&
            i + k < n &&
            s[i - k] == s[i + k]
        ) {
            k++;
        }

        oddRadius[i] = k;

        if (i + k - 1 > right) {
            left = i - k + 1;
            right = i + k - 1;
        }
    }

    left = 0;
    right = -1;

    /*
     * evenRadius[i] 表示以 i-1 与 i
     * 之间的位置为中心的偶数回文半径。
     */
    for (int i = 0; i < n; i++) {
        int k;

        if (i > right) {
            k = 0;
        } else {
            k = min_int(
                evenRadius[left + right - i + 1],
                right - i + 1
            );
        }

        while (
            i - k - 1 >= 0 &&
            i + k < n &&
            s[i - k - 1] == s[i + k]
        ) {
            k++;
        }

        evenRadius[i] = k;

        if (i + k - 1 > right) {
            left = i - k;
            right = i + k - 1;
        }
    }
}

/*
 * O(1) 判断 s[left...right] 是否为回文。
 */
static int is_palindrome(int left, int right) {

    /* 空串和单个字符都为回文 */
    if (left >= right) {
        return 1;
    }

    int len = right - left + 1;

    if (len & 1) {
        int center = (left + right) / 2;

        return oddRadius[center] >= len / 2 + 1;
    } else {
        int center = (left + right + 1) / 2;

        return evenRadius[center] >= len / 2;
    }
}

int main(void) {

    scanf("%100000s", s);

    int n = (int)strlen(s);

    build_manacher(n);

    /*
     * matchedOuter 表示原字符串最外层
     * 连续镜像相等的字符对数量。
     */
    int matchedOuter = 0;

    while (
        matchedOuter < n / 2 &&
        s[matchedOuter] ==
        s[n - 1 - matchedOuter]
    ) {
        matchedOuter++;
    }

    int first = 1;

    for (int i = 0; i < n; i++) {

        int mirror = n - 1 - i;
        int valid = 0;

        if (i <= mirror) {

            /*
             * 删除左半部分字符:
             * 外层字符必须已匹配,
             * 中间剩余区间必须为回文。
             */
            valid =
                i <= matchedOuter &&
                is_palindrome(i + 1, mirror);

        } else {

            /*
             * 删除右半部分字符,
             * 与左半部分对称处理。
             */
            valid =
                mirror <= matchedOuter &&
                is_palindrome(mirror, i - 1);
        }

        if (valid) {

            if (!first) {
                printf(",");
            }

            printf("%d", i);

            first = 0;
        }
    }

    return 0;
}

九、C++算法源码

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> oddRadius;
vector<int> evenRadius;

/*
 * Manacher 算法:
 * 预处理奇数和偶数回文半径。
 */
void buildManacher(const string& s) {
    int n = (int)s.size();

    oddRadius.assign(n, 0);
    evenRadius.assign(n, 0);

    // --------------------
    // 奇数长度回文
    // --------------------
    for (int i = 0, left = 0, right = -1;
         i < n;
         i++) {

        int k;

        if (i > right) {
            k = 1;
        } else {
            k = min(
                oddRadius[left + right - i],
                right - i + 1
            );
        }

        // 向左右继续扩展
        while (
            i - k >= 0 &&
            i + k < n &&
            s[i - k] == s[i + k]
        ) {
            ++k;
        }

        oddRadius[i] = k;

        if (i + k - 1 > right) {
            left = i - k + 1;
            right = i + k - 1;
        }
    }

    // --------------------
    // 偶数长度回文
    // --------------------
    for (int i = 0, left = 0, right = -1;
         i < n;
         i++) {

        int k;

        if (i > right) {
            k = 0;
        } else {
            k = min(
                evenRadius[left + right - i + 1],
                right - i + 1
            );
        }

        while (
            i - k - 1 >= 0 &&
            i + k < n &&
            s[i - k - 1] == s[i + k]
        ) {
            ++k;
        }

        evenRadius[i] = k;

        if (i + k - 1 > right) {
            left = i - k;
            right = i + k - 1;
        }
    }
}

/*
 * 使用 Manacher 预处理结果,
 * O(1) 判断任意连续子串是否为回文。
 */
bool isPalindrome(int left, int right) {

    // 空串和单字符都是回文
    if (left >= right) {
        return true;
    }

    int len = right - left + 1;

    if (len & 1) {

        int center = (left + right) / 2;

        return oddRadius[center]
               >= len / 2 + 1;

    } else {

        int center = (left + right + 1) / 2;

        return evenRadius[center]
               >= len / 2;
    }
}

vector<int> solve(const string& s) {

    int n = (int)s.size();

    buildManacher(s);

    /*
     * 从字符串两端开始,
     * 统计连续镜像相等的字符对数量。
     */
    int matchedOuter = 0;

    while (
        matchedOuter < n / 2 &&
        s[matchedOuter] ==
        s[n - 1 - matchedOuter]
    ) {
        ++matchedOuter;
    }

    vector<int> ans;

    // 枚举所有删除位置
    for (int i = 0; i < n; i++) {

        int mirror = n - 1 - i;

        if (i <= mirror) {

            /*
             * i 位于左半部分。
             * 外层 i 对字符必须原本相等;
             * 删除后中间区间 [i+1, mirror]
             * 必须为回文。
             */
            if (
                i <= matchedOuter &&
                isPalindrome(i + 1, mirror)
            ) {
                ans.push_back(i);
            }

        } else {

            /*
             * i 位于右半部分,
             * 与左侧情况完全对称。
             */
            if (
                mirror <= matchedOuter &&
                isPalindrome(mirror, i - 1)
            ) {
                ans.push_back(i);
            }
        }
    }

    return ans;
}

int main() {

    string s;
    cin >> s;

    vector<int> ans = solve(s);

    for (int i = 0; i < (int)ans.size(); i++) {

        if (i > 0) {
            cout << ",";
        }

        cout << ans[i];
    }

    return 0;
}


🏆下一篇:华为OD机试真题 - 简易内存池(Python/JS/C/C++ 新系统 200分)

🏆本文收录于,华为OD机试真题(Python/JS/C/C++)

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值