3111.覆盖所有点的最少矩形数目

目标

给你一个二维整数数组 point ,其中 points[i] = [xi, yi] 表示二维平面内的一个点。同时给你一个整数 w 。你需要用矩形 覆盖所有 点。

每个矩形的左下角在某个点 (x1, 0) 处,且右上角在某个点 (x2, y2) 处,其中 x1 <= x2 且 y2 >= 0 ,同时对于每个矩形都 必须 满足 x2 - x1 <= w 。

如果一个点在矩形内或者在边上,我们说这个点被矩形覆盖了。

请你在确保每个点都 至少 被一个矩形覆盖的前提下,最少 需要多少个矩形。

注意:一个点可以被多个矩形覆盖。

示例 1:

输入:points = [[2,1],[1,0],[1,4],[1,8],[3,5],[4,6]], w = 1
输出:2
解释:
上图展示了一种可行的矩形放置方案:
一个矩形的左下角在 (1, 0) ,右上角在 (2, 8) 。
一个矩形的左下角在 (3, 0) ,右上角在 (4, 8) 。

示例 2:

输入:points = [[0,0],[1,1],[2,2],[3,3],[4,4],[5,5],[6,6]], w = 2
输出:3
解释:
上图展示了一种可行的矩形放置方案:
一个矩形的左下角在 (0, 0) ,右上角在 (2, 2) 。
一个矩形的左下角在 (3, 0) ,右上角在 (5, 5) 。
一个矩形的左下角在 (6, 0) ,右上角在 (6, 6) 。

示例 3:

输入:points = [[2,3],[1,2]], w = 0
输出:2
解释:
上图展示了一种可行的矩形放置方案:
一个矩形的左下角在 (1, 0) ,右上角在 (1, 2) 。
一个矩形的左下角在 (2, 0) ,右上角在 (2, 3) 。

说明:

  • 1 <= points.length <= 10^5
  • points[i].length == 2
  • 0 <= xi == points[i][0] <= 10^9
  • 0 <= yi == points[i][1] <= 10^9
  • 0 <= w <= 10^9
  • 所有点坐标 (xi, yi) 互不相同。

思路

有一个二维数组表示平面中的点,给定一个整数w表示矩形x方向的最大宽度,矩形的左下角在x坐标轴上某个点,右上角在第一象限某点。问覆盖坐标平面上所有点最少需要多少个矩形,在矩形边上也算被覆盖了。

由于y轴方向没有限制,我们只考虑x轴方向,可以将坐标平面中的点按照x轴坐标大小排序,然后按w宽度分组计数即可。

最快的写法是将二维坐标转成了一维的x坐标,排序只需要寻址一次。

代码

/**
 * @date 2024-07-31 9:12
 */
public class MinRectanglesToCoverPoints3111 {

    public int minRectanglesToCoverPoints_v1(int[][] points, int w) {
        int n = points.length;
        int[] x = new int[n];
        for (int i = 0; i < n; i++) {
            x[i] = points[i][0];
        }
        Arrays.sort(x);
        int res = 0;
        int r = -1;
        for (int i = 0; i < n; i++) {
            if (x[i] > r) {
                res++;
                r = x[i] + w;
            }
        }
        return res;
    }

    public int minRectanglesToCoverPoints(int[][] points, int w) {
        Arrays.sort(points, (a, b) -> a[0] - b[0]);
        int n = points.length;
        int res = 0;
        int l = 0;
        for (int i = 1; i < n; i++) {
            if (points[i][0] - points[l][0] > w) {
                res++;
                l = i;
            }
        }
        // res 计数的是之前的矩形个数,+1表示将最后的矩形计入
        return res + 1;
    }
}

性能

2961.双模幂运算

目标

给你一个下标从 0 开始的二维数组 variables ,其中 variables[i] = [ai, bi, ci, mi],以及一个整数 target 。

如果满足以下公式,则下标 i 是 好下标:

  • 0 <= i < variables.length
  • ((ai^bi % 10)^ci) % mi == target

返回一个由 好下标 组成的数组,顺序不限 。

示例 1:

