1123.最深叶节点的最近公共祖先

目标

给你一个有根节点 root 的二叉树,返回它 最深的叶节点的最近公共祖先 。

回想一下:

  • 叶节点 是二叉树中没有子节点的节点
  • 树的根节点的 深度 为 0,如果某一节点的深度为 d,那它的子节点的深度就是 d+1
  • 如果我们假定 A 是一组节点 S 的 最近公共祖先,S 中的每个节点都在以 A 为根节点的子树中,且 A 的深度达到此条件下可能的最大值。

示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4]
输出:[2,7,4]
解释:我们返回值为 2 的节点,在图中用黄色标记。
在图中用蓝色标记的是树的最深的节点。
注意,节点 6、0 和 8 也是叶节点,但是它们的深度是 2 ,而节点 7 和 4 的深度是 3 。

示例 2:

输入:root = [1]
输出:[1]
解释:根节点是树中最深的节点,它是它本身的最近公共祖先。

示例 3:

输入:root = [0,1,3,null,2]
输出:[2]
解释:树中最深的叶节点是 2 ,最近公共祖先是它自己。

说明:

  • 树中的节点数将在 [1, 1000] 的范围内。
  • 0 <= Node.val <= 1000
  • 每个节点的值都是 独一无二 的。

思路

找到二叉树最深的叶节点并返回它们的最近公共祖先。

dfs 记录最大深度,返回子树深度,如果左右子树深度相同且等于最大深度则更新返回节点。

代码


/**
 * @date 2025-04-04 19:09
 */
public class LcaDeepestLeaves1123 {

    TreeNode res;
    int maxDepth;

    public TreeNode lcaDeepestLeaves(TreeNode root) {
        dfs(root, -1);
        return res;
    }

    public int dfs(TreeNode node, int depth) {
        if (node == null) {
            return depth;
        }
        int leftDepth = dfs(node.left, depth + 1);
        int rightDepth = dfs(node.right, depth + 1);
        int d = Math.max(leftDepth, rightDepth);
        maxDepth = Math.max(maxDepth, d);
        if (rightDepth == maxDepth && leftDepth == maxDepth) {
            res = node;
        }
        return d;
    }
}

性能

2874.有序三元组中的最大值II

目标

给你一个下标从 0 开始的整数数组 nums 。

请你从所有满足 i < j < k 的下标三元组 (i, j, k) 中,找出并返回下标三元组的最大值。如果所有满足条件的三元组的值都是负数,则返回 0 。

下标三元组 (i, j, k) 的值等于 (nums[i] - nums[j]) * nums[k] 。

示例 1:

输入:nums = [12,6,1,2,7]
输出:77
解释:下标三元组 (0, 2, 4) 的值是 (nums[0] - nums[2]) * nums[4] = 77 。
可以证明不存在值大于 77 的有序下标三元组。

示例 2:

输入:nums = [1,10,3,4,19]
输出:133
解释:下标三元组 (1, 2, 4) 的值是 (nums[1] - nums[2]) * nums[4] = 133 。
可以证明不存在值大于 133 的有序下标三元组。 

示例 3:

输入:nums = [1,2,3]
输出:0
解释:唯一的下标三元组 (0, 1, 2) 的值是一个负数,(nums[0] - nums[1]) * nums[2] = -3 。因此,答案是 0 。

说明:

  • 3 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^6

思路

求数组 nums 的下标 0 <= i < j < k < n(nums[i] - nums[j]) * nums[k] 的最大值。

2873.有序三元组中的最大值I 相比数组最大长度变成了 10^5。

只能选择枚举 k,维护前缀最大值,以及最大值与当前元素的最大差值。

由于题目要求负值取 0 所以更新最大值与最大差值的顺序不影响。但是更新 maxDiff * nums[k] 就必须放在第一位,因为这时 maxDiff 还未更新,还是 k 左侧的最大差值。

代码


