1536.排布二进制网格的最少交换次数

目标

给你一个 n x n 的二进制网格 grid,每一次操作中,你可以选择网格的 相邻两行 进行交换。

一个符合要求的网格需要满足主对角线以上的格子全部都是 0 。

请你返回使网格满足要求的最少操作次数,如果无法使网格符合要求,请你返回 -1 。

主对角线指的是从 (1, 1) 到 (n, n) 的这些格子。

示例 1:

输入:grid = [[0,0,1],[1,1,0],[1,0,0]]
输出:3

示例 2:

输入:grid = [[0,1,1,0],[0,1,1,0],[0,1,1,0],[0,1,1,0]]
输出:-1
解释:所有行都是一样的,交换相邻行无法使网格符合要求。

示例 3:

输入:grid = [[1,0,0],[1,1,0],[1,1,1]]
输出:0

说明:

  • n == grid.length
  • n == grid[i].length
  • 1 <= n <= 200
  • grid[i][j] 要么是 0 要么是 1 。

提示:

  • For each row of the grid calculate the most right 1 in the grid in the array maxRight.
  • To check if there exist answer, sort maxRight and check if maxRight[i] ≤ i for all possible i's.
  • If there exist an answer, simulate the swaps.

思路

有一个 n x n 的二进制矩阵,每次操作可以交换相邻的两行,求使得矩阵主对角线 之上 的所有格子变为 0 所需的最小操作次数。

i 行最右侧的 1 的下标不能超过 i,如果不满足条件,找到第一个满足条件的行进行交换。这种贪心策略之所以可行,是因为如果存在多个满足条件的行,由于行从上到下的条件越来越宽松,满足当前行的条件也必定满足后续行,因此选最近的行交换即可。

代码


/**
 * @date 2026-03-02 8:45
 */
public class MinSwaps1536 {

    public int minSwaps(int[][] grid) {
        int n = grid.length;
        int[] maxRight = new int[n];
        for (int i = 0; i < n; i++) {
            for (int j = n - 1; j >= 0; j--) {
                if (grid[i][j] == 1) {
                    maxRight[i] = j;
                    break;
                }
            }
        }
        int res = 0;
        for (int i = 0; i < n; i++) {
            if (maxRight[i] <= i) {
                continue;
            }
            boolean flag = false;
            int prev = maxRight[i];
            for (int j = i + 1; j < n; j++) {
                if (maxRight[j] <= i) {
                    res += j - i;
                    flag = true;
                    maxRight[j] = prev;
                    break;
                }
                int tmp = maxRight[j];
                maxRight[j] = prev;
                prev = tmp;
            }
            if (!flag) {
                return -1;
            }
        }
        return res;
    }

}

性能

1689.十-二进制数的最少数目

目标

如果一个十进制数字不含任何前导零,且每一位上的数字不是 0 就是 1 ,那么该数字就是一个 十-二进制数 。例如,101 和 1100 都是 十-二进制数,而 112 和 3001 不是。

给你一个表示十进制整数的字符串 n ,返回和为 n 的 十-二进制数 的最少数目。

示例 1:

输入:n = "32"
输出:3
解释:10 + 11 + 11 = 32

示例 2:

输入:n = "82734"
输出:8

示例 3:

输入:n = "27346209830709182346"
输出:9

说明:

  • 1 <= n.length <= 10^5
  • n 仅由数字组成
  • n 不含任何前导零并总是表示正整数

思路

返回整数 n 中的最大数字即可。

代码


/**
 * @date 2026-04-17 16:06
 */
public class MinPartitions1689 {

    public int minPartitions(String n) {
        int res = 0;
        char[] chars = n.toCharArray();
        for (char c : chars) {
            res = Math.max(res, c - '0');
        }
        return res;
    }
}

性能

1680.连接连续二进制数字

目标

给你一个整数 n ,请你将 1 到 n 的二进制表示连接起来,并返回连接结果对应的 十进制 数字对 10^9 + 7 取余的结果。

示例 1:

输入:n = 1
输出:1
解释:二进制的 "1" 对应着十进制的 1 。

示例 2:

输入:n = 3
输出:27
解释:二进制下,1,2 和 3 分别对应 "1" ,"10" 和 "11" 。
将它们依次连接,我们得到 "11011" ,对应着十进制的 27 。

示例 3:

