3863.将一个字符串排序的最小操作次数

目标

给你一个由小写英文字母组成的字符串 s。

在一次操作中,你可以选择 s 的任意 子字符串(但 不能 是整个字符串),并将其按 非降序字母顺序 进行 排序。

返回使 s 按 非降序 排列所需的 最小 操作次数。如果无法做到,则返回 -1。

示例 1:

输入: s = "dog"
输出: 1
解释:
将子字符串 "og" 排序为 "go"。
现在,s = "dgo",已按升序排列。因此,答案是 1。

示例 2:

输入: s = "card"
输出: 2
解释:
将子字符串 "car" 排序为 "acr",得到 s = "acrd"。
将子字符串 "rd" 排序为 "dr",得到 s = "acdr",已按升序排列。因此,答案是 2。

示例 3:

输入: s = "gf"
输出: -1
解释:
在给定提示下,无法对 s 进行排序。因此,答案是 -1。

说明:

  • 1 <= s.length <= 10^5
  • s 仅由小写英文字母组成。

思路

有一个字符串 s,每次操作可以将其子串(不能是整个字符串)按非递减排序,求使整个字符串按非递减排列所需的最小操作次数,如果无法实现返回 -1。

分类讨论:

  • 如果 s 本身非递减,操作次数为 0。
  • 否则,找出最大字符 max 与 最小字符 min:
    • 如果 s[0] == min || s[n - 1] == max,只需将剩余部分排序即可,操作次数为 1。
    • 如果 s[0] == max && s[n - 1] == min:
    • 如果字符串中有多余的 min 或者 max,可以直接排序前缀 s[0, n - 2] 或后缀 s[1, n - 1] 使得最大或最小归位,然后再排序剩余部分,操作次数为 2。
    • 否则,需要先将 min 与 max 移出来:
      • 如果 n == 2,返回 -1。
      • 否则,将 min 与 前一个的元素交换,排序前缀 s[0, n - 2],然后再排序后缀 s[1, n - 1],操作次数为 3。
    • 其余情况,先排序前缀 s[0, n - 2] 再排序后缀 s[1, n - 1] 即可,操作次数为 2。

代码


/**
 * @date 2026-10-21 9:10
 */
public class MinOperations3863 {

    public int minOperations(String s) {
        int max = s.charAt(0);
        int min = max;
        char[] chars = s.toCharArray();
        int[] cnt = new int[26];
        int n = chars.length;
        boolean ordered = true;
        for (char c : chars) {
            if (c < max) {
                ordered = false;
            }
            cnt[c - 'a']++;
            max = Math.max(max, c);
            min = Math.min(min, c);
        }
        if (ordered) {
            return 0;
        } else if (max == s.charAt(n - 1) || min == s.charAt(0)) {
            return 1;
        } else if (max == s.charAt(0) && min == s.charAt(n - 1)) {
            if (n == 2) {
                return -1;
            } else if (cnt[min - 'a'] > 1 || cnt[max - 'a'] > 1) {
                return 2;
            } else {
                return 3;
            }
        }
        return 2;
    }

}

性能

1541.平衡括号字符串的最少插入次数

目标

给你一个括号字符串 s ,它只包含字符 '(' 和 ')' 。一个括号字符串被称为平衡的当它满足:

  • 任何左括号 '(' 必须对应两个连续的右括号 '))' 。
  • 左括号 '(' 必须在对应的连续两个右括号 '))' 之前。

比方说 "())", "())(())))" 和 "(())())))" 都是平衡的, ")()", "()))" 和 "(()))" 都是不平衡的。

你可以在任意位置插入字符 '(' 和 ')' 使字符串平衡。

请你返回让 s 平衡的最少插入次数。

示例 1:

输入:s = "(()))"
输出:1
解释:第二个左括号有与之匹配的两个右括号,但是第一个左括号只有一个右括号。我们需要在字符串结尾额外增加一个 ')' 使字符串变成平衡字符串 "(())))" 。

示例 2:

输入:s = "())"
输出:0
解释:字符串已经平衡了。

示例 3:

输入:s = "))())("
输出:3
解释:添加 '(' 去匹配最开头的 '))' ,然后添加 '))' 去匹配最后一个 '(' 。

示例 4:

输入:s = "(((((("
输出:12
解释:添加 12 个 ')' 得到平衡字符串。

示例 5:

输入:s = ")))))))"
输出:5
解释:在字符串开头添加 4 个 '(' 并在结尾添加 1 个 ')' ,字符串变成平衡字符串 "(((())))))))" 。