/**
 * @date 2025-04-03 0:41
 */
public class MaximumTripletValue2874 {

    public long maximumTripletValue(int[] nums) {
        long res = 0L;
        int n = nums.length;
        int max = 0;
        long maxDiff = 0;
        for (int i = 0; i < n; i++) {
            res = Math.max(res, maxDiff * nums[i]);
            maxDiff = Math.max(maxDiff, max - nums[i]);
            max = Math.max(max, nums[i]);
        }
        return res;
    }

}

性能

2140.解决智力问题

目标

给你一个下标从 0 开始的二维整数数组 questions ,其中 questions[i] = [pointsi, brainpoweri] 。

这个数组表示一场考试里的一系列题目,你需要 按顺序 (也就是从问题 0 开始依次解决),针对每个问题选择 解决 或者 跳过 操作。解决问题 i 将让你 获得 pointsi 的分数,但是你将 无法 解决接下来的 brainpoweri 个问题(即只能跳过接下来的 brainpoweri 个问题)。如果你跳过问题 i ,你可以对下一个问题决定使用哪种操作。

比方说,给你 questions = [[3, 2], [4, 3], [4, 4], [2, 5]] :

  • 如果问题 0 被解决了, 那么你可以获得 3 分,但你不能解决问题 1 和 2 。
  • 如果你跳过问题 0 ,且解决问题 1 ,你将获得 4 分但是不能解决问题 2 和 3 。

请你返回这场考试里你能获得的 最高 分数。

示例 1:

输入:questions = [[3,2],[4,3],[4,4],[2,5]]
输出:5
解释:解决问题 0 和 3 得到最高分。
- 解决问题 0 :获得 3 分,但接下来 2 个问题都不能解决。
- 不能解决问题 1 和 2
- 解决问题 3 :获得 2 分
总得分为:3 + 2 = 5 。没有别的办法获得 5 分或者多于 5 分。

示例 2:

输入:questions = [[1,1],[2,2],[3,3],[4,4],[5,5]]
输出:7
解释:解决问题 1 和 4 得到最高分。
- 跳过问题 0
- 解决问题 1 :获得 2 分,但接下来 2 个问题都不能解决。
- 不能解决问题 2 和 3
- 解决问题 4 :获得 5 分
总得分为:2 + 5 = 7 。没有别的办法获得 7 分或者多于 7 分。

说明:

  • 1 <= questions.length <= 10^5
  • questions[i].length == 2
  • 1 <= pointsi, brainpoweri <= 10^5

思路

有一个二维数组 questions 表示一场考试里的一系列题目,questions[i][0] 表示解决第 i 题能获得的分数,questions[i][1] 表示解决该题需要消耗的脑力,即解决了第 i 题后,i 后面的 questions[i][1] 个题目都无法解决。返回在该场考试所能获得的最高分。

这个题有许多值得思考的地方,有空整理一下。//todo

代码


/**
 * @date 2025-04-01 8:47
 */
public class MostPoints2140 {

    public long mostPoints(int[][] questions) {
        int n = questions.length;
        long[] dp = new long[n + 1];
        for (int i = n - 1; i >= 0; i--) {
            int j = Math.min(i + questions[i][1] + 1, n);
            dp[i] = Math.max(dp[i + 1], dp[j] + questions[i][0]);
        }
        return dp[0];
    }

}

性能

2712.使所有字符相等的最小成本

目标

给你一个下标从 0 开始、长度为 n 的二进制字符串 s ,你可以对其执行两种操作:

  • 选中一个下标 i 并且反转从下标 0 到下标 i(包括下标 0 和下标 i )的所有字符,成本为 i + 1 。
  • 选中一个下标 i 并且反转从下标 i 到下标 n - 1(包括下标 i 和下标 n - 1 )的所有字符,成本为 n - i 。

返回使字符串内所有字符 相等 需要的 最小成本 。

