0
点赞
收藏
分享

微信扫一扫

LeetCode 89 双周赛


2437. 有效时间的数目

给你一个长度为 ​​5​​​ 的字符串 ​​time​​​ ,表示一个电子时钟当前的时间,格式为 ​​"hh:mm"​​ 。最早 可能的时间是 ​​"00:00"​​ ,最晚 可能的时间是 ​​"23:59"​​ 。

在字符串 ​​time​​​ 中,被字符 ​​?​​ 替换掉的数位是 未知的 ,被替换的数字可能是 ​​0​​​ 到 ​​9​​ 中的任何一个。

请你返回一个整数 ​​answer​​​ ,将每一个 ​​?​​​ 都用 ​​0​​​ 到 ​​9​​ 中一个数字替换后,可以得到的有效时间的数目。

示例

输入:time = "?5:00"
输出:2
解释:我们可以将 ? 替换成 0 或 1 ,得到 "05:00" 或者 "15:00" 。注意我们不能替换成 2 ,因为时间 "25:00" 是无效时间。所以我们有两个选择。

思路

思路一:分类讨论,又称 战神,哈哈哈 ,一堆​​​if else​

// C++
class Solution {
public:
int countTime(string s) {
int ans = 1;
char h1 = s[0], h2 = s[1];
if (h1 == '?') {
if (h2 == '?') ans *= 24;
else if (h2 <= '3') ans *= 3; // 0 1 2
else ans *= 2; // 0 1
} else {
if (h2 == '?') {
//x?
if (h1 == '2') ans *= 4; //0 1 2 3
else ans *= 10;
}
}

char m1 = s[3], m2 = s[4];
if (m1 == '?') {
if (m2 == '?') ans *= 60;
else ans *= 6; // 0 1 2 3 4 5
} else {
if (m2 == '?') ans *= 10;
}
return ans;
}
};

思路二:暴力枚举+判断有效性

所有情况一共就​​24 * 60 = 1440​​种,可以直接枚举所有情况,然后判断是否有效。

// Java
class Solution {
public int countTime(String t) {
int ans = 0;
for (int h = 0; h < 24; h++) {
for (int m = 0; m < 60; m++) {
String s = String.format("%02d:%02d", h, m);
boolean valid = true;
for (int i = 0; i < 5; i++) {
char cs = s.charAt(i), ct = t.charAt(i);
if (ct != '?' && ct != cs) {
valid = false;
break;
}
}
if (valid) ans++;
}
}
return ans;
}
}

2438. 二的幂数组中查询范围内的乘积

给你一个正整数 ​​n​​ ,你需要找到一个下标从 0 开始的数组 ​​powers​​ ,它包含 最少 数目的 ​​2​​​ 的幂,且它们的和为 ​​n​​​ 。​​powers​​ 数组是 非递减 顺序的。根据前面描述,构造 ​​powers​​ 数组的方法是唯一的。

同时给你一个下标从 0 开始的二维整数数组 ​​queries​​​ ,其中 ​​queries[i] = [left_i, right_i]​​​ ,其中 ​​queries[i]​​​ 表示请你求出满足 ​​left_i <= j <= right_i​​​ 的所有 ​​powers[j]​​ 的乘积。

请你返回一个数组 ​​answers​​​ ,长度与 ​​queries​​​ 的长度相同,其中 ​​answers[i]​​​是第 ​​i​​​ 个查询的答案。由于查询的结果可能非常大,请你将每个 ​​answers[i]​​​ 都对 ​​10^9 + 7​取余

提示:

  • 1 <= n <= 10^9
  • 1 <= queries.length <= 10^5
  • 0 <= start[i] <= end[i] < powers.length

示例

输入:n = 15, queries = [[0,1],[2,2],[0,3]]
输出:[2,4,64]
解释:
对于 n = 15 ,得到 powers = [1,2,4,8] 。没法得到元素数目更少的数组。
第 1 个查询的答案:powers[0] * powers[1] = 1 * 2 = 2 。
第 2 个查询的答案:powers[2] = 4 。
第 3 个查询的答案:powers[0] * powers[1] * powers[2] * powers[3] = 1 * 2 * 4 * 8 = 64 。
每个答案对 10^9 + 7 得到的结果都相同,所以返回 [2,4,64] 。

