1742.盒子中小球的最大数量

目标

你在一家生产小球的玩具厂工作,有 n 个小球,编号从 lowLimit 开始,到 highLimit 结束(包括 lowLimit 和 highLimit ,即 n == highLimit - lowLimit + 1)。另有无限数量的盒子,编号从 1 到 infinity 。

你的工作是将每个小球放入盒子中,其中盒子的编号应当等于小球编号上每位数字的和。例如,编号 321 的小球应当放入编号 3 + 2 + 1 = 6 的盒子,而编号 10 的小球应当放入编号 1 + 0 = 1 的盒子。

给你两个整数 lowLimit 和 highLimit ,返回放有最多小球的盒子中的小球数量。如果有多个盒子都满足放有最多小球,只需返回其中任一盒子的小球数量。

示例 1:

输入:lowLimit = 1, highLimit = 10
输出:2
解释:
盒子编号:1 2 3 4 5 6 7 8 9 10 11 ...
小球数量:2 1 1 1 1 1 1 1 1 0  0  ...
编号 1 的盒子放有最多小球,小球数量为 2 。

示例 2:

输入:lowLimit = 5, highLimit = 15
输出:2
解释:
盒子编号:1 2 3 4 5 6 7 8 9 10 11 ...
小球数量:1 1 1 1 2 2 1 1 1 0  0  ...
编号 5 和 6 的盒子放有最多小球,每个盒子中的小球数量都是 2 。

示例 3:

输入:lowLimit = 19, highLimit = 28
输出:2
解释:
盒子编号:1 2 3 4 5 6 7 8 9 10 11 12 ...
小球数量:0 1 1 1 1 1 1 1 1 2  0  0  ...
编号 10 的盒子放有最多小球,小球数量为 2 。

说明:

  • 1 <= lowLimit <= highLimit <= 10^5

思路

n 个编号从 lowLimithighLimit 的小球,同时有编号 [1, +∞] 的盒子。将小球放入它编号数位之和对应的盒子中,求放有最多小球的盒子中的小球数量。

暴力做法是针对每一个数字计算其数位之和。时间复杂度为 O(l * n)l 表示数字的长度,最大 6 位数字,总规模为 6 * 10^5 可行。

注意到编号是连续的,如果编号没有进位,那么盒号加 1;如果编号进位,盒号进位加 1,还要减去原来编号末尾的 9,有几个 9 就减几个。省去了每一个数的数位求和。

代码


/**
 * @date 2025-02-13 0:57
 */
public class CountBalls1742 {

    public int countBalls_v2(int lowLimit, int highLimit) {
        int boxNo = 0;
        int num = lowLimit;
        while (num > 0) {
            boxNo += num % 10;
            num /= 10;
        }
        int[] cnt = new int[46];
        for (int i = lowLimit; i <= highLimit; i++) {
            cnt[boxNo++]++;
            int tmp = i;
            while (tmp % 10 == 9) {
                boxNo -= 9;
                tmp /= 10;
            }
        }
        return Arrays.stream(cnt).max().getAsInt();
    }

    public int countBalls(int lowLimit, int highLimit) {
        Map<Integer,Integer> map = new HashMap<>();
        for(int i = lowLimit; i <= highLimit; i++){
            int sum = 0;
            int num = i;
            while(num > 0){
                sum += num % 10;
                num /= 10;
            }
            map.merge(sum, 1, Integer::sum);
        }
        return map.values().stream().max(Integer::compareTo).orElse(0);
    }
}

性能

1387.将整数按权重排序

目标

我们将整数 x 的 权重 定义为按照下述规则将 x 变成 1 所需要的步数:

  • 如果 x 是偶数,那么 x = x / 2
  • 如果 x 是奇数,那么 x = 3 * x + 1

比方说,x=3 的权重为 7 。因为 3 需要 7 步变成 1 (3 --> 10 --> 5 --> 16 --> 8 --> 4 --> 2 --> 1)。

给你三个整数 lo, hi 和 k 。你的任务是将区间 [lo, hi] 之间的整数按照它们的权重 升序排序 ,如果大于等于 2 个整数有 相同 的权重,那么按照数字自身的数值 升序排序 。

请你返回区间 [lo, hi] 之间的整数按权重排序后的第 k 个数。

注意,题目保证对于任意整数 x (lo <= x <= hi) ,它变成 1 所需要的步数是一个 32 位有符号整数。

示例 1:

输入:lo = 12, hi = 15, k = 2
输出:13
解释:12 的权重为 9(12 --> 6 --> 3 --> 10 --> 5 --> 16 --> 8 --> 4 --> 2 --> 1)
13 的权重为 9
14 的权重为 17
15 的权重为 17
区间内的数按权重排序以后的结果为 [12,13,14,15] 。对于 k = 2 ,答案是第二个整数也就是 13 。
注意,12 和 13 有相同的权重,所以我们按照它们本身升序排序。14 和 15 同理。

示例 2:

输入:lo = 7, hi = 11, k = 4
输出:7
解释:区间内整数 [7, 8, 9, 10, 11] 对应的权重为 [16, 3, 19, 6, 14] 。
按权重排序后得到的结果为 [8, 10, 11, 7, 9] 。
排序后数组中第 4 个数字为 7 。

说明:

1 <= lo <= hi <= 1000
1 <= k <= hi - lo + 1

思路

定义整数 x 的权重为 将其变为 1 的操作次数,根据整数的奇偶性,可以执行不同的操作:

  • x 为偶数,x -> x / 2
  • x 为奇数,x -> 3 * x + 1

返回区间 [lo, hi] 之间的整数按权重排序后的第 k 个数。

根据题意模拟计算出每个数字的权重,将它和数字一起保存起来,然后按照权重、数值排序即可。

可以预处理 1 ~ 1000 内的所有权重,保存中间结果减少重复计算。

看到题目时我们都会有这样的疑问,如何证明 x 最终都会回到 1?有网友提到题目中的操作与考拉兹猜想(Collatz conjecture)的操作一样,由于操作过程与冰雹的形成和下落过程相似,因此也叫冰雹猜想。

代码


/**
 * @date 2024-12-22 16:20
 */
public class GetKth1387 {

    public int getKth(int lo, int hi, int k) {
        int n = hi - lo + 1;
        int[][] w = new int[n][2];
        int c = 0;
        for (int i = lo; i <= hi; i++) {
            w[c++] = new int[]{getWeight(i), i};
        }
        Arrays.sort(w, (a, b) -> {
            int compare = a[0] - b[0];
            if (compare != 0) {
                return compare;
            }
            return a[1] - b[1];
        });
        return w[k - 1][1];
    }

    public int getWeight(int x) {
        int cnt = 0;
        while (x > 1) {
            if (x % 2 == 0) {
                x >>= 1;
            } else {
                x = 3 * x + 1;
            }
            cnt++;
        }
        return cnt;
    }
}

性能

3242.设计相邻元素求和服务

目标

给你一个 n x n 的二维数组 grid,它包含范围 [0, n^2 - 1] 内的不重复元素。

实现 neighborSum 类:

  • neighborSum(int [][]grid) 初始化对象。
  • int adjacentSum(int value) 返回在 grid 中与 value 相邻的元素之和,相邻指的是与 value 在上、左、右或下的元素。
  • int diagonalSum(int value) 返回在 grid 中与 value 对角线相邻的元素之和,对角线相邻指的是与 value 在左上、右上、左下或右下的元素。

示例 1:

输入:
["neighborSum", "adjacentSum", "adjacentSum", "diagonalSum", "diagonalSum"]
[[[[0, 1, 2], [3, 4, 5], [6, 7, 8]]], [1], [4], [4], [8]]
输出: [null, 6, 16, 16, 4]
解释:
1 的相邻元素是 0、2 和 4。
4 的相邻元素是 1、3、5 和 7。
4 的对角线相邻元素是 0、2、6 和 8。
8 的对角线相邻元素是 4。

示例 2:

输入:
["neighborSum", "adjacentSum", "diagonalSum"]
[[[[1, 2, 0, 3], [4, 7, 15, 6], [8, 9, 10, 11], [12, 13, 14, 5]]], [15], [9]]
输出: [null, 23, 45]
解释:
15 的相邻元素是 0、10、7 和 6。
9 的对角线相邻元素是 4、12、14 和 15。

说明:

  • 3 <= n == grid.length == grid[0].length <= 10
  • 0 <= grid[i][j] <= n^2 - 1
  • 所有 grid[i][j] 值均不重复。
  • adjacentSum 和 diagonalSum 中的 value 均在范围 [0, n^2 - 1] 内。
  • 最多会调用 adjacentSum 和 diagonalSum 总共 2 * n^2 次。

思路

有一个 n x n 的二维矩阵,其元素值的范围是 0 ~ n^2 - 1,且元素值互不相同。给定一个元素值,求其上下左右的元素和以及对角线元素和(左上、右上、左下和右下)。

