2786.访问数组中的位置使分数最大

目标

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

你 一开始 在数组的位置 0 处,你可以按照下述规则访问数组中的其他位置:

  • 如果你当前在位置 i ,那么你可以移动到满足 i < j 的 任意 位置 j 。
  • 对于你访问的位置 i ,你可以获得分数 nums[i] 。
  • 如果你从位置 i 移动到位置 j 且 nums[i] 和 nums[j] 的 奇偶性 不同,那么你将失去分数 x 。

请你返回你能得到的 最大 得分之和。

注意 ,你一开始的分数为 nums[0] 。

示例 1:

输入:nums = [2,3,6,1,9,2], x = 5
输出:13
解释:我们可以按顺序访问数组中的位置:0 -> 2 -> 3 -> 4 。
对应位置的值为 2 ,6 ,1 和 9 。因为 6 和 1 的奇偶性不同,所以下标从 2 -> 3 让你失去 x = 5 分。
总得分为:2 + 6 + 1 + 9 - 5 = 13 。

示例 2:

输入:nums = [2,4,6,8], x = 3
输出:20
解释:数组中的所有元素奇偶性都一样,所以我们可以将每个元素都访问一次,而且不会失去任何分数。
总得分为:2 + 4 + 6 + 8 = 20 。

说明:

  • 2 <= nums.length <= 10^5
  • 1 <= nums[i], x <= 10^6

思路

给定一个数组 nums 与 正整数 x,从下标 0 开始,允许从任意位置 i 开始向后访问位置 j,如果nums[i]nums[j] 的奇偶性相同,则可以获得 nums[j] 分,否则获得 nums[j] - x 分。求能够获得的分数总和的最大值。

刚开始就想到要从后向前,自底向上动态规划,如果当前的奇偶性与与后面的奇偶性相同就累加,否则就将后面的值减去x。接着又想到并不是要每一个节点都要访问,如果节点没有访问奇偶性和谁比较呢?并且后面的得分取决于前一个元素的奇偶性,考虑到昨天的题 子序列最大优雅度,觉得可能方向又错了。

于是就尝试贪心算法,从下标0开始,执行while循环,如果后面的元素奇偶性与之相同,直接累加。对于奇偶性不同的,我们可以考虑累加或者跳过。这样问题就变成了从这个新位置开始向后能获取的最大分数。注意新的位置奇偶性发生了变化。

这么一想问题又变成记忆化搜索了,于是就可以转换为递推/动态规划问题。

// todo 转换为动态规划的写法

代码

/**
 * @date 2024-06-14 8:43
 */
public class MaxScore2786 {

    public long maxScore(int[] nums, int x) {
        int n = nums.length;
        long[][] mem = new long[n + 1][2];
        for (int i = 0; i < mem.length; i++) {
            mem[i] = new long[]{Integer.MIN_VALUE, Integer.MIN_VALUE};
        }
        long res = nums[0];
        int flag = nums[0] % 2;
        int i = 1;
        while (i < n && nums[i] % 2 == flag) {
            res += nums[i];
            i++;
        }
        res += Math.max(0, maxScore(nums, x, i, flag, mem));
        return res;
    }

    public long maxScore(int[] nums, int x, int start, int preFlag, long[][] mem) {
        int n = nums.length;
        if (start >= n) {
            return 0;
        }
        // 如果选择该节点
        int flag = nums[start] % 2;
        long select = nums[start];
        if (preFlag != flag) {
            select -= x;
        }
        int i = start + 1;
        while (i < n && nums[i] % 2 == flag) {
            select += nums[i];
            i++;
        }
        if (mem[i][flag] == Integer.MIN_VALUE) {
            mem[i][flag] = maxScore(nums, x, i, flag, mem);
        }
        select += Math.max(0, mem[i][flag]);
        // 如果跳过该节点
        if (mem[start + 1][preFlag] == Integer.MIN_VALUE) {
            mem[start + 1][preFlag] = maxScore(nums, x, start + 1, preFlag, mem);
        }
        return Math.max(select, mem[start + 1][preFlag]);
    }

}

性能

419.甲板上的战舰

目标

给你一个大小为 m x n 的矩阵 board 表示甲板,其中,每个单元格可以是一艘战舰 'X' 或者是一个空位 '.' ,返回在甲板 board 上放置的 战舰 的数量。