思路

题目的描述挺抽象的:对于一个正数​​n​​,要找到包含最少数目的​​2​​​的幂,且它们和为​​n​​​。用人话来说,就是找到​​n​​​的二进制表示。比如​​n = 15​​​,其二进制表示为​​1111​​​,对应的​​powers​​​数组,对应的就是​​[2^0, 2^1, 2^2, 2^3]​​​。比如​​n = 9​​​,其二进制表示为​​1001​​​,对应的​​powers​​​数组,就是​​[2^0, 2^3]​​。

然后要求解的是对于每个区间​​[l, r]​​​,对区间内的每个坐标​​j​​​,求解所有​​powers[j]​​​ 的乘积。而我们观察到,​​powers​​数组中全是2的幂,多个2的幂的乘积,其实可以转变为幂次的和。

比如

那么我们可以将​​powers​​​数组转变为存储2的幂,比如对​​n = 9​​​,​​powers​​​数组我们不存​​[2^0, 2^3]​​​,而存​​[0, 3]​​​,只把幂次数存下来。那么对于每个区间​​[l, r]​​​,就可以用前缀和来将时间复杂度优化到 ,求出幂次数后,再使用快速幂求出结果。(其实这道题因为数据范围比较小,也可以不用快速幂)

// C++  568ms
class Solution {
public:
vector<int> productQueries(int n, vector<vector<int>>& queries) {
int MOD = 1e9 + 7;
vector<int> p;
for (int i = 0; i < 30; i++) {
if (n >> i & 1) p.push_back(i);
}
n = p.size();
for (int i = 1; i < n; i++) p[i] += p[i - 1]; // 前缀和

vector<int> ans;
for (auto &q : queries) {
int l = q[0], r = q[1];
int mi = l == 0 ? p[r] : p[r] - p[l - 1]; // 2的多少次方
// 计算2的mi次方
int t = 1;
for (int i = 0; i < mi; i++) {
t = (t * 2) % MOD;
}
ans.push_back(t);
}
return ans;
}
};

// C++ 
// 快速幂 296ms
typedef long long LL;
class Solution {
public:

int qmi(int a, int b, int c) {
LL res = 1;
while (b) {
if (b & 1) res = res * a % c;
a = (LL)a * a % c;
b >>= 1;
}
return (int) res;
}

vector<int> productQueries(int n, vector<vector<int>>& queries) {
int MOD = 1e9 + 7;
int p[30], k = 0;
for (int i = 0; i < 30; i++) {
if (n >> i & 1) p[k++] = i;
}

for (int i = 1; i < k; i++) p[i] += p[i - 1]; // 前缀和

vector<int> ans;
for (auto &q : queries) {
int l = q[0], r = q[1];
int mi = l == 0 ? p[r] : p[r] - p[l - 1]; // 2的多少次方
// 计算2的mi次方
int t = qmi(2, mi, MOD);
ans.push_back(t);
}
return ans;
}
};

2439. 最小化数组中的最大值

给你一个下标从 0 开始的数组 ​​nums​​​ ,它含有 ​​n​​ 个非负整数。

每一步操作中,你需要:

  • 选择一个满足​​1 <= i < n​​​ 的整数​​i​​​ ,且​​nums[i] > 0​​ 。
  • 将​​nums[i]​​ 减 1 。
  • 将​​nums[i - 1]​​ 加 1 。

你可以对数组执行 任意 次上述操作,请你返回可以得到的 ​​nums​​ 数组中 最大值 最小 为多少。

示例

输入:nums = [3,7,1,6]
输出:5
解释:
一串最优操作是:
1. 选择 i = 1 ,nums 变为 [4,6,1,6] 。
2. 选择 i = 3 ,nums 变为 [4,6,2,5] 。
3. 选择 i = 1 ,nums 变为 [5,5,2,5] 。
nums 中最大值为 5 。无法得到比 5 更小的最大值。
所以我们返回 5 。

