51.N皇后

目标

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

示例 1:

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入:n = 1
输出:[["Q"]]

说明:

  • 1 <= n <= 9

思路

将 n 个皇后放在 n x n 的棋盘上,使它们不在同一行、不在同一列并且不在同一斜线上。返回所有不同的解决方案,使用 Q 表示皇后,. 表示空位。

枚举棋盘的每个格子,当该格子放置皇后时,其所在行、列、斜线的格子都不能有皇后。使用回溯算法,如果格子遍历完皇后数量刚好为 n 个则计数。

假设 (i, j) 位置上放置了皇后,那么所有 (i, *) (*, j) 以及所有满足 a - b == i - j || i + j == a + b(a, b) 都不能再放置皇后。

使用整数的二进制位来表示列是否允许放置皇后,从左到右对应二进制的低位到高位:

  • 由于逻辑与、或没有逆运算,所以必须以参数的方式传递,否则无法恢复现场
  • 如果我们在第 0 行的第 i 列放置了皇后,那么第 1 行的第 i - 1, i, i + 1 列无法放置皇后,使用位运算的角度来看就是对 1 << i, 1 << (i + 1), 1 << (i - 1) 相与,即 1 << i, (1 << i) << 1, (1 << i) >> 1。令 c = 1 << i,再考虑当前行与前面行的限制,下一行 不能放置皇后的列为:colIndexSet | c,(leftDiagonalSet | c) << 1,(rightDiagonalSet | c) >> 1),其中 colIndexSet 的 bit 1 表示无法放置皇后的列;leftDiagonalSet 的 bit 1 表示受 斜线限制,无法放置皇后的列;rightDiagonalSet 的 bit 1 表示受 斜线限制,无法放置皇后的列;c 表示将当前行放置皇后的列对应的 bit 位 置 1 所代表的数字
  • valid = ((1 << n) - 1) & ~(colIndexSet | leftDiagonalSet | rightDiagonalSet) 表示合法的位置
  • c = valid & -valid 得到 bit 1 的最低位表示的数字
  • Integer.bitCount(c - 1) 表示列下标
  • valid ^= c 将当前处理过的最低位 bit 1 置 0

当然也可以使用数组来记录不可放置皇后的列信息,差别不大,见 52_N皇后II

代码


/**
 * @date 2024-12-01 16:45
 */
public class SolveNQueens51 {

    public static class SolveNQueens {
        public List<List<String>> res;
        public int n;
        public int[] queens;

        public List<List<String>> solveNQueens(int n) {
            this.n = n;
            queens = new int[n];
            res = new ArrayList<>();
            backTracing(0, 0, 0, 0);
            return res;
        }

        public void backTracing(int row, int colIndexSet, int leftDiagonalSet, int rightDiagonalSet) {
            if (row == n) {
                List<String> solution = new ArrayList<>(n);
                char[] chars = new char[n];
                Arrays.fill(chars, '.');
                for (int i = 0; i < n; i++) {
                    chars[queens[i]] = 'Q';
                    solution.add(new String(chars));
                    chars[queens[i]] = '.';
                }
                res.add(solution);
                return;
            }
            int valid = ((1 << n) - 1) & ~(colIndexSet | leftDiagonalSet | rightDiagonalSet);
            while (valid > 0){
                int c = valid & -valid;
                queens[row] = Integer.bitCount(c - 1);
                backTracing(row + 1, colIndexSet | c,
                        (leftDiagonalSet | c) << 1,
                        (rightDiagonalSet | c) >> 1);
                valid ^= c;
            }
        }

    }

    public List<List<String>> res;
    public Set<Integer> rowIndexSet;
    public Set<Integer> colIndexSet;
    public Set<Integer> leftDiagonalSet;
    public Set<Integer> rightDiagonalSet;
    public ArrayList<int[]> indexList;
    public int end;
    public int n;

    public List<List<String>> solveNQueens(int n) {
        res = new ArrayList<>(n);
        rowIndexSet = new HashSet<>(n);
        colIndexSet = new HashSet<>(n);
        leftDiagonalSet = new HashSet<>(n);
        rightDiagonalSet = new HashSet<>(n);
        indexList = new ArrayList<>(n);
        end = n * n;
        this.n = n;
        for (int i = 0; i < n; i++) {
            backTracing(i);
        }
        return res;
    }

