2769.找出最大的可达成数字

目标

给你两个整数 num 和 t 。

如果整数 x 可以在执行下述操作不超过 t 次的情况下变为与 num 相等,则称其为 可达成数字 :

  • 每次操作将 x 的值增加或减少 1 ,同时可以选择将 num 的值增加或减少 1 。

返回所有可达成数字中的最大值。可以证明至少存在一个可达成数字。

示例 1:

输入:num = 4, t = 1
输出:6
解释:最大可达成数字是 x = 6 ,执行下述操作可以使其等于 num :
- x 减少 1 ,同时 num 增加 1 。此时,x = 5 且 num = 5 。 
可以证明不存在大于 6 的可达成数字。

示例 2:

输入:num = 3, t = 2
输出:7
解释:最大的可达成数字是 x = 7 ,执行下述操作可以使其等于 num :
- x 减少 1 ,同时 num 增加 1 。此时,x = 6 且 num = 4 。 
- x 减少 1 ,同时 num 增加 1 。此时,x = 5 且 num = 5 。 
可以证明不存在大于 7 的可达成数字。

说明:

  • 1 <= num, t <= 50

思路

给定一个正整数 num,以及操作次数 t,求一个数字 x 能够在 t 次操作后与 num 相等。每一次操作可以使 x 增大或减小1,同时可以选择使 num 增大或减少1。问 x 最大是多少。

要使 x 最大则应该选大于 num 的数,操作减1,为了使 x 尽可能大,应该同时增大 num,使 xnum 相向变化。很明显,x 最大为 num + 2 * t

代码

/**
 * @date 2024-05-21 8:47
 */
public class TheMaximumAchievableX2769 {
    public int theMaximumAchievableX(int num, int t) {
        return num + 2 * t;
    }
}

性能

2644.找出可整除性得分最大的整数

目标

给你两个下标从 0 开始的整数数组 nums 和 divisors 。

divisors[i] 的 可整除性得分 等于满足 nums[j] 能被 divisors[i] 整除的下标 j 的数量。

返回 可整除性得分 最大的整数 divisors[i] 。如果有多个整数具有最大得分,则返回数值最小的一个。

示例 1:

输入:nums = [4,7,9,3,9], divisors = [5,2,3]
输出:3
解释:divisors 中每个元素的可整除性得分为:
divisors[0] 的可整除性得分为 0 ,因为 nums 中没有任何数字能被 5 整除。
divisors[1] 的可整除性得分为 1 ,因为 nums[0] 能被 2 整除。 
divisors[2] 的可整除性得分为 3 ,因为 nums[2]、nums[3] 和 nums[4] 都能被 3 整除。 
因此,返回 divisors[2] ,它的可整除性得分最大。

示例 2:

输入:nums = [20,14,21,10], divisors = [5,7,5]
输出:5
解释:divisors 中每个元素的可整除性得分为:
divisors[0] 的可整除性得分为 2 ,因为 nums[0] 和 nums[3] 都能被 5 整除。
divisors[1] 的可整除性得分为 2 ,因为 nums[1] 和 nums[2] 都能被 7 整除。
divisors[2] 的可整除性得分为 2 ,因为 nums[0] 和 nums[3] 都能被5整除。 
由于 divisors[0]、divisors[1] 和 divisors[2] 的可整除性得分都是最大的,因此,我们返回数值最小的一个,即 divisors[2] 。

示例 3:

输入:nums = [12], divisors = [10,16]
输出:10
解释:divisors 中每个元素的可整除性得分为:
divisors[0] 的可整除性得分为 0 ,因为 nums 中没有任何数字能被 10 整除。
divisors[1] 的可整除性得分为 0 ,因为 nums 中没有任何数字能被 16 整除。 
由于 divisors[0] 和 divisors[1] 的可整除性得分都是最大的,因此,我们返回数值最小的一个,即 divisors[0] 。

说明:

  • 1 <= nums.length, divisors.length <= 1000
  • 1 <= nums[i], divisors[i] <= 10^9

思路

给定一个被除数数组 nums 与除数数组 divisorsnums 中能够被 divisors 整除的元素个数称为相应除数的分数,求分数最大的除数 divisor,如果分数相同则取最小的。

通俗的讲就是找到能够被更多的元素整除的除数,如果有多个则取最小的。

简单题直接放入优先队列即可,但排序其实是没必要的,可以直接在循环中记录结果。