思路

我的思路

题目中的操作,实际上是可以把数从右往左匀一下。比如​​[5, 8]​​,则可以把右侧的8减1,将左侧的5加1。即可以从右侧更大的数,匀给左侧更小的数,这样就能使得最大值变小。

通过举例子观察,可以发现规律,只要存在一个单调递增的区间,我们就可以把这个区间中右侧的数,匀给左侧更小的数,最终使得整个区间非常平均。比如有个区间​​[1 2 5 9 20]​​​,我们只需要计算一个平均数,这里是​​37/5=7...2​​​,则对于这个区间,我们可以最终将它变成​​[8 8 7 7 7]​​。

于是,周赛当晚,我初步的想法是,遍历一次​​nums​​​数组,对于每个单调递增的区间,计算出这个区间的平均值,再对所有单调递增区间的平均值,取一个​​max​​。

第一版代码:

// C++
typedef long long LL;
class Solution {
public:
int minimizeArrayValue(vector<int>& nums) {
int ans = 0, cnt = 1;
LL sum = nums[0];
// 统计单调递增区间, 并求出平均值
for (int i = 1; i < nums.size(); i++) {
if (nums[i - 1] < nums[i]) {
sum += nums[i];
cnt++;
} else {
LL t = sum / cnt;
if (sum % cnt != 0) t++;
ans = max(ans, (int)t);
cnt = 1;
sum = nums[i];
}
}
return ans;
}
};

提交发现WA了,根据错误样例​​[13,13,20,0,8,9,9]​​发现,不需要严格单调递增,只需要单调非递减即可。于是修改代码

typedef long long LL;
class Solution {
public:
int minimizeArrayValue(vector<int>& nums) {
int ans = 0, cnt = 1;
LL sum = nums[0];
// 统计单调递增区间, 并求出平均值
for (int i = 1; i < nums.size(); i++) {
if (nums[i - 1] <= nums[i]) { // 单调非递减即可
sum += nums[i];
cnt++;
} else {
LL t = sum / cnt;
if (sum % cnt != 0) t++;
ans = max(ans, (int)t);
cnt = 1;
sum = nums[i];
}
}
return ans;
}
};

提交又WA了,错误样例是​​[4,7,2,2,9,19,16,0,3,15]​

观察这组数据,发现只做一次遍历是不行的,模拟一下我的处理过程,从左往右遍历,第一个非递减区间是​​[4,7]​​​,将其变成​​[5,6]​​​;第二非递减区间是​​[2,2,9,19]​​​,将其变成​​[8,8,8,8]​​​;第三个非递减区间​​[16]​​​,不处理;第四个非递减区间​​[0,3,15]​​​,变成​​[6,6,6]​

那么最终整个数组是:​​[5,6,8,8,8,8,16,6,6,6]​​​,我们发现这个数组还可以继续操作,区间​​[5,6,8,8,8,8,16]​​​是非递减区间,可以匀成​​[8,8,8,8,9,9,9]​​​。最终数组是​​[8,8,8,8,9,9,9,6,6,6]​​。我们发现,需要对数组做多次遍历。直到什么时候停止呢?

假设某次遍历时,我们分别遇到了3个单调非递减区间,分别是​​s1​​​,​​s2​​​,​​s3​​,在每个区间内,我们可以直接将数字非常平均的分到每个位置,假设称这个操作为匀了一下。那么对​​s1​匀了一下后,能得到​​s1​​​区间内的一个最大值,设其为​​m1​​​,那么同理,对于​​s2​​,在匀了一下后,我们能得到一个最大值​​m2​​​,对​​s3​​​能得到一个​​m3​​,则只要后面的区间的最大值,大于前面区间的最大值,说明这个区间还可以往前。比如我们在匀了一下后,​​s1 = [5,5,5,6,6]​​​,​​s2 = [7,7,7,7,8]​​​,那么还需要进行下一次遍历。直到某一次遍历时,从左往右的每个非递减区间的​​m​​​,满足​​m1 >= m2 >= m3 >= m4 >= ....​​​,即,直到满足后出现的区间的最大值,不会超过前面区间的最大值。则可以停止操作。可以比较粗糙的看出,遍历整个数组的次数,不会超过,所以总的时间复杂度不会超过