    public void backTracing(int index) {
        int row = index / n;
        int col = index % n;
        int leftDiagonal = row - col;
        int rightDiagonal = row + col;
        rowIndexSet.add(row);
        colIndexSet.add(col);
        leftDiagonalSet.add(leftDiagonal);
        rightDiagonalSet.add(rightDiagonal);
        indexList.add(new int[]{row, col});
        if (indexList.size() == n) {
            StringBuilder sb = new StringBuilder();
            List<String> solution = new ArrayList<>(n);
            for (int[] cor : indexList) {
                for (int i = 0; i < n; i++) {
                    if (i == cor[1]) {
                        sb.append('Q');
                    } else {
                        sb.append('.');
                    }
                }
                solution.add(sb.toString());
                sb.setLength(0);
            }
            res.add(solution);
        }
        for (int i = index + 1; i < end; i++) {
            int r = i / n;
            int c = i % n;
            int ld = r - c;
            int rd = r + c;
            if (rowIndexSet.contains(r) || colIndexSet.contains(c)
                    || leftDiagonalSet.contains(ld)
                    || rightDiagonalSet.contains(rd)) {
                continue;
            }
            backTracing(i);
        }
        rowIndexSet.remove(row);
        colIndexSet.remove(col);
        leftDiagonalSet.remove(leftDiagonal);
        rightDiagonalSet.remove(rightDiagonal);
        indexList.remove(indexList.size() - 1);
    }

}

性能

3251.单调数组对的数目II

目标

给你一个长度为 n 的 正 整数数组 nums 。

如果两个 非负 整数数组 (arr1, arr2) 满足以下条件,我们称它们是 单调 数组对:

  • 两个数组的长度都是 n 。
  • arr1 是单调 非递减 的,换句话说 arr1[0] <= arr1[1] <= ... <= arr1[n - 1] 。
  • arr2 是单调 非递增 的,换句话说 arr2[0] >= arr2[1] >= ... >= arr2[n - 1] 。
  • 对于所有的 0 <= i <= n - 1 都有 arr1[i] + arr2[i] == nums[i] 。

请你返回所有 单调 数组对的数目。

由于答案可能很大,请你将它对 109 + 7 取余 后返回。

示例 1:

输入:nums = [2,3,2]
输出:4
解释:
单调数组对包括:
([0, 1, 1], [2, 2, 1])
([0, 1, 2], [2, 2, 0])
([0, 2, 2], [2, 1, 0])
([1, 2, 2], [1, 1, 0])

示例 2:

输入:nums = [5,5,5,5]
输出:126

说明:

  • 1 <= n == nums.length <= 2000
  • 1 <= nums[i] <= 1000

思路

有一个长度为 n 正整数数组 nums,可以将其拆成两个数组 arr1 arr2,使之满足 arr1[i] + arr2[i] == nums[i]。问 有多少种拆分方法使得 arr1 非递减 且 arr2 非递增。

与昨天的 3250.单调数组对的数目I 相比,nums[i] 的最大值从 50 变成了 1000。时间复杂度大概为 O(n*m^2),mnums[i] 的最大值,如果还沿用昨天的解法就会超时。

先将昨天的题目改写为动态规划,定义 dp[i][j] 表示最后一个元素为 j,长度为 i + 1 的满足条件的 arr1 个数。由于 arr1 是非递减的,如果最后一个元素为 arr1[i] = j 那么倒数第二个元素arr1[i - 1] <= j。同时我们还要考虑到 arr2 非递增,即 arr2[i - 1] >= arr2[i]nums[i - 1] - arr1[i - 1] >= nums[i] - arr1[i]arr1[i - 1] <= nums[i - 1] - nums[i] + arr1[i]。综上,arr1[i - 1] <= Math.min(j, nums[i - 1] - nums[i] + j)

经过上面的分析,dp[i][j] = Σdp[i - 1][k],其中 k ∈ [0, Math.min(j, nums[i - 1] - nums[i] + j)]。这样写会超时,针对每个 j,我们会进行多次重复的计算。

d = nums[i - 1] - nums[i],当 d >= 0 时,上界为 j,否则上界为 j + d

考虑 nums[i - 1] < nums[i],即 d < 0

  • arr1[i - 1] = j 时,令arr2[i - 1] = nums[i - 1] - j = a
  • arr1[i] = j 时,arr2[i] = nums[i] - arr1[i] = nums[i - 1] - d - j = a - dd < 0

也就是说,当 arr1[i] 的取值与上一层一样时,arr2[i] 比上一层的值大了 |d|。为了使第 iarr2 非递增,那么 arr1 的取值只能从 |d| 开始。

它们之间的约束关系是这样的,当 nums[i] 变大,arr1i 层取 j 时,arr2 的第 i 层比上一层增大了 |d|,这时我们必须舍弃 [0, |d|) 的取值,因为它必定大于上一层 arr2 的最大值。然后考虑第 i 层的 arr1[|d|, nums[i]] 的情况,由于第 i 层的 arr2 相比第 i - 1 层增大了 |d|,因此需要减小第 i - 1 层的 arr1,使第 i - 1 层的 arr2 增大。所以第 i 层的 j 对应第 i - 1 层的 j - |d|