输入:variables = [[2,3,3,10],[3,3,3,1],[6,1,1,4]], target = 2
输出:[0,2]
解释:对于 variables 数组中的每个下标 i :
1) 对于下标 0 ,variables[0] = [2,3,3,10] ,(23 % 10)3 % 10 = 2 。
2) 对于下标 1 ,variables[1] = [3,3,3,1] ,(33 % 10)3 % 1 = 0 。
3) 对于下标 2 ,variables[2] = [6,1,1,4] ,(61 % 10)1 % 4 = 2 。
因此,返回 [0,2] 作为答案。

示例 2:

输入:variables = [[39,3,1000,1000]], target = 17
输出:[]
解释:对于 variables 数组中的每个下标 i :
1) 对于下标 0 ,variables[0] = [39,3,1000,1000] ,(393 % 10)1000 % 1000 = 1 。
因此,返回 [] 作为答案。

说明:

  • 1 <= variables.length <= 100
  • variables[i] == [ai, bi, ci, mi]
  • 1 <= ai, bi, ci, mi <= 10^3
  • 0 <= target <= 10^3

思路

已知二维数组 variables[i] = [ai, bi, ci, mi],定义好下标为 ((ai^bi % 10)^ci) % mi == target,返回好下标组成的数组。

尝试使用Math.pow暴力求解,存在精度丢失,例如 Math.pow(31, 12) = 7.8766278378854976 E17 ,mod 10 结果为 0。

提前mod也没用,比如 ai^bi % 10 == 66 ^ 83 % 81还是会丢失精度,只能手动循环计算了。

官网题解使用了快速幂。不用一个一个的乘,使用递归,根据幂的奇偶,偶数直接求 x^(n/2) * x^(n/2),奇数求 x^(⌊n/2⌋) * x^(⌊n/2⌋) * x,直到n为0。时间复杂度为 O(logn)。

//todo 快速幂

代码

/**
 * @date 2024-07-30 9:11
 */
public class GetGoodIndices2961 {

    public List<Integer> getGoodIndices(int[][] variables, int target) {
        List<Integer> res = new ArrayList<>();
        int n = variables.length;
        for (int i = 0; i < n; i++) {
            int[] variable = variables[i];
            int base = variable[0];
            int tmp1 = 1;
            for (int j = 0; j < variable[1]; j++) {
                tmp1 = (tmp1 * base) % 10;
            }
            base = tmp1;
            int tmp = 1;
            for (int j = 0; j < variable[2]; j++) {
                tmp = (tmp * base) % variable[3];
            }
            if (tmp % variable[3] == target) {
                res.add(i);
            }
        }
        return res;
    }
}

性能

682.棒球比赛

目标

你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。

比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops,其中 ops[i] 是你需要记录的第 i 项操作,ops 遵循下述规则:

  1. 整数 x - 表示本回合新获得分数 x
  2. "+" - 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。
  3. "D" - 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。
  4. "C" - 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。

请你返回记录中所有得分的总和。

示例 1:

输入:ops = ["5","2","C","D","+"]
输出:30
解释:
"5" - 记录加 5 ,记录现在是 [5]
"2" - 记录加 2 ,记录现在是 [5, 2]
"C" - 使前一次得分的记录无效并将其移除,记录现在是 [5].
"D" - 记录加 2 * 5 = 10 ,记录现在是 [5, 10].
"+" - 记录加 5 + 10 = 15 ,记录现在是 [5, 10, 15].
所有得分的总和 5 + 10 + 15 = 30

示例 2:

输入:ops = ["5","-2","4","C","D","9","+","+"]
输出:27
解释:
"5" - 记录加 5 ,记录现在是 [5]
"-2" - 记录加 -2 ,记录现在是 [5, -2]
"4" - 记录加 4 ,记录现在是 [5, -2, 4]
"C" - 使前一次得分的记录无效并将其移除,记录现在是 [5, -2]
"D" - 记录加 2 * -2 = -4 ,记录现在是 [5, -2, -4]
"9" - 记录加 9 ,记录现在是 [5, -2, -4, 9]
"+" - 记录加 -4 + 9 = 5 ,记录现在是 [5, -2, -4, 9, 5]
"+" - 记录加 9 + 5 = 14 ,记录现在是 [5, -2, -4, 9, 5, 14]
所有得分的总和 5 + -2 + -4 + 9 + 5 + 14 = 27