输入:n = 12
输出:505379714
解释:连接结果为 "1101110010111011110001001101010111100" 。
对应的十进制数字为 118505380540 。
对 10^9 + 7 取余后,结果为 505379714 。

说明:

  • 1 <= n <= 10^5

思路

拼接 1 ~ n 的二进制表示,将结果对应的十进制数对 10^9 + 7 取模。

遍历 1 ~ n 模拟拼接过程,将之前拼接的数字左移当前数字的 bitLength 并对 mod 取模。拼接 a b c 可以视为 (a * 2^bitLength(b) + b) * 2^bitLength(c) + c,可以先对括号内的运算取模,即 (a << bitLength(b)) + b ) % mod

代码


/**
 * @date 2026-02-28 9:18
 */
public class ConcatenatedBinary1680 {

    public int concatenatedBinary(int n) {
        int mod = 1000000007;
        long res = 0L;
        for (int i = 1; i <= n; i++) {
            int bl = 32 - Integer.numberOfLeadingZeros(i);
            res = ((res << bl) + i) % mod;
        }
        return (int) res;
    }

}

性能

1404.将二进制表示减到1的步骤数

目标

给你一个以二进制形式表示的数字 s 。请你返回按下述规则将其减少到 1 所需要的步骤数:

如果当前数字为偶数,则将其除以 2 。

如果当前数字为奇数,则将其加上 1 。

题目保证你总是可以按上述规则将测试用例变为 1 。

示例 1:

输入:s = "1101"
输出:6
解释:"1101" 表示十进制数 13 。
Step 1) 13 是奇数,加 1 得到 14 
Step 2) 14 是偶数,除 2 得到 7
Step 3) 7  是奇数,加 1 得到 8
Step 4) 8  是偶数,除 2 得到 4  
Step 5) 4  是偶数,除 2 得到 2 
Step 6) 2  是偶数,除 2 得到 1  

示例 2:

输入:s = "10"
输出:1
解释:"10" 表示十进制数 2 。
Step 1) 2 是偶数,除 2 得到 1 

示例 3:

输入:s = "1"
输出:0

说明:

  • 1 <= s.length <= 500
  • s 由字符 '0' 或 '1' 组成。
  • s[0] == '1'

思路

有一个二进制表示的数字,如果当前是偶数,可以将数字除以 2,即右移;如果当前是奇数,将数字加 1。求将数字变为 1 需要的操作数。

根据题意模拟,使用变量 carry 保存进位,从右向左遍历,判断最后一位数字 dcarry 相加后的奇偶性,即 sum = d + carry。如果 sum 是偶数(0 或者 2),右移后这一位消失,只需考虑 carry = sum / 2;如果 sum 是奇数,加一后进位 carry = 1,此时当前数字变为偶数,可以将这两个操作合并。

代码


/**
 * @date 2026-02-26 8:51
 */
public class NumSteps1404 {

    public int numSteps(String s) {
        int res = 0;
        int n = s.length();
        char[] chars = s.toCharArray();
        int carry = 0;
        for (int i = n - 1; i > 0; i--) {
            int d = chars[i] - '0';
            int sum = d + carry;
            if (sum % 2 == 0) {
                carry = sum / 2;
                res++;
            } else {
                carry = 1;
                res += 2;
            }
        }
        return res + carry;
    }

}

性能

1461.检查一个字符串是否包含所有长度为K的二进制子串

目标

给你一个二进制字符串 s 和一个整数 k 。如果所有长度为 k 的二进制字符串都是 s 的子串,请返回 true ,否则请返回 false 。

示例 1:

输入:s = "00110110", k = 2
输出:true
解释:长度为 2 的二进制串包括 "00","01","10" 和 "11"。它们分别是 s 中下标为 0,1,3,2 开始的长度为 2 的子串。

示例 2:

输入:s = "0110", k = 1
输出:true
解释:长度为 1 的二进制串包括 "0" 和 "1",显然它们都是 s 的子串。

示例 3:

输入:s = "0110", k = 2
输出:false
解释:长度为 2 的二进制串 "00" 没有出现在 s 中。

说明:

  • 1 <= s.length <= 5 * 10^5
  • s[i] 不是 '0' 就是 '1'
  • 1 <= k <= 20

思路

检查一个二进制字符串是否包含所有长度为 k 的二进制子串。

长度为 k 的二进制子串的个数有 2^k 个,将子串视为二进制数字,使用长度为 k 的滑动窗口,记录窗口内子串表示的数字,判断是否所有数字都出现过即可。将左边界移出窗口需要将左边第一位置零 num = (num & (1 << (k - 1))) ^ num。先提取左边第一位,其余位均为 0,然后与 num 异或即可。

