感觉难度标签真的没啥参考价值了。这个题通过率64.1%。比有的简单题还要高,复制粘贴的多了就把难度拉下来了。真正有思路自己写的难免会出错呀。像这种题,想到解题思路很困难。
分类: leetcode
2908.元素和最小的山形三元组I
目标
给你一个下标从 0 开始的整数数组 nums 。
如果下标三元组 (i, j, k) 满足下述全部条件,则认为它是一个 山形三元组 :
- i < j < k
- nums[i] < nums[j] 且 nums[k] < nums[j]
请你找出 nums 中 元素和最小 的山形三元组,并返回其 元素和 。如果不存在满足条件的三元组,返回 -1 。
示例 1:
输入:nums = [8,6,1,5,3]
输出:9
解释:三元组 (2, 3, 4) 是一个元素和等于 9 的山形三元组,因为:
- 2 < 3 < 4
- nums[2] < nums[3] 且 nums[4] < nums[3]
这个三元组的元素和等于 nums[2] + nums[3] + nums[4] = 9 。可以证明不存在元素和小于 9 的山形三元组。
示例 2:
输入:nums = [5,4,8,7,10,2]
输出:13
解释:三元组 (1, 3, 5) 是一个元素和等于 13 的山形三元组,因为:
- 1 < 3 < 5
- nums[1] < nums[3] 且 nums[5] < nums[3]
这个三元组的元素和等于 nums[1] + nums[3] + nums[5] = 13 。可以证明不存在元素和小于 13 的山形三元组。
示例 3:
输入:nums = [6,5,4,3,4,5]
输出:-1
解释:可以证明 nums 中不存在山形三元组。
提示:
- 3 <= nums.length <= 50
- 1 <= nums[i] <= 50
思路
它标的是一道简单题,但是在我思考的过程中甚至产生了自我怀疑。以至于后面有些抓狂,直接暴力求解,不考虑性能,不考虑优雅,什么都不顾,只要能做出来。好以此来证明自己不是那么傻。
这道题让我们求数组中满足某些条件的三元组之中和最小的那一个,满足的条件就是 小 大 小
,可以不连续。
暴力解法上来直接就排除了,什么动态规划直接pass。简单题需要这些吗?然后我就被上了一课,不用这些真想不出来。方法没有高低之分。
代码
/**
* @date 2024-03-29 0:52
*/
public class MinimumSum2908 {
public int minimumSum(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = Integer.MAX_VALUE;
dp[nums.length - 1] = Integer.MAX_VALUE;
for (int i = 1; i < nums.length; i++) {
int tmp = i;
int left = Integer.MAX_VALUE;
while (i >= 1) {
if (nums[tmp] > nums[i - 1]) {
left = Math.min(left, nums[i - 1]);
}
i--;
}
if (left == Integer.MAX_VALUE) {
dp[tmp] = Integer.MAX_VALUE;
i = tmp;
continue;
}
int right = Integer.MAX_VALUE;
i = tmp;
while (i < nums.length - 1) {
if (nums[tmp] > nums[i + 1]) {
right = Math.min(right, nums[i + 1]);
}
i++;
}
if (right != Integer.MAX_VALUE) {
dp[tmp] = left + nums[tmp] + right;
} else {
dp[tmp] = Integer.MAX_VALUE;
}
i = tmp;
}
Arrays.sort(dp);
return dp[0] == Integer.MAX_VALUE ? -1 : dp[0];
}
}
性能
1997.访问完所有房间的第一天
目标
你需要访问 n 个房间,房间从 0 到 n - 1 编号。同时,每一天都有一个日期编号,从 0 开始,依天数递增。你每天都会访问一个房间。
最开始的第 0 天,你访问 0 号房间。给你一个长度为 n 且 下标从 0 开始 的数组 nextVisit 。在接下来的几天中,你访问房间的 次序 将根据下面的 规则 决定:
- 假设某一天,你访问 i 号房间。
- 如果算上本次访问,访问 i 号房间的次数为 奇数 ,那么 第二天 需要访问 nextVisit[i] 所指定的房间,其中 0 <= nextVisit[i] <= i 。
- 如果算上本次访问,访问 i 号房间的次数为 偶数 ,那么 第二天 需要访问 (i + 1) mod n 号房间。
请返回你访问完所有房间的第一天的日期编号。题目数据保证总是存在这样的一天。由于答案可能很大,返回对 10^9 + 7 取余后的结果。
示例 1:
输入:nextVisit = [0,0]
输出:2
解释:
- 第 0 天,你访问房间 0 。访问 0 号房间的总次数为 1 ,次数为奇数。
下一天你需要访问房间的编号是 nextVisit[0] = 0
- 第 1 天,你访问房间 0 。访问 0 号房间的总次数为 2 ,次数为偶数。
下一天你需要访问房间的编号是 (0 + 1) mod 2 = 1
- 第 2 天,你访问房间 1 。这是你第一次完成访问所有房间的那天。
示例 2:
输入:nextVisit = [0,0,2]
输出:6
解释:
你每天访问房间的次序是 [0,0,1,0,0,1,2,...] 。
第 6 天是你访问完所有房间的第一天。
示例 3:
输入:nextVisit = [0,1,2,0]
输出:6
解释:
你每天访问房间的次序是 [0,0,1,1,2,2,3,...] 。
第 6 天是你访问完所有房间的第一天。
说明:
- n == nextVisit.length
- 2 <= n <= 10^5
- 0 <= nextVisit[i] <= i
思路
这道题用了3.5小时,也不知道花费这么多精力到底值不值得。这道题基本上是调试出来的,好多坑都没有考虑到。
说回这道题,有 0 到 n-1 共 n 个房间,每天可以访问一个房间,第 0 天访问 0 号房,然后根据当前房间的被访问次数来决定明天访问的房间。通俗来讲就是,如果当前房间cur被访问次数为奇数,访问包括当前房间在内的由参数指定的房间[0, cur]
;如果为偶数,则访问编号为cur+1
的房间。需要我们返回首次访问 n-1 房间是第几天。
我上来想都没想就直接按照题意把代码写出来了,主要是理解题意。不出所料,提交超时。
在第30个用例的时候超时了,参数是这样的 [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
,即每前进一个房间都退回到第0个房间从头开始。总共 74 个房间,由于每个房间需要访问偶数次才可以前进,因此 f(n) = 2(f(n-1) + 1)
其中 f(0)=0, f(1)=2
,n为正整数。两边都加上2得f(n) + 2 = 2(f(n-1) + 2)
,根据等比数列公式an = a1*q^(n-1)
,代入a1 = f(1) + 2 = 4, q = 2
,得 f(n) = 2^(n+1) - 2
。访问到编号为 74-1
的房间要等到第 2^74 -2
天。计算这个主要是为了说明问题的规模,按照题目描述去循环肯定是不行的。
很明显我们需要使用动态规划来求解。那么状态转移方程如何写?哪些子问题是重复计算的?经过观察我们知道,首次访问到某一房间的时候,之前所有房间的访问次数一定是偶数。当我们根据参数向后返回的时候,相当于是从指定的房间到当前房间又重新经历了一遍,因为参数指定的房间是固定的。于是我们可以保存首次访问到某房间是第几天,当后退到某房间之后就不用再重新循环了,直接计算天数,即天数累加上 dp[max] - dp[back] + 1
。
上面的算法是题目完成之后才弄明白的,写代码的时候遇到了许多坑:
-
首先是初值问题,dp[0]到底取1还是0。刚开始没有想明白dp[n]的含义,根据上面的定义,第一次访问到0号房间是第0天,应该取0。但是程序里需要在第一次访问到房间的时候,天数加1然后赋值给dp,但是对于第0个房间,会错误地将dp[0]改为1,那么后续的天数计算就需要多加1,因为少减了1天(这个问题可以将初始的maxRoom置为0,可以回避该分支的执行)。刚开始我根据错误的初始条件,观察上面超时的用例写的状态转移方程为
dp[max] - dp[back] + 2
,提交之后发现有的测试用例给出的结果比预期结果多了1,排查了半天才发现问题。 -
日期编号溢出的问题,刚开始dp与day都使用的int类型,在计算状态转移方程的时候有可能溢出,修改为long之后能够通过一些测试用例,但是后面还是会出现负值。这时我开始注意到这个问题了,就是dp保存的是取模之后的值,相减的时候会不会有问题?我们不可能无限制地扩展bit位,不可能存储准确的数值,取模是必要的。但是结果为什么还是有负值?这时灵光一闪,发现返回的负值只要加上MOD就可以得到正确的结果。这一定不是偶然的,后来才明白是因为后面的天数取模变小了,相减出现了负值,并不是溢出。但是,即便是负值一直取模,到最后返回结果的时候再加MOD也是正确的(看了官方题解,是在相减的时候加上了MOD)。
注意以下事实:
在java中进行取模运算时,结果的符号与被除数(即左边的操作数)相同:
-7 % 15 = -7
7 % -15 = 7
-7 % -15 = -7
-7 % 3 = -1
7 % -3 = 1
-7 % -3 = -1
(-7 + 15) % 15 = -7 + 15
补码(Two's Complement)是有符号数的一种二进制表示方式,主要用于计算机系统中数值的表示和存储。这种表示方式具有统一处理符号位和数值位、统一处理加法和减法的优点。
补码命名中的“2的补数”描述了补码的一个特性:一个补码可以通过被2的位长次方减去,得到它的相反数。例如,对于一个4位的补码,0001(十进制为1)的相反数可以通过计算2^4 - 0001得到,结果为1111(十进制为-1)。
在计算机中,正数的补码与其原码相同,而负数的补码则是通过对其绝对值的原码取反后加1得到。这种转换过程与原码的转换过程几乎相同,不需要额外的硬件电路。
补码的使用在电路设计上相当方便,因为只要有加法电路及补码电路,就可以完成各种有号数的加法和减法运算。
对一个正数取反加1,可以得到其相反数的补码。
对一个负数取反加1,可以得到其绝对值。因为负数a的补码就是
2^n - |a|
。 其中2^n 可以想象成 n 个bit位均为1的二进制数加1。用所有bit位为1的数去减任意数就是取反,因为肯定是1变0,0变1。最后再加上多出来的1,就得到了补码。对n位2进制数(不考虑符号位)求补码其实就是求相对于2^n的补数。
-
循环的结束条件问题,最开始使用的结束条件
dp[roomNums - 1] == 0
,判断是否是第一次访问使用的是dp[maxRoom] == 0
。这就有问题了,第233/239
个测试用例很特殊,因为结束时的天数刚好等于MOD,取模后为0,导致程序无法结束。于是后面改成了根据访问次数是否为0来判断。但是条件修改之后,访问次数在哪里累加也成了关键,如果在判断之前加就不对,牵一发而动全身。
代码
/**
* @date 2024-03-28 0:02
*/
public class FirstDayBeenInAllRooms1997 {
private static final int MOD = 1000000007;
public int firstDayBeenInAllRooms_v1(int[] nextVisit) {
int roomNums = nextVisit.length;
int[] visitCount = new int[roomNums];
long[] dp = new long[roomNums];
dp[0] = 0;
long day = 0;
int currentRoom = 0;
int maxRoom = 0;
visitCount[currentRoom] = 1;
while (visitCount[roomNums - 1] == 0) {
if (visitCount[currentRoom] % 2 == 1) {
currentRoom = nextVisit[currentRoom];
} else {
currentRoom = (currentRoom + 1) % roomNums;
maxRoom = Math.max(currentRoom, maxRoom);
}
if (visitCount[maxRoom] == 0) {
visitCount[currentRoom] += 1;
day = (day + 1) % MOD;
dp[currentRoom] = day;
} else {
if (currentRoom != maxRoom) {
day = (day + dp[maxRoom] - dp[currentRoom] + 1L) % MOD;
currentRoom = maxRoom;
} else {
day = (day + 1) % MOD;
}
visitCount[currentRoom]++;
}
}
return (int) (day < 0 ? MOD + day : day);
}
public static void main(String[] args) {
FirstDayBeenInAllRooms1997 main = new FirstDayBeenInAllRooms1997();
System.out.println(main.firstDayBeenInAllRooms_v1(new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
System.out.println(FastPowerMod.fastPowerMod(2, 74, MOD));
// System.out.println(main.firstDayBeenInAllRooms_v1(new int[]{0,1,2,0}));
// System.out.println(main.firstDayBeenInAllRooms_v1(new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 8}));
// System.out.println(main.firstDayBeenInAllRooms_v1(new int[]{0, 0, 1, 2, 4, 0, 1, 6, 0, 0, 2, 3, 4, 3, 4, 11, 6, 0, 16, 14, 20, 16, 9, 9, 1, 8, 8, 4, 14, 13, 5, 12, 8, 18, 27, 34, 36, 13, 10, 35, 13, 31, 13, 29, 2, 45, 17, 30, 10, 18, 41, 14, 41, 22, 2, 4, 1, 15, 27, 35, 12, 10, 46, 25, 61, 8, 65, 57, 48, 61, 8, 35, 2, 66, 47, 5, 54, 76, 73, 51, 13, 64, 15, 2}));
// System.out.println(main.firstDayBeenInAllRooms_v1(new int[]{0,0,0,0,2,2,1,2,3,8,9,5,1,6,8,5,10,5,18,2,15,7,22,4,6,10,19,16,3,25,12,28,1,27,25,25,35,16,7,23,37,8,28,8,18,41,36,29}));
}
性能
2671.频率跟踪器
目标
请你设计并实现一个能够对其中的值进行跟踪的数据结构,并支持对频率相关查询进行应答。
实现 FrequencyTracker 类:
- FrequencyTracker():使用一个空数组初始化 FrequencyTracker 对象。
- void add(int number):添加一个 number 到数据结构中。
- void deleteOne(int number):从数据结构中删除一个 number 。数据结构 可能不包含 number ,在这种情况下不删除任何内容。
- bool hasFrequency(int frequency): 如果数据结构中存在出现 frequency 次的数字,则返回 true,否则返回 false。
示例 1:
输入
["FrequencyTracker", "add", "add", "hasFrequency"]
[[], [3], [3], [2]]
输出
[null, null, null, true]
解释
FrequencyTracker frequencyTracker = new FrequencyTracker();
frequencyTracker.add(3); // 数据结构现在包含 [3]
frequencyTracker.add(3); // 数据结构现在包含 [3, 3]
frequencyTracker.hasFrequency(2); // 返回 true ,因为 3 出现 2 次
示例 2:
输入
["FrequencyTracker", "add", "deleteOne", "hasFrequency"]
[[], [1], [1], [1]]
输出
[null, null, null, false]
解释
FrequencyTracker frequencyTracker = new FrequencyTracker();
frequencyTracker.add(1); // 数据结构现在包含 [1]
frequencyTracker.deleteOne(1); // 数据结构现在为空 []
frequencyTracker.hasFrequency(1); // 返回 false ,因为数据结构为空
示例 3:
输入
["FrequencyTracker", "hasFrequency", "add", "hasFrequency"]
[[], [2], [3], [1]]
输出
[null, false, null, true]
解释
FrequencyTracker frequencyTracker = new FrequencyTracker();
frequencyTracker.hasFrequency(2); // 返回 false ,因为数据结构为空
frequencyTracker.add(3); // 数据结构现在包含 [3]
frequencyTracker.hasFrequency(1); // 返回 true ,因为 3 出现 1 次
说明:
- 1 <= number <= 10^5
- 1 <= frequency <= 10^5
- 最多调用 add、deleteOne 和 hasFrequency 共计 2 * 10^5 次
思路
这道题要我们写一个数据结构,能够实时追踪已保存数字的出现频率。我们可以很方便地使用Map记录数字出现的频率,但是无法直接判断频率是否存在。只能遍历EntrySet一个一个找。
于是考虑再记录一个以频率为Key,相应频率的数字个数为value的Map,以便直接判断是否存在相应的频率。
那么第一个Map是否可以省略呢?当然不行,因为数字新增或删除后,相应的频率也会发生变化。如果不记录的话,无法更新第二个Map。
当数字出现频率增加,除了要累加第一个Map相应数字的频率,还要同时将第二个Map原频率对应数字的个数减1,新频率对应数字的个数加1。
当数字出现频率减少,除了要减小第一个Map相应数字的频率,还要同时将第二个Map原频率对应数字的个数减1,新频率对应数字的个数加1。
代码
/**
* @date 2024-03-21 8:57
*/
public class FrequencyTracker2671 {
class FrequencyTracker {
private final Map<Integer, Integer> elements;
private final Map<Integer, Integer> fRecord;
public FrequencyTracker() {
elements = new HashMap<>();
fRecord = new HashMap<>();
}
public void add(int number) {
Integer f = elements.get(number) == null ? 0 : elements.get(number);
if (f != 0) {
fRecord.put(f, fRecord.get(f) - 1);
}
elements.merge(number, 1, Integer::sum);
fRecord.merge(++f, 1, Integer::sum);
}
public void deleteOne(int number) {
Integer f = elements.get(number) == null ? 0 : elements.get(number);
if (f != 0) {
elements.put(number, f - 1);
fRecord.put(f, fRecord.get(f) - 1);
fRecord.merge(--f, 1, Integer::sum);
}
}
public boolean hasFrequency(int frequency) {
return fRecord.get(frequency) != null && fRecord.get(frequency) > 0;
}
}
性能
有网友写的变长数组性能更高一些,如果是直接根据题目最大范围创建数组,针对这些测试案例性能反而不好。
2642.设计可以求最短路径的图类
目标
给你一个有 n 个节点的 有向带权 图,节点编号为 0 到 n - 1 。图中的初始边用数组 edges 表示,其中 edges[i] = [fromi, toi, edgeCosti] 表示从 fromi 到 toi 有一条代价为 edgeCosti 的边。
请你实现一个 Graph 类:
- Graph(int n, int[][] edges) 初始化图有 n 个节点,并输入初始边。
- addEdge(int[] edge) 向边集中添加一条边,其中 edge = [from, to, edgeCost] 。数据保证添加这条边之前对应的两个节点之间没有有向边。
- int shortestPath(int node1, int node2) 返回从节点 node1 到 node2 的路径 最小 代价。如果路径不存在,返回 -1 。一条路径的代价是路径中所有边代价之和。
示例 1:
输入:
["Graph", "shortestPath", "shortestPath", "addEdge", "shortestPath"]
[[4, [[0, 2, 5], [0, 1, 2], [1, 2, 1], [3, 0, 3]]], [3, 2], [0, 3], [[1, 3, 4]], [0, 3]]
输出:
[null, 6, -1, null, 6]
解释:
Graph g = new Graph(4, [[0, 2, 5], [0, 1, 2], [1, 2, 1], [3, 0, 3]]);
g.shortestPath(3, 2); // 返回 6 。从 3 到 2 的最短路径如第一幅图所示:3 -> 0 -> 1 -> 2 ,总代价为 3 + 2 + 1 = 6 。
g.shortestPath(0, 3); // 返回 -1 。没有从 0 到 3 的路径。
g.addEdge([1, 3, 4]); // 添加一条节点 1 到节点 3 的边,得到第二幅图。
g.shortestPath(0, 3); // 返回 6 。从 0 到 3 的最短路径为 0 -> 1 -> 3 ,总代价为 2 + 4 = 6 。
说明:
- 1 <= n <= 100
- 0 <= edges.length <= n * (n - 1)
- edges[i].length == edge.length == 3
- 0 <= fromi, toi, from, to, node1, node2 <= n - 1
- 1 <= edgeCosti, edgeCost <= 10^6
- 图中任何时候都不会有重边和自环。
- 调用 addEdge 至多 100 次。
- 调用 shortestPath 至多 100 次。
思路
今天又手写了一遍Dijkstra算法,虽然通过了,但是性能差好多。对照着官网题解研究了一会,我也想把一些优化的点表达出来,但还是感觉没有理解透彻。又看了耗时最少的题解一脸懵,也看到了网友讲解的朴素 Dijkstra算法,有机会再研究补上吧。
代码
/**
* @date 2024-03-26 8:35
*/
public class Graph {
private final ArrayList<int[]>[] g;
private PriorityQueue<int[]> q;
private int[] dp;
private int n;
public Graph(int n, int[][] edges) {
g = new ArrayList[n];
for (int i = 0; i < g.length; i++) {
g[i] = new ArrayList<>();
}
for (int i = 0; i < edges.length; i++) {
g[edges[i][0]].add(new int[]{edges[i][1], edges[i][2]});
}
this.n = n;
}
public void addEdge(int[] edge) {
g[edge[0]].add(new int[]{edge[1], edge[2]});
}
public int shortestPath(int node1, int node2) {
q = new PriorityQueue<int[]>((a, b) -> a[1] - b[1]);
dp = new int[n];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[node1] = 0;
q.offer(new int[]{node1, 0});
while (!q.isEmpty()) {
int[] e = q.poll();
if (e[0] == node2) {
return dp[node2];
}
for (int[] edge : g[e[0]]) {
if (dp[e[0]] + edge[1] < dp[edge[0]]) {
dp[edge[0]] = dp[e[0]] + edge[1];
q.offer(new int[]{edge[0], dp[edge[0]]});
}
}
}
return dp[node2] == Integer.MAX_VALUE ? -1 : dp[node2];
}
}
性能
518.零钱兑换II
目标
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:
输入:amount = 10, coins = [10]
输出:1
说明:
- 1 <= coins.length <= 300
- 1 <= coins[i] <= 5000
- coins 中的所有值 互不相同
- 0 <= amount <= 5000
思路
// todo
代码
/**
* @date 2024-03-25 8:31
*/
public class CoinChange {
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int coin : coins) {
for (int j = coin; j <= amount; j++) {
dp[j] += dp[j - coin];
}
}
return dp[amount];
}
public static void main(String[] args) {
CoinChange main = new CoinChange();
// int amount = 5;
int amount = 500;
int[] coins = new int[]{1, 2, 5};
// System.out.println(main.change(amount, coins));
// System.out.println(main.change(amount, coins));
System.out.println(main.change(amount, coins));
}
}
性能
// todo
322.零钱兑换
目标
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
说明:
- 1 <= coins.length <= 12
- 1 <= coins[i] <= 2^31 - 1
- 0 <= amount <= 10^4
思路
// todo
思考的方向错了,试图用贪心算法枚举可能的组合。做法是优先选面值最大的,取得余数,再计算下一个面值的余数,直到余数为0。但是这样得到的不一定是最优解。尝试将最大面值的个数减一,然后余数加上最大面值,重新计算。但是还是一样的,如果要调整的话,所有面值的个数都要调整,不能只调整最大的,后面的还用贪心,这样问题就不可解了。
代码
/**
* @date 2024-03-24 0:04
*/
public class CoinChange {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1];
Arrays.fill(dp, max);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
public static void main(String[] args) {
CoinChange main = new CoinChange();
// int[] coins = new int[]{3, 7};
int[] coins = new int[]{186, 419, 83, 408};
// System.out.println(main.coinChange(coins, 9));
System.out.println(main.coinChange(coins, 6249));
}
}
性能
// todo
2549. 统计桌面上的不同数字
目标
给你一个正整数 n ,开始时,它放在桌面上。在 10^9 天内,每天都要执行下述步骤:
- 对于出现在桌面上的每个数字 x ,找出符合 1 <= i <= n 且满足 x % i == 1 的所有数字 i 。
- 然后,将这些数字放在桌面上。
返回在 10^9 天之后,出现在桌面上的 不同 整数的数目。
注意:
- 一旦数字放在桌面上,则会一直保留直到结束。
- % 表示取余运算。例如,14 % 3 等于 2 。
示例 1:
输入:n = 5
输出:4
解释:最开始,5 在桌面上。
第二天,2 和 4 也出现在桌面上,因为 5 % 2 == 1 且 5 % 4 == 1 。
再过一天 3 也出现在桌面上,因为 4 % 3 == 1 。
在十亿天结束时,桌面上的不同数字有 2 、3 、4 、5 。
示例 2:
输入:n = 3
输出:2
解释:
因为 3 % 2 == 1 ,2 也出现在桌面上。
在十亿天结束时,桌面上的不同数字只有两个:2 和 3 。
说明:
1 <= n <= 100
思路
这虽然是个简单题,但也不是一眼就能看出答案的。甚至条件稍微改一下就变得麻烦了,比如将10^9天改为有限的几天。
说回这道题,开始向桌面上放一个数字,然后需要找到对桌面数字取模余1的数,第二天将其也放在桌面上,如此循环。
对于数字n来说,n-1与1肯定是满足条件的,然后n-1的约数也符合条件。
考虑到是经过10^9天,每一天都可以减1,那么最终桌面上肯定有n-1个数字(除非桌面上一开始就一个数字1)。
代码
直接返回 n == 1 ? 1 : n - 1
即可。
1793.好子数组的最大分数
目标
给你一个整数数组 nums (下标从 0 开始)和一个整数 k 。
一个子数组 (i, j) 的 分数 定义为 min(nums[i], nums[i+1], ..., nums[j]) * (j - i + 1) 。一个 好 子数组的两个端点下标需要满足 i <= k <= j 。
请你返回 好 子数组的最大可能 分数 。
示例 1:
输入:nums = [1,4,3,7,4,5], k = 3
输出:15
解释:最优子数组的左右端点下标是 (1, 5) ,分数为 min(4,3,7,4,5) * (5-1+1) = 3 * 5 = 15 。
示例 2:
输入:nums = [5,5,4,5,4,1,1,1], k = 0
输出:20
解释:最优子数组的左右端点下标是 (0, 4) ,分数为 min(5,5,4,5,4) * (4-0+1) = 4 * 5 = 20 。
提示:
- 1 <= nums.length <= 10^5
- 1 <= nums[i] <= 2 * 10^4
- 0 <= k < nums.length
思路
题目中定义的好子数组必须要包含下标k,且其元素最小值乘以它的长度应最大。相同长度的子数组其最小值通常不同,应取最小值中最大的,这样才能在窗口固定的情况下求得最大分数。
刚开始我把这个问题作为一个动态规划问题来求解:有一个窗口,在下标k的位置有一个固定轴,窗口可以左右滑动,拉伸,但窗口边缘不能越过k。然后求解窗口大小固定时,滑动窗口内最小元素取最大时的状态。接着扩展窗口,新窗口的取值依赖于上一窗口,只需在上一窗口的基础上左右各扩展一个元素进行比较即可。
但是我马上就遇到了问题,因为k的位置是不确定的,窗口左右滑动总会有一边先到达边界,然后怎么处理?上一个窗口取得较大的最小值可能是在k左侧,当窗口到达左侧边界后就无法再移动了,这样势必会有一部分覆盖到k右侧,我们无法再用一侧的最优解来掩盖另一侧了。而右边新加入窗口的元素与上一个状态选择的最小值无法确定新的最小值。因为窗口记录的是左右两侧的最优解,单独某一侧的状态并没有被记录。比如 nums=[10,9,8,7,6,5,3,2,2,4,9,4,11,3,4],k=5
,当窗口大小为6时,左侧的最小值是5,右侧最小值是2(但是我们并没有记录),我们记录的是较大的5。当窗口大小为7时,左侧窗口最小值为3(必须跨过k了),右侧新加入窗口的值是4,如果与上一个状态比较,我们可能会选择4,但是右侧最小值是2,我们应该选3。
于是我想可能需要分别记录左右两侧的状态。我们为什么要记录状态?上面记录状态是为了与新进入窗口的元素比较来选择最优解,我们现在记录左右两侧的什么呢?
随着思考的深入,我觉得应该放弃所谓滑动窗口这个概念了,不应该在左右两侧同时求解。
思考这个问题,窗口增大之后,其中元素的最小值会怎么变?反正最小值一定不会变大。于是只要新加入的元素比窗口内已经选定的最小值大就可以一直扩张,因为最小值没有变化,窗口大小越大分数就越大。当遇到比当前窗口内最小值小的元素时就需要比较窗口另一侧的值,哪边的更大就从哪边扩张。如此反复即可。
代码
/**
* @date 2024-03-19 0:16
*/
public class MaximumScore {
public int maximumScore_v2(int[] nums, int k) {
if (nums.length == 1) {
return nums[0];
}
int res = 0;
int l = k - 1, r = k + 1;
int lmin = nums[k], rmin = nums[k];
while (l >= 0 || r < nums.length) {
if (l >= 0) {
lmin = Math.min(lmin, nums[l]);
}
if (r < nums.length) {
rmin = Math.min(rmin, nums[r]);
}
if ((lmin >= rmin && l >= 0) || r >= nums.length) {
l--;
while (l >= 0 && lmin <= nums[l]) {
l--;
}
// r-l是窗口大小(不包括r),由于l多减了1,所以这里要减去
res = Math.max(res, lmin * (r - l - 1));
} else {
r++;
while (r < nums.length && rmin <= nums[r]) {
r++;
}
// r-l是窗口大小(不包括l)由于r多加了1,所以这里要减去
res = Math.max(res, rmin * (r - l - 1));
}
}
return res;
}
}
性能
303.区域和检索_数组不可变
目标
给定一个整数数组 nums,处理以下类型的多个查询:
计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left <= right
实现 NumArray 类:
- NumArray(int[] nums) 使用数组 nums 初始化对象
- int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )
示例 1:
输入:
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
输出:
[null, 1, -1, -3]
解释:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))
说明:
- 1 <= nums.length <= 10^4
- -10^5 <= nums[i] <= 10^5
- 0 <= i <= j < nums.length
- 最多调用 104 次 sumRange 方法
思路
这个题看到之后没多想,提交之后发现和别人的性能差了10倍。这里的技巧就是提前将和计算的结果保存起来,用的时候直接用 prefix[right+1] - prefix[left]
即可。因为数组不可变所以这样是可行的。
这里没有使用 prefix[right] - prefix[left-1]
因为可以省去left为0的判断,不过多占用了4字节。其实没有必要纠结这些,真要计较的话,当left为0时还少了两次减法呢,并且cpu指令执行也有分支预测,无需关注这些细节。
代码
/**
* @date 2024-03-18 8:36
*/
public class NumArray {
private final int[] prefixSum;
public NumArray(int[] nums) {
prefixSum = new int[nums.length + 1];
for (int i = 0; i < nums.length; i++) {
prefixSum[i + 1] = prefixSum[i] + nums[i];
}
}
public int sumRange(int left, int right) {
return prefixSum[right + 1] - prefixSum[left];
}
public static void main(String[] args) {
NumArray main = new NumArray(new int[]{-2, 0, 3, -5, 2, -1});
System.out.println(main.sumRange(0, 2));
System.out.println(main.sumRange(2, 5));
System.out.println(main.sumRange(0, 5));
}
}