示例 3:

输入:ops = ["1"]
输出:1

说明:

  • 1 <= ops.length <= 1000
  • ops[i] 为 "C"、"D"、"+",或者一个表示整数的字符串。整数范围是 [-3 10^4, 3 10^4]
  • 对于 "+" 操作,题目数据保证记录此操作时前面总是存在两个有效的分数
  • 对于 "C" 和 "D" 操作,题目数据保证记录此操作时前面总是存在一个有效的分数

思路

这是一道模拟题,按照规则计算即可。

代码

/**
 * @date 2024-07-29 8:47
 */
public class CalPoints682 {

    public int calPoints(String[] operations) {
        int res = 0;
        int n = operations.length;
        // 出错点就是将分数定义为数组,那么当认为上一次分数无效,而没有移除时,后面的根据下标的计算就会出错
        List<Integer> points = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            int cur = points.size();
            switch (operations[i]) {
                case "+":
                    points.add(points.get(cur - 1) + points.get(cur - 2));
                    break;
                case "D":
                    points.add(points.get(cur - 1) * 2);
                    break;
                case "C":
                    points.remove(cur - 1);
                    break;
                default:
                    points.add(Integer.parseInt(operations[i]));
            }
        }
        for (int point : points) {
            res += point;
        }
        return res;
    }
}

性能

3106.满足距离约束且字典序最小的字符串

目标

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

定义函数 distance(s1, s2) ,用于衡量两个长度为 n 的字符串 s1 和 s2 之间的距离,即:

  • 字符 'a' 到 'z' 按 循环 顺序排列,对于区间 [0, n - 1] 中的 i ,计算所有「 s1[i] 和 s2[i] 之间 最小距离」的 和 。

例如,distance("ab", "cd") == 4 ,且 distance("a", "z") == 1 。

你可以对字符串 s 执行 任意次 操作。在每次操作中,可以将 s 中的一个字母 改变 为 任意 其他小写英文字母。

返回一个字符串,表示在执行一些操作后你可以得到的 字典序最小 的字符串 t ,且满足 distance(s, t) <= k 。

示例 1:

输入:s = "zbbz", k = 3
输出:"aaaz"
解释:在这个例子中,可以执行以下操作:
将 s[0] 改为 'a' ,s 变为 "abbz" 。
将 s[1] 改为 'a' ,s 变为 "aabz" 。
将 s[2] 改为 'a' ,s 变为 "aaaz" 。
"zbbz" 和 "aaaz" 之间的距离等于 k = 3 。
可以证明 "aaaz" 是在任意次操作后能够得到的字典序最小的字符串。
因此,答案是 "aaaz" 。

示例 2:

输入:s = "xaxcd", k = 4
输出:"aawcd"
解释:在这个例子中,可以执行以下操作:
将 s[0] 改为 'a' ,s 变为 "aaxcd" 。
将 s[2] 改为 'w' ,s 变为 "aawcd" 。
"xaxcd" 和 "aawcd" 之间的距离等于 k = 4 。
可以证明 "aawcd" 是在任意次操作后能够得到的字典序最小的字符串。
因此,答案是 "aawcd" 。

示例 3:

输入:s = "lol", k = 0
输出:"lol"
解释:在这个例子中,k = 0,更改任何字符都会使得距离大于 0 。
因此,答案是 "lol" 。

说明:

  • 1 <= s.length <= 100
  • 0 <= k <= 2000
  • s 只包含小写英文字母。

思路

定义两个由 a - z 组成的长度相等字符串之间的距离为相应位置上字母的最小距离,比如 az 的最小距离为1,ao 之间的距离为12,而非14,an 的距离为13,即距离的最大值为13。因此两字符相减 diff 如果大于13,应取 26 - diff

a b c d e f g h i j k l m

n o p q r s t u v w x y z

