1312. 让字符串成为回文串的最少插入次数

题目地址

https://leetcode-cn.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/

题目描述

给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。
请你返回让 s 成为回文串的 最少操作次数 。
「回文串」是正读和反读都相同的字符串。


示例 1:
输入:s = "zzazz"
输出:0
解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作。

示例 2:
输入:s = "mbadm"
输出:2
解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。

示例 3:
输入:s = "leetcode"
输出:5
解释:插入 5 个字符后字符串变为 "leetcodocteel" 。

示例 4:
输入:s = "g"
输出:0

示例 5:
输入:s = "no"
输出:1
 

提示:
1 <= s.length <= 500
s 中所有字符都是小写字母。

函数签名

// 返回字符串 s 变成回文串的最少操作次数
public int minInsertions(String s);

思考

解题之前先来思考几个问题:

如何构造一个回文串?

构造回文串的步骤:

  • (1) 一个空串 "" 或者一个字符的字符串,比如 "a"
  • (2) 在步骤(1)的基础上,首尾追加单个相同的字符,比如 "a" => "bab"
  • (3) 不断重复步骤(2)的操作

回文串的具体定义是什么?

  • (1) 空串 "" 是一个回文串
  • (2) 单个字符组成的字符串是一个回文串,比如 "a"
  • (3) 两个相同字符组成的字符串是一个回文串,比如 "aa"
  • (4) 去掉首尾两个相同的字符后的字符串仍然是一个回文串。比如 "bab""baab"

题解

穷举法

class Solution {
    public int minInsertions(String s) {
            if (s.length() == 0 || s.length() == 1) {
                return 0;
            }

            if (s.charAt(0) == s.charAt(s.length() - 1)) {
                return minInsertions(s.substring(1, s.length() - 1));
            }

            return Math.min(minInsertions(s.substring(1)), minInsertions(s.substring(0, s.length() - 1))) + 1;
    }
}

递归算法的时间复杂度怎么计算?子问题总数 解决一个子问题需要的时间。

递归算法的问题规模不断缩减,因此可以看成一棵树,每个问题是一个节点,每个问题的子问题相当于其子节点。

这里最坏的情况是每次都从【问题规模 】分裂为两个【问题规模 】的子问题,对应代码的最后一行。所以可以看成是一个深度为 的二叉树,其总节点数量为 。

解决一个问题需要的时间是 ,所以这里的时间复杂度为 ——指数级别

剪枝

因为字符串是一个有序集合,长度为 的字符串其子串的数量为 。

推导过程如下所示:

n:     长度为 1 的子串数量(单个字符串,假设其字符都不相同。一共有 n 个)
(n-1):长度为 2 的子串数量
(n-2):长度为 3 的子串数量
...
2:     长度为 n-1 的子串数量(字符串去掉首字符,字符串去掉尾字符)
1:     长度为 n 的子串数量(就是字符串自己)

// 运用【等差数列求和公式】可知
n + (n-1) + (n-2) + ... + 2 + 1
= (n + 1) * n / 2

显而易见,每个子串是一个独立的子问题,子问题总数等于所有子串的数量和。

所以穷举法存在大量的重复子问题。

因此可以通过备忘录的形式进行剪枝,代码如下:

class Solution {
    public int minInsertions(String s) {
        if (memo.get(s) != null) {
            return memo.get(s);
        }

        if (s.length() == 0 || s.length() == 1) {
            return 0;
        }

        if (s.charAt(0) == s.charAt(s.length() - 1)) {
            return minInsertions(s.substring(1, s.length() - 1));
        }

        int result = Math.min(minInsertions(s.substring(1)), minInsertions(s.substring(0, s.length() - 1))) + 1;
        memo.put(s, result);
        return result;
    }

    private Map memo = new HashMap<>();
}

上述算法的时间复杂度为:。

因为 substring() 的调用十分耗时,所以可以通过引入首位索引指针的形式减少 substring() 的调用开销。

首先定义函数签名:

// 返回字符串 s[left:right] 变成回文串的最少操作次数
public int minInsertions(String s, int left, int right)

代码实现如下所示:

class Solution {
    public int minInsertions(String s) {
        return minInsertions(s, 0, s.length() - 1);
    }

    public int minInsertions(String s, int left, int right) {
        int key = getKey(left, right);
        if (memo.get(key) != null) {
            return memo.get(key);
        }

        if (left >= right) {
            return 0;
        }

        if (s.charAt(left) == s.charAt(right)) {
            return minInsertions(s, left + 1, right - 1);
        }
        int result = Math.min(
            minInsertions(s, left + 1, right),
            minInsertions(s, left, right - 1)
        ) + 1;
        memo.put(key, result);
        return result;
    }

    private Map memo = new HashMap<>();