代码


/**
 * @date 2026-02-24 11:31
 */
public class HasAllCodes1461 {

    public boolean hasAllCodes(String s, int k) {
        if (k > 18) {
            return false;
        }
        int n = 1 << k;
        boolean[] visited = new boolean[n];
        int num = 0;
        char[] chars = s.toCharArray();
        int l = 0;
        for (int r = 0; r < chars.length; r++) {
            num = (num << 1) | (chars[r] - '0');
            if (r - l + 1 == k) {
                visited[num] = true;
                num = (num & (1 << (k - 1))) ^ num;
                l++;
            }
        }
        for (boolean b : visited) {
            if (!b) {
                return false;
            }
        }
        return true;
    }

}

性能

799.香槟塔

目标

我们把玻璃杯摆成金字塔的形状,其中 第一层 有 1 个玻璃杯, 第二层 有 2 个,依次类推到第 100 层,每个玻璃杯将盛有香槟。

从顶层的第一个玻璃杯开始倾倒一些香槟,当顶层的杯子满了,任何溢出的香槟都会立刻等流量的流向左右两侧的玻璃杯。当左右两边的杯子也满了,就会等流量的流向它们左右两边的杯子,依次类推。(当最底层的玻璃杯满了,香槟会流到地板上)

例如,在倾倒一杯香槟后,最顶层的玻璃杯满了。倾倒了两杯香槟后,第二层的两个玻璃杯各自盛放一半的香槟。在倒三杯香槟后,第二层的香槟满了 - 此时总共有三个满的玻璃杯。在倒第四杯后,第三层中间的玻璃杯盛放了一半的香槟,他两边的玻璃杯各自盛放了四分之一的香槟,如下图所示。

现在当倾倒了非负整数杯香槟后,返回第 i 行 j 个玻璃杯所盛放的香槟占玻璃杯容积的比例( i 和 j 都从0开始)。

示例 1:

输入: poured(倾倒香槟总杯数) = 1, query_glass(杯子的位置数) = 1, query_row(行数) = 1
输出: 0.00000
解释: 我们在顶层(下标是(0,0))倒了一杯香槟后,没有溢出,因此所有在顶层以下的玻璃杯都是空的。

示例 2:

输入: poured(倾倒香槟总杯数) = 2, query_glass(杯子的位置数) = 1, query_row(行数) = 1
输出: 0.50000
解释: 我们在顶层(下标是(0,0)倒了两杯香槟后,有一杯量的香槟将从顶层溢出,位于(1,0)的玻璃杯和(1,1)的玻璃杯平分了这一杯香槟,所以每个玻璃杯有一半的香槟。

示例 3:

输入: poured = 100000009, query_row = 33, query_glass = 17
输出: 1.00000

说明:

  • 0 <= poured <= 10^9
  • 0 <= query_glass <= query_row < 100

思路

将玻璃杯摆成金字塔,第一层 1 个杯子,第二层 2 个杯子,依次类推。持续向第一层第一杯倒入 poured 杯香槟,当杯子满了之后,会等流量的流向左右两个玻璃杯,返回 query_row 行(从 0 开始计数),第 query_glass 杯(从 0 开始)中的香槟与容积的比例。

模拟香槟左右的流量即可。第 query_row 行有 query_row + 1 个杯子,初始化流量数组 vol,比如,总共倾倒 poured 杯,如果 vol[0] > 1,多余的流量会从左右两侧流下,vol[1] = (vol[0] - 1)/2vol[0] = (vol[0] - 1)/2,依次类推,直到查询的杯子。

注意由于没有对流量数组分层,需要从后向前遍历才能保证数据更新的正确性。

代码


/**
 * @date 2026-02-25 15:06
 */
public class ChampagneTower799 {

    public double champagneTower(int poured, int query_row, int query_glass) {
        double[] vol = new double[query_row + 1];
        vol[0] = poured;
        for (int i = 0; i < query_row; i++) {
            for (int j = i; j >= 0; j--) {
                if (vol[j] > 1) {
                    vol[j]--;
                    double half = vol[j] / 2.0;
                    vol[j + 1] += half;
                    vol[j] = half;
                } else {
                    vol[j] = 0.0;
                }
            }
        }
        return Math.min(vol[query_glass], 1.0);
    }

}

性能

3713.最长的平衡子串I

目标

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

如果一个 子串 中所有 不同 字符出现的次数都 相同 ,则称该子串为 平衡 子串。

请返回 s 的 最长平衡子串 的 长度 。

子串 是字符串中连续的、非空 的字符序列。

示例 1:

输入: s = "abbac"
输出: 4
解释:
最长的平衡子串是 "abba",因为不同字符 'a' 和 'b' 都恰好出现了 2 次。

示例 2:

输入: s = "zzabccy"
输出: 4
解释:
最长的平衡子串是 "zabc",因为不同字符 'z'、'a'、'b' 和 'c' 都恰好出现了 1 次。

示例 3:

输入: s = "aba"
输出: 2
解释:
最长的平衡子串之一是 "ab",因为不同字符 'a' 和 'b' 都恰好出现了 1 次。另一个最长的平衡子串是 "ba"。

说明:

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

思路

定义平衡子串是字符出现次数相同的字符串,给定一个由小写英文字母组成的字符串,返回最长的平衡子串长度。

3719.最长平衡子数组I 类似,本题需要保证子串中字母出现的次数都相等,可以统计子串内字母的出现次数并判断是否完全相同。

在遍历的过程中,如何判断出现的字符是否都达到 k 个?字母种类数 * k 应当等于子串长度。

代码


/**
 * @date 2026-02-12 9:19
 */
public class LongestBalanced3713 {

    public int longestBalanced(String s) {
        int n = s.length();
        int res = 0;
        for (int i = 0; i < n; i++) {
            Map<Character, Integer> cnt = new HashMap<>();
            here:
            for (int j = i; j < n; j++) {
                char c = s.charAt(j);
                cnt.merge(c, 1, Integer::sum);
                int t = cnt.get(c);
                for (Integer value : cnt.values()) {
                    if (value != t) {
                        continue here;
                    }
                }
                res = Math.max(res, j - i + 1);
            }
        }
        return res;
    }
}

性能

3719.最长平衡子数组I

目标

给你一个整数数组 nums。

如果子数组中 不同偶数 的数量等于 不同奇数 的数量,则称该 子数组 是 平衡的 。

返回 最长 平衡子数组的长度。

子数组 是数组中连续且 非空 的一段元素序列。

示例 1:

输入: nums = [2,5,4,3]
输出: 4
解释:
最长平衡子数组是 [2, 5, 4, 3]。
它有 2 个不同的偶数 [2, 4] 和 2 个不同的奇数 [5, 3]。因此,答案是 4 。

示例 2:

输入: nums = [3,2,2,5,4]
输出: 5
解释:
最长平衡子数组是 [3, 2, 2, 5, 4] 。
它有 2 个不同的偶数 [2, 4] 和 2 个不同的奇数 [3, 5]。因此,答案是 5。

示例 3:

输入: nums = [1,2,3,2]
输出: 3
解释:
最长平衡子数组是 [2, 3, 2]。
它有 1 个不同的偶数 [2] 和 1 个不同的奇数 [3]。因此,答案是 3。

说明:

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

思路

定义平衡子数组是 不同奇数元素个数 与 不同偶数元素个数 相等的子数组,求数组 nums 的平衡子数组的最大长度。

暴力解法是枚举所有可能的子数组,判断是否是平衡子数组,即子数组中不相同奇数个数与不相同偶数个数是否相等。

无需针对每一个子数组都重新计数,考虑使用两个哈希表分别记录奇数元素与偶数元素的出现次数,判断哈希表中的 key 的个数是否相等即可。注意针对每一个起点,需要重新初始化哈希表。

代码


/**
 * @date 2026-02-10 9:16
 */
public class LongestBalanced3719 {

    public int longestBalanced(int[] nums) {
        int n = nums.length;
        int res = 0;
        for (int i = 0; i < n; i++) {
            Map<Integer, Integer>[] map = new HashMap[2];
            Arrays.setAll(map, x -> new HashMap<>());
            int l = nums[i] % 2;
            map[l].merge(nums[i], 1, Integer::sum);
            for (int j = i + 1; j < n; j++) {
                int r = nums[j] % 2;
                map[r].merge(nums[j], 1, Integer::sum);
                if (map[l].size() == map[l ^ 1].size()) {
                    res = Math.max(res, j - i + 1);
                }
            }
        }
        return res;
    }

}

性能

1382.将二叉搜索树变平衡

目标

给你一棵二叉搜索树,请你返回一棵 平衡后 的二叉搜索树,新生成的树应该与原来的树有着相同的节点值。如果有多种构造方法,请你返回任意一种。

如果一棵二叉搜索树中,每个节点的两棵子树高度差不超过 1 ,我们就称这棵二叉搜索树是 平衡的 。

示例 1:

输入:root = [1,null,2,null,3,null,4,null,null]
输出:[2,1,3,null,null,null,4]
解释:这不是唯一的正确答案,[3,1,4,null,2,null,null] 也是一个可行的构造方案。

示例 2:

输入: root = [2,1,3]
输出: [2,1,3]

说明:

  • 树节点的数目在 [1, 10^4] 范围内。
  • 1 <= Node.val <= 10^5

思路

将给定的二叉搜索树变平衡。

中序遍历二叉搜索树,结果列表有序,从中间重新构建 BST。

代码


/**
 * @date 2026-02-09 8:56
 */
public class BalanceBST1382 {

    public TreeNode balanceBST(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        dfs(list, root);
        int l = 0, r = list.size() - 1;
        return buildTree(list, l, r);
    }

    public void dfs(List<Integer> nums, TreeNode node) {
        if (node == null) {
            return;
        }
        dfs(nums, node.left);
        nums.add(node.val);
        dfs(nums, node.right);
    }

    public TreeNode buildTree(List<Integer> nums, int l, int r) {
        if (l == r) {
            return new TreeNode(nums.get(l));
        } else if (l > r) {
            return null;
        }
        int m = l + (r - l) / 2;
        return new TreeNode(nums.get(m), buildTree(nums, l, m - 1), buildTree(nums, m + 1, r));
    }

}

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 * int val;
 * TreeNode left;
 * TreeNode right;
 * TreeNode() {}
 * TreeNode(int val) { this.val = val; }
 * TreeNode(int val, TreeNode left, TreeNode right) {
 * this.val = val;
 * this.left = left;
 * this.right = right;
 * }
 * }
 */