现在给定k表示字符串距离,需要我们找出与给定字符串距离为k的字典序最小的字符串。所谓字典序,先取第一个字符比较它在字母表中的排序,如果相等则比较下一个。

因此我们可以遍历给定的字符串的每一个字符,在距离范围内优先将其改为 a,如果距离还有多余,则继续改下一个字母,如果距离不够直接向下取剩余距离的字符。由于字母是首尾相接的,需要考虑是从哪边计算距离最短。第一行从前计算,第二行从后计算,或者两者都计算,比较大小决定如何处理。

代码

/**
 * @date 2024-07-27 22:22
 */
public class GetSmallestString3106 {

    public String getSmallestString(String s, int k) {
        char[] chars = s.toCharArray();
        int n = s.length();
        int i = 0;
        while (k > 0 && i < n) {
            char c = chars[i];
            int dist = Math.min('z' - c + 1, c - 'a');
            if (k < dist) {
                chars[i] = (char) (c - k);
                break;
            }
            chars[i] = 'a';
            k -= dist;
            i++;
        }

        return new String(chars);
    }

}

性能

2740.找出分区值

目标

给你一个 正 整数数组 nums 。

将 nums 分成两个数组:nums1 和 nums2 ,并满足下述条件:

  • 数组 nums 中的每个元素都属于数组 nums1 或数组 nums2 。
  • 两个数组都 非空 。
  • 分区值 最小 。

分区值的计算方法是 |max(nums1) - min(nums2)| 。

其中,max(nums1) 表示数组 nums1 中的最大元素,min(nums2) 表示数组 nums2 中的最小元素。

返回表示分区值的整数。

示例 1:

输入:nums = [1,3,2,4]
输出:1
解释:可以将数组 nums 分成 nums1 = [1,2] 和 nums2 = [3,4] 。
- 数组 nums1 的最大值等于 2 。
- 数组 nums2 的最小值等于 3 。
分区值等于 |2 - 3| = 1 。
可以证明 1 是所有分区方案的最小值。

示例 2:

输入:nums = [100,1,10]
输出:9
解释:可以将数组 nums 分成 nums1 = [10] 和 nums2 = [100,1] 。 
- 数组 nums1 的最大值等于 10 。 
- 数组 nums2 的最小值等于 1 。 
分区值等于 |10 - 1| = 9 。 
可以证明 9 是所有分区方案的最小值。

说明:

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

思路

看了半天题目似乎就是要找出数组内任意两个元素差的最小值,直接排序后两两比较即可。

代码

/**
 * @date 2024-07-26 0:39
 */
public class FindValueOfPartition2740 {

    public int findValueOfPartition(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        int res = Integer.MAX_VALUE;
        for (int i = 1; i < n; i++) {
            res = Math.min(res, nums[i] - nums[i - 1]);
        }
        return res;
    }
}

性能

2844.生成特殊数字的最少操作

目标

给你一个下标从 0 开始的字符串 num ,表示一个非负整数。

在一次操作中,您可以选择 num 的任意一位数字并将其删除。请注意,如果你删除 num 中的所有数字,则 num 变为 0。

返回最少需要多少次操作可以使 num 变成特殊数字。

如果整数 x 能被 25 整除,则该整数 x 被认为是特殊数字。

示例 1:

输入:num = "2245047"
输出:2
解释:删除数字 num[5] 和 num[6] ,得到数字 "22450" ,可以被 25 整除。
可以证明要使数字变成特殊数字,最少需要删除 2 位数字。

示例 2:

输入:num = "2908305"
输出:3
解释:删除 num[3]、num[4] 和 num[6] ,得到数字 "2900" ,可以被 25 整除。
可以证明要使数字变成特殊数字,最少需要删除 3 位数字。

示例 3:

输入:num = "10"
输出:1
解释:删除 num[0] ,得到数字 "0" ,可以被 25 整除。
可以证明要使数字变成特殊数字,最少需要删除 1 位数字。

说明

  • 1 <= num.length <= 100
  • num 仅由数字 '0' 到 '9' 组成
  • num 不含任何前导零

思路