// C++ 136ms
typedef long long LL;
class Solution {
public:
int minimizeArrayValue(vector<int>& nums) {
int ret = 1e9;
bool stop = false;
// 一直统计到, 整个区间是非递增, 结束
while (!stop) {
stop = true;
int ans = -1;
LL sum = nums[0];
int left = 0;
// 统计单调递增区间, 并求出平均值
for (int i = 1; i < nums.size(); i++) {
if (nums[i - 1] <= nums[i]) {
sum += nums[i];
}
if (nums[i - 1] > nums[i] || (nums[i - 1] <= nums[i] && i == nums.size() - 1)) {
int cnt = i - left;
if (nums[i - 1] <= nums[i] && i == nums.size() - 1) cnt++;
LL t = sum / cnt;
int needPlus = sum % cnt;
// printf("i = %d, cnt = %d, t = %lld, needPlus = %d\n", i, cnt, t, needPlus);
for (int k = 0; k < cnt; k++) {
if (k < needPlus) nums[k + left] = (int)t + 1;
else nums[k + left] = (int) t;
}
if (needPlus > 0) t++;
if (ans != -1 && t > ans) stop = false; // 后面的连续递增序列平均值大于前面的, 则下次还要继续迭代
ans = max(ans, (int)t);
left = i;
sum = nums[i];
}
}
// for (int i = 0; i < nums.size(); i++) printf("%d ", nums[i]);
// cout << endl;
ret = min(ret, ans);
}
return ret;
}
};

这道题在周赛当晚WA了3次,在最后10分钟时才提交成功,真是非常艰辛。

二分

其实本题还可以用二分来做。我们每次检查某个值,是否能够被整个数组所承受。

所谓一个数能被整个数组所承受的意思是:给定一个数​​x​​​,我们判断一下数组中的每个元素,能否通过操作,变成​​<= x​​。

若对于数组中的每个数,都能通过操作,最终变成​​<= x​​​,那么整个数组的最大值就是​​<= x​​的。

我们定义一个​​check​​​函数来检测某个数​​x​​​都否被数组承受。若能,则说明整个数组,在操作后的最大值,至多是​​x​​​;若不能承受,则说明整个数组,在操作后的最大值,至少是​​x + 1​​。如此,满足二分的性质,则我们可以用二分来求解答案。

对于​​check​​​函数的编写,有一种很巧妙的思路。我们将数组中的每个数看成是水的多少。则水只能从右侧,流向左侧(右侧数字减1,左侧数字加1,且是可以传递的)。那么对于​​> x​​​的那些数,我们要想办法将其多余的水,往左流。而左侧那些原本​​< x​​​的位置,便可以承接右侧流过来的水。则我们从左往右进行遍历,当遇到​​nums[i] < x​​​时,表示该位置可以承接右侧流过来的多余的水,能承接的最多的量是​​x - nums[i]​​​;而当遇到​​nums[i] > x​​​时,我们尝试将多余的水​​nums[i] - x​​​流到左边去,此时看一下左侧所有为止能承接的量的总和,若能承接下这多余的水,则该位置的数,能变成​​x​​​。如此遍历到末尾,如果整个过程中,每个位置都能保证至多变成​​<= x​​​,那么称​​x​​能被整个数组所承受。

时间复杂度

// C++ 124ms
class Solution {
public:

bool check(vector<int>& v, int k) {
long sum = 0;
for (auto &i : v) {
if (i <= k) sum += k - i;
else if (sum < i - k) return false; // 累积的量不能承受该位置多余的量, 直接返回false
else sum -= i - k; // 能承受, 则从能承受的量中减去
}
return true;
}

int minimizeArrayValue(vector<int>& nums) {
int m = 0;
for (auto &i : nums) m = max(m, i);
// 开始二分
int l = 0, r = m;
while (l < r) {
int mid = l + r >> 1;
if (check(nums, mid)) r = mid;
else l = mid + 1;
}
return l;
}
};