性能

1653.使字符串平衡的最少删除次数

目标

给你一个字符串 s ,它仅包含字符 'a' 和 'b' 。

你可以删除 s 中任意数目的字符,使得 s 平衡 。当不存在下标对 (i,j) 满足 i < j ,且 s[i] = 'b' 的同时 s[j]= 'a' ,此时认为 s 是 平衡 的。

请你返回使 s 平衡 的 最少 删除次数。

示例 1:

输入:s = "aababbab"
输出:2
解释:你可以选择以下任意一种方案:
下标从 0 开始,删除第 2 和第 6 个字符("aababbab" -> "aaabbb"),
下标从 0 开始,删除第 3 和第 6 个字符("aababbab" -> "aabbbb")。

示例 2:

输入:s = "bbaaaaabb"
输出:2
解释:唯一的最优解是删除最前面两个字符。

说明:

  • 1 <= s.length <= 10^5
  • s[i] 要么是 'a' 要么是 'b'​ 。​

思路

有一个仅包含 a, b 两个字符的字符串 s,如果 a 的左边没有 bb 的右边没有 a 则认为 s 是平衡的。求使得 s 平衡需要删除的最少次数。

可以枚举分割线,分割线左侧的 b 都要删除,右侧的 a 也要删除,取最小值即可。可以使用前后缀分解来解决。

也可也使用动态规划,定义 dp[i] 表示前 i 个字符平衡需要删除的最小次数。

  • ib 时,dp[i] = dp[i - 1]
  • ia 时,dp[i] = min(dp[i - 1] + 1, cntB),其中 cntB 表示 i 前面的 b 的个数

代码


/**
 * @date 2026-02-09 11:31
 */
public class MinimumDeletions1653 {

    public int minimumDeletions(String s) {
        int n = s.length();
        int[] prefixB = new int[n + 1];
        char[] chars = s.toCharArray();
        for (int i = 0; i < n; i++) {
            int c = chars[i] - 'a';
            prefixB[i + 1] = prefixB[i] + c;
        }
        int deletedA = 0;
        int res = Integer.MAX_VALUE;
        for (int i = n - 1; i > 0; i--) {
            int c = chars[i] - 'a';
            if (c == 0) {
                res = Math.min(res, prefixB[i] + deletedA);
                deletedA++;
            }
        }
        res = Math.min(res, deletedA);
        return res;
    }

}

性能