    // 1 <= s.length <= 500
    // 所以可以通过 left * 1000 的形式让后三位表示 right,前三位表示 left 索引指针的值
    private int getKey(int left, int right) {
        return left * 1000 + right;
    }
}

拓展

返回任意一个回文串结果

如果需要获取上述操作后生成的一个回文串,该如何处理呢?

首先定义函数签名:

// 返回字符串 s 通过插入操作变成回文串后的最短长度回文串
public String minInsertions(String s)

// 返回字符串 s[left:right] 通过插入操作变成回文串后的最短长度回文串
public String minInsertions(String s, int left, int right)

代码实现如下所示:

class Solution {
    public String minInsertions(String s) {
        return minInsertions(s, 0, s.length() - 1);
    }

    public String minInsertions(String s, int left, int right) {
        int key = getKey(left, right);
        if (memo.get(key) != null) {
            return memo.get(key);
        }

        if (left >= right) {
            memo.put(key, s.substring(left, right + 1));
            return memo.get(key);
        }

        if (s.charAt(left) == s.charAt(right)) {
            String subPart = minInsertions(s, left + 1, right - 1);
            memo.put(key, s.charAt(left) + subPart + s.charAt(right));
            return memo.get(key);
        }
        String leftPart = minInsertions(s, left + 1, right);
        String rightPart = minInsertions(s, left, right - 1);

        if (leftPart.length() > rightPart.length()) {
            memo.put(key, s.charAt(right) + rightPart + s.charAt(right));
        } else {
            memo.put(key, s.charAt(left) + leftPart + s.charAt(left));
        }

        return memo.get(key);
    }

    private Map memo = new HashMap<>();

    // 1 <= s.length <= 500
    // 所以可以通过 left * 1000 的形式让后三位表示 right,前三位表示 left 索引指针的值
    private int getKey(int left, int right) {
        return left * 1000 + right;
    }
}

返回所有的回文串结果

如果需要获取上述操作后生成的所有回文串集合,该如何处理呢?

首先定义函数签名:

// 返回字符串 s 通过插入操作变成回文串后的最短长度回文串集合
public List minInsertions(String s)

// 返回字符串 s[left:right] 通过插入操作变成回文串后的最短长度回文串集合
public List minInsertions(String s, int left, int right)

代码实现如下所示:

class Solution {
    public List minInsertions(String s) {
        return minInsertions(s, 0, s.length() - 1);
    }

    public List minInsertions(String s, int left, int right) {
        int key = getKey(left, right);
        if (memo.get(key) != null) {
            return memo.get(key);
        }

        if (left >= right) {
            putMemo(key, s.substring(left, right + 1));
            return memo.get(key);
        }

        if (s.charAt(left) == s.charAt(right)) {
            List subParts = minInsertions(s, left + 1, right - 1);
            putMemo(key, subParts, s.charAt(left));
            return memo.get(key);
        }
        List leftParts = minInsertions(s, left + 1, right);
        List rightParts = minInsertions(s, left, right - 1);

        int leftLength = leftParts.get(0).length();
        int rightLength = rightParts.get(0).length();
        if (leftLength > rightLength) {
            putMemo(key, rightParts, s.charAt(right));
        } else if (leftLength < rightLength) {
            putMemo(key, leftParts, s.charAt(left));
        } else {
            // leftLength == rightLength
            putMemo(key, rightParts, s.charAt(right));
            putMemo(key, leftParts, s.charAt(left));
        }

        return memo.get(key);
    }

    private Map> memo = new HashMap<>();

    // 1 <= s.length <= 500
    // 所以可以通过 left * 1000 的形式让后三位表示 right,前三位表示 left 索引指针的值
    private int getKey(int left, int right) {
        return left * 1000 + right;
    }

    private void putMemo(int key, String value) {
        memo.put(key, Arrays.asList(value));
    }

    private void putMemo(int key, List subParts, char addon) {
        List newValues = subParts.stream()
            .map(string -> addon + string + addon)
            .collect(Collectors.toList());

        List oldValues = memo.get(key);
        if (oldValues != null) {
            newValues.addAll(oldValues);
        }
        memo.put(key, newValues);
    }
}

递归算法的时间复杂度怎么计算?子问题总数 x 解决一个子问题需要的时间。

这里的子问题总数仍然是 。

但是一个子问题需要的时间等于 putMemo(int key, List subParts, char addon) 的耗时,这个和当前字符串的子串拥有的回文串数量相关,没有一个很好的计算方式。

通过实测 s = "tldjbqjdogipebqsohdypcxjqkrqla"(字符串长度为 ),发现其回文串数量为 。

由此可知其时间复杂度是远远大于 的。

你可能感兴趣的:(1312. 让字符串成为回文串的最少插入次数)