找规律

累加并计算平均值即可。

从左往右遍历,每遍历到一个位置,将该位置的数纳入考虑。若该位置的数,大于前面的最大值,则该位置的数,可以匀给前面的所有数,计算一个平均值(向上取整),作为新的最大值即可。若遇到该位置的数,小于等于前面的最大值,则该位置的数对答案没有影响。

时间复杂度

// C++  88ms
typedef long long LL;
class Solution {
public:
int minimizeArrayValue(vector<int>& nums) {
// 平均数向上取整
// n/a向上取整的计算方式为 (n + a - 1) / a
LL sum = 0, ans = 0;
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
ans = max(ans, (sum + i) / (i + 1));
}
return (int) ans;
}
};

2440. 创建价值相同的连通块

有一棵 ​​n​​​ 个节点的无向树,节点编号为 ​​0​​​ 到 ​​n - 1​​ 。

给你一个长度为 ​​n​​ 下标从 0 开始的整数数组 ​​nums​​​ ,其中 ​​nums[i]​​​ 表示第 ​​i​​​ 个节点的值。同时给你一个长度为 ​​n - 1​​​ 的二维整数数组 ​​edges​​​ ,其中 ​​edges[i] = [ai, bi]​​​ 表示节点 ​​ai​​​ 与 ​​bi​​ 之间有一条边。

你可以 删除 一些边,将这棵树分成几个连通块。一个连通块的 价值 定义为这个连通块中 所有 节点 ​​i​​​ 对应的 ​​nums[i]​​ 之和。

你需要删除一些边,删除后得到的各个连通块的价值都相等。请返回你可以删除的边数 最多 为多少。

示例

LeetCode 89 双周赛_leetcode_08

输入:nums = [6,2,2,2,6], edges = [[0,1],[1,2],[1,3],[3,4]] 
输出:2
解释:上图展示了我们可以删除边 [0,1] 和 [3,4] 。得到的连通块为 [0] ,[1,2,3] 和 [4] 。每个连通块的价值都为 6 。可以证明没有别的更好的删除方案存在了,所以答案为 2 。

提示:

  • ​1 <= n <= 2 * 10^4​
  • ​nums.length == n​
  • ​1 <= nums[i] <= 50​
  • ​edges.length == n - 1​
  • ​edges[i].length == 2​
  • ​0 <= edges[i][0], edges[i][1] <= n - 1​
  • ​edges​​ 表示一棵合法的树。

思路

注意是​​n​​​个节点的无向树,共有​​n - 1​​​条边,并且价值​​nums[i] > 0​​​。问题是删除一些边,将这个树切开,形成多个连通块,需要保证每个连通块的价值都相等。由于每个连通块价值相等,不妨设其为​​x​​​,设连通块个数为​​k​​​,若所有节点的价值的总和为​​sum​​​,容易得到这样的关系式:​​kx = sum​​​。由于​​nums[i] > 0​​​,则​​x​​​越小,连通块的个数越多,删除的边也就越多。并且​​x​​​必须是​​sum​​的一个约数。

则我们从小到大枚举​​sum​​​的全部约数​​x​​​,依次判断能够以​​x​​​将树切割成若干个连通块,使得每个连通块中的点的价值和都等于​​x​​。

因为是从小到大枚举的​​x​​​,则遇到的第一个满足条件的​​x​​,就找到了答案。

问题的关键是,如何判断能否以​​x​​将树切割成若干连通块。