战舰 只能水平或者垂直放置在 board 上。换句话说,战舰只能按 1 x k(1 行,k 列)或 k x 1(k 行,1 列)的形状建造,其中 k 可以是任意大小。两艘战舰之间至少有一个水平或垂直的空位分隔 (即没有相邻的战舰)。

示例 1:

输入:board = [["X",".",".","X"],[".",".",".","X"],[".",".",".","X"]]
输出:2

示例 2:

输入:board = [["."]]
输出:0

说明:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 200
  • board[i][j] 是 '.' 或 'X'

进阶:你可以实现一次扫描算法,并只使用 O(1) 额外空间,并且不修改 board 的值来解决这个问题吗?

思路

这道题的描述很容易让人误解成只允许在'X'的位置上建造战舰,并且满足规则战舰不能相邻,问最多能造多少战舰。

但实际上题目的含义是,有一个棋盘,棋盘上放着战舰,每个战舰由k个'X'表示,战舰的形状是 1*k 或者 k*1,战舰的排放规则是不能相邻,让我们统计战舰数量。

刚开始想到的是使用一个二维数组保存是否访问过,从第一行开始从左到右访问,如果为'X'则向下延伸并将visited置为true。

后来看了题解,可以将原数组的'X'置为其它标记,这样就不用额外的空间了。

进阶的问题是不修改board,根据排列规则,仅统计战舰的起点,即上面与左边不能为'X'。

也有网友使用dfs来处理,子节点是下方与右侧的元素,如果是'X'则改为其它标记,否则返回。其实直接循环遍历更方便一些。

还有网友使用并查集计算连通分量,其实也没有必要。并查集主要用于快速判断元素是否属于同一个集合,统计集合中元素个数,集合元素的合并。这里仅需要连通分量个数,并且连通的关系很简单,纵向或横向,并不像图遍历那样复杂,直接计数即可。

代码

/**
 * @date 2024-06-11 8:40
 */
public class CountBattleships419 {

    /**
     * 网友题解
     * 一次扫描,不使用额外空间,不修改board
     * 统计左上顶点
     * 需满足条件:顶点左侧与上侧不能为'X'
     */
    public int countBattleships_v2(char[][] board) {
        int ans = 0;
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[i].length; j++) {
                if (board[i][j] == 'X' &&
                        (j == 0 || board[i][j - 1] != 'X') &&
                        (i == 0 || board[i - 1][j] != 'X')) {
                    ans++;
                }
            }
        }
        return ans;
    }

    /**
     * 看了题解,可以直接将board中的'X'置为'.',这样就不需要额外的visited数组了
     * 一次扫描,但是修改了board
     */
    public int countBattleships_v1(char[][] board) {
        int res = 0;
        int m = board.length;
        int n = board[0].length;
        for (int i = 0; i < m; i++) {
            int j = 0;
            while (j < n) {
                if (board[i][j] == 'X') {
                    res++;
                    int k = i + 1;
                    while (k < m && board[k][j] == 'X') {
                        board[k][j] = '.';
                        k++;
                    }
                    while (j < n && board[i][j] == 'X') {
                        board[i][j] = '.';
                        j++;
                    }
                } else {
                    j++;
                }
            }
        }

        return res;
    }

    /**
     * 一次扫描,但是使用了额外的空间
     */
    public int countBattleships(char[][] board) {
        int res = 0;
        int m = board.length;
        int n = board[0].length;
        boolean[][] visited = new boolean[m][n];
        for (int i = 0; i < m; i++) {
            int j = 0;
            while (j < n) {
                if (visited[i][j]) {
                    j++;
                    continue;
                }
                if (board[i][j] == 'X') {
                    res++;
                    int k = i;
                    while (k < m && board[k][j] == 'X') {
                        visited[k][j] = true;
                        k++;
                    }
                    while (j < n && board[i][j] == 'X') {
                        visited[i][j] = true;
                        j++;
                    }
                } else {
                    j++;
                }
            }
        }

        return res;
    }
}

性能

使用额外空间

不使用额外空间

881.救生艇

目标

给定数组 people 。people[i]表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit。

每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。

返回 承载所有人所需的最小船数 。

示例 1:

输入:people = [1,2], limit = 3
输出:1
解释:1 艘船载 (1, 2)

示例 2:

输入:people = [3,2,2,1], limit = 3
输出:3
解释:3 艘船分别载 (1, 2), (2) 和 (3)

示例 3:

输入:people = [3,5,3,4], limit = 5
输出:4
解释:4 艘船分别载 (3), (3), (4), (5)

说明:

  • 1 <= people.length <= 5 * 10^4
  • 1 <= people[i] <= limit <= 3 * 10^4

思路

给定一个数组,数组元素是待救援人员的体重,每条救生艇最多载两人且体重不超过限制,问最少需要多少救生艇。

尽可能多地让两人乘一艇,将体重轻的与重的搭配,如果都是轻的则造成运力浪费。

因此先按体重从小到大排序,然后使用双指针将重的与轻的搭配即可。

代码

/**
 * @date 2024-06-10 21:18
 */
public class NumRescueBoats881 {

    public int numRescueBoats(int[] people, int limit) {
        int n = people.length;
        Arrays.sort(people);
        int end = Arrays.binarySearch(people, limit);
        if (end < 0) {
            end = -end - 1;
        }
        // [end,n) 下标小于end的元素均比limit小,end 为0则可直接返回n
        int res = n - end;
        int start = 0;
        while (start < end) {
            // 如果start == end -1 即仅剩start一个元素 不再重复累加
            // start < end 防止end - 1为负即end==0
            // 由于取开区间,不包括end,因此从end-1开始
            while (start < end - 1 && people[start] + people[end - 1] > limit) {
                end--;
                res++;
            }
            start++;
            end--;
            res++;
        }
        return res;
    }
}

性能

3040.相同分数的最大操作数目II

目标

给你一个整数数组 nums ,如果 nums 至少 包含 2 个元素,你可以执行以下操作中的 任意 一个:

  • 选择 nums 中最前面两个元素并且删除它们。
  • 选择 nums 中最后两个元素并且删除它们。
  • 选择 nums 中第一个和最后一个元素并且删除它们。

一次操作的 分数 是被删除元素的和。

在确保 所有操作分数相同 的前提下,请你求出 最多 能进行多少次操作。

请你返回按照上述要求 最多 可以进行的操作次数。

示例 1:

输入:nums = [3,2,1,2,3,4]
输出:3
解释:我们执行以下操作:
- 删除前两个元素,分数为 3 + 2 = 5 ,nums = [1,2,3,4] 。
- 删除第一个元素和最后一个元素,分数为 1 + 4 = 5 ,nums = [2,3] 。
- 删除第一个元素和最后一个元素,分数为 2 + 3 = 5 ,nums = [] 。
由于 nums 为空,我们无法继续进行任何操作。

示例 2:

输入:nums = [3,2,6,1,4]
输出:2
解释:我们执行以下操作:
- 删除前两个元素,分数为 3 + 2 = 5 ,nums = [6,1,4] 。
- 删除最后两个元素,分数为 1 + 4 = 5 ,nums = [6] 。
至多进行 2 次操作。

说明:

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

思路

相同分数的最大操作数目I 增加了两种操作,可以删除最后两个元素或者一前一后两个元素。

我的思路是使用回溯算法,为了防止环的形成,使用自定义hash函数 (long) start << 16 | end; 记录已经搜索过的区间,并存入哈希表。

勉强通过了,看了官网题解,说是要用记忆搜索。网友还给出了递推的解法。//todo

代码

/**
 * @date 2024-06-08 20:03
 */
public class MaxOperations3040 {
    private Set<Long> set;

    public int maxOperations(int[] nums) {
        int res = 0;
        int n = nums.length;
        set = new HashSet<>();
        set.add(n - 1L);
        res = dfs(nums, 2, n - 1, nums[0] + nums[1], 1);
        res = Math.max(res, dfs(nums, 0, n - 3, nums[n - 2] + nums[n - 1], 1));
        res = Math.max(res, dfs(nums, 1, n - 2, nums[0] + nums[n - 1], 1));
        return res;
    }