网友还提供了一种算法,不用对每一个 nums 中的元素进行mod运算,而是将 nums 记录到map中,value是其重复次数,同时求出 nums 的最大值。在循环除数的时候,只需要判断 i * divisor 是否在map中即可,结束条件是小于等于最大值。但是这种算法太有针对性,对于那种最大值很大而除数很小的情况可能会超时。

也有网友提供了排序加优化的算法,更通用一些。

代码

/**
 * @date 2024-05-18 10:10
 */
public class MaxDivScore2644 {

    /**
     * 网友的解法 排序加优化 70ms
     */
    public int maxDivScore_v3(int[] nums, int[] divisors) {
        Arrays.sort(nums);
        int n = nums.length;
        int dup = 0;
        for (int i = 1; i < n; i++) {
            if (nums[i] == nums[i - 1]) {
                dup++;
            }
        }
        Arrays.sort(divisors);

        int ans = 0;
        int maxCnt = -1;
        for (int d : divisors) {
            // 提前结束说明 d 的倍数 d,2d,3d,⋯ ,(maxCnt−dup+1)⋅d 中的最大值已经超出了 nums 的最大值,
            // 即使把 nums 中的重复元素也算上,我们也无法统计出比 maxCnt 还多的倍数。
            if (maxCnt - dup >= nums[n - 1] / d) {
                break;
            }
            int cnt = 0;
            for (int i = n - 1; i >= 0; i--) {
                int x = nums[i];
                if (x < d) {
                    break;
                }
                if (x % d == 0) {
                    cnt++;
                }
            }
            if (cnt > maxCnt) {
                maxCnt = cnt;
                ans = d;
            }
        }
        return ans;
    }

    /**
     * 25ms
     */
    public int maxDivScore_v1(int[] nums, int[] divisors) {
        Map<Integer, Integer> cntMap = new HashMap<>();
        int maxNum = 0;
        for (int num : nums) {
            cntMap.put(num, cntMap.getOrDefault(num, 0) + 1);
            maxNum = Math.max(maxNum, num);
        }
        // 出错点:这里maxCnt的初值为-1,如果为0会跳过无法整除的情况,进入不到if条件里,res还是初值0,而非dividsor
        int res = 0, maxCnt = -1;
        for (int divisor : divisors) {
            int cnt = 0;
            int num = divisor;
            for (int i = 2; num <= maxNum; i++) {
                if (cntMap.containsKey(num)) {
                    cnt += cntMap.get(num);
                }
                num = i * divisor;
            }
            if (cnt > maxCnt || (cnt == maxCnt && res > divisor)) {
                maxCnt = cnt;
                res = divisor;
            }
        }
        return res;
    }

    /**
     * 使用优先队列 180ms
     */
    public int maxDivScore(int[] nums, int[] divisors) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> {
            int c = b[1] - a[1];
            return c == 0 ? a[0] - b[0] : c;
        });
        for (int divisor : divisors) {
            int cnt = 0;
            for (int num : nums) {
                if (num % divisor == 0) {
                    cnt++;
                }
            }
            q.offer(new int[]{divisor, cnt});
        }
        return q.poll()[0];
    }

}

性能

使用优先队列

不使用排序,使用hashmap保存nums,成倍增加除数,当除数很大的时候可以跳过一些循环。

但还是取决于测试用例,如果nums数组没有几个元素,而max又很大,循环次数并不会减少,反而会增加。

例如这个测试用例,上面的方法直接超出限制

排序加优化:

2960.统计已测试设备

目标

给你一个长度为 n 、下标从 0 开始的整数数组 batteryPercentages ,表示 n 个设备的电池百分比。

你的任务是按照顺序测试每个设备 i,执行以下测试操作:

  • 如果 batteryPercentages[i] 大于 0:
    • 增加 已测试设备的计数。
    • 将下标在 [i + 1, n - 1] 的所有设备的电池百分比减少 1,确保它们的电池百分比 不会低于 0 ,即 batteryPercentages[j] = max(0, batteryPercentages[j] - 1)。
    • 移动到下一个设备。
  • 否则,移动到下一个设备而不执行任何测试。

返回一个整数,表示按顺序执行测试操作后 已测试设备 的数量。

示例 1:

输入:batteryPercentages = [1,1,2,1,3]
输出:3
解释:按顺序从设备 0 开始执行测试操作:
在设备 0 上,batteryPercentages[0] > 0 ,现在有 1 个已测试设备,batteryPercentages 变为 [1,0,1,0,2] 。
在设备 1 上,batteryPercentages[1] == 0 ,移动到下一个设备而不进行测试。
在设备 2 上,batteryPercentages[2] > 0 ,现在有 2 个已测试设备,batteryPercentages 变为 [1,0,1,0,1] 。
在设备 3 上,batteryPercentages[3] == 0 ,移动到下一个设备而不进行测试。
在设备 4 上,batteryPercentages[4] > 0 ,现在有 3 个已测试设备,batteryPercentages 保持不变。
因此,答案是 3 。