dp[i][j] 的取值类似前缀和,只不过有约束条件,并不是所有值都合法。考虑简单的情况 nums[0] == nums[1] && i == 1,

  • 当 j == 0 时,dp[1][0] = dp[0][0] = 1
  • 当 j == 1 时,上一层(i == 0) arr1 可以取 0、1,dp[1][1] = dp[1][0] + dp[0][1] = 2
  • 当 j == 2 时,上一层(i == 0) arr1 可以取 0、1、2,dp[1][2] = dp[1][1] + dp[0][2] = 3

因此我们有 dp[i][j] = (dp[i][j - 1] + dp[i - 1][j - d])

代码


/**
 * @date 2024-11-29 9:39
 */
public class CountOfPairs3251 {

    public static int MOD = 1000000007;

    public int countOfPairs(int[] nums) {
        int res = 0;
        int n = nums.length;
        int[][] dp = new int[n][1001];
        for (int i = 0; i <= nums[0]; i++) {
            dp[0][i] = 1;
        }
        for (int i = 1; i < n; i++) {
            int d = Math.max(nums[i] - nums[i - 1], 0);
            for (int j = d; j <= nums[i]; j++) {
                if (j == 0) {
                    dp[i][j] = dp[i - 1][0] % MOD;
                } else {
                    dp[i][j] = (dp[i][j - 1] + dp[i - 1][j - d]) % MOD;
                }
            }
        }
        for (int i : dp[n - 1]) {
            res = (res + i) % MOD;
        }
        return res;
    }

}

性能

3250.单调数组对的数目I

目标

给你一个长度为 n 的 正 整数数组 nums 。

如果两个 非负 整数数组 (arr1, arr2) 满足以下条件,我们称它们是 单调 数组对:

  • 两个数组的长度都是 n 。
  • arr1 是单调 非递减 的,换句话说 arr1[0] <= arr1[1] <= ... <= arr1[n - 1] 。
  • arr2 是单调 非递增 的,换句话说 arr2[0] >= arr2[1] >= ... >= arr2[n - 1] 。
  • 对于所有的 0 <= i <= n - 1 都有 arr1[i] + arr2[i] == nums[i] 。

请你返回所有 单调 数组对的数目。

由于答案可能很大,请你将它对 10^9 + 7 取余 后返回。

示例 1:

输入:nums = [2,3,2]
输出:4
解释:
单调数组对包括:
([0, 1, 1], [2, 2, 1])
([0, 1, 2], [2, 2, 0])
([0, 2, 2], [2, 1, 0])
([1, 2, 2], [1, 1, 0])

示例 2:

输入:nums = [5,5,5,5]
输出:126

说明:

  • 1 <= n == nums.length <= 2000
  • 1 <= nums[i] <= 50

提示:

  • Let dp[i][s] is the number of monotonic pairs of length i with the arr1[i - 1] = s.
  • If arr1[i - 1] = s, arr2[i - 1] = nums[i - 1] - s.
  • Check if the state in recurrence is valid.

思路

有一个长度为 n 的正整数数组 nums,可以将其拆成两个数组 arr1 arr2,使之满足 arr1[i] + arr2[i] == nums[i]。问 有多少种拆分方法使得 arr1 非递减 且 arr2 非递增。

显然 arr1 确定之后,arr2 也就确定了。考虑枚举 arr1,判断 arr1 是否非递减, 以及arr2 是否非递增。可以使用记忆化搜索,对于位置 iarr1arr2 需要满足下面的条件:

  • arr1[i] >= arr1[i - 1]
  • arr2[i] = nums[i] - arr1[i] <= arr2[i - 1] = nums[i - 1] - arr1[i - 1],即 arr1[i] >= nums[i] - nums[i - 1] + arr1[i - 1]

也就是 nums[i] >= arr1[i] >= Math.max(nums[i] - nums[i - 1] + arr1[i - 1], arr1[i - 1])

代码


/**
 * @date 2024-11-28 10:36
 */
public class CountOfPairs3250 {

    public static int MOD = 1000000007;

    public int countOfPairs(int[] nums) {
        int res = 0;
        int n = nums.length;
        int[][] mem = new int[n + 1][51];
        for (int[] arr : mem) {
            Arrays.fill(arr, -1);
        }
        for (int i = 0; i <= nums[0]; i++) {
            res = (res + dfs(nums, 1, i, mem)) % MOD;
        }
        return res;
    }

    public int dfs(int[] nums, int i, int prev, int[][] mem) {
        int n = nums.length;
        if (i == n) {
            return 1;
        }
        int lowerBound = Math.max(prev, nums[i] - nums[i - 1] + prev);
        int next = i + 1;
        int res = 0;
        for (int j = lowerBound; j <= nums[i]; j++) {
            if (mem[next][j] == -1) {
                mem[next][j] = dfs(nums, next, j, mem) % MOD;
            }
            res = (res + mem[next][j]) % MOD;
        }
        return res;
    }

}

性能

632.最小区间

目标

你有 k 个 非递减排列 的整数列表。找到一个 最小 区间,使得 k 个列表中的每个列表至少有一个数包含在其中。