反转 字符意味着:如果原来的值是 '0' ,则反转后值变为 '1' ,反之亦然。

示例 1:

输入:s = "0011"
输出:2
解释:执行第二种操作,选中下标 i = 2 ,可以得到 s = "0000" ,成本为 2 。可以证明 2 是使所有字符相等的最小成本。

示例 2:

输入:s = "010101"
输出:9
解释:执行第一种操作,选中下标 i = 2 ,可以得到 s = "101101" ,成本为 3 。
执行第一种操作,选中下标 i = 1 ,可以得到 s = "011101" ,成本为 2 。
执行第一种操作,选中下标 i = 0 ,可以得到 s = "111101" ,成本为 1 。
执行第二种操作,选中下标 i = 4 ,可以得到 s = "111110" ,成本为 2 。
执行第二种操作,选中下标 i = 5 ,可以得到 s = "111111" ,成本为 1 。
使所有字符相等的总成本等于 9 。可以证明 9 是使所有字符相等的最小成本。 

说明:

  • 1 <= s.length == n <= 10^5
  • s[i] 为 '0' 或 '1'

思路

有一个二进制字符串,每次操作可以反转前缀 0 ~ i,成本是 i + 1,也可以反转后缀 i ~ n - 1,成本是 n - i。求使字符串所有字符相等的最小成本。

如何操作才能使字符相等?相等字符是 0 还是 1?操作哪边才能使成本最小?

关键点是想清楚与是 0 还是 1 没有关系,只要相邻的元素值不同,就必须要反转,无非是考虑反转前缀还是后缀,每次操作只影响相邻的元素关系。

代码


/**
 * @date 2025-03-27 1:33
 */
public class MinimumCost2712 {

    public long minimumCost(String s) {
        int n = s.length();
        long res = 0;
        for (int i = 1; i < n; i++) {
            if (s.charAt(i) != s.charAt(i - 1)) {
                // i 表示反转 0 ~ i - 1,n - i 表示反转 i ~ n - 1
                res += Math.min(i, n - i);
            }
        }
        return res;
    }
}

性能

2829.k-avoiding数组的最小总和

目标

给你两个整数 n 和 k 。

对于一个由 不同 正整数组成的数组,如果其中不存在任何求和等于 k 的不同元素对,则称其为 k-avoiding 数组。

返回长度为 n 的 k-avoiding 数组的可能的最小总和。

示例 1:

输入:n = 5, k = 4
输出:18
解释:设若 k-avoiding 数组为 [1,2,4,5,6] ,其元素总和为 18 。
可以证明不存在总和小于 18 的 k-avoiding 数组。

示例 2:

输入:n = 2, k = 6
输出:3
解释:可以构造数组 [1,2] ,其元素总和为 3 。
可以证明不存在总和小于 3 的 k-avoiding 数组。 

说明:

  • 1 <= n, k <= 50

思路

定义 k-avoiding 数组是由不同的正整数组成,并且任意两个元素的和不等于 k 的数组。求长度为 n 的 k-avoiding 数组的最小和。

构造一个长度为 n 的正整数数组,要使和最小,需要从 num = 1 开始选,跳过 k - num

网友指出可以使用等差数列求和来计算,第一部分是 1 ~ m, m = min(k / 2, n) 和为 m * (m + 1) / 2,第二部分是 k ~ k + n - m - 1,和为 (n - m) * (2 * k + n - m - 1) / 2

代码


/**
 * @date 2025-03-26 0:13
 */
public class MinimumSum2829 {

    public int minimumSum(int n, int k) {
        int res = 0;
        int length = 0;
        int num = 1;
        Set<Integer> avoiding = new HashSet<>();
        while (length < n) {
            if (avoiding.contains(num)) {
                num++;
                continue;
            }
            if (num < k) {
                avoiding.add(k - num);
            }
            length++;
            res += num;
            num++;
        }
        return res;
    }

}

性能

2711.对角线上不同值的数量差

目标

