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" 是无效时间。所以我们有两个选择。
思路
思路一:分类讨论,又称
O
(
1
)
O(1)
O(1) 战神,哈哈哈 ,一堆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的幂的乘积,其实可以转变为幂次的和。
比如 2 0 × 2 2 × 2 3 × 2 5 = 2 0 + 2 + 3 + 5 2^0 × 2^2 × 2^3 × 2^5 = 2^{0 + 2 + 3 + 5} 20×22×23×25=20+2+3+5
那么我们可以将powers
数组转变为存储2的幂,比如对n = 9
,powers
数组我们不存[2^0, 2^3]
,而存[0, 3]
,只把幂次数存下来。那么对于每个区间[l, r]
,就可以用前缀和来将时间复杂度优化到
O
(
1
)
O(1)
O(1),求出幂次数后,再使用快速幂求出结果。(其实这道题因为数据范围比较小,也可以不用快速幂)
// 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 >= ....
,即,直到满足后出现的区间的最大值,不会超过前面区间的最大值。则可以停止操作。可以比较粗糙的看出,遍历整个数组的次数,不会超过
l
o
g
n
logn
logn,所以总的时间复杂度不会超过
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
// 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
能被整个数组所承受。
时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
// 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;
}
};
找规律
累加并计算平均值即可。
从左往右遍历,每遍历到一个位置,将该位置的数纳入考虑。若该位置的数,大于前面的最大值,则该位置的数,可以匀给前面的所有数,计算一个平均值(向上取整),作为新的最大值即可。若遇到该位置的数,小于等于前面的最大值,则该位置的数对答案没有影响。
时间复杂度 O ( n ) O(n) O(n)
// 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]
之和。
你需要删除一些边,删除后得到的各个连通块的价值都相等。请返回你可以删除的边数 最多 为多少。
示例
输入: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;
}
}
总结
T1模拟;T2位运算+前缀和;T3找规律/二分;T4 图论DFS。
这次周赛,对我来说感觉难度不小。非常侥幸并吃力地做出了3题。还要继续努力啊。