我们定义如果 b-a < d-c 或者在 b-a == d-c 时 a < c,则区间 [a,b] 比 [c,d] 小。

示例 1:

输入:nums = [[4,10,15,24,26], [0,9,12,20], [5,18,22,30]]
输出:[20,24]
解释: 
列表 1:[4, 10, 15, 24, 26],24 在区间 [20,24] 中。
列表 2:[0, 9, 12, 20],20 在区间 [20,24] 中。
列表 3:[5, 18, 22, 30],22 在区间 [20,24] 中。

示例 2:

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

说明:

  • nums.length == k
  • 1 <= k <= 3500
  • 1 <= nums[i].length <= 50
  • -10^5 <= nums[i][j] <= 10^5
  • nums[i] 按非递减顺序排列

思路

k 个非递减排列的整数列表,找一个最小区间,使得 k 个列表中每个列表至少有一个元素包含其中。所谓最小区间指区间长度最小,如果长度相同,则起点小的区间更小。

可以将元素标记属于哪个列表,然后从小到大排序,使用滑动窗口,找到最小的窗口。

当元素移入/移出窗口,如何判断是否应该从集合中增加/删除列表种类?

  • 可以记录每个列表元素的最大下标,如果移出的元素等于该下标则说明窗口内不包含该列表中的元素。元素移入窗口时可以根据最大下标是否小于左边界来判断是否应该增加列表种类,考虑到开始时左边界为 0,应该将数组初始化为 -1
  • 也可以记录每个列表在窗口元素的个数,如果从 0 变为 1 则增加种类,如果从 1 变为 0 则减少。

代码


/**
 * @date 2024-11-24 15:14
 */
public class SmallestRange632 {

    /**
     * 优化代码逻辑
     */
    public int[] smallestRange_v1(List<List<Integer>> nums) {
        List<int[]> list = new ArrayList<>();
        for (int i = 0; i < nums.size(); i++) {
            for (Integer num : nums.get(i)) {
                list.add(new int[]{num, i});
            }
        }
        int[] res = new int[]{0, 1000000};
        list.sort((a, b) -> a[0] - b[0]);
        int n = list.size(), k = nums.size();
        int l = 0;
        int[] typeMaxIndex = new int[k];
        Arrays.fill(typeMaxIndex, -1);
        int types = 0;
        for (int r = 0; r < n; r++) {
            int[] e = list.get(r);
            int right = e[0];
            int type = e[1];
            int lastTypeMaxIndex = typeMaxIndex[type];
            typeMaxIndex[type] = r;
            if (lastTypeMaxIndex < l){
                types++;
            }
            while (types == k) {
                int[] left = list.get(l);
                int lType = left[1];
                if (typeMaxIndex[lType] == l++) {
                    types--;
                    if (right - left[0] < res[1] - res[0]) {
                        res[0] = left[0];
                        res[1] = right;
                    }
                }
            }
        }
        return res;
    }

    public int[] smallestRange(List<List<Integer>> nums) {
        List<int[]> list = new ArrayList<>();
        for (int i = 0; i < nums.size(); i++) {
            for (Integer num : nums.get(i)) {
                list.add(new int[]{num, i});
            }
        }
        int[] res = new int[]{0, 1000000};
        list.sort((a, b) -> a[0] - b[0]);
        int n = list.size(), k = nums.size();
        int l = 0;
        int[] typeMaxIndex = new int[k];
        Set<Integer> set = new HashSet<>();
        for (int r = 0; r < n; r++) {
            int[] e = list.get(r);
            int right = e[0];
            int type = e[1];
            typeMaxIndex[type] = r;
            set.add(type);
            while (set.size() == k) {
                int[] left = list.get(l);
                int lType = left[1];
                if (typeMaxIndex[lType] == l++) {
                    set.remove(lType);
                }
            }
            if (l >= 1 && !set.contains(list.get(l - 1)[1]) && set.size() == k - 1) {
                int left = list.get(l - 1)[0];
                if (right - left < res[1] - res[0]) {
                    res[0] = left;
                    res[1] = right;
                }
            }
        }
        return res;
    }
}

性能

3244.新增道路查询后的最短距离II

目标

给你一个整数 n 和一个二维整数数组 queries。

有 n 个城市,编号从 0 到 n - 1。初始时,每个城市 i 都有一条单向道路通往城市 i + 1( 0 <= i < n - 1)。

queries[i] = [ui, vi] 表示新建一条从城市 ui 到城市 vi 的单向道路。每次查询后,你需要找到从城市 0 到城市 n - 1 的最短路径的长度。

所有查询中不会存在两个查询都满足 queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]

返回一个数组 answer,对于范围 [0, queries.length - 1] 中的每个 i,answer[i] 是处理完前 i + 1 个查询后,从城市 0 到城市 n - 1 的最短路径的长度。

示例 1:

输入: n = 5, queries = [[2, 4], [0, 2], [0, 4]]
输出: [3, 2, 1]
解释:
新增一条从 2 到 4 的道路后,从 0 到 4 的最短路径长度为 3。
新增一条从 0 到 2 的道路后,从 0 到 4 的最短路径长度为 2。
新增一条从 0 到 4 的道路后,从 0 到 4 的最短路径长度为 1。

示例 2:

输入: n = 4, queries = [[0, 3], [0, 2]]
输出: [1, 1]
解释:
新增一条从 0 到 3 的道路后,从 0 到 3 的最短路径长度为 1。
新增一条从 0 到 2 的道路后,从 0 到 3 的最短路径长度仍为 1。

说明:

  • 3 <= n <= 10^5
  • 1 <= queries.length <= 10^5
  • queries[i].length == 2
  • 0 <= queries[i][0] < queries[i][1] < n
  • 1 < queries[i][1] - queries[i][0]
  • 查询中不存在重复的道路。
  • 不存在两个查询都满足 i != j 且 queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]

思路

n 个城市,刚开始每一个城市 i 有一条单项道路通向城市 i + 1,有一个二维数组 queriesqueries[i] 表示增加一条从 queries[i][0]queries[i][1] 的单项道路,返回 answer 数组,answer[i] 表示增加了 queries[i] 之后从城市 0 到城市 n - 1 的最短路径。增加的路径保证不会出现相交的情况。

比昨天的题 3243.新增道路查询后的最短距离I 多了一个条件,新增的路径不会交叉。昨天首先考虑的就是今天这个思路,因为起点与终点是固定的,可以通过查询区间的关系来减小最短路径。当时考虑的差分数组解决不了区间包含的关系。

定义区间数组 intervalinterval[i] = j, 表示 i -> j 有直达道路。如果发现查询的路径 [l, r] 已经被包含,直接略过。否则,循环记录区间 (l, r) 内的路径数,保存下一跳的城市 next = interval[l],将 interval[l] 置为 r,l = next 直到 l >= r。注意最后一跳到 r 是没有计数的,相当于减去了将前面多余的步数,与直达的效果一样。 interval[l] = r 是第一次进入循环必须进行的操作,幸运的是它并不影响我们标记其它区间内的元素,当有更大的查询路径时,直接在 l 处就跳过了。

核心点是如何表示区间,如何判断区间是否重合,如何针对大区间减少的路径计数。

代码


/**
 * @date 2024-11-20 10:10
 */
public class ShortestDistanceAfterQueries3244 {

    public int[] shortestDistanceAfterQueries(int n, int[][] queries) {
        int ql = queries.length;
        int[] res = new int[ql];
        int[] interval = new int[n - 1];
        Arrays.setAll(interval, i -> i + 1);
        int shortestPath = n - 1;
        for (int i = 0; i < ql; i++) {
            int l = queries[i][0];
            int r = queries[i][1];
            while (interval[l] < r) {
                int next = interval[l];
                interval[l] = r;
                l = next;
                shortestPath--;
            }
            res[i] = shortestPath;
        }
        return res;
    }

}

性能

3261.统计满足K约束的子字符串数量II

目标

给你一个 二进制 字符串 s 和一个整数 k。

另给你一个二维整数数组 queries ,其中 queries[i] = [li, ri] 。

如果一个 二进制字符串 满足以下任一条件,则认为该字符串满足 k 约束:

  • 字符串中 0 的数量最多为 k。
  • 字符串中 1 的数量最多为 k。

返回一个整数数组 answer ,其中 answer[i] 表示 s[li..ri] 中满足 k 约束 的 子字符串 的数量。

示例 1:

输入:s = "0001111", k = 2, queries = [[0,6]]
输出:[26]
解释:
对于查询 [0, 6], s[0..6] = "0001111" 的所有子字符串中,除 s[0..5] = "000111" 和 s[0..6] = "0001111" 外,其余子字符串都满足 k 约束。

示例 2:

输入:s = "010101", k = 1, queries = [[0,5],[1,4],[2,3]]
输出:[15,9,3]
解释:
s 的所有子字符串中,长度大于 3 的子字符串都不满足 k 约束。

说明:

  • 1 <= s.length <= 10^5
  • s[i] 是 '0' 或 '1'
  • 1 <= k <= s.length
  • 1 <= queries.length <= 10^5
  • queries[i] == [li, ri]
  • 0 <= li <= ri < s.length
  • 所有查询互不相同

思路

定义二进制字符串满足 k 约束的条件是字符串中 0 的个数 或者 1 的个数都不超过 k。求给定字符串满足 k 约束的子字符串数量。子字符串 是字符串中 连续非空 字符序列。

这道题与昨天的 3258.统计满足K约束的子字符串数量I 相比多了一个查询数组,并且字符串的长度也来到了 10^5,返回结果是 long[],暴力枚举会超时。