给你一个下标从 0 开始、大小为 m x n 的二维矩阵 grid ,请你求解大小同样为 m x n 的答案矩阵 answer 。

矩阵 answer 中每个单元格 (r, c) 的值可以按下述方式进行计算:

  • topLeft[r][c] 为矩阵 grid 中单元格 (r, c) 左上角对角线上 不同值 的数量。
  • bottomRight[r][c] 为矩阵 grid 中单元格 (r, c) 右下角对角线上 不同值 的数量。

然后 answer[r][c] = |topLeft[r][c] - bottomRight[r][c]|

返回矩阵 answer 。

矩阵对角线 是从最顶行或最左列的某个单元格开始,向右下方向走到矩阵末尾的对角线。

如果单元格 (r1, c1) 和单元格 (r, c) 属于同一条对角线且 r1 < r ,则单元格 (r1, c1) 属于单元格 (r, c) 的左上对角线。类似地,可以定义右下对角线。

示例 1:

输入:grid = [[1,2,3],[3,1,5],[3,2,1]]
输出:[[1,1,0],[1,0,1],[0,1,1]]
解释:第 1 个图表示最初的矩阵 grid 。 
第 2 个图表示对单元格 (0,0) 计算,其中蓝色单元格是位于右下对角线的单元格。
第 3 个图表示对单元格 (1,2) 计算,其中红色单元格是位于左上对角线的单元格。
第 4 个图表示对单元格 (1,1) 计算,其中蓝色单元格是位于右下对角线的单元格,红色单元格是位于左上对角线的单元格。
- 单元格 (0,0) 的右下对角线包含 [1,1] ,而左上对角线包含 [] 。对应答案是 |1 - 0| = 1 。
- 单元格 (1,2) 的右下对角线包含 [] ,而左上对角线包含 [2] 。对应答案是 |0 - 1| = 1 。
- 单元格 (1,1) 的右下对角线包含 [1] ,而左上对角线包含 [1] 。对应答案是 |1 - 1| = 0 。
其他单元格的对应答案也可以按照这样的流程进行计算。

示例 2:

输入:grid = [[1]]
输出:[[0]]
解释:- 单元格 (0,0) 的右下对角线包含 [] ,左上对角线包含 [] 。对应答案是 |0 - 0| = 0 。

说明:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n, grid[i][j] <= 50

思路

计算矩阵元素左上对角线的不同元素个数与右下对角线的不同元素个数的差的绝对值。即将元素所在的左上右下对角线,以当前元素为分界点(不包括当前元素),分成左上与右下两部分,计算每部分不同元素的个数,取差的绝对值。

暴力解法是枚举每个格子的左上右下元素,每一对角线都要遍历它所包含的元素个数次。

可以直接按对角线遍历,先记录前缀中的不同元素个数,然后再倒着遍历,计算差值的绝对值。

网友提到由于元素值不大,可以将其保存到 long 型数字中。

代码


/**
 * @date 2025-03-25 0:12
 */
public class DifferenceOfDistinctValues2711 {

    public int[][] differenceOfDistinctValues(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] res = new int[m][n];
        for (int i = 0; i < m + n - 1; i++) {
            int row = i / n < 1 ? 0 : i - n + 1 % m;
            int col = row > 0 ? 0 : n - 1 - i;
            Set<Integer> set = new HashSet<>();
            while (row < m && col < n){
                res[row][col] = set.size();
                set.add(grid[row][col]);
                row++;
                col++;
            }
            row--;
            col--;
            set.clear();
            while (row >= 0 && col >= 0){
                res[row][col] = Math.abs(res[row][col] - set.size());
                set.add(grid[row][col]);
                row--;
                col--;
            }
        }
        return res;
    }

}

性能

2116.判断一个括号字符串是否有效

目标