    public int dfs(int[] nums, int start, int end, int target, int ops) {
        int res = ops;
        long key = (long) start << 16 | end;
        if (set.contains(key) || start >= end || res == nums.length / 2) {
            return res;
        }
        set.add(key);
        if (start < nums.length - 1 && nums[start] + nums[start + 1] == target) {
            res = dfs(nums, start + 2, end, target, ops + 1);
        }
        if (end >= 1 && nums[end] + nums[end - 1] == target) {
            res = Math.max(res, dfs(nums, start, end - 2, target, ops + 1));
        }
        if (end >= 0 && start < nums.length && nums[start] + nums[end] == target) {
            res = Math.max(res, dfs(nums, start + 1, end - 1, target, ops + 1));
        }
        return res;
    }

}

性能

2938.区分黑球与白球

目标

桌子上有 n 个球,每个球的颜色不是黑色,就是白色。

给你一个长度为 n 、下标从 0 开始的二进制字符串 s,其中 1 和 0 分别代表黑色和白色的球。

在每一步中,你可以选择两个相邻的球并交换它们。

返回「将所有黑色球都移到右侧,所有白色球都移到左侧所需的 最小步数」。

示例 1:

输入:s = "101"
输出:1
解释:我们可以按以下方式将所有黑色球移到右侧:
- 交换 s[0] 和 s[1],s = "011"。
最开始,1 没有都在右侧,需要至少 1 步将其移到右侧。

示例 2:

输入:s = "100"
输出:2
解释:我们可以按以下方式将所有黑色球移到右侧:
- 交换 s[0] 和 s[1],s = "010"。
- 交换 s[1] 和 s[2],s = "001"。
可以证明所需的最小步数为 2 。

示例 3:

输入:s = "0111"
输出:0
解释:所有黑色球都已经在右侧。

说明:

  • 1 <= n == s.length <= 10^5
  • s[i] 不是 '0',就是 '1'

思路

有一个数组,其元素值不是0就是1,现在需要将所有的1都移到右边,每一步可以选择相邻的两个元素交换其位置,问移动的最小步数。

从左向右遍历数组元素,如果值为1就累加cnt,如果值为0就将移动步数加上 cnt。简单来说就是遇到1就合并,记录其个数,遇到0就整体移动 res += cnt。每次移动都贪心地将0移至其最终位置上。

有网友提到可以使用归并排序记录逆序对。

还有网友是基于下标和计算的。因为最终0都在右边,其下标和可以通过等差数列求和得到。我们只需在遍历过程中记录0的个数,并累加0的下标,然后与最终状态的下标和相减即可。

代码

package medium;

/**
 * @date 2024-06-06 0:03
 */
public class MinimumSteps2938 {

    /**
     * 将黑球视为一个整体,遇到黑球则合并到一起增加其权重,这样就可以视为将一个带权黑球从左移到右,每一步都是必要的。
     * 这其实也算是在移动的过程中统计逆序对的个数
     */
    public long minimumSteps(String s) {
        long res = 0;
        int n = s.length();
        int i = 0;
        long cnt = 0;
        while (i < n) {
            if (s.charAt(i) == '0') {
                // 遇到0就移动  累加移动步数,可以使用双指针优化
                res += cnt;
            } else {
                // 遇到1则合并
                cnt++;
            }
            i++;
        }
        return res;
    }

    /**
     * 优化
     * 使用双指针可以减少累加次数
     */
    public long minimumSteps_v1(String s) {
        long res = 0;
        int n = s.length();
        int i = 0;
        // left指向1的位置,如果第一值是0,那么left与i一起右移
        // 如果第一个值是1,仅移动i,当遇到0时,左侧1的个数就是i-left
        // 本来从下标left到i元素个数为 i - left + 1,由于i指向的不是1,所以不用加1
        int left = 0;
        while (i < n) {
            if (s.charAt(i) == '0') {
                res += i - left;
                left++;
            }
            i++;
        }
        return res;
    }
}

性能

3067.在带权树网络中统计可连接服务器对数目

目标

给你一棵无根带权树,树中总共有 n 个节点,分别表示 n 个服务器,服务器从 0 到 n - 1 编号。同时给你一个数组 edges ,其中 edges[i] = [ai, bi, weighti] 表示节点 ai 和 bi 之间有一条双向边,边的权值为 weighti 。再给你一个整数 signalSpeed 。