滑动窗口的时间复杂度为 O(n),不可能对每一次查询都重新滑动一遍。显然需要在滑动的过程中记录下查询的结果。每次滑动我们可以得到一个区间 [l, r],这个区间的所有子数组是合法的。

使用一维数组记录这个区间,使用下标与值的映射,我们有两种选择:

  • 记录的左端点的最大右端点,即 left[l] = r
  • 记录的右端点的最小左端点,即 right[r] = l

对于查询区间 [ql, qr],它与我们已知的合法区间存在两种关系,被包含或者部分相交。

  • 如果是被包含的关系,那么查询区间的所有子数组均合法,子数组个数为 (qr - ql + 1) * (qr - ql + 2) / 2
  • 如果是相交的关系,说明 ql < r[ql, r] 的所有子数组是合法的,剩下的 [r + 1, qr] 的合法子数组如何求?可以在滑动过程中记录以每个元素为终点的合法子数组个数,以前缀和的形式保存。

这里区间的划分与结果集的构成是非常有讲究的。前缀和记录的是以元素为 终点 的合法子数组,如果我们在滑动的过程中根据查询区间终点匹配当前元素,即 qr == r,那么可能的情况为 ql >= l 查询区间被完全包含。如果 ql < l 则查询区间与合法区间相交。如果这时使用前缀和计算 [ql, l],使用公式计算 [l, r] 就错了。因为后面区间使用公式计算的子数组并不包括以前面区间内的元素为起点的子数组,并且前缀和记录的子数组的起点可能在查询的左边界之外。而反过来前面使用公式计算,后面使用前缀和计算,被减去的那部分子数组个数中就包含了以 前面区间所有元素 为终点的子数组,也就是前面使用公式计算的子数组个数。我们不用担心后面通过前缀和计算的子数组的起点超出左边界,如果超出的话,一定是被包含的关系。

核心点在于想清楚这两部分集合的关系, [i, j] 的所有子数组包括了以 b ∈ [i, j] 为终点,a ∈ [i, b] 为起点的子数组。而使用前缀和相减计算的是所有以 c ∈ [m, n] 为终点的合法子数组,起点可以不在该区间,但是不会超出 ql

代码


/**
 * @date 2024-11-13 0:25
 */
public class CountKConstraintSubstrings3261 {

    public long[] countKConstraintSubstrings_v1(String s, int k, int[][] queries) {
        int n = s.length();
        char[] chars = s.toCharArray();
        int[] cnt = new int[2];
        long[] res = new long[queries.length];
        long[] prefix = new long[n + 1];
        int[] right = new int[n];
        Arrays.fill(right, n);
        int l = 0;
        for (int i = 0; i < n; i++) {
            cnt[chars[i] - '0']++;
            while (l <= i && cnt[0] > k && cnt[1] > k) {
                right[l] = i;
                cnt[chars[l++] - '0']--;
            }
            prefix[i + 1] += prefix[i] + i - l + 1;
        }
        for (int i = 0; i < queries.length; i++) {
            int ql = queries[i][0];
            int qr = queries[i][1];
            int r = Math.min(right[ql], qr + 1);
            res[i] = (r - ql + 1L) * (r - ql) / 2 + prefix[qr + 1] - prefix[r];

        }
        return res;
    }

}

性能

1547.切棍子的最小成本

目标

有一根长度为 n 个单位的木棍,棍上从 0 到 n 标记了若干位置。例如,长度为 6 的棍子可以标记如下:

给你一个整数数组 cuts ,其中 cuts[i] 表示你需要将棍子切开的位置。

你可以按顺序完成切割,也可以根据需要更改切割的顺序。

每次切割的成本都是当前要切割的棍子的长度,切棍子的总成本是历次切割成本的总和。对棍子进行切割将会把一根木棍分成两根较小的木棍(这两根木棍的长度和就是切割前木棍的长度)。请参阅第一个示例以获得更直观的解释。

返回切棍子的 最小总成本 。

示例 1:

输入:n = 7, cuts = [1,3,4,5]
输出:16
解释:按 [1, 3, 4, 5] 的顺序切割的情况如下所示:
第一次切割长度为 7 的棍子,成本为 7 。第二次切割长度为 6 的棍子(即第一次切割得到的第二根棍子),第三次切割为长度 4 的棍子,最后切割长度为 3 的棍子。总成本为 7 + 6 + 4 + 3 = 20 。
而将切割顺序重新排列为 [3, 5, 1, 4] 后,总成本 = 16(如示例图中 7 + 4 + 3 + 2 = 16)。

示例 2:

输入:n = 9, cuts = [5,6,1,4,2]
输出:22
解释:如果按给定的顺序切割,则总成本为 25 。总成本 <= 25 的切割顺序很多,例如,[4, 6, 5, 2, 1] 的总成本 = 22,是所有可能方案中成本最小的。