示例 2:

输入:batteryPercentages = [0,1,2]
输出:2
解释:按顺序从设备 0 开始执行测试操作:
在设备 0 上,batteryPercentages[0] == 0 ,移动到下一个设备而不进行测试。
在设备 1 上,batteryPercentages[1] > 0 ,现在有 1 个已测试设备,batteryPercentages 变为 [0,1,1] 。
在设备 2 上,batteryPercentages[2] > 0 ,现在有 2 个已测试设备,batteryPercentages 保持不变。
因此,答案是 2 。

说明:

  • 1 <= n == batteryPercentages.length <= 100
  • 0 <= batteryPercentages[i] <= 100

思路

如果设备的剩余电量大于0,已测试设备加1且后续设备的所有电量减1。

简而言之,后续设备需要减去的电量为已检测的设备数。

题解上说这是差分的思想。我们没必要检测一个设备就向后循环并将电量减1,当前设备需要减去的电量等于上一个设备需要减去的电量+1。

代码

/**
 * @date 2024-05-10 8:43
 */
public class CountTestedDevices2960 {
    public int countTestedDevices(int[] batteryPercentages) {
        int res = 0;
        for (int i = 0; i < batteryPercentages.length; i++) {
            if (batteryPercentages[i] > res) {
                res++;
            }
        }
        return res;
    }
}

性能

2639.查询网格图中每一列的宽度

目标

给你一个下标从 0 开始的 m * n 整数矩阵 grid 。矩阵中某一列的宽度是这一列数字的最大 字符串长度 。

  • 比方说,如果 grid = [[-10], [3], [12]] ,那么唯一一列的宽度是 3 ,因为 -10 的字符串长度为 3 。

请你返回一个大小为 n 的整数数组 ans ,其中 ans[i] 是第 i 列的宽度。

一个有 len 个数位的整数 x ,如果是非负数,那么 字符串长度 为 len ,否则为 len + 1 。

示例 1:

输入:grid = [[1],[22],[333]]
输出:[3]
解释:第 0 列中,333 字符串长度为 3 。

示例 2:

输入:grid = [[-15,1,3],[15,7,12],[5,6,-2]]
输出:[3,1,2]
解释:
第 0 列中,只有 -15 字符串长度为 3 。
第 1 列中,所有整数的字符串长度都是 1 。
第 2 列中,12 和 -2 的字符串长度都为 2 。

说明:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 100
  • -10^9 <= grid[r][c] <= 10^9

思路

这个题核心就是如何计算数字的长度。我们可以枚举 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE,分别与这些值比较,Integer.stringSize就是如此实现的。对于负数需要将长度加1 int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);

也可以循环除以10直到0来计算长度。一个优化的点是不必求每一个数的长度,只需要求出最大值 max 与 最小值 min的长度即可。网友还提供了一个小技巧可以减少对较小值的长度计算。选取 max/10-min 中的较大值来计算长度 l,取 l+1。解释如下:如果 min > 0 那么 -min < 0,我们取到 max/10 的长度,所以长度为 l+1。如果 min < 0,并且 -min > max/10,我们取到 -min,长度需要加上负号,即 l+1。这里需要解释一下 -min < max && -min > max/10 的情况,这时 max 的长度与 -min 的长度加1 是相同的。而如果 min < 0,且 -min < max/10,取 max/10 长度减小了1,所以取 l+1

代码

/**
 * @date 2024-04-27 18:39
 */
public class FindColumnWidth2639 {

    int[] sizeTable = {9, 99, 999, 9999, 99999, 999999, 9999999,
            99999999, 999999999, Integer.MAX_VALUE};

    int stringSize(int x) {
        int j;
        if (x < 0) {
            x = -x;
            j = 2;
        } else {
            j = 1;
        }
        for (int i = 0; ; i++) {
            if (x <= sizeTable[i]) {
                return i + j;
            }
        }
    }

    public int[] findColumnWidth(int[][] grid) {
        int n = grid[0].length;
        int[] res = new int[n];
        int[] negative = new int[n];
        for (int[] ints : grid) {
            for (int i = 0; i < n; i++) {
                if (ints[i] < negative[i]) {
                    negative[i] = ints[i];
                } else if (ints[i] > res[i]) {
                    res[i] = ints[i];
                }
            }
        }
        for (int i = 0; i < n; i++) {
            res[i] = Math.max(stringSize(res[i]), stringSize(negative[i]));
        }
        return res;
    }

}