考虑用DFS来做。若我们当前要凑出的连通块的价值和为​​x​​,我们从叶子节点往上来考虑。

  • 叶子节点的价值若​​< x​​,则叶子节点必须与其父节点连通,以增大连通块的价值;
    叶子节点与父节点连通后,形成一颗新的子树,继续往上加节点,直到遇到形成的子树的价值和​​= x​​,进行切断
  • 若叶子节点的价值​​= x​​,则一定要进行切断,因为每个点的价值都是​​> 0​​的,新加进来的点一定会使得连通块的价值和增大;
  • 若叶子节点的价值​​> x​​,则说明无法进行划分。

归纳一下,脑子里形成这样一个画面:从每个叶子节点开始,往上走,尝试进行切断或者合并。若子树价值和​​< x​​​则一定需要继续往上合并;若​​= x​​​则直接切断;若​​> x​​则无法划分。

我们用一个DFS来做,计算子树对父节点的价值贡献。若子树自身价值和​​= x​​​,则直接切断,相当于其对父节点的价值贡献为​​0​​​;若子树自身价值和​​< x​​​,则其必须要往上走,与父节点合并;若子树自身价值和​​> x​​​,则无法进行划分,返回​​-1​​。

我们以任意一个节点作为根节点,进行DFS,若最终返回​​0​​,说明整棵树可以被划分。

C++:

// C++
class Solution {
public:
vector<vector<int>> ad; // 树的邻接表表示
vector<int> nums;
/**
* fx是x的父节点, 加入这个参数是为了防止走回头路
* 当然也可以用 visited 数组来实现
* 0 以x为根节点的子树, 能够被划分
* -1 以x为根节点的子树, 不够被划分
* 否则返回以x为根节点的子树的价值和
**/
int dfs(int x, int fx, int target) {
int res = nums[x]; // 价值和
for (auto& i: ad[x]) {
// 无向图, 不往回走, 遇到父节点则跳过
if (i == fx) continue;
// 递归对子节点进行划分
int tmp = dfs(i, x, target);
if (tmp == -1) return -1; // 无法划分
res += tmp; //加上贡献, tmp只可能 < target 或者 = 0
}
if (res > target) return -1;
if (res == target) return 0;
return res;
}

int componentValue(vector<int>& nums, vector<vector<int>>& edges) {
int n = nums.size();
ad.resize(n);
this->nums = nums;
for (auto& e: edges) {
int a = e[0], b = e[1];
ad[a].push_back(b);
ad[b].push_back(a);
}

int sum = 0;
for (auto& v : nums) sum += v;

// 枚举sum的所有约数
for (int i = 1; i <= sum; i++) {
if (sum % i == 0) {
// 若能够被这个约数划分, 连通块的个数为 sum / i, 删去的边数 = 连通块个数 - 1
if (dfs(0, -1, i) == 0) return sum / i - 1;
}
}
return 0;
}
};

Java:

// Java
class Solution {
int[] h;
int[] e;
int[] ne;
int idx = 0;

int[] nums;

private void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}

private int dfs(int x, int fx, int target) {
int res = nums[x];
for (int i = h[x]; i != -1; i = ne[i]) {
int u = e[i];
if (u == fx) continue;
int t = dfs(u, x, target);
if (t == -1) return -1;
res += t;
}
if (res > target) return -1;
return res == target ? 0 : res;
}

public int componentValue(int[] nums, int[][] edges) {
// 建图
int n = nums.length;
h = new int[n];
e = new int[2 * n];
ne = new int[2 * n];
Arrays.fill(h, -1);
for (int[] e : edges) {
int a = e[0], b = e[1];
add(a, b);
add(b, a);
}
this.nums = nums;
// 整棵树的价值和
int sum = 0;
for (int i : nums) sum += i;
// 枚举约数
for (int i = 1; i <= sum; i++) {
if (sum % i == 0) {
// i是一个约数, 判断是否能以i来进行划分
if (dfs(0, -1, i) == 0) return sum / i - 1;
}
}
return 0;
}
}

总结

LeetCode 89 双周赛_i++_09

T1模拟;T2位运算+前缀和;T3找规律/二分;T4 图论DFS。

这次周赛,对我来说感觉难度不小。非常侥幸并吃力地做出了3题。还要继续努力啊。


举报

相关推荐

0 条评论