说明:

  • 2 <= n <= 10^6
  • 1 <= cuts.length <= min(n - 1, 100)
  • 1 <= cuts[i] <= n - 1
  • cuts 数组中的所有整数都 互不相同

提示:

  • Build a dp array where dp[i][j] is the minimum cost to achieve all the cuts between i and j.
  • When you try to get the minimum cost between i and j, try all possible cuts k between them, dp[i][j] = min(dp[i][k] + dp[k][j]) + (j - i) for all possible cuts k between them.

思路

有一个长度为 n 的木棍,刻度从 0 ~ n,有一个整数数组 cutscuts[i] 表示需要在刻度 i 处进行切割,切割的成本为该刻度所在棍子的长度,求切割棍子的最小成本。

许多算法书上引入动态规划经常举的一个例子是钢条切割问题。已知特定长度钢条的价值,问怎样切可以使价值最大。而本题是给出必须要切的点,问按照什么顺序切成本最小。

定义 dp[i][j] 表示完成 (i, j) 之间所有切割点所需要的最小成本,dp[0][n] 就是答案。状态转移方程为 dp[i][j] = min(dp[i][k] + dp[k][j]) + j - i)。根据定义 dp 数组应该初始化为 0,因为无法切割时成本为 0。

但是由于切割点的范围太大 2 ~ 10^6,如果直接定义的话会超出内存限制。可以先将切割点排序,定义 ij 为切点的下标,切点个数 m 最大为 100,时间复杂度为 O(m^3)

考虑到切点本身不包含木棍的两个端点 0n,我们定义端点 endpoint 数组,将这两个端点加进来,dp[0][m - 1] 即为所求。状态转移方程为 dp[i][j] = min(dp[i][k] + dp[k][j]) + endpoint[j] - endpoint[i]

特别需要注意的是 dp 数组的遍历的顺序。当我们计算 dp[i][j] 时需要已经计算出 dp[k][j],枚举起点 i 应该倒序,因为 k > i,同理还需要计算出 dp[i][k],枚举终点 j 应该正序,因为 k > y。枚举 k 正序倒序都可以。

枚举 i,j 的先后顺序也是可以交换的。

代码


/**
 * @date 2024-11-11 10:07
 */
public class MinCost1547 {

    public int minCost(int n, int[] cuts) {
        Arrays.sort(cuts);
        int m = cuts.length;
        int[] endpoint = new int[m + 2];
        System.arraycopy(cuts, 0, endpoint, 1, m);
        endpoint[m + 1] = n;

        m = endpoint.length;
        int[][] dp = new int[m][m];

        for (int i = m - 3; i >= 0; i--) {
            for (int j = i + 2; j < m; j++) {
                int min = Integer.MAX_VALUE;
                for (int k = i + 1; k < j; k++) {
                    min = Math.min(dp[i][k] + dp[k][j], min);
                }
                dp[i][j] = min + endpoint[j] - endpoint[i];
            }
        }
        return dp[0][m - 1];
    }

}

性能

3235.判断矩形的两个角落是否可达

目标

给你两个正整数 xCorner 和 yCorner 和一个二维整数数组 circles ,其中 circles[i] = [xi, yi, ri] 表示一个圆心在 (xi, yi) 半径为 ri 的圆。

坐标平面内有一个左下角在原点,右上角在 (xCorner, yCorner) 的矩形。你需要判断是否存在一条从左下角到右上角的路径满足:路径 完全 在矩形内部,不会 触碰或者经过 任何 圆的内部和边界,同时 只 在起点和终点接触到矩形。

如果存在这样的路径,请你返回 true ,否则返回 false 。

示例 1:

输入:X = 3, Y = 4, circles = [[2,1,1]]
输出:true
解释:
黑色曲线表示一条从 (0, 0) 到 (3, 4) 的路径。

示例 2:

输入:X = 3, Y = 3, circles = [[1,1,2]]
输出:false
解释:
不存在从 (0, 0) 到 (3, 3) 的路径。

示例 3:

输入:X = 3, Y = 3, circles = [[2,1,1],[1,2,1]]
输出:false
解释:
不存在从 (0, 0) 到 (3, 3) 的路径。

示例 4:

输入:X = 4, Y = 4, circles = [[5,5,1]]
输出:true
解释:

说明:

  • 3 <= xCorner, yCorner <= 10^9
  • 1 <= circles.length <= 1000
  • circles[i].length == 3
  • 1 <= xi, yi, ri <= 10^9

思路

有一个以原点为左下顶点, [xCorner, yCorner] 为右上顶点的矩形,还有一些圆 circlescircles[i, j, r] 表示圆的圆心在 (i, j) 半径为 r。问是否存在一条从原点到 [xCorner, yCorner] 的路径,满足路径在矩形内部(不与矩形边界重合),且不触碰或经过任何园的内部与边界。

评论说这是史上分数最高的题目,周赛全球也没几个人做出来,直接放弃了。

代码

性能