说明:

  • 1 <= s.length <= 10^5
  • s 只包含 '(' 和 ')' 。

思路

定义平衡括号字符串是每一个左括号都对应有两个连续右括号的字符串。有一个括号字符串 s,每次操作可以插入一个 ( 或者 ),求使括号字符串变平衡的最小操作次数。

记录未匹配的左括号数量 lc,遇到连续的右括号直接滑倒结束并计数 rc,贪心策略:

  • 如果 lc >= rc,如果 rc 为奇数,需要补一个右括号,操作加一,lc 减去已匹配的数量,rc 置零。
  • 否则,每两个右括号匹配需要补一个左括号,操作加 rc / 2,如果 rc 是奇数,需要补一对括号,操作加 2。

代码


/**
 * @date 2026-10-09 9:09
 */
public class MinInsertions1541 {

    public int minInsertions(String s) {
        char[] chars = s.toCharArray();
        int n = s.length();
        int lc = 0, rc = 0, res = 0;
        int i = 0;
        while (i < n) {
            if (chars[i] == '(') {
                lc += 2;
                i++;
            } else {
                while (i < n && chars[i] == ')') {
                    i++;
                    rc++;
                }
            }
            if (lc >= rc) {
                res += rc % 2;
                lc -= rc + rc % 2;
                rc = 0;
            } else {
                rc -= lc;
                int rem = rc % 2;
                res += rc / 2 + (rem == 0 ? 0 : 2);
                rc = 0;
                lc = 0;
            }
        }
        return res + lc;
    }

}

性能

1021.删除最外层的括号

目标

有效括号字符串为空 ""、"(" + A + ")" 或 A + B ,其中 A 和 B 都是有效的括号字符串,+ 代表字符串的连接。

  • 例如,"","()","(())()" 和 "(()(()))" 都是有效的括号字符串。

如果有效字符串 s 非空,且不存在将其拆分为 s = A + B 的方法,我们称其为原语(primitive),其中 A 和 B 都是非空有效括号字符串。

给出一个非空有效字符串 s,考虑将其进行原语化分解,使得:s = P_1 + P_2 + ... + P_k,其中 P_i 是有效括号字符串原语。

对 s 进行原语化分解,删除分解中每个原语字符串的最外层括号,返回 s 。

示例 1:

输入:s = "(()())(())"
输出:"()()()"
解释:
输入字符串为 "(()())(())",原语化分解得到 "(()())" + "(())",
删除每个部分中的最外层括号后得到 "()()" + "()" = "()()()"。

示例 2:

输入:s = "(()())(())(()(()))"
输出:"()()()()(())"
解释:
输入字符串为 "(()())(())(()(()))",原语化分解得到 "(()())" + "(())" + "(()(()))",
删除每个部分中的最外层括号后得到 "()()" + "()" + "()(())" = "()()()()(())"。

示例 3:

输入:s = "()()"
输出:""
解释:
输入字符串为 "()()",原语化分解得到 "()" + "()",
删除每个部分中的最外层括号后得到 "" + "" = ""。

说明:

  • 1 <= s.length <= 10^5
  • s[i] 为 '(' 或 ')'
  • s 是一个有效括号字符串

思路

有一个仅由 ( 或 ) 组成非空有效括号字符串。删掉最外层的括号并返回删除后的字符串。最外层指没有被嵌套在其它括号内部的括号。

如果左括号的数量等于右括号的数量说明匹配到了最外层括号,去掉首尾保留中间即可。

代码


/**
 * @date 2026-10-08 9:12
 */
public class RemoveOuterParentheses1021 {

    public String removeOuterParentheses(String s) {
        StringBuilder sb = new StringBuilder();
        int n = s.length();
        int lc = 0, rc = 0, l = 0;
        for (int i = 0; i < n; i++) {
            if (s.charAt(i) == '(') {
                lc++;
                if (lc == 1) {
                    l = i;
                }
            } else {
                rc++;
                if (lc == rc) {
                    sb.append(s.substring(l + 1, i));
                    lc = 0;
                    rc = 0;
                }
            }
        }
        return sb.toString();
    }
}

性能

1614.括号的最大嵌套深度

目标

给定 有效括号字符串 s,返回 s 的 嵌套深度。嵌套深度是嵌套括号的 最大 数量。

示例 1:

输入:s = "(1+(2*3)+((8)/4))+1"
输出:3
解释:数字 8 在嵌套的 3 层括号中。

