文章目录
- 双周赛105
- [2706. 购买两块巧克力](https://leetcode.cn/problems/buy-two-chocolates/)
- [2707. 字符串中的额外字符](https://leetcode.cn/problems/extra-characters-in-a-string/)
- [2708. 一个小组的最大实力值](https://leetcode.cn/problems/maximum-strength-of-a-group/)
- [2709. 最大公约数遍历](https://leetcode.cn/problems/greatest-common-divisor-traversal/)
双周赛105
2706. 购买两块巧克力
难度简单0
给你一个整数数组 prices
,它表示一个商店里若干巧克力的价格。同时给你一个整数 money
,表示你一开始拥有的钱数。
你必须购买 恰好 两块巧克力,而且剩余的钱数必须是 非负数 。同时你想最小化购买两块巧克力的总花费。
请你返回在购买两块巧克力后,最多能剩下多少钱。如果购买任意两块巧克力都超过了你拥有的钱,请你返回 money
。注意剩余钱数必须是非负数。
示例 1:
输入:prices = [1,2,2], money = 3
输出:0
解释:分别购买价格为 1 和 2 的巧克力。你剩下 3 - 3 = 0 块钱。所以我们返回 0 。
示例 2:
输入:prices = [3,2,3], money = 3
输出:3
解释:购买任意 2 块巧克力都会超过你拥有的钱数,所以我们返回 3 。
提示:
2 <= prices.length <= 50
1 <= prices[i] <= 100
1 <= money <= 100
贪心
class Solution {
public int buyChoco(int[] prices, int money) {
Arrays.sort(prices);
if(prices[0] +prices[1] > money)
return money;
return money - prices[0] - prices[1];
}
}
2707. 字符串中的额外字符
难度中等0
给你一个下标从 0 开始的字符串 s
和一个单词字典 dictionary
。你需要将 s
分割成若干个 互不重叠 的子字符串,每个子字符串都在 dictionary
中出现过。s
中可能会有一些 额外的字符 不在任何子字符串中。
请你采取最优策略分割 s
,使剩下的字符 最少 。
示例 1:
输入:s = "leetscode", dictionary = ["leet","code","leetcode"]
输出:1
解释:将 s 分成两个子字符串:下标从 0 到 3 的 "leet" 和下标从 5 到 8 的 "code" 。只有 1 个字符没有使用(下标为 4),所以我们返回 1 。
示例 2:
输入:s = "sayhelloworld", dictionary = ["hello","world"]
输出:3
解释:将 s 分成两个子字符串:下标从 3 到 7 的 "hello" 和下标从 8 到 12 的 "world" 。下标为 0 ,1 和 2 的字符没有使用,所以我们返回 3 。
提示:
1 <= s.length <= 50
1 <= dictionary.length <= 50
1 <= dictionary[i].length <= 50
dictionary[i]
和s
只包含小写英文字母。dictionary
中的单词互不相同。
记忆化搜索
class Solution {
Set<String> set;
int res;
int[][] cache;
String s;
public int minExtraChar(String s, String[] dictionary) {
set = new HashSet<>();
for(String d : dictionary){
set.add(d);
}
this.s = s;
res = s.length();
cache = new int[res + 1][res + 1];
for(int i = 0; i < res; i++){
Arrays.fill(cache[i], -1);
}
return dfs(0, s.length());
}
// 定义 dfs(i, last) 表示为枚举到s[i]时,s采取最优分割策略,剩下last个字符,那么
// 若当前s[i]不参与分割 dfs(i, last) = dfs(i+1, last)
// 若当前s[i]参与分割,以s[i]为分割首字母:dfs(i, last) = dfs(j+1, last - (j+1-i))
// ,其中j满足(set.contains(s[i,j+1]) and j < len(s))
public int dfs(int i, int last){
if(i == s.length()){
return last;
}
if(cache[i][last] != -1) return cache[i][last];
int res = dfs(i+1, last); // 当前字符不分割
// 当前字符分割
for(int j = i; j < s.length(); j++){
if(set.contains(s.substring(i, j+1)))
res = Math.min(res, dfs(j+1, last - (j+1 - i)));
}
return cache[i][last] = res;
}
}
记忆化搜索 ==> DP(递推)
不会记忆化转递推😭
换一种思路思考记忆化搜索
对于同个状态可以从多个状态转移过来的情况,用dp写是最好的,dp[i]表示从i开始到n的子字符串最少需要移除多少个字符,转移方程为
dp[i] = dp[i + 1] + 1
直接移除第i
个字符,从i+1
转移
dp[i] = min(dp[j]) if s[i:j] in d for j in range(i + 1,n)
不移除第i
个字符,需要在d
中找到子字符串匹配一起删除
class Solution {
Set<String> set;
int res;
int[] cache;
String s;
public int minExtraChar(String s, String[] dictionary) {
set = new HashSet<>();
for(String d : dictionary){
set.add(d);
}
this.s = s;
res = s.length();
cache = new int[res + 1];
Arrays.fill(cache, -1);
return dfs(0);
}
// 记忆化搜索时间复杂度与状态个数有关
public int dfs(int i){ // O(n) n个状态
if(i == s.length())
return 0; // 表示当前不需要移除字符了
if(cache[i] >= 0) return cache[i];
int ans = dfs(i+1) + 1; // 移除当前 i 位置字符,从dfs(i+1)转移
// 不移除当前i位置字符,需要从d中转移
for(int j = i; j < s.length(); j++){ // O(n)
if(set.contains(s.substring(i, j+1))) // O(n)
ans = Math.min(ans, dfs(j+1));
}
return cache[i] = ans;
}
}
时间复杂度:O(n^3)
转为递推
class Solution {
// 定义 f(i) 表示只分割前 i 个字符,最后能剩下几个字符
// 状态转移方程:
// f(i) = f(i-1) + 1, 这个字符时剩下的
// f(i) = f(j),若s[j+1, i] 在字典中
// 两者取最小值,即:f(i) = min(f(i-1)+1, f(j))
// 初值 f(0) = 0
// 最后返回f(n)
public int minExtraChar(String s, String[] dictionary) {
Set set = new HashSet<>();
for(String d : dictionary){
set.add(d);
}
int n = s.length();
int[] f = new int[n+1];
for(int i = 1; i <= n; i++){
f[i] = Integer.MAX_VALUE;
}
f[0] = 0;
for(int i = 1; i <= n; i++){
// i不参与分割,直接转移
f[i] = f[i-1] + 1;
// i参与分割,枚举所有[0,i]为起点的j
for(int j = 0; j < i; j++){
if(set.contains(s.substring(j, i)))
f[i] = Math.min(f[i], f[j]);
}
}
return f[n];
}
}
2708. 一个小组的最大实力值
难度中等0
给你一个下标从 0 开始的整数数组 nums
,它表示一个班级中所有学生在一次考试中的成绩。老师想选出一部分同学组成一个 非空 小组,且这个小组的 实力值 最大,如果这个小组里的学生下标为 i0
, i1
, i2
, … , ik
,那么这个小组的实力值定义为 nums[i0] * nums[i1] * nums[i2] * ... * nums[ik]
。
请你返回老师创建的小组能得到的最大实力值为多少。
示例 1:
输入:nums = [3,-1,-5,2,5,-9]
输出:1350
解释:一种构成最大实力值小组的方案是选择下标为 [0,2,3,4,5] 的学生。实力值为 3 * (-5) * 2 * 5 * (-9) = 1350 ,这是可以得到的最大实力值。
示例 2:
输入:nums = [-4,-5,-4]
输出:20
解释:选择下标为 [0, 1] 的学生。得到的实力值为 20 。我们没法得到更大的实力值。
提示:
1 <= nums.length <= 13
-9 <= nums[i] <= 9
排序 + 贪心
排序 + 贪心 小组最大实力值与元素位置无关,先排序,排序后,从左到右都是从负到正,从左边开始遍历到0值,若有两个负数则相乘计算答案,越小的负数相乘对答案贡献越大。从右边开始遍历乘上所有正数。
class Solution {
public long maxStrength(int[] nums) {
if(nums.length == 1) return nums[0];
Arrays.sort(nums);
long ans = 1l;
for(int i = 0; i+1 < nums.length && nums[i] < 0 && nums[i+1] < 0; i += 2){
ans *= nums[i] * nums[i+1];
}
// // 特判:[0,-1]、[-4,-5,-4]、[-1, -1]
if((ans == 1l && nums[nums.length-1] <= 0) && (nums[1] >= 0)) return nums[nums.length-1];
for(int i = nums.length-1; i >= 0 && nums[i] > 0; i -= 1){
ans *= nums[i];
}
return ans;
}
}
DFS枚举所有子集
class Solution {
// 枚举所有子集,计算最大答案
long ans = Long.MIN_VALUE;
int[] nums;
public long maxStrength(int[] nums) {
this.nums = nums;
dfs(0, 1l, true);
return ans;
}
// 因为要去掉空集,使用is_empty判断是否为空集,初始状态下是空集
public void dfs(int i, long prod, boolean is_empty){
if(i == nums.length){
if(!is_empty)
ans = Math.max(ans, prod);
return;
}
// 不选
dfs(i+1, prod, is_empty);
// 选
dfs(i+1, prod * nums[i], false);
}
}
动态规划(O(n))
class Solution {
/**
选或不选模型 ==> 动态规划:
由于有负数,考虑最小值和最大值:
最大值有四种情况转移:
mx[i] = mx[i-1] 不选nums[i]
= nums[i] nums[i] 自成最大值
= mx[i-1] * nums[i] 选nums[i]
= mn[i-1] * nums[i] 选nums[i]
*/
public long maxStrength(int[] nums) {
long max = nums[0], min = nums[0];
for(int i = 1; i < nums.length; i++){
long tmp = max;
max = Math.max(Math.max(max, nums[i]), Math.max(max * nums[i], min * nums[i]));
min = Math.min(Math.min(min, nums[i]), Math.min(tmp * nums[i], min * nums[i]));
}
return max;
}
}
2709. 最大公约数遍历
难度困难1
给你一个下标从 0 开始的整数数组 nums
,你可以在一些下标之间遍历。对于两个下标 i
和 j
(i != j
),当且仅当 gcd(nums[i], nums[j]) > 1
时,我们可以在两个下标之间通行,其中 gcd
是两个数的 最大公约数 。
你需要判断 nums
数组中 任意 两个满足 i < j
的下标 i
和 j
,是否存在若干次通行可以从 i
遍历到 j
。
如果任意满足条件的下标对都可以遍历,那么返回 true
,否则返回 false
。
示例 1:
输入:nums = [2,3,6]
输出:true
解释:这个例子中,总共有 3 个下标对:(0, 1) ,(0, 2) 和 (1, 2) 。
从下标 0 到下标 1 ,我们可以遍历 0 -> 2 -> 1 ,我们可以从下标 0 到 2 是因为 gcd(nums[0], nums[2]) = gcd(2, 6) = 2 > 1 ,从下标 2 到 1 是因为 gcd(nums[2], nums[1]) = gcd(6, 3) = 3 > 1 。
从下标 0 到下标 2 ,我们可以直接遍历,因为 gcd(nums[0], nums[2]) = gcd(2, 6) = 2 > 1 。同理,我们也可以从下标 1 到 2 因为 gcd(nums[1], nums[2]) = gcd(3, 6) = 3 > 1 。
示例 2:
输入:nums = [3,9,5]
输出:false
解释:我们没法从下标 0 到 2 ,所以返回 false 。
示例 3:
输入:nums = [4,3,12,8]
输出:true
解释:总共有 6 个下标对:(0, 1) ,(0, 2) ,(0, 3) ,(1, 2) ,(1, 3) 和 (2, 3) 。所有下标对之间都存在可行的遍历,所以返回 true 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 105
分解质因数 + 并查集
把 nums
中的每个位置看成一个点,把所有质数也都看成一个点。如果 nums[i]
被质数 p
整除,那么从位置点 i
向质数点 p
连一条边。因为每个数至多只能被 log
个质数整除,因此连边的总数是 O(nlogA)
的。
这样,问题就变为:检查所有位置点是否处于同一连通块内。用并查集解决即可。
nums 数组中 任意 两个满足 i < j 的下标 i 和 j ,是否存在若干次通行可以从 i 遍历到 j 。
其实换句话说就是判断这个nums数组是不是整体是一个连通分量,这个适合用并查集来做。
我们考虑什么情况下可以进行连边,很明显两两之间去算gcd然后去连边这部分的时间复杂度是O(n^2)的,无法通过本题。
我们可以让每个nums中的数分别与自身的质因数之间连一条边,这样当gcd(nums[i],nums[j]) >= 2
时,这时候i和j一定会通过一个质因数间接将他们连接起来。
代码实现的时候用了埃氏筛去做区间范围内每个数的快速求质因数,并查集中前MX个节点为质因数,后n个节点为数组中的下标,通过质因数的方式间接连接,最后判断连通性即可。
https://leetcode.cn/circle/discuss/lOcuHV/
class Solution {
private class DSU{
int[] parent;
public DSU(int N) {
parent = new int[N];
for (int i = 0; i < N; ++i) {
parent[i] = i;
}
}
public int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
public void union(int x, int y) {
parent[find(x)] = find(y);
}
}
public boolean canTraverseAllPairs(int[] nums) {
HashMap<Integer, Integer> map = new HashMap<>();
DSU uf = new DSU(nums.length);
for (int i = 0; i < nums.length; ++i) {
int t = nums[i];
for (int j = 2; j * j <= t; ++j) {
while (t % j == 0) {
t /= j;
if (!map.containsKey(j)) {
map.put(j, i);
} else {
uf.union(map.get(j), i);
}
}
}
if (t != 1) {
if (!map.containsKey(t)) {
map.put(t, i);
} else {
uf.union(map.get(t), i);
}
}
}
int f = uf.find(0);
for (int i = 0; i < nums.length; ++i) {
if (uf.find(i) != f) {
return false;
}
}
return true;
}
}