如果两个服务器 a ,b 和 c 满足以下条件,那么我们称服务器 a 和 b 是通过服务器 c 可连接的 :

  • a < b ,a != c 且 b != c 。
  • 从 c 到 a 的距离是可以被 signalSpeed 整除的。
  • 从 c 到 b 的距离是可以被 signalSpeed 整除的。
  • 从 c 到 b 的路径与从 c 到 a 的路径没有任何公共边。

请你返回一个长度为 n 的整数数组 count ,其中 count[i] 表示通过服务器 i 可连接 的服务器对的 数目 。

说明:

  • 2 <= n <= 1000
  • edges.length == n - 1
  • edges[i].length == 3
  • 0 <= ai, bi < n
  • edges[i] = [ai, bi, weighti]
  • 1 <= weighti <= 10^6
  • 1 <= signalSpeed <= 10^6
  • 输入保证 edges 构成一棵合法的树。

思路

有一颗无根带权树,所有到服务器 c 的路径,如果路径长度能够被 signalSpeed 整除,并且路径没有重合,则这些服务器可以通过 c 连接。即 c 的每个分支上满足条件的节点可以与其它分支满足条件的节点连接。

遍历每一个节点,以其为根,使用dfs分别计算各分支满足条件的节点,然后计算服务器对。

假设根节点R有4个分支,每个分支上满足条件的节点个数为 a、b、c、d,我们可以使用下面两个方法计算服务器对:

for:
    a:ab + ac + ad
    b:bc + bd
    c:cd

或者

for
    a:0 * a
    b:a * b
    c:(a+b) * c
    d:(a+b+c) * d

第二种方法计算的其实是第一种方法斜线上的和
    a:ab + ac + ad
          /    /
    b:bc + bd
          /
    c:cd

最快的解法应该是换根dp,但是换根后节点数如何变化,处理起来比较复杂,考虑的情况也更多,容易出错。

代码

/**
 * @date 2024-06-04 8:41
 */
public class CountPairsOfConnectableServers3067 {

    private List<int[]>[] g;
    private int speed;

    public int[] countPairsOfConnectableServers_v1(int[][] edges, int signalSpeed) {
        int n = edges.length;
        g = new List[n + 1];
        speed = signalSpeed;
        int[] res = new int[n + 1];
        for (int i = 0; i <= n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int i = 0; i < n; i++) {
            g[edges[i][0]].add(new int[]{edges[i][1], edges[i][2]});
            g[edges[i][1]].add(new int[]{edges[i][0], edges[i][2]});
        }
        for (int i = 0; i <= n; i++) {
            int pre = 0;
            if (g[i].size() == 1) {
                continue;
            }
            for (int j = 0; j < g[i].size(); j++) {
                int cnt = dfs(g[i].get(j)[0], i, g[i].get(j)[1]);
                res[i] += pre * cnt;
                pre += cnt;
            }
        }
        return res;
    }

    public int dfs(int root, int parent, int path) {
        int cnt = path % speed == 0 ? 1 : 0;
        if (g[root].size() == 1 && parent != -1) {
            return cnt;
        }
        for (int[] child : g[root]) {
            if (child[0] == parent) {
                continue;
            }
            cnt += dfs(child[0], root, path + child[1]);
        }
        return cnt;
    }

}

性能

2981.找出出现至少三次的最长特殊子字符串I

目标

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

如果一个字符串仅由单一字符组成,那么它被称为 特殊 字符串。例如,字符串 "abc" 不是特殊字符串,而字符串 "ddd"、"zz" 和 "f" 是特殊字符串。

返回在 s 中出现 至少三次 的 最长特殊子字符串 的长度,如果不存在出现至少三次的特殊子字符串,则返回 -1 。

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

示例 1:

输入:s = "aaaa"
输出:2
解释:出现三次的最长特殊子字符串是 "aa" :子字符串 "aaaa"、"aaaa" 和 "aaaa"。
可以证明最大长度是 2 。

示例 2:

输入:s = "abcdef"
输出:-1
解释:不存在出现至少三次的特殊子字符串。因此返回 -1 。

示例 3:

输入:s = "abcaba"
输出:1
解释:出现三次的最长特殊子字符串是 "a" :子字符串 "abcaba"、"abcaba" 和 "abcaba"。
可以证明最大长度是 1 。

说明:

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

思路