有一个字符串表示的数字,每一次操作可以删除任意一个数字。问最少需要操作多少次能够使数字被25整除。

通过观察可知,能够被25整除,个位数字只能是 0 或 5。十位数字只能是 0、2、5、7 ,其中 如果 十位为 0 必须要有更高位不为0 num 不含任何前导零0、25、50、75、100、125、200、……、1000、1025、……,更高位的数字可以是任意的。如果个位是5,那么十位只能是 2、7,如果个位是 0,那么十位只能是 0、5

2908305 从末尾查找,如果保留5,需要删除中间 5 个,如果删掉5,保留0,则只需要再删除 8, 3 两个数字即可,我们应该取二者的最小值。这就是我们的贪心策略。

代码

/**
 * @date 2024-07-25 11:17
 */
public class MinimumOperations2844 {

    public int minimumOperations(String num) {
        int n = num.length();
        int res = 0;
        int cursor = n - 1;
        while (cursor >= 0 && num.charAt(cursor) != '0') {
            cursor--;
        }
        res = n - 1 - cursor;
        cursor--;
        while (cursor >= 0 && (num.charAt(cursor) != '0' && num.charAt(cursor) != '5')) {
            cursor--;
            res++;
        }

        int res5 = 0;
        cursor = n - 1;
        while (cursor >= 0 && num.charAt(cursor) != '5') {
            cursor--;
        }
        res5 = n - 1 - cursor;
        cursor--;
        while (cursor >= 0 && (num.charAt(cursor) != '2' && num.charAt(cursor) != '7')) {
            cursor--;
            res5++;
        }
        // 这里需要判断,前面个位为5,但是十位没有合法数字的情况,这时需要把5也删掉
        if (cursor < 0) {
            res5++;
        }
        return Math.min(res, res5);
    }

}

性能

2766.重新放置石块

目标

给你一个下标从 0 开始的整数数组 nums ,表示一些石块的初始位置。再给你两个长度 相等 下标从 0 开始的整数数组 moveFrom 和 moveTo 。

在 moveFrom.length 次操作内,你可以改变石块的位置。在第 i 次操作中,你将位置在 moveFrom[i] 的所有石块移到位置 moveTo[i] 。

完成这些操作后,请你按升序返回所有 有 石块的位置。

注意:

  • 如果一个位置至少有一个石块,我们称这个位置 有 石块。
  • 一个位置可能会有多个石块。

示例 1:

输入:nums = [1,6,7,8], moveFrom = [1,7,2], moveTo = [2,9,5]
输出:[5,6,8,9]
解释:一开始,石块在位置 1,6,7,8 。
第 i = 0 步操作中,我们将位置 1 处的石块移到位置 2 处,位置 2,6,7,8 有石块。
第 i = 1 步操作中,我们将位置 7 处的石块移到位置 9 处,位置 2,6,8,9 有石块。
第 i = 2 步操作中,我们将位置 2 处的石块移到位置 5 处,位置 5,6,8,9 有石块。
最后,至少有一个石块的位置为 [5,6,8,9] 。

示例 2:

输入:nums = [1,1,3,3], moveFrom = [1,3], moveTo = [2,2]
输出:[2]
解释:一开始,石块在位置 [1,1,3,3] 。
第 i = 0 步操作中,我们将位置 1 处的石块移到位置 2 处,有石块的位置为 [2,2,3,3] 。
第 i = 1 步操作中,我们将位置 3 处的石块移到位置 2 处,有石块的位置为 [2,2,2,2] 。
由于 2 是唯一有石块的位置,我们返回 [2] 。

说明:

  • 1 <= nums.length <= 10^5
  • 1 <= moveFrom.length <= 10^5
  • moveFrom.length == moveTo.length
  • 1 <= nums[i], moveFrom[i], moveTo[i] <= 10^9
  • 测试数据保证在进行第 i 步操作时,moveFrom[i] 处至少有一个石块。

思路

有一个数组 nums ,其元素值表示该位置上有石头,另用两个长度相同的数组 moveFrom moveTo,表示将from位置上 所有 的石头搬到to的操作。问依次完成这些操作后,哪些位置上有石头,按位置从小到大顺序返回。