性能

2739.总行驶距离

目标

卡车有两个油箱。给你两个整数,mainTank 表示主油箱中的燃料(以升为单位),additionalTank 表示副油箱中的燃料(以升为单位)。

该卡车每耗费 1 升燃料都可以行驶 10 km。每当主油箱使用了 5 升燃料时,如果副油箱至少有 1 升燃料,则会将 1 升燃料从副油箱转移到主油箱。

返回卡车可以行驶的最大距离。

注意:从副油箱向主油箱注入燃料不是连续行为。这一事件会在每消耗 5 升燃料时突然且立即发生。

示例 1:

输入:mainTank = 5, additionalTank = 10
输出:60
解释:
在用掉 5 升燃料后,主油箱中燃料还剩下 (5 - 5 + 1) = 1 升,行驶距离为 50km 。
在用掉剩下的 1 升燃料后,没有新的燃料注入到主油箱中,主油箱变为空。
总行驶距离为 60km 。

示例 2:

输入:mainTank = 1, additionalTank = 2
输出:10
解释:
在用掉 1 升燃料后,主油箱变为空。
总行驶距离为 10km 。

说明:

  • 1 <= mainTank, additionalTank <= 100

思路

这里面的关键点在于第一次消耗完5升燃料之后会触发副油箱注入,后面消耗的5升燃料中包含有副油箱注入的1升。即第一次消耗完成后主油箱每消耗4升就会触发燃料注入。

例如主油箱有9L,副油箱有1L,那么总共可以消耗10L燃料。

注入燃料的总次数等于 1 + (mainTank -5)/4 = (mainTank - 1)/4

代码

/**
 * @date 2024-04-25 0:11
 */
public class DistanceTraveled2739 {

    public int distanceTraveled_v1(int mainTank, int additionalTank) {
        return 10 * (mainTank + Math.min((mainTank - 1) / 4, additionalTank));
    }

    public int distanceTraveled(int mainTank, int additionalTank) {
        int res = 0;
        while (mainTank >= 5) {
            res += 5;
            mainTank -= 5;
            if (additionalTank > 0) {
                additionalTank--;
                mainTank++;
            }
        }
        res += mainTank;
        return res * 10;
    }
}

性能

2923.找到冠军I

目标

一场比赛中共有 n 支队伍,按从 0 到 n - 1 编号。

给你一个下标从 0 开始、大小为 n * n 的二维布尔矩阵 grid 。对于满足 0 <= i, j <= n - 1i != j 的所有 i, j :如果 grid[i][j] == 1,那么 i 队比 j 队 强 ;否则,j 队比 i 队 强 。

在这场比赛中,如果不存在某支强于 a 队的队伍,则认为 a 队将会是 冠军 。

返回这场比赛中将会成为冠军的队伍。

示例 1:

输入:grid = [[0,1],[0,0]]
输出:0
解释:比赛中有两支队伍。
grid[0][1] == 1 表示 0 队比 1 队强。所以 0 队是冠军。

示例 2:

输入:grid = [[0,0,1],[1,0,1],[0,0,0]]
输出:1
解释:比赛中有三支队伍。
grid[1][0] == 1 表示 1 队比 0 队强。
grid[1][2] == 1 表示 1 队比 2 队强。
所以 1 队是冠军。

说明:

  • n == grid.length
  • n == grid[i].length
  • 2 <= n <= 100
  • grid[i][j] 的值为 0 或 1
  • 对于所有 i, grid[i][i] 等于 0.
  • 对于满足 i != j 的所有 i, j ,grid[i][j] != grid[j][i] 均成立
  • 生成的输入满足:如果 a 队比 b 队强,b 队比 c 队强,那么 a 队比 c 队强

思路

行和为n-1或者列全为0的为冠军。

代码

/**
 * @date 2024-04-12 0:13
 */
public class FindChampion2923 {
    public int findChampion(int[][] grid) {
        int n = grid.length;
        for (int i = 0; i < n; i++) {
            int sum = 0;
            for (int j = 0; j < n; j++) {
                sum += grid[i][j];
            }
            if (sum == n - 1) {
                return i;
            }
        }
        return -1;
    }
}

性能

2529.正整数和负整数的最大计数

目标

给你一个按 非递减顺序 排列的数组 nums ,返回正整数数目和负整数数目中的最大值。

  • 换句话讲,如果 nums 中正整数的数目是 pos ,而负整数的数目是 neg ,返回 pos 和 neg二者中的最大值。

