todo
分类: leetcode
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;
}
}
性能
3038.相同分数的最大操作数目I
目标
给你一个整数数组 nums ,如果 nums 至少 包含 2 个元素,你可以执行以下操作:
- 选择 nums 中的前两个元素并将它们删除。
一次操作的 分数 是被删除元素的和。
在确保 所有操作分数相同 的前提下,请你求出 最多 能进行多少次操作。
请你返回按照上述要求 最多 可以进行的操作次数。
示例 1:
输入:nums = [3,2,1,4,5]
输出:2
解释:我们执行以下操作:
- 删除前两个元素,分数为 3 + 2 = 5 ,nums = [1,4,5] 。
- 删除前两个元素,分数为 1 + 4 = 5 ,nums = [5] 。
由于只剩下 1 个元素,我们无法继续进行任何操作。
示例 2:
输入:nums = [3,2,6,1,4]
输出:1
解释:我们执行以下操作:
- 删除前两个元素,分数为 3 + 2 = 5 ,nums = [6,1,4] 。
由于下一次操作的分数与前一次不相等,我们无法继续进行任何操作。
说明:
- 2 <= nums.length <= 100
- 1 <= nums[i] <= 1000
思路
按照题目模拟即可。
代码
/**
* @date 2024-06-07 0:00
*/
public class MaxOperations3038 {
public int maxOperations(int[] nums) {
int cnt = 1;
int n = nums.length;
int i = 2;
int score = nums[0] + nums[1];
while (i < n - 1) {
int tmp = nums[i] + nums[i + 1];
if (tmp == score) {
i += 2;
cnt++;
} else {
break;
}
}
return cnt;
}
}
性能
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;
}
}
性能
1103.分糖果II
目标
排排坐,分糖果。
我们买了一些糖果 candies,打算把它们分给排好队的 n = num_people 个小朋友。
给第一个小朋友 1 颗糖果,第二个小朋友 2 颗,依此类推,直到给最后一个小朋友 n 颗糖果。
然后,我们再回到队伍的起点,给第一个小朋友 n + 1 颗糖果,第二个小朋友 n + 2 颗,依此类推,直到给最后一个小朋友 2 * n 颗糖果。
重复上述过程(每次都比上一次多给出一颗糖果,当到达队伍终点后再次从队伍起点开始),直到我们分完所有的糖果。注意,就算我们手中的剩下糖果数不够(不比前一次发出的糖果多),这些糖果也会全部发给当前的小朋友。
返回一个长度为 num_people、元素之和为 candies 的数组,以表示糖果的最终分发情况(即 ans[i] 表示第 i 个小朋友分到的糖果数)。
示例 1:
输入:candies = 7, num_people = 4
输出:[1,2,3,1]
解释:
第一次,ans[0] += 1,数组变为 [1,0,0,0]。
第二次,ans[1] += 2,数组变为 [1,2,0,0]。
第三次,ans[2] += 3,数组变为 [1,2,3,0]。
第四次,ans[3] += 1(因为此时只剩下 1 颗糖果),最终数组变为 [1,2,3,1]。
示例 2:
输入:candies = 10, num_people = 3
输出:[5,2,3]
解释:
第一次,ans[0] += 1,数组变为 [1,0,0]。
第二次,ans[1] += 2,数组变为 [1,2,0]。
第三次,ans[2] += 3,数组变为 [1,2,3]。
第四次,ans[0] += 4,最终数组变为 [5,2,3]。
说明:
- 1 <= candies <= 10^9
- 1 <= num_people <= 1000
思路
现在有一些糖果要分给一排小朋友,第一个小朋友分1个,第二个分2个,依次类推,即分配的糖果总比上一次分的多一个,如果不够就剩多少分多少。如果一轮下来没有分完,就接着从第一个小朋友开始依次分配。问最终每个小朋友能够分得的糖果数量。
我们可以很容易地模拟这个分配过程,记录当前分配的糖果数量,并计算剩余糖果数量直到0。
网友最快的题解使用了数学公式,使时间复杂度从O(num_people+sqrt(candies)) 降为O(num_people)。
代码
/**
* @date 2024-06-03 0:06
*/
public class DistributeCandies1103 {
public int[] distributeCandies_v1(int candies, int num_people) {
int[] res = new int[num_people];
// 足量发放人次,解不等式:(n+1)n/2 <= candies
int n = (int) ((Math.sqrt(candies * 8F + 1) - 1) / 2);
// 足量发放的糖果数量,num_people <= 1000,其实不会溢出
int distributed = (int) ((1L + n) * n / 2);
// 剩余糖数量
int remainder = candies - distributed;
// 所有小朋友都得到糖果的轮次
int allGetLoops = n / num_people;
// 最后一轮获得糖果的人数
int lastLoopNum = n % num_people;
for (int i = 0; i < num_people; i++) {
// 收到糖果的次数 = 所有小朋友都得到糖果的轮次 + 最后一轮是否发放
int times = (lastLoopNum > i ? 1 : 0) + allGetLoops;
// 等差数列求和,公差为 num_people,a1 = i + 1,n = times;
res[i] = ((times - 1) * num_people + i * 2 + 2) * times / 2;
}
// 如果是最后一个,将剩余的全部分配
res[lastLoopNum] += remainder;
return res;
}
public int[] distributeCandies(int candies, int num_people) {
int[] res = new int[num_people];
int cnt = 1;
while (candies > 0) {
for (int i = 0; i < num_people && candies > 0; i++) {
int num = Math.min(cnt++, candies);
candies -= num;
res[i] += num;
}
}
return res;
}
}
性能
575.分糖果
目标
Alice 有 n 枚糖,其中第 i 枚糖的类型为 candyType[i] 。Alice 注意到她的体重正在增长,所以前去拜访了一位医生。
医生建议 Alice 要少摄入糖分,只吃掉她所有糖的 n / 2 即可(n 是一个偶数)。Alice 非常喜欢这些糖,她想要在遵循医生建议的情况下,尽可能吃到最多不同种类的糖。
给你一个长度为 n 的整数数组 candyType ,返回: Alice 在仅吃掉 n / 2 枚糖的情况下,可以吃到糖的 最多 种类数。
示例 1:
输入:candyType = [1,1,2,2,3,3]
输出:3
解释:Alice 只能吃 6 / 2 = 3 枚糖,由于只有 3 种糖,她可以每种吃一枚。
示例 2:
输入:candyType = [1,1,2,3]
输出:2
解释:Alice 只能吃 4 / 2 = 2 枚糖,不管她选择吃的种类是 [1,2]、[1,3] 还是 [2,3],她只能吃到两种不同类的糖。
示例 3:
输入:candyType = [6,6,6,6]
输出:1
解释:Alice 只能吃 4 / 2 = 2 枚糖,尽管她能吃 2 枚,但只能吃到 1 种糖。
说明:
- n == candyType.length
- 2 <= n <= 10^4
- n 是一个偶数
- -10^5 <= candyType[i] <= 10^5
思路
统计糖果种类,然后与n/2比较,取其中的最小值。
可以使用 HashSet
来计算种类数量,题解中最快的解法是自定义了hash函数,将种类映射到数组下标,通过判断相应位置上的值来计数。
代码
/**
* @date 2024-06-02 9:26
*/
public class DistributeCandies575 {
public int distributeCandies(int[] candyType) {
int n = candyType.length;
Set<Integer> set = new HashSet<>();
for (int i = 0; i < n; i++) {
set.add(candyType[i]);
}
return Math.min(set.size(), n / 2);
}
public int distributeCandies_v1(int[] candyType) {
int res = 0;
int n = candyType.length;
byte[] mask = new byte[200001];
for (int i = 0; i < n; i++) {
int c = candyType[i];
// 由于类型可能为负数,所以使用c+100000
if (mask[c + 100000] == 0) {
mask[c + 100000] = 1;
res++;
if (res > n / 2) return n / 2;
}
}
return res;
}
}
性能
2928.给小朋友们分糖果I
目标
给你两个正整数 n 和 limit 。
请你将 n 颗糖果分给 3 位小朋友,确保没有任何小朋友得到超过 limit 颗糖果,请你返回满足此条件下的 总方案数 。
示例 1:
输入:n = 5, limit = 2
输出:3
解释:总共有 3 种方法分配 5 颗糖果,且每位小朋友的糖果数不超过 2 :(1, 2, 2) ,(2, 1, 2) 和 (2, 2, 1) 。
示例 2:
输入:n = 3, limit = 3
输出:10
解释:总共有 10 种方法分配 3 颗糖果,且每位小朋友的糖果数不超过 3 :(0, 0, 3) ,(0, 1, 2) ,(0, 2, 1) ,(0, 3, 0) ,(1, 0, 2) ,(1, 1, 1) ,(1, 2, 0) ,(2, 0, 1) ,(2, 1, 0) 和 (3, 0, 0) 。
说明:
- 1 <= n <= 50
- 1 <= limit <= 50
思路
将 n
颗糖果分给 3
位小朋友,每个小朋友分到的糖果数量不超过 limit
,求分配的方案数。注意,糖果必须分完,比如示例 1
,不存在分得糖果数量为 0
的情况。
- 第一个小朋友分到的糖果数为
a ∈ [0, Math.min(n, limit)]
- 第二个小朋友分到的糖果数为
b ∈ [Math.max(0, n - a - limit), Math.min(n - a, limit)]
- 第三个小朋友分到的糖果数为
n - a - b
代码
/**
* @date 2024-06-01 15:59
*/
public class DistributeCandies2928 {
public int distributeCandies(int n, int limit) {
int res = 0;
// 为第一个小朋友分配有0~limit种可能
for (int i = 0; i <= limit; i++) {
// 这时为第二个小朋友可以有n - i种可能,这里不要忘了不能超过limit
for (int j = 0; j <= Math.min(limit, n - i); j++) {
// 第三个小朋友,获取剩下的糖果,但不能超过limit
if(n - i - j <= limit){
res++;
}
}
}
return res;
}
}
性能
2965.找出缺失和重复的数字
目标
给你一个下标从 0 开始的二维整数矩阵 grid,大小为 n * n ,其中的值在 [1, n2] 范围内。除了 a 出现 两次,b 缺失 之外,每个整数都 恰好出现一次 。
任务是找出重复的数字a 和缺失的数字 b 。
返回一个下标从 0 开始、长度为 2 的整数数组 ans ,其中 ans[0] 等于 a ,ans[1] 等于 b 。
示例 1:
输入:grid = [[1,3],[2,2]]
输出:[2,4]
解释:数字 2 重复,数字 4 缺失,所以答案是 [2,4] 。
示例 2:
输入:grid = [[9,1,7],[8,9,2],[3,4,6]]
输出:[9,5]
解释:数字 9 重复,数字 5 缺失,所以答案是 [9,5] 。
说明:
2 <= n == grid.length == grid[i].length <= 50
1 <= grid[i][j] <= n * n
- 对于所有满足
1 <= x <= n * n
的 x ,恰好存在一个 x 与矩阵中的任何成员都不相等。 - 对于所有满足
1 <= x <= n * n
的 x ,恰好存在一个 x 与矩阵中的两个成员相等。 - 除上述的两个之外,对于所有满足
1 <= x <= n * n
的 x ,都恰好存在一对 i, j 满足0 <= i, j <= n - 1
且grid[i][j] == x
。
思路
记录元素的出现次数,选出其中出现次数为2和0的值即可。
网友还给出了一些位运算与数学公式求解的方法,可以降低空间复杂度,不用对每个元素计次,而是使用异或和与数学公式求解。
异或和求解的关键是如何区分得到的 a ^ b
,由于a 与 b 不同,异或的结果至少有一个非0位,可以根据该bit位分组,这样每一组中就只含有a或b。
至于数学公式求解的关键是,通过各项和的差可以得到 b - a
,各项平方和的差可以得到 b^2 - a^2
,进而求得 b + a
,解方程组可得a、b。
代码
/**
* @date 2024-05-31 0:12
*/
public class FindMissingAndRepeatedValues2965 {
/**
* 公式求和 1,2,……,n^2 以及 1^2,2^2,……,n^2
* 然后遍历求和,二者之差为b - a 和 b^2 - a^2
* 进而可以求得b + a,解方程可得a、b
*/
public int[] findMissingAndRepeatedValues_v2(int[][] grid) {
int n = grid.length;
int n2 = n * n;
long sum = 0;
long sum2 = 0;
for (int[] row : grid) {
for (int i : row) {
sum += i;
sum2 += i * i;
}
}
long bsuba = n2 * (n2 + 1L) / 2 - sum;
long b2suba2 = n2 * (n2 + 1L) * (2 * n2 + 1) / 6 - sum2;
long badda = b2suba2 / bsuba;
long b = (badda + bsuba) / 2L;
long a = badda - b;
return new int[]{(int) a, (int) b};
}
/**
* 异或和
*/
public int[] findMissingAndRepeatedValues_v1(int[][] grid) {
int n = grid.length;
int n2 = n * n;
int prefix = 0;
for (int i = 1; i <= n2; i++) {
prefix ^= i;
}
int tmp = 0;
for (int[] row : grid) {
for (int element : row) {
tmp ^= element;
}
}
int axorb = prefix ^ tmp;
int shift = Integer.numberOfTrailingZeros(axorb);
int[] res = new int[2];
// 由于axorb至少有1bit是不同的,那么根据该bit分组,这样每一组里面a b是分开的
for (int i = 1; i <= n2; i++) {
res[i >> shift & 1] ^= i;
}
for (int[] row : grid) {
for (int element : row) {
res[element >> shift & 1] ^= element;
}
}
// 由于顺序是不确定的,判断出现的为a
for (int[] row : grid) {
for (int element : row) {
if (element == res[0]){
return res;
}
}
}
// 调换位置
return new int[]{res[1], res[0]};
}
public int[] findMissingAndRepeatedValues(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int l = m * n + 1;
int[] occurrence = new int[l];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
occurrence[grid[i][j]]++;
}
}
int[] res = new int[2];
for (int i = 1; i < l; i++) {
if (occurrence[i] == 0) {
res[1] = i;
} else if (occurrence[i] == 2) {
res[0] = i;
}
}
return res;
}
}
性能
2982.找出出现至少三次的最长特殊子字符串 II
和昨天的题一样,只不过数据范围变了