一个括号字符串是只由 '(' 和 ')' 组成的 非空 字符串。如果一个字符串满足下面 任意 一个条件,那么它就是有效的:

  • 字符串为 ().
  • 它可以表示为 AB(A 与 B 连接),其中A 和 B 都是有效括号字符串。
  • 它可以表示为 (A) ,其中 A 是一个有效括号字符串。

给你一个括号字符串 s 和一个字符串 locked ,两者长度都为 n 。locked 是一个二进制字符串,只包含 '0' 和 '1' 。对于 locked 中 每一个 下标 i :

  • 如果 locked[i] 是 '1' ,你 不能 改变 s[i] 。
  • 如果 locked[i] 是 '0' ,你 可以 将 s[i] 变为 '(' 或者 ')' 。

如果你可以将 s 变为有效括号字符串,请你返回 true ,否则返回 false 。

示例 1:

输入:s = "))()))", locked = "010100"
输出:true
解释:locked[1] == '1' 和 locked[3] == '1' ,所以我们无法改变 s[1] 或者 s[3] 。
我们可以将 s[0] 和 s[4] 变为 '(' ,不改变 s[2] 和 s[5] ,使 s 变为有效字符串。

示例 2:

输入:s = "()()", locked = "0000"
输出:true
解释:我们不需要做任何改变,因为 s 已经是有效字符串了。

示例 3:

输入:s = ")", locked = "0"
输出:false
解释:locked 允许改变 s[0] 。
但无论将 s[0] 变为 '(' 或者 ')' 都无法使 s 变为有效字符串。

示例 4:

输入:s = "(((())(((())", locked = "111111010111"
输出:true
解释:locked 允许我们改变 s[6] 和 s[8]。
我们将 s[6] 和 s[8] 改为 ')' 使 s 变为有效字符串。

说明:

  • n == s.length == locked.length
  • 1 <= n <= 10^5
  • s[i] 要么是 '(' 要么是 ')' 。
  • locked[i] 要么是 '0' 要么是 '1' 。

思路

有一个由 () 组成的非空字符串,如果 locked[i]0,可以任意修改 i 位置上的字符,判断能否使括号字符串变得有效。