很容易想到用哈希表记录位置上 石头的数量,然后根据操作增减相应位置的石头。最后遍历找出石头数量大于0的位置,然后排序即可。每次移走所有石头,所有不用计数。

换一个角度考虑,to位置上是否一定有石头?不一定,因为可能被后序操作移走。这样我们就不能整体考虑,通过集合操作得到结果。

再换一个角度考虑,能否根据from to 建图,我们只需求出出度为0的节点即可。节点数量太多还是算了。

还是常规做法吧。

看了题解,大家都是这么做的。无非是有的用 HashSet 有的用 HashMap。最快的做法是最后排序的时候转为数组,使用数组排序

代码

/**
 * @date 2024-07-24 21:43
 */
public class RelocateMarbles2766 {

    public List<Integer> relocateMarbles(int[] nums, int[] moveFrom, int[] moveTo) {
        Set<Integer> hasStone = new HashSet<>(nums.length);
        for (int num : nums) {
            hasStone.add(num);
        }
        int n = moveFrom.length;
        for (int i = 0; i < n; i++) {
            hasStone.remove(moveFrom[i]);
            hasStone.add(moveTo[i]);
        }
        List<Integer> res = new ArrayList<>(hasStone);
        Collections.sort(res);
        return res;
    }

}

性能

3098.求出所有子序列的能量和

目标

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

一个 子序列 的 能量 定义为子序列中 任意 两个元素的差值绝对值的 最小值 。

请你返回 nums 中长度 等于 k 的 所有 子序列的 能量和 。

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

示例 1:

输入:nums = [1,2,3,4], k = 3
输出:4
解释:
nums 中总共有 4 个长度为 3 的子序列:[1,2,3] ,[1,3,4] ,[1,2,4] 和 [2,3,4] 。能量和为 |2 - 3| + |3 - 4| + |2 - 1| + |3 - 4| = 4 。

示例 2:

输入:nums = [2,2], k = 2
输出:0
解释:
nums 中唯一一个长度为 2 的子序列是 [2,2] 。能量和为 |2 - 2| = 0 。

示例 3:

输入:nums = [4,3,-1], k = 2
输出:10
解释:
nums 总共有 3 个长度为 2 的子序列:[4,3] ,[4,-1] 和 [3,-1] 。能量和为 |4 - 3| + |4 - (-1)| + |3 - (-1)| = 10 。

说明:

  • 2 <= n == nums.length <= 50
  • -10^8 <= nums[i] <= 10^8
  • 2 <= k <= n

思路

定义数组子序列的能量为子序列中任意两个元素差的绝对值的最小值,求数组长度为k的子序列的能量和。

首先思考,数组 nums 长度为n的子序列有多少?根据每一个元素是否在序列中可知有 2^n种可能。n 最大为50,2^50 = 1125899906842624。那么长度为k的子序列有多少? C(n,k) = n!/(k!(n-k)!),当 k=2 时,有 n(n-1)/2 个子序列。

只能考虑动态规划,自底向上地求解。我们可以使用状态压缩来表示子序列,当状态发生转移的时候该如何处理呢?假设我们知道了某k-1子序列的能量,那么再增加一个元素能量会如何变化?比如子序列1,3,6,10,能量为2,再加入一个元素9,能量变为1。还是需要遍历子序列的。那么我们可以根据压缩的状态仅遍历哪些bit位为1的元素。剩下的问题就是如何遍历子序列了。

看了提示,首先要排序,这样差值最小的就两两相邻。刚开始的时候我也在想能不能排序,虽然子序列要保持相对顺序,但本题考虑的是子序列中绝对值差最小的,与顺序无关。

// todo

代码

性能

2101.引爆最多的炸弹

目标

给你一个炸弹列表。一个炸弹的 爆炸范围 定义为以炸弹为圆心的一个圆。

炸弹用一个下标从 0 开始的二维整数数组 bombs 表示,其中 bombs[i] = [xi, yi, ri] 。xi 和 yi 表示第 i 个炸弹的 X 和 Y 坐标,ri 表示爆炸范围的 半径 。