这道题要我们求给定字符串中至少出现三次的由相同字符组成的子串的最大长度。下面分情况讨论:

  • 如果特殊子串是连续的,那么取最大子串长度-2。例如:aaaa 有以下符合条件的特殊子串 (aa)aaa(aa)aaa(aa),至少出现三次的最长特殊子字符串长度为2。
  • 如果特殊子串个数为2:
    • 如果这两个子串长度相同,取长度-1。例如:aaa aaa 有以下符合条件的特殊子串 (aa)a a(aa) (aa)a a(aa),出现了4次,最长特殊子串长度为2。
    • 如果这两个子串长度不同,取 max(last -2 , secondTolast)。例如:aa aaa aaaa 有以下符合条件的特殊子串 (aaa) (aaa)a a(aaa),出现了3次,最长特殊子串长度为3。
  • 如果特殊子串个数大于2,取max(last -2 , thirdTolast)。例如:aa aaa aaa 有以下符合条件的特殊子串 (aa) (aa)a a(aa) (aa)a a(aa),出现了5次,最长特殊子串长度为3。

代码

/**
 * @date 2024-05-29 8:42
 */
public class MaximumLength2981 {
    public int maximumLength(String s) {
        int res = -1;
        Map<Character, List<Integer>> map = new HashMap<>(26);
        for (int i = 'a'; i <= 'z'; i++) {
            map.put((char) i, new ArrayList<>());
        }
        int n = s.length();
        char last = s.charAt(0);
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (c == last) {
                cnt++;
            } else {
                map.get(last).add(cnt);
                cnt = 1;
                last = c;
            }
            if (i == n - 1) {
                map.get(c).add(cnt);
            }
        }
        for (Map.Entry<Character, List<Integer>> entry : map.entrySet()) {
            List<Integer> occurrence = entry.getValue();
            int size = occurrence.size();
            Collections.sort(occurrence);
            if (size >= 2) {
                Integer secondToLastOccurrence = occurrence.get(size - 2);
                Integer lastOccurrence = occurrence.get(size - 1);
                if (lastOccurrence - secondToLastOccurrence >= 1) {
                    res = Math.max(res, Math.max(secondToLastOccurrence, lastOccurrence - 2));
                } else {
                    res = Math.max(res, lastOccurrence - 1);
                }
                if (size >= 3) {
                    res = Math.max(res, Math.max(occurrence.get(size - 3), occurrence.get(size - 1) - 2));
                }
            } else if (size == 1) {
                res = Math.max(res, occurrence.get(0) - 2);
            }
        }
        return res == 0 ? -1 : res;
    }
}

性能

// todo 性能优化

2028.找出缺失的观测数据

目标

现有一份 n + m 次投掷单个 六面 骰子的观测数据,骰子的每个面从 1 到 6 编号。观测数据中缺失了 n 份,你手上只拿到剩余 m 次投掷的数据。幸好你有之前计算过的这 n + m 次投掷数据的 平均值 。

给你一个长度为 m 的整数数组 rolls ,其中 rolls[i] 是第 i 次观测的值。同时给你两个整数 mean 和 n 。

返回一个长度为 n 的数组,包含所有缺失的观测数据,且满足这 n + m 次投掷的 平均值 是 mean 。如果存在多组符合要求的答案,只需要返回其中任意一组即可。如果不存在答案,返回一个空数组。

k 个数字的 平均值 为这些数字求和后再除以 k 。

注意 mean 是一个整数,所以 n + m 次投掷的总和需要被 n + m 整除。

示例 1:

输入:rolls = [3,2,4,3], mean = 4, n = 2
输出:[6,6]
解释:所有 n + m 次投掷的平均值是 (3 + 2 + 4 + 3 + 6 + 6) / 6 = 4 。

示例 2:

输入:rolls = [1,5,6], mean = 3, n = 4
输出:[2,3,2,2]
解释:所有 n + m 次投掷的平均值是 (1 + 5 + 6 + 2 + 3 + 2 + 2) / 7 = 3 。

示例 3:

输入:rolls = [1,2,3,4], mean = 6, n = 4
输出:[]
解释:无论丢失的 4 次数据是什么,平均值都不可能是 6 。

示例 4:

输入:rolls = [1], mean = 3, n = 1
输出:[5]
解释:所有 n + m 次投掷的平均值是 (1 + 5) / 2 = 3 。