示例 2:

输入:s = "(1)+((2))+(((3)))"
输出:3
解释:数字 3 在嵌套的 3 层括号中。

示例 3:

输入:s = "()(())((()()))"
输出:3

说明:

  • 1 <= s.length <= 100
  • s 由数字 0-9 和字符 '+'、'-'、'*'、'/'、'('、')' 组成
  • 题目数据保证括号字符串 s 是 有效的括号字符串

思路

s 是一个有效的括号字符串,其中的括号是能够合法匹配的。返回 s 中括号嵌套的最大深度。

遇到 ( 深度加一,遇到 ) 深度减一,记录深度的最大值即可。

代码


/**
 * @date 2026-09-28 10:04
 */
public class MaxDepth1614 {

    public int maxDepth(String s) {
        int res = 0;
        int n = s.length();
        int depth = 0;
        for (int i = 0; i < n; i++) {
            if (s.charAt(i) == '(') {
                depth++;
                res = Math.max(res, depth);
            } else if (s.charAt(i) == ')') {
                depth--;
            }
        }
        return res;
    }
}

性能

3498.字符串的反转度

目标

给你一个字符串 s,计算其 反转度。

反转度的计算方法如下:

  1. 对于每个字符,将其在 反转 字母表中的位置('a' = 26, 'b' = 25, ..., 'z' = 1)与其在字符串中的位置(下标从1 开始)相乘。
  2. 将这些乘积加起来,得到字符串中所有字符的和。

返回 反转度。

示例 1:

输入: s = "abc"
输出: 148
解释:
字母 反转字母表中的位置 字符串中的位置 乘积
'a'        26              1        26
'b'        25              2        50
'c'        24              3        72
反转度是 26 + 50 + 72 = 148 。

示例 2:

输入: s = "zaza"
输出: 160
解释:
字母 反转字母表中的位置 字符串中的位置 乘积
'z'        1             1         1
'a'        26            2        52
'z'        1             3         3
'a'        26            4        104
反转度是 1 + 52 + 3 + 104 = 160 。

说明:

  • 1 <= s.length <= 1000
  • s 仅包含小写字母。

思路

定义字符串中字符的反转度为每个字符的下标(从 1 开始)乘以该字符在反转字符表中的位置的乘积,字符串的反转度是其字符反转度之和。

依题意模拟即可。

代码


/**
 * @date 2026-09-21 16:49
 */
public class ReverseDegree3498 {

    public int reverseDegree(String s) {
        int res = 0;
        int n = s.length();
        for (int i = 1; i <= n; i++) {
            res += ('z' - s.charAt(i - 1) + 1) * i;
        }
        return res;
    }
}

性能

1520.最多的不重叠子字符串

目标

给你一个只包含小写字母的字符串 s ,你需要找到 s 中最多数目的非空子字符串,满足如下条件:

  1. 这些字符串之间互不重叠,也就是说对于任意两个子字符串 s[i..j] 和 s[x..y] ,要么 j < x 要么 i > y 。
  2. 如果一个子字符串包含字符 char ,那么 s 中所有 char 字符都应该在这个子字符串中。

请你找到满足上述条件的最多子字符串数目。如果有多个解法有相同的子字符串数目,请返回这些子字符串总长度最小的一个解。可以证明最小总长度解是唯一的。

请注意,你可以以 任意 顺序返回最优解的子字符串。

示例 1:

输入:s = "adefaddaccc"
输出:["e","f","ccc"]
解释:下面为所有满足第二个条件的子字符串:
[
  "adefaddaccc"
  "adefadda",
  "ef",
  "e",
  "f",
  "ccc",
]
如果我们选择第一个字符串,那么我们无法再选择其他任何字符串,所以答案为 1 。如果我们选择 "adefadda" ,剩下子字符串中我们只可以选择 "ccc" ,它是唯一不重叠的子字符串,所以答案为 2 。同时我们可以发现,选择 "ef" 不是最优的,因为它可以被拆分成 2 个子字符串。所以最优解是选择 ["e","f","ccc"] ,答案为 3 。不存在别的相同数目子字符串解。

示例 2:

输入:s = "abbaccd"
输出:["d","bb","cc"]
解释:注意到解 ["d","abba","cc"] 答案也为 3 ,但它不是最优解,因为它的总长度更长。

说明:

  • 1 <= s.length <= 10^5
  • s 只包含小写英文字母。

思路

代码

性能

2472.不重叠回文子字符串的最大数目