首先要找到这个给定的元素,再根据其下标计算元素和,对于大量重复地查询,可以记录每个元素值对应的下标。

每次定位的时间复杂度为 100,由于存在缓存,最多搜索 100 次,总复杂度为 10^4

我们也可以在初始化的时候直接建立 元素值 与 坐标的映射,时间复杂度降为 O(n^2) 即 100

还可以进一步优化,由于数据值是不可变的,可以提前将 相邻元素和 都计算好,查的时候无需重复计算。此外,由于元素各不相同,可以直接将元素值映射为下标,将元素值与坐标的映射改为元素值与相邻元素和的映射。

代码


/**
 * @date 2024-11-09 11:21
 */
public class NeighborSum3242 {

    class NeighborSum {

        int[][] adjacent = new int[][]{
                new int[]{-1, 0},
                new int[]{1, 0},
                new int[]{0, -1},
                new int[]{0, 1},
        };
        int[][] diagonal = new int[][]{
                new int[]{-1, -1},
                new int[]{-1, 1},
                new int[]{1, -1},
                new int[]{1, 1},
        };
        int[] adjacentSum;
        int[] diagonalSum;

        public NeighborSum(int[][] grid) {
            int index = 0;
            int n = grid.length;
            int length = n * n;
            adjacentSum = new int[length];
            diagonalSum = new int[length];
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    int value = grid[i][j];
                    adjacentSum[value] = sum(grid, i, j, adjacent);
                    diagonalSum[value] = sum(grid, i, j, diagonal);
                }
            }
        }

        public int adjacentSum(int value) {
            return adjacentSum[value];
        }

        public int diagonalSum(int value) {
            return diagonalSum[value];
        }

        private int sum(int[][] grid, int i, int j, int[][] directions){
            int res = 0;
            for (int[] direction : directions) {
                int row = direction[0] + i;
                int col = direction[1] + j;
                if (row < 0 || col < 0 || row >= grid.length || col >= grid.length) {
                    continue;
                }
                res += grid[row][col];
            }
            return res;
        }
    }

}

性能

1766.互质树

目标

给你一个 n 个节点的树(也就是一个无环连通无向图),节点编号从 0 到 n - 1 ,且恰好有 n - 1 条边,每个节点有一个值。树的 根节点 为 0 号点。

给你一个整数数组 nums 和一个二维数组 edges 来表示这棵树。nums[i] 表示第 i 个点的值,edges[j] = [uj, vj] 表示节点 uj 和节点 vj 在树中有一条边。

当 gcd(x, y) == 1 ,我们称两个数 x 和 y 是 互质的 ,其中 gcd(x, y) 是 x 和 y 的 最大公约数 。

从节点 i 到 根 最短路径上的点都是节点 i 的祖先节点。一个节点 不是 它自己的祖先节点。

请你返回一个大小为 n 的数组 ans ,其中 ans[i]是离节点 i 最近的祖先节点且满足 nums[i] 和 nums[ans[i]] 是 互质的 ,如果不存在这样的祖先节点,ans[i] 为 -1 。

说明:

  • nums.length == n
  • 1 <= nums[i] <= 50
  • 1 <= n <= 10^5
  • edges.length == n - 1
  • edges[j].length == 2
  • 0 <= uj, vj < n
  • uj != vj

思路

今天这道题超时了,看了答案才发现节点值不超过50。没有注意到这个点,答案是先计算1到50内每个数字互质的数字列表。然后在dfs的时候记录节点值的最大深度,以及最近的编号。

我是直接记录了parent数组,一步一步向上找,在第35/37个案例超时了,这棵树是单链,并且除了根节点,向上找都不互质,只能从叶子找到根。

这样在递归中套递归直接堆栈溢出了。后来又将这两个递归分开,不溢出了,但还是超时。

后来又试图利用已求得的结果,记录了value -> 最近互质父节点编号的映射,错误地认为如果值相等就可以直接返回这个编号。其实是不对的,因为这二者之间的父节点也可能与当前节点互质。

其实我想到了应该维护一个去重的父节点序列,但是今天没时间了,只能去看答案了。预处理这个点没有想到,记录值的最大深度与最近编号这个也不好想,也许时间充裕可能会想到吧。

好多经过深度思考得到的复杂的算法,时间久了就会忘记许多细节。没必要非得自己想出来,有这时间多看看算法书进步的更快吧。

代码

// todo

性能

// todo