说明:

  • m == rolls.length
  • 1 <= n, m <= 10^5
  • 1 <= rolls[i], mean <= 6

思路

已知 m+n 个投骰子的观测数据的均值 mean,以及其中 m 个观察数据 rolls,返回缺失的观测数据,如果存在多个只返回其中一组,如果不存在答案返回空数组。

我们可以很容易计算出观测数据的总和 mean * (m + n),用它减去已知的观测数据和 sum,得到 diff

  • 如果 diff > n * 6 说明 剩余的 n 次都得到 6 点也不够,返回空数组。
  • 如果 diff < n 说明 剩余的 n 次都得到 1 点也多余,返回空数组。
  • 否则,问题变成选 n 个数字使其和等于 diff,每个数的取值范围是 1 ~ 6

这让我想起了背包问题还有之前做过的硬币找零,组合总和等问题。这里只需要返回一种可能就行了,不需要动态规划。

可以先计算 val = diff / n,如果有剩余 r,就为 r 个值加1。

代码

/**
 * @date 2024-05-27 9:20
 */
public class MissingRolls2028 {

    public int[] missingRolls(int[] rolls, int mean, int n) {
        int m = rolls.length;
        int sum = 0;
        for (int roll : rolls) {
            sum += roll;
        }
        int diff = mean * (m + n) - sum;
        if (diff > n * 6 || diff < n) {
            return new int[0];
        }
        int[] res = new int[n];
        int val = diff / n;
        diff = diff - val * n;
        Arrays.fill(res, val);
        for (int i = 0; i < diff; i++) {
            res[i]++;
        }
        return res;
    }
}

性能

1738.找出第K大的异或坐标值

目标

给你一个二维矩阵 matrix 和一个整数 k ,矩阵大小为 m x n 由非负整数组成。

矩阵中坐标 (a, b) 的 值 可由对所有满足 0 <= i <= a < m 且 0 <= j <= b < n 的元素 matrix[i][j](下标从 0 开始计数)执行异或运算得到。

请你找出 matrix 的所有坐标中第 k 大的值(k 的值从 1 开始计数)。

示例 1:

输入:matrix = [[5,2],[1,6]], k = 1
输出:7
解释:坐标 (0,1) 的值是 5 XOR 2 = 7 ,为最大的值。

示例 2:

输入:matrix = [[5,2],[1,6]], k = 2
输出:5
解释:坐标 (0,0) 的值是 5 = 5 ,为第 2 大的值。

示例 3:

输入:matrix = [[5,2],[1,6]], k = 3
输出:4
解释:坐标 (1,0) 的值是 5 XOR 1 = 4 ,为第 3 大的值。

示例 4:

输入:matrix = [[5,2],[1,6]], k = 4
输出:0
解释:坐标 (1,1) 的值是 5 XOR 2 XOR 1 XOR 6 = 0 ,为第 4 大的值。

说明:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 1000
  • 0 <= matrix[i][j] <= 10^6
  • 1 <= k <= m * n

思路

求二维矩阵 matrix 的第k大的异或坐标值,元素 matrix[i][j] 的异或坐标值等于对matrix[0][0] ~ matrix[i][j]矩阵中的所有值进行异或运算。

我们可以先分别对每一行按列进行异或运算,然后再针对每一列按行进行异或运算。然后将它们放入优先队列,再取第k大的值即可。

官网题解用到了二维前缀和 + 排序/快速选择。// todo

代码

/**
 * @date 2024-05-26 19:07
 */
public class KthLargestValue11738 {
    public int kthLargestValue(int[][] matrix, int k) {
        int m = matrix.length;
        int n = matrix[0].length;
        PriorityQueue<Integer> q = new PriorityQueue<>((a, b) -> b - a);
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; i++) {
            dp[i][0] = matrix[i][0];
        }
        for (int i = 0; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i][j - 1] ^ matrix[i][j];
            }
        }
        for (int i = 1; i < m; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][j] ^= dp[i - 1][j];
                q.offer(dp[i][j]);
            }
        }
        for (int i = 0; i < n; i++) {
            q.offer(dp[0][i]);
        }
        int res = 0;
        while (k > 0) {
            res = q.poll();
            k--;
        }

        return res;
    }
}

性能