注意:0 既不是正整数也不是负整数。

示例 1:

输入:nums = [-2,-1,-1,1,2,3]
输出:3
解释:共有 3 个正整数和 3 个负整数。计数得到的最大值是 3 。

示例 2:

输入:nums = [-3,-2,-1,0,0,1,2]
输出:3
解释:共有 2 个正整数和 3 个负整数。计数得到的最大值是 3 。

示例 3:

输入:nums = [5,20,66,1314]
输出:4
解释:共有 4 个正整数和 0 个负整数。计数得到的最大值是 4 。

说明:

  • 1 <= nums.length <= 2000
  • -2000 <= nums[i] <= 2000
  • nums 按 非递减顺序 排列。

进阶:你可以设计并实现时间复杂度为 O(log(n)) 的算法解决此问题吗?

思路

这个题简单做法就是循环计数,O(n)的时间复杂度。O(log(n))需要使用二分查找。Arrays.binarySearch() 是处理不了非严格递增的情况的,如果查找的key有多个,无法保证返回的是哪一个,通常就是中间的那一个。

这里的难点是弄清楚当有多个相同值的时候如何找到第一个。具体来说就是 low 与 high 的更新以及结束条件,自己可以用一个具体的例子来模拟查找的过程。

  • 结束条件是 low == high
  • 如果寻找下界,那么 nums[middle] >= key 更新 high = middlenums[middle] < key 更新 low = middle + 1。返回第一个大于等于key的index。
  • 如果寻找上界,那么 nums[middle] > key 更新 high = middle - 1nums[middle] <= key 更新 low = middle 寻找上界的话只需将等号去掉即可,得到的是第一个小于等于key的index+1。

为什么要 +1 或者 -1? 因为middle指的位置不等于key,不是我们要找的值,不应该再出现在下一次的查找范围内,这是能说的通的。其实最核心的目的是保证最终low、high指向同一个位置,防止出现low与high相差1,但是middle指向的位置也无法触发更新的情况。以[-3,0,0,3,4]为例,我们要找0的下界,如果low的更新不 +1,那么最终就是 low,middle 指向 -3,high 指向第一个0,这不是我们想要的。

代码


/**
 * @date 2024-04-09 1:33
 */
public class MaximumCount2529 {

    public int maximumCount(int[] nums) {
        int pos = 0;
        int neg = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] < 0) {
                neg++;
            } else if (nums[i] > 0) {
                pos = nums.length - i;
                break;
            }
        }
        return Math.max(pos, neg);
    }

    /**
     * 二分查找
     */
    public int maximumCount_v2(int[] nums) {
        int neg = bs(nums, 0);
        int pos = bs(nums, 1);
        return Math.max(neg, nums.length - pos);
    }

    public int bs(int[] nums, int key) {
        int low = 0, high = nums.length;
        while (low != high) {
            int middle = (low + high) >> 1;
            if (nums[middle] >= key) {
                high = middle;
            } else {
                low = middle + 1;
            }
        }
        return low;
    }
}

性能

1379.找出克隆二叉树中的相同节点

目标

给你两棵二叉树,原始树 original 和克隆树 cloned,以及一个位于原始树 original 中的目标节点 target。

其中,克隆树 cloned 是原始树 original 的一个 副本 。

请找出在树 cloned 中,与 target 相同 的节点,并返回对该节点的引用(在 C/C++ 等有指针的语言中返回 节点指针,其他语言返回节点本身)。

注意:你 不能 对两棵二叉树,以及 target 节点进行更改。只能 返回对克隆树 cloned 中已有的节点的引用。

说明:

  • 树中节点的数量范围为 [1, 10^4] 。
  • 同一棵树中,没有值相同的节点。
  • target 节点是树 original 中的一个节点,并且不会是 null 。

进阶:如果树中允许出现值相同的节点,将如何解答?

思路

这道题挺简单的,这让我想起 100.相同的树 这道题,都是两棵树的同步遍历。

代码

/**
 * @date 2024-04-03 0:01
 */
public class GetTargetCopy1379 {
    public final TreeNode getTargetCopy(final TreeNode original, final TreeNode cloned, final TreeNode target) {
        if (original == null) {
            return null;
        } else if (target.equals(original)) {
            return cloned;
        } else {
            TreeNode res = getTargetCopy(original.left, cloned.left, target);
            if (res == null) {
                res = getTargetCopy(original.right, cloned.right, target);
            }
            return res;
        }
    }
}

性能