3165.不包含相邻元素的子序列的最大和

目标

给你一个整数数组 nums 和一个二维数组 queries,其中 queries[i] = [posi, xi]。

对于每个查询 i,首先将 nums[posi] 设置为 xi,然后计算查询 i 的答案,该答案为 nums 中 不包含相邻元素 的 子序列 的 最大 和。

返回所有查询的答案之和。

由于最终答案可能非常大,返回其对 10^9 + 7 取余 的结果。

子序列 是指从另一个数组中删除一些或不删除元素而不改变剩余元素顺序得到的数组。

示例 1:

输入:nums = [3,5,9], queries = [[1,-2],[0,-3]]
输出:21
解释:
执行第 1 个查询后,nums = [3,-2,9],不包含相邻元素的子序列的最大和为 3 + 9 = 12。
执行第 2 个查询后,nums = [-3,-2,9],不包含相邻元素的子序列的最大和为 9 。

示例 2:

输入:nums = [0,-1], queries = [[0,-5]]
输出:0
解释:
执行第 1 个查询后,nums = [-5,-1],不包含相邻元素的子序列的最大和为 0(选择空子序列)。

说明:

  • 1 <= nums.length <= 5 * 10^4
  • -10^5 <= nums[i] <= 10^5
  • 1 <= queries.length <= 5 * 10^4
  • queries[i] == [posi, xi]
  • 0 <= posi <= nums.length - 1
  • -10^5 <= xi <= 10^5

思路

// todo

代码

性能

685.冗余连接II

目标

在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。

返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

示例 1:

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

示例 2:

输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]
输出:[4,1]

说明:

  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ui, vi <= n

思路

有一颗 n 个节点的树,节点编号 1 ~ n。使用 edges 表示向树中两个没有直接连接的节点之间加一条边之后的边的集合,找出一条可以删除的边使得 edges 变为一颗有 n 个节点的树。如果有多种选择,返回 edges 中最后出现的那个,即下标最大的边。与 冗余连接 不同的是 edges有向边 的集合。

如果直接使用昨天无向图寻找环的做法会有两个问题:

  • 无法处理 a -> b, b -> a 的情况,因为在无向图中为了防止环,直接回避了这种情况
  • 并不是删去环上任意一条边都可以的,因为边是有向的,如果某个节点出现两个父节点,那么一定要删去以该节点为终点的边

官网题解使用的还是并查集。// todo

代码


/**
 * @date 2024-10-28 8:51
 */
public class FindRedundantDirectedConnection685 {

    List<Integer>[] g;
    Set<Integer> loop;
    List<Integer> path;
    int start;
    int end;

    public int[] findRedundantDirectedConnection(int[][] edges) {
        int n = edges.length;
        g = new List[n + 1];
        for (int i = 0; i <= n; i++) {
            g[i] = new ArrayList<>();
        }
        int[] degree = new int[n + 1];
        Set<Integer> e = new HashSet<>(n);
        int end = -1;
        int[] self = null;
        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            int fromto = from << 10 | to;
            int tofrom = to << 10 | from;
            if (e.contains(fromto)) {
                self = new int[]{from, to};
            }
            e.add(fromto);
            e.add(tofrom);
            g[from].add(to);
            g[to].add(from);
            if (degree[to] == 1) {
                end = to;
            } else {
                degree[to]++;
            }
        }

        if (self != null) {
            if (end == -1) {
                for (int i = n - 1; i >= 0; i--) {
                    if ((self[0] == edges[i][0] && edges[i][1] == self[1])
                            || (self[0] == edges[i][1] && edges[i][0] == self[1])) {
                        return edges[i];
                    }
                }
            } else {
                return new int[]{self[0] == end ? self[1] : self[0], end};
            }

        }

        loop = new HashSet<>(n);
        path = new ArrayList<>();
        loop.add(1);
        path.add(1);
        dfs(0, 1);
        loop = new HashSet<>();
        for (int i = path.size() - 1; i >= 0; i--) {
            loop.add(path.get(i));
            if (start == path.get(i)) {
                break;
            }
        }
        if (end == -1) {
            for (int i = n - 1; i >= 0; i--) {
                if (loop.contains(edges[i][0]) && loop.contains(edges[i][1])) {
                    return edges[i];
                }
            }
        } else {
            for (int i = n - 1; i >= 0; i--) {
                if (edges[i][1] == end && loop.contains(edges[i][0])) {
                    return edges[i];
                }
            }
        }

        return null;
    }

    private boolean dfs(int parent, int current) {
        for (Integer next : g[current]) {
            if (next == parent) {
                continue;
            }
            if (loop.contains(next)) {
                start = next;
                return true;
            } else {
                loop.add(next);
                path.add(next);
                if (dfs(current, next)) {
                    return true;
                }
                path.remove(path.size() - 1);
                loop.remove(next);
            }
        }
        return false;
    }

}

性能