目标

给你一个字符串 s 和一个 正 整数 k 。

从字符串 s 中选出一组满足下述条件且 不重叠 的子字符串:

  • 每个子字符串的长度 至少 为 k 。
  • 每个子字符串是一个 回文串 。

返回最优方案中能选择的子字符串的 最大 数目。

子字符串 是字符串中一个连续的字符序列。

示例 1 :

输入:s = "abaccdbbd", k = 3
输出:2
解释:可以选择 s = "abaccdbbd" 中斜体加粗的子字符串。"aba" 和 "dbbd" 都是回文,且长度至少为 k = 3 。
可以证明,无法选出两个以上的有效子字符串。

示例 2 :

输入:s = "adbcda", k = 2
输出:0
解释:字符串中不存在长度至少为 2 的回文子字符串。

说明:

  • 1 <= k <= s.length <= 2000
  • s 仅由小写英文字母组成

思路

返回字符串 s 中长度 至少 为 k 的不重叠回文子串的最大个数。

定义 dp[i] 表示 s[i, n - 1] 中长度为 k 的不重叠回文子串的最大数目,

  • 如果不选 s[i],dp[i] = dp[i + 1]
  • 如果选择 s[i],枚举终点 j,使得 j - i + 1 >= k,如果 s[i, j] 是回文,dp[i] = max(dp[i], 1 + dp[j + 1]))

需要快速判断子串是否是回文,可以预处理。

代码


/**
 * @date 2026-09-15 9:24
 */
public class MaxPalindromes2472 {

    public int maxPalindromes(String s, int k) {
        int n = s.length();
        boolean[][] isPalindrome = new boolean[n][n];
        for (int i = 0; i < n; i++) {
            isPalindrome[i][i] = true;
            int l = i - 1, r = i + 1;
            while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)){
                isPalindrome[l--][r++] = true;
            }
            l = i - 1;
            r = i;
            while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)){
                isPalindrome[l--][r++] = true;
            }
        }
        int[] dp = new int[n + 1];
        for (int i = n - k; i >= 0; i--) {
            dp[i] = dp[i + 1];
            for (int j = n - 1; j >= i + k - 1; j--) {
                if (isPalindrome[i][j]) {
                    dp[i] = Math.max(dp[i], 1 + dp[j + 1]);
                }
            }
        }
        return dp[0];
    }

}

性能

940.不同的子序列II

目标

给定一个字符串 s,计算 s 的 不同非空子序列 的个数。因为结果可能很大,所以返回答案需要对 10^9 + 7 取余 。

字符串的 子序列 是经由原字符串删除一些(也可能不删除)字符但不改变剩余字符相对位置的一个新字符串。

例如,"ace" 是 "abcde" 的一个子序列,但 "aec" 不是。

示例 1:

输入:s = "abc"
输出:7
解释:7 个不同的子序列分别是 "a", "b", "c", "ab", "ac", "bc", 以及 "abc"。

示例 2:

输入:s = "aba"
输出:6
解释:6 个不同的子序列分别是 "a", "b", "ab", "ba", "aa" 以及 "aba"。

示例 3:

输入:s = "aaa"
输出:3
解释:3 个不同的子序列分别是 "a", "aa" 以及 "aaa"。

说明:

  • 1 <= s.length <= 2000
  • s 仅由小写英文字母组成

思路

返回字符串 s 不重复的非空子序列的个数。

定义 dp[i + 1][j] 表示 [0, i] 中以字母 j 结尾的子序列个数。使用当前字母作为子序列右端点的方案数为 1 + Σdp[i][j],即 当前字母自身 加上 与之前的方案拼接。其它右端点的方案数与之前方案数相同 dp[i + 1][j] = dp[i][j]。

代码


/**
 * @date 2026-09-08 14:54
 */
public class DistinctSubseqII940 {

    public int distinctSubseqII(String s) {
        int n = s.length();
        int MOD = 1000000007;
        int[][] dp = new int[n + 1][26];
        for (int i = 0; i < n; i++) {
            int sum = 0;
            int c = s.charAt(i) - 'a';
            for (int j = 0; j < 26; j++) {
                dp[i + 1][j] = dp[i][j];
                sum = (sum + dp[i][j]) % MOD;
            }
            dp[i + 1][c] = 1 + sum;
        }
        int res = 0;
        for (int num : dp[n]) {
            res = (res + num) % MOD;
        }
        return res;
    }
}

性能

115.不同的子序列

目标

给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。