从左到右累加未匹配的左括号数量,遇到 ( 加一,遇到 ) 减一,对于有效字符串最终会得到 0,并且有效字符串的任意前缀中未匹配的 ( 的数量总是大于等于 0

利用这一性质维护一个未匹配左括号数量的可能取值集合,如果最终集合中包含 0,则说明可以变为有效。具体实现时只需维护集合的最大值与最小值,因为集合中的数字都是同时加一减一,所以它们是连续的奇数或偶数。

具体来说就是遇到不可变字符,( min max 加一,) min max 减一。如果 max < 0,返回 false,如果 min < 0,则取 1(如果 min < 0,说明原来是偶数,由于 max 没有减为负数,说明还有比 0 大的偶数,那么最小值为 2 - 1)。如果遇到可变字符,max 加一,min 减一,如果 min < 0,则取 1

代码


/**
 * @date 2025-03-23 21:12
 */
public class CanBeValid2116 {

    public boolean canBeValid(String s, String locked) {
        int n = s.length();
        if (n % 2 == 1) {
            return false;
        }
        int max = 0, min = 0;
        char[] chars = s.toCharArray();
        for (int i = 0; i < n; i++) {
            if (locked.charAt(i) == '0') {
                max++;
                if (--min < 0) {
                    min = 1;
                }
                continue;
            }
            if ('(' == chars[i]) {
                max++;
                min++;
            } else {
                if (--max < 0) {
                    return false;
                }
                if (--min < 0) {
                    min = 1;
                }
            }
        }
        return min == 0;
    }
}

性能

2680.最大或值

目标

给你一个下标从 0 开始长度为 n 的整数数组 nums 和一个整数 k 。每一次操作中,你可以选择一个数并将它乘 2 。

你最多可以进行 k 次操作,请你返回 nums[0] | nums[1] | ... | nums[n - 1] 的最大值。

a | b 表示两个整数 a 和 b 的 按位或 运算。

示例 1:

输入:nums = [12,9], k = 1
输出:30
解释:如果我们对下标为 1 的元素进行操作,新的数组为 [12,18] 。此时得到最优答案为 12 和 18 的按位或运算的结果,也就是 30 。

示例 2:

输入:nums = [8,1,2], k = 2
输出:35
解释:如果我们对下标 0 处的元素进行操作,得到新数组 [32,1,2] 。此时得到最优答案为 32|1|2 = 35 。

说明:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= k <= 15

思路

有一个整数数组 nums,最多可以对它执行 k 次操作,每次操作可以任选一个数将其左移 1 位。求操作后数组所有元素的最大或值。

操作集中到同一个数上可以将最高有效位移到最高,少一次操作最高位就低一位。问题的关键在于,如果所有数字都有相同的最高有效位,移哪个是不确定的。

这时就需要枚举操作数了,每选择一个操作数就需要重新计算其余元素的或值。由于或运算没有逆运算,我们无法撤销已经或进去的值。

直接的想法是计算或前缀、后缀,枚举所有元素,将其左移 k 次,然后与前后缀进行或运算,取最大值。

网友提供了另一种基于位运算的解法,计算全体元素的或值,同时计算所有 bit 位上存在重复 1 的位置,通过异或运算将当前元素对或值的贡献抵消,然后补上重复位置上的 1

代码


/**
 * @date 2025-03-21 0:08
 */
public class MaximumOr2680 {

    public long maximumOr_v1(int[] nums, int k) {
        int n = nums.length;
        int or = 0;
        int multiBits = 0;
        for (int i = 0; i < n; i++) {
            multiBits = multiBits | or & nums[i];
            or = or | nums[i];
        }
        long res = 0L;
        for (int i = 0; i < n; i++) {
            res = Math.max(res, or ^ nums[i] | multiBits | ((long) nums[i] << k));
        }
        return res;
    }

    public long maximumOr(int[] nums, int k) {
        int n = nums.length;
        int[] prefix = new int[n + 1];
        int[] suffix = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            prefix[i] = prefix[i - 1] | nums[i - 1];
            suffix[n - i] = suffix[n - i + 1] | nums[n - i];
        }
        long res = 0L;
        for (int i = 0; i < n; i++) {
            res = Math.max(res, prefix[i] | suffix[i + 1] | ((long) nums[i] << k));
        }
        return res;
    }

}

性能

2610.转换二维数组

目标

给你一个整数数组 nums 。请你创建一个满足以下条件的二维数组:

  • 二维数组应该 只 包含数组 nums 中的元素。
  • 二维数组中的每一行都包含 不同 的整数。
  • 二维数组的行数应尽可能 少 。

返回结果数组。如果存在多种答案,则返回其中任何一种。

请注意,二维数组的每一行上可以存在不同数量的元素。

示例 1:

输入:nums = [1,3,4,1,2,3,1]
输出:[[1,3,4,2],[1,3],[1]]
解释:根据题目要求可以创建包含以下几行元素的二维数组:
- 1,3,4,2
- 1,3
- 1
nums 中的所有元素都有用到,并且每一行都由不同的整数组成,所以这是一个符合题目要求的答案。
可以证明无法创建少于三行且符合题目要求的二维数组。

示例 2:

输入:nums = [1,2,3,4]
输出:[[4,3,2,1]]
解释:nums 中的所有元素都不同,所以我们可以将其全部保存在二维数组中的第一行。

说明:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= nums.length

思路

将数组转化为二维数组,要求每一行元素没有重复,并且行数尽可能地少。

使用哈希表对相同元素计数,将所有 key 放入一行并将所有 key 的计数减一,如果计数减为 0 则删除 key,直到哈希表为空,时间复杂度为 O(n)

也可以将哈希表改为数组相当于计数排序。

代码