你需要选择引爆 一个 炸弹。当这个炸弹被引爆时,所有 在它爆炸范围内的炸弹都会被引爆,这些炸弹会进一步将它们爆炸范围内的其他炸弹引爆。

给你数组 bombs ,请你返回在引爆 一个 炸弹的前提下,最多 能引爆的炸弹数目。

示例 1:

输入:bombs = [[2,1,3],[6,1,4]]
输出:2
解释:
上图展示了 2 个炸弹的位置和爆炸范围。
如果我们引爆左边的炸弹,右边的炸弹不会被影响。
但如果我们引爆右边的炸弹,两个炸弹都会爆炸。
所以最多能引爆的炸弹数目是 max(1, 2) = 2 。

示例 2:

输入:bombs = [[1,1,5],[10,10,5]]
输出:1
解释:
引爆任意一个炸弹都不会引爆另一个炸弹。所以最多能引爆的炸弹数目为 1 。

示例 3:

输入:bombs = [[1,2,3],[2,3,1],[3,4,2],[4,5,3],[5,6,4]]
输出:5
解释:
最佳引爆炸弹为炸弹 0 ,因为:
- 炸弹 0 引爆炸弹 1 和 2 。红色圆表示炸弹 0 的爆炸范围。
- 炸弹 2 引爆炸弹 3 。蓝色圆表示炸弹 2 的爆炸范围。
- 炸弹 3 引爆炸弹 4 。绿色圆表示炸弹 3 的爆炸范围。
所以总共有 5 个炸弹被引爆。

说明:

  • 1 <= bombs.length <= 100
  • bombs[i].length == 3
  • 1 <= xi, yi, ri <= 10^5

思路

坐标平面上有一些炸弹,并且已知炸弹的爆炸范围。现在可以选择引爆其中的一颗炸弹,被引爆炸弹的爆炸范围内的其它炸弹也会被引爆,问最多可以引爆的炸弹数量。

我们可以将问题转换为有向图,一枚炸弹能够波及到的其它炸弹认为是连通的,然后遍历每一枚炸弹,求出连通炸弹数量最多的个数即可。

那么如何建立这个有向图呢?固定一个,依次与其余的节点比较,时间复杂度为O(n^2),炸弹最多100个,应该可行。

实现过程中需要判断爆炸范围内的是否存在其它炸弹,可以使用炸弹坐标(圆心)之间的距离与各自的引爆半径相比较。这里需要注意防止数据溢出,另外还有一个小技巧,比较 sqrt(dx^2 + dy^2) 与 半径 r 的效率没有 dx^2 + dy^2r^2 高。

网友最快的题解使用的是 Floyd 算法。// todo

代码

/**
 * @date 2024-07-22 9:24
 */
public class MaximumDetonation2101 {

    public int maximumDetonation_v1(int[][] bombs) {
        int n = bombs.length;
        List<Integer>[] g = new List[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int i = 0; i < n; i++) {
            int ix = bombs[i][0];
            int iy = bombs[i][1];
            long ir = bombs[i][2];
            for (int j = i + 1; j < n; j++) {
                int jx = bombs[j][0];
                int jy = bombs[j][1];
                long jr = bombs[j][2];
                // 防止溢出
                long diffx = ix - jx;
                long diffy = iy - jy;
                long dist = diffx * diffx + diffy * diffy;
                if (ir * ir >= dist) {
                    g[i].add(j);
                }
                if (jr * jr >= dist) {
                    g[j].add(i);
                }
            }
        }
        int res = 0;
        for (int i = 0; i < n; i++) {
            boolean[] visited = new boolean[n];
            res = Math.max(res, dfs(i, g, visited));
        }
        return res;
    }

    int dfs(int root, List<Integer>[] g, boolean[] visited) {
        visited[root] = true;
        int res = 1;
        for (Integer next : g[root]) {
            if (visited[next]) {
                continue;
            }
            res += dfs(next, g, visited);
        }
        return res;
    }

}

性能

图中至多有 n^2 条边,dfs的时间复杂度是O(n^2),然后再遍历n个起点,时间复杂度为O(n^3)。