测试用例保证结果在 32 位有符号整数范围内。

示例 1:

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit

示例 2:

输入:s = "babgbag", t = "bag"
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
babgbag
babgbag
babgbag
babgbag
babgbag

说明:

  • 1 <= s.length, t.length <= 1000
  • s 和 t 由英文字母组成

思路

有两个字符串 s 和 t,返回 s 的子序列中 t 的出现个数。

定义 dp[i][j] 表示 s[i, s.length - 1] 中 t[j, t.length - 1] 的子序列个数。dp[i][j] = (s[i] == t[j] ? dp[i + 1][j + 1] : 0) + dp[i + 1][j]。t 为空串时,初始化 dp[i][tl] = 1。

代码


/**
 * @date 2026-09-07 10:32
 */
public class NumDistinct115 {

    public int numDistinct(String s, String t) {
        int sl = s.length();
        int tl = t.length();
        char[] scs = s.toCharArray();
        char[] tcs = t.toCharArray();
        int[][] dp = new int[sl + 1][tl + 1];
        for (int i = 0; i <= sl; i++) {
            dp[i][tl] = 1;
        }
        for (int i = sl - 1; i >= 0; i--) {
            for (int j = tl - 1; j >= 0; j--) {
                dp[i][j] = (scs[i] == tcs[j] ? dp[i + 1][j + 1] : 0) + dp[i + 1][j];
            }
        }
        return dp[0][0];
    }

}

性能

3014.输入单词需要的最少按键次数I

目标

给你一个字符串 word,由 不同 小写英文字母组成。

电话键盘上的按键与 不同 小写英文字母集合相映射,可以通过按压按键来组成单词。例如,按键 2 对应 ["a","b","c"],我们需要按一次键来输入 "a",按两次键来输入 "b",按三次键来输入 "c"。

现在允许你将编号为 2 到 9 的按键重新映射到 不同 字母集合。每个按键可以映射到 任意数量 的字母,但每个字母 必须 恰好 映射到 一个 按键上。你需要找到输入字符串 word 所需的 最少 按键次数。

返回重新映射按键后输入 word 所需的 最少 按键次数。

下面给出了一种电话键盘上字母到按键的映射作为示例。注意 1,*,# 和 0 不 对应任何字母。

示例 1:

输入:word = "abcde"
输出:5
解释:图片中给出的重新映射方案的输入成本最小。
"a" -> 在按键 2 上按一次
"b" -> 在按键 3 上按一次
"c" -> 在按键 4 上按一次
"d" -> 在按键 5 上按一次
"e" -> 在按键 6 上按一次
总成本为 1 + 1 + 1 + 1 + 1 = 5 。
可以证明不存在其他成本更低的映射方案。

示例 2:

输入:word = "xycdefghij"
输出:12
解释:图片中给出的重新映射方案的输入成本最小。
"x" -> 在按键 2 上按一次
"y" -> 在按键 2 上按两次
"c" -> 在按键 3 上按一次
"d" -> 在按键 3 上按两次
"e" -> 在按键 4 上按一次
"f" -> 在按键 5 上按一次
"g" -> 在按键 6 上按一次
"h" -> 在按键 7 上按一次
"i" -> 在按键 8 上按一次
"j" -> 在按键 9 上按一次
总成本为 1 + 2 + 1 + 2 + 1 + 1 + 1 + 1 + 1 + 1 = 12 。
可以证明不存在其他成本更低的映射方案。

说明:

  • 1 <= word.length <= 26
  • word 仅由小写英文字母组成。
  • word 中的所有字母互不相同。

思路

可以将 26 个字母映射到 2 ~ 9 按键上,比如将 abc 映射到 2,那么输入 b 需要按两下 2,输入 c 需要按三下 2。给定一个由 不同字母 组成的单词 word,返回输入该字符所需的最少按键次数。

字母出现的频次都是 1,先将所有按键的第一个位置填满,然后是第二个、第三个。

录入单词所需的按键次数为 8 * (1 + 2 + …… + k) + (n % 8) * (k + 1) = 8 * (1 + k) * k / 2 + (n % 8) * (k + 1) = 4 * k * (k + 1) + (n % 8) * (k + 1) = (4 * k + n % 8) * (k + 1)。

代码


/**
 * @date 2026-07-30 9:10
 */
public class MinimumPushes3014 {

    public int minimumPushes_v1(String word) {
        int n = word.length();
        int k = n / 8;
        return (4 * k + n % 8) * (k + 1);
    }
}

性能