/**
 * @date 2025-03-19 0:21
 */
public class FindMatrix2610 {

    public List<List<Integer>> findMatrix(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.merge(num, 1, Integer::sum);
        }
        List<List<Integer>> res = new ArrayList<>();
        while (!map.isEmpty()) {
            res.add(new ArrayList<>());
            List<Integer> list = res.get(res.size() - 1);
            Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<Integer, Integer> entry = iterator.next();
                list.add(entry.getKey());
                entry.setValue(entry.getValue() - 1);
                if (entry.getValue() == 0) {
                    iterator.remove();
                }
            }
        }
        return res;
    }

}

性能

1963.使字符串平衡的最小交换次数

目标

给你一个字符串 s ,下标从 0 开始 ,且长度为偶数 n 。字符串 恰好 由 n / 2 个开括号 '[' 和 n / 2 个闭括号 ']' 组成。

只有能满足下述所有条件的字符串才能称为 平衡字符串 :

  • 字符串是一个空字符串,或者
  • 字符串可以记作 AB ,其中 A 和 B 都是 平衡字符串 ,或者
  • 字符串可以写成 [C] ,其中 C 是一个 平衡字符串 。

你可以交换 任意 两个下标所对应的括号 任意 次数。

返回使 s 变成 平衡字符串 所需要的 最小 交换次数。

示例 1:

输入:s = "][]["
输出:1
解释:交换下标 0 和下标 3 对应的括号,可以使字符串变成平衡字符串。
最终字符串变成 "[[]]" 。

示例 2:

输入:s = "]]][[["
输出:2
解释:执行下述操作可以使字符串变成平衡字符串:
- 交换下标 0 和下标 4 对应的括号,s = "[]][][" 。
- 交换下标 1 和下标 5 对应的括号,s = "[[][]]" 。
最终字符串变成 "[[][]]" 。

示例 3:

输入:s = "[]"
输出:0
解释:这个字符串已经是平衡字符串。

说明:

  • n == s.length
  • 2 <= n <= 10^6
  • n 为偶数
  • s[i] 为'[' 或 ']'
  • 开括号 '[' 的数目为 n / 2 ,闭括号 ']' 的数目也是 n / 2

思路

有一个长度为偶数 n 的字符串,有 n / 2[]。定义括号能够匹配的字符串是平衡字符串。每一次操作可以交换任意两个下标对应的括号,求将字符串变平衡的最少操作次数。

[[]][][] 都是平衡字符串,如何确定该与哪个括号交换才能使操作次数最小?对于 ]][[][][,第一个与最后一个交换之后变为平衡字符串 [][][[]]。而如果选择与倒数第二个 [ 交换,会变成 []][,需要再交换一次才能变为平衡字符串。

平衡字符串有这样的性质:其前缀中 [ 的个数总是大于等于 ] 的个数。如果发现 ] 的个数超过 [,那么必须要进行交换。最好将它交换到最后,因为这样可以减少它在前面前缀中对 ] 的贡献,将交换操作尽可能地延后,减小必须进行交换的次数。

也可以先将平衡的括号消掉,剩下的必然是 ]]]......[[[ 的形式。交换一次最多可以抵消 4 个,最少抵消 2 个,即 (stack.size() + 2) / 4

代码


/**
 * @date 2025-03-17 16:32
 */
public class MinSwaps1963 {

    public int minSwaps_v1(String s) {
        int n = s.length();
        int left = 0, right = 0;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (c == '[') {
                left++;
            } else if (c == ']' && left > 0) {
                left--;
            } else {
                right++;
            }
        }
        return (left + right + 2) / 4;
    }

    public int minSwaps(String s) {
        int n = s.length();
        ArrayDeque<Character> stack = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (c == ']' && !stack.isEmpty() && stack.peek() == '[') {
                stack.pop();
            } else {
                stack.push(c);
            }
        }
        return (stack.size() + 2) / 4;
    }

}

性能