这个比赛,除了最后一题,其他四题其实没啥难度。
第一题:模拟。
第二题:BFS 或者 DP。
第三题:二分。
第四题:BFS。
第五题:思维 + 树的 DFS。
详细题解如下。
2020 力扣杯!Code Your Future 春季全国编程大赛 地址:
https://leetcode-cn.com/contest/season/2020-spring/ranking/solo/
1.拿硬币
题目链接
https://leetcode-cn.com/problems/na-ying-bi/
题意
桌上有
n
堆力扣币,每堆的数量保存在数组coins
中。我们每次可以选择任意一堆,拿走其中的一枚或者两枚,求拿完所有力扣币的最少次数。示例 1:
输入:[4,2,1] 输出:4 解释:第一堆力扣币最少需要拿 2 次,第二堆最少需要拿 1 次,第三堆最少需要拿 1 次,总共 4 次即可拿完。
示例 2:
输入:[2,3,10] 输出:8
提示:
1 <= n <= 4
1 <= coins[i] <= 10
解题思路
热身题,很简单,遍历每一堆,我们每一次都拿 2,那么每一堆需要拿多少次才拿完。每一堆的次数相加就是答案。
AC代码(C++)
class Solution {
public:
int minCount(vector<int>& coins) {
int ans = 0;
for(auto c : coins)
{
ans += (c + 1) / 2;
}
return ans;
}
};
2. 传递信息
题目链接
https://leetcode-cn.com/problems/chuan-di-xin-xi/
题意
小朋友 A 在和 ta 的小伙伴们玩传信息游戏,游戏规则如下:
- 有 n 名玩家,所有玩家编号分别为 0 ~ n-1,其中小朋友 A 的编号为 0
- 每个玩家都有固定的若干个可传信息的其他玩家(也可能没有)。传信息的关系是单向的(比如 A 可以向 B 传信息,但 B 不能向 A 传信息)。
- 每轮信息必须需要传递给另一个人,且信息可重复经过同一个人
给定总玩家数 n,以及按 [玩家编号,对应可传递玩家编号] 关系组成的二维数组 relation。返回信息从小 A (编号 0 ) 经过 k 轮传递到编号为 n-1 的小伙伴处的方案数;若不能到达,返回 0。
示例 1:
输入:n = 5, relation = [[0,2],[2,1],[3,4],[2,3],[1,4],[2,0],[0,4]], k = 3 输出:3 解释:信息从小 A 编号 0 处开始,经 3 轮传递,到达编号 4。共有 3 种方案,分别是 0->2->0->4, 0->2->1->4, 0->2->3->4。
示例 2:
输入:n = 3, relation = [[0,2],[2,1]], k = 2 输出:0 解释:信息不能从小 A 处经过 2 轮传递到编号 2
提示:
2 <= n <= 10
1 <= k <= 5
1 <= relation.length <= 90, 且 relation[i].length == 2
0 <= relation[i][0],relation[i][1] < n 且 relation[i][0] != relation[i][1]
解题思路
方法一、搜索 BFS(也是可以用 DFS 的)
根据题目的数据范围,我们知道最多 5 轮,而且最多 10 个小朋友,那么我们可以用 BFS,对每一次可以走到那里遍历完,然后利用 k 作为限制,当转移 k 次时,判断对应位置是不是 n - 1,是的话,方案数 + 1。
因为是 5 轮,那么用 BFS,一开始是 一个点,第一次,可能是 10 个点。第二次,100 个点,第五次 100000。相当于 BFS 遍历那么多个可能的点。不会超时。
方法二、动态规划 DP
我们可以构造一个状态 dp[ k ][ i ]
在第 k 轮是,信息在 i 位置处的方案数。
转移方程就是,遍历所有可能转移的情况,即 从 k - 1 的 XXX 可以转移到 k 轮的 YYY上(这个关系记录在 relation 中)。所以 dp[ k ][ YYY ] += dp[ k - 1][ XXX ],其中由 relation 得到所有的 XXX -> YYY。
初始值:dp[ 0 ][ 0 ] = 1,第 0 轮时 在小朋友 0 位置。其他的都是 0。
最后答案就是, dp[ k ][ n - 1] 。
这个是,遍历 k 次,每一次要得到所有的转移情况 (XXX -> YYY),也就是遍历 relation。所以时间复杂度是 O(k * relation.length
),是比 方法一 快的。
AC代码(方法一、BFS C++)
class Solution {
public:
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<vector<int> > G(n, vector<int> (n, 0));
for(auto& r : relation)
{
G[r[0]][r[1]] = 1; // 有向图
}
queue<pair<int, int> > q;
while(!q.empty()) q.pop();
q.push(make_pair(0, 0));
int ans = 0;
while(!q.empty()) // BFS
{
int cur = q.front().first, d = q.front().second;
q.pop();
if(cur == n - 1 && d == k) ++ans;
if(d >= k) continue;
for(int i = 0;i < n; ++i)
{
if(G[cur][i] == 0) continue;
q.push(make_pair(i, d + 1));
}
}
return ans;
}
};
AC代码(方法二、DP C++)
class Solution {
public:
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<vector<int> > dp(k + 1, vector<int> (n, 0));
dp[0][0] = 1;
for(int i = 1;i <= k; ++i)
{
for(auto r : relation)
{
dp[i][r[1]] += dp[i - 1][r[0]];
}
}
return dp[k][n - 1];
}
};
3.剧情触发时间
题目链接
https://leetcode-cn.com/problems/ju-qing-hong-fa-shi-jian/
题意
在战略游戏中,玩家往往需要发展自己的势力来触发各种新的剧情。一个势力的主要属性有三种,分别是文明等级(C),资源储备(R)以及人口数量(H)。在游戏开始时(第 0 天),三种属性的值均为 0。
随着游戏进程的进行,每一天玩家的三种属性都会对应增加,我们用一个二维数组 increase 来表示每天的增加情况。这个二维数组的每个元素是一个长度为 3 的一维数组,例如 [[1,2,1],[3,4,2]] 表示第一天三种属性分别增加 1,2,1 而第二天分别增加 3,4,2。
所有剧情的触发条件也用一个二维数组 requirements 表示。这个二维数组的每个元素是一个长度为 3 的一维数组,对于某个剧情的触发条件 c[i], r[i], h[i],如果当前 C >= c[i] 且 R >= r[i] 且 H >= h[i] ,则剧情会被触发。
根据所给信息,请计算每个剧情的触发时间,并以一个数组返回。如果某个剧情不会被触发,则该剧情对应的触发时间为 -1 。
示例 1:
输入: increase = [[2,8,4],[2,5,0],[10,9,8]] requirements = [[2,11,3],[15,10,7],[9,17,12],[8,1,14]] 输出: [2,-1,3,-1] 解释: 初始时,C = 0,R = 0,H = 0 第 1 天,C = 2,R = 8,H = 4 第 2 天,C = 4,R = 13,H = 4,此时触发剧情 0 第 3 天,C = 14,R = 22,H = 12,此时触发剧情 2 剧情 1 和 3 无法触发。
示例 2:
输入: increase = [[0,4,5],[4,8,8],[8,6,1],[10,10,0]] requirements = [[12,11,16],[20,2,6],[9,2,6],[10,18,3],[8,14,9]] 输出: [-1,4,3,3,3]
示例 3:
输入: increase = [[1,1,1]] requirements = [[0,0,0]] 输出: [0]
提示:
1 <= increase.length <= 10000
1 <= requirements.length <= 100000
0 <= increase[i] <= 10
0 <= requirements[i] <= 100000
解题分析
根据题意,就是我们可以得到 三个状态(C,R,H)分别经过 increase 后,得到在每一天上的状态值。
然后要求计算出,对应的 requirements 满足其要求时的最少天数。
那么一开始,想到的是,我们先得到每一天的状态变化,然后每一天变化后,去遍历 requirements ,看看有那些是满足的(这样子可以保证是 最少时间 满足)。但这样子的暴力枚举 时间复杂度是 O(n * m),其中 n 是 increase 的长度,m 是 requirements 的长度,这样子会超时。
因此我们继续分析,我们发现,三个状态(C,R,H)要满足的时候,其实是互不相干的,也就是说,对于 requirements,我们可以先计算三个状态分别在那一天(最少)就可以满足,比如时间是 t1,t2,t3。那么最后这个 requirements 满足的时间是 max(t1, max(t2, t3)),因为要保证同时满足,所以是取最大。
然后又分析,对于三个状态,都是增量,那么单独一个状态值的变化而言,是一个有序的,那么就是,我们需要在这个有序的值中,找到满足 requirements 对应状态的 ( >= 要求) 中的最小。
那么就是一个二分的过程。
也就是,对于每一个 requirements,我们去 二分 状态变化值 ,那么时间复杂度是 O(logn * m),就不会超时。
所以整个思路就是
- 先分别计算每个状态在每一天的值(有序的),其中注意,第 0 天的 0 也是一个值(示例 3)
- 然后对于 requirements 每一个要求,去找到三个状态要求的值,然后分别去 二分 对应的最少天数 t1,t2,t3。然后答案就是 max(t1, max(t2, t3))。
- 但是要注意,可能是完不成的,即,如果至少有一个状态的值,比 增加到 最后的值 还大,那么也就是,对于这个状态,找不到满足的 最少天数,那么对于总的而言,也就找不到,返回 -1。
AC代码(C++)
class Solution {
public:
int bFind(vector<vector<int>>& inc, int a, int b, int c) //三个状态要求的值,分别二分查找
{
int n = inc.size();
if(a > inc[n - 1][0] || b > inc[n - 1][1] || c > inc[n - 1][2]) return -1; // 如果比其 最大值还大,说明找不到 最少天数,那么就返回 -1
// 找满足第一个状态 a 的最少天数
int ret = 0;
int l = 0, r = n - 1;
while(l < r)
{
int mid = (l + r) / 2;
if(inc[mid][0] < a) l = mid + 1;
else r = mid;
}
ret = r;
// 找满足第二个状态 b 的最少天数
l = 0, r = n - 1;
while(l < r)
{
int mid = (l + r) / 2;
if(inc[mid][1] < b) l = mid + 1;
else r = mid;
}
ret = max(ret, r); // 都是取其中的最大值
// // 找满足第三个状态 c 的最少天数
l = 0, r = n - 1;
while(l < r)
{
int mid = (l + r) / 2;
if(inc[mid][2] < c) l = mid + 1;
else r = mid;
}
ret = max(ret, r); // 都是取其中的最大值
return ret; // 返回三个状态同时满足的最少天数
}
vector<int> getTriggerTime(vector<vector<int>>& increase, vector<vector<int>>& re) {
int n = increase.size();
int m = re.size();
vector<vector<int>> inc(n + 1, vector<int> (3)); // 一开始 0 0 0状态也要考虑,所以总共的状态变化值有 n + 1 个
inc[0][0] = 0, inc[0][1] = 0, inc[0][2] = 0;
// 计算每一天 i 的 状态当前值
for(int i = 0;i < n; ++i)
{
inc[i + 1][0] = inc[i][0] + increase[i][0];
inc[i + 1][1] = inc[i][1] + increase[i][1];
inc[i + 1][2] = inc[i][2] + increase[i][2];
}
vector<int> ans(m ,-1);
for(int i = 0;i < m; ++i)
{
ans[i] = bFind(inc, re[i][0], re[i][1], re[i][2]); // 二分
}
return ans;
}
};
4.最小跳跃次数
题目链接
https://leetcode-cn.com/problems/zui-xiao-tiao-yue-ci-shu/
题意
为了给刷题的同学一些奖励,力扣团队引入了一个弹簧游戏机。游戏机由 N 个特殊弹簧排成一排,编号为 0 到 N-1。初始有一个小球在编号 0 的弹簧处。若小球在编号为 i 的弹簧处,通过按动弹簧,可以选择把小球向右弹射 jump[i] 的距离,或者向左弹射到任意左侧弹簧的位置。也就是说,在编号为 i 弹簧处按动弹簧,小球可以弹向 0 到 i-1 中任意弹簧或者 i+jump[i] 的弹簧(若 i+jump[i]>=N ,则表示小球弹出了机器)。小球位于编号 0 处的弹簧时不能再向左弹。
为了获得奖励,你需要将小球弹出机器。请求出最少需要按动多少次弹簧,可以将小球从编号 0 弹簧弹出整个机器,即向右越过编号 N-1 的弹簧。
示例 1:
输入:jump = [2, 5, 1, 1, 1, 1] 输出:3 解释:小 Z 最少需要按动 3 次弹簧,小球依次到达的顺序为 0 -> 2 -> 1 -> 6,最终小球弹出了机器。
提示:
1 <= jump.length <= 10^6
1 <= jump[i] <= 10000
解题分析
看题目,就是一个最短路,即当前位置 i,可以两种选择,跳到 i + jump[ i ], 或者 跳到 0 到 i - 1 的任意一个或多个。
那么其实就是一个 BFS 的最短路,但是会出现一个问题,即当我们 对于每一个点都 i 都要去 0 到 i - 1。假如是 1 1 1 1 1 1 1 1 1 ...,这样子的话,对于每一个点,都要找到前面所有,这样子时间复杂度就变为了 O(n ^ 2)。
那么怎么优化这个呢,其实我们发现 如果本来是 k - > i,同时原本 k 会去检查了 所有 0 到 k - 1。
那么对于 i 而言,直接可以 i -> i + jump[ i ],然后 0 到 i - 1 不用全部,只需要检查 k + 1 到 i - 1。这是因为,我们肯定要尽可能往远的跳,你如果跳回原来 <= k 的地方,相当于 又会检查一次以前的(那么相当于没跳远,但是步数又增加了)。
所以这样子当我们数据是 1 1 1 1 1 1 1 1 1 ...,检查 检查 k + 1 到 i - 1 就不会出现 O(n ^ 2) 的复杂度 出来。
所以这道题主要就是 BFS 最短路,每个点最多考虑一次。
(我BFS 直接用的 是STL 自带的 queue 来实现的,看了第一名的大佬,是自己用 数组来进行 模拟 queue,运行时间比直接用 queue 快了一倍多。但是我们的用 queue 还是可以成功的)
AC代码(C++)
class Solution {
public:
int minJump(vector<int>& jump) {
int n = jump.size();
vector<int> vis(n, 0);
vector<int> dist(n, -1);
queue<int> q;
while(!q.empty()) q.pop();
q.push(0);
vis[0] = 1;
dist[0] = 0;
int k = 0;
while(!q.empty())
{
int x = q.front();
q.pop();
int next = x + jump[x];
if(next > n - 1) return dist[x] + 1; // 如果下一次 next 直接出去了,就是答案。
if(vis[next] == 0) // 可以跳到 next
{
vis[next] = 1;
q.push(next);
dist[next] = dist[x] + 1;
}
for(int i = k + 1;i < x; ++i) // 可以往回跳,用 k 记录上一次的边缘,那么就只用考虑 k + 1 到 i - 1
{
if(vis[i] == 1) continue;
vis[i] = 1;
q.push(i);
dist[i] = dist[x] + 1;
}
k = x; // 更新 k
}
return -1;
}
};
5.二叉树任务调度
题目链接
https://leetcode-cn.com/problems/er-cha-shu-ren-wu-diao-du/
题意
任务调度优化是计算机性能优化的关键任务之一。在任务众多时,不同的调度策略可能会得到不同的总体执行时间,因此寻求一个最优的调度方案是非常有必要的。
通常任务之间是存在依赖关系的,即对于某个任务,你需要先完成他的前导任务(如果非空),才能开始执行该任务。我们保证任务的依赖关系是一棵二叉树,其中 root 为根任务,root.left 和 root.right 为他的两个前导任务(可能为空),root.val 为其自身的执行时间。
在一个 CPU 核执行某个任务时,我们可以在任何时刻暂停当前任务的执行,并保留当前执行进度。在下次继续执行该任务时,会从之前停留的进度开始继续执行。暂停的时间可以不是整数。
现在,系统有两个 CPU 核,即我们可以同时执行两个任务,但是同一个任务不能同时在两个核上执行。给定这颗任务树,请求出所有任务执行完毕的最小时间。
示例 1:
【示例有图,具体看链接】 输入:root = [47, 74, 31] 输出:121 解释:根节点的左右节点可以并行执行31分钟,剩下的43+47分钟只能串行执行,因此总体执行时间是121分钟。
示例 2:
【示例有图,具体看链接】 输入:root = [15, 21, null, 24, null, 27, 26] 输出:87
示例 3:
【示例有图,具体看链接】 输入:root = [1,3,2,null,null,4,4] 输出:7.5
提示:
1 <= 节点数量 <= 1000
1 <= 单节点执行时间 <= 1000
解题分析
怎么说呢,这道题,说难嘛,其实就是一个 DFS 的题目,代码简单,很短,比第四题的代码还短。说不难嘛,主要难在分析,如果分析出来了,代码很简单。
根据题目意思,我们来分析示例(结合 链接里的图,更好理解)
- 示例 1,先双核工作 31 + 31 分钟,那么剩下的,由于根节点任务必须先要两个子任务做完才能做,所以剩下的时候只能单核,即 47 + 74 + 31 - 31 - 31 = 90。所以总时间 = 单核 + 双核 / 2 = 90 + 31 = 121.
- 示例 2,类似于 示例 1 分析。
- 示例 3 ,这个很重要。我在比赛中,示例 3 的分析,都分析不出来为什么,所以一点思路都木有。。。如果按照 示例 1 那样子,一开始 双核 4 + 4,然后双核 2 + 2,剩下单核 1 + 1,那么总时间 = 4 + 2 + 2 = 8。那么 7.5 是怎么来的呢?
- 7.5 的意思是,我们先 一个核运行 3 中的 0.5,另一个 核运行 4 中的 0.5。同样的,一个核运行 3 中的又 0.5,另一个 核运行另一个 4 中的 0.5。那么此时 3 变成了 2,两个 4 变成了 3.5。然后双核 3.5 +3.5,双核 2 + 2,单核 1。
- 总时间 0.5 +0.5 +3.5+2+1 = 7.5
这道题的难点,主要就是体现在了示例 3。也就是,我们两个子树,左子树,右子树,左子树的单核时间(原本是 3),可以和另一个右子树的双核时间(原本是 4 + 4),组合起来,使得左子树的单核时间减小(单核时间减小了,那么双核时间就增加,那么时间消耗就少了,因为尽可能的双核了)。
上面的说法,其实类似一个问题
- 我有一台面包机,它有两个槽,每个槽可以烘烤面包的一面,需要1分钟。
- 现在要烤3片面包做三明治,总共要烤6个面,需要几分钟?
- 显然同时烤2块面包4个面,需要2min。但是单独烤第3片面包,面包机就浪费了一个仓位。
- 因此我们可以,(a1, b1),(a2,c1),(b2,c2)。
所以上面的分析也是,我们假设每一个子树最后返回得到的答案,有两个值,即 单核运行时间,和 双核运行时间
- 那么这样子,最后的答案就是 单核运行时间 + 双核运行时间 / 2
那么对于一颗根节点的树,首先父节点 肯定是要 单核运行的。
那么我们假设 左子树 的单核时间 >= 右子树的 单核时间
- 为了好说明,设变量把,即 左子树 的 单核时间 和 双核时间 分别是 s1 和 d1。那么右子树对应是 s2 和 d2。
那么我们假设 s1 >= s2 (假如不成立,我们就将两个子树换位置,不会影响最后答案)
那么根据上面的方法,我们要使得 总的单核时间尽可能少,父节点一定是 单核运行的。
那么即,我们要将 s1 和 s2 尽可能配对,也就是 将 s2 + s2 的时间用作 双核运行,那么还剩下 s1 - s2
注意了, s1 - s2 并不是最后的 单核时间,因为上面说了
- 左子树的单核时间(原本是 3),可以和另一个右子树的双核时间(原本是 4 + 4),组合起来
- 类似于那个面包机
所以 s1 - s2 可以 和 d2 组合一下,具体怎么组合我们不关心,我们只关心可以组合多少。那么总的 有 d2 时间是 双核运行,那么我们可以将这个 双核 分配 s1 - s2 和 d2,那么也就是,我们最多可以分配出 d2 那么多时间来进行 双核
- s1 - s2 - d2 就是最后的单核时间(因为 多了 d2 去配对 双核),注意 单核时间 >= 0,所以应该是 单核时间 = max(0.0, s1 - s2 - d2)
因此,两颗子树 + 父节点 = 新的子树
此时的
- 单核时间 = max(0.0, s1 - s2 - d2) + 父节点时间
- 双核时间 = 两个子树的总时间 (s1 + s2 + d1 + d2) - 两个子树配对后剩余的单核时间( max(0.0, s1 - s2 - d2) )
然后剩下的就是 DFS,即对于每一个父树,我们先得到两个子树的 单核 双核时间,然后拼接成 父树 的 单核 双核时间。一直到最后的根节点的树,就是返回答案。
那么最后答案就是 单核 + 双核 / 2
(当遇到了 NULL 节点,单核时间 = 0,双核时间 = 0 ).
AC代码(C++)
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
#define pdd pair<double, double> // 每一个树的返回,有两个,所以用 pair,或者 结构体也可以
class Solution {
public:
pdd dfs(TreeNode* root)
{
// 对于每一个 父树,先得到两个 子树的
if(root == NULL) return {0.0, 0.0};
auto l = dfs(root->left);
auto r = dfs(root->right);
if(l.first < r.first) swap(l, r); // 我们使 左子树的单核 >= 右子树,不然就交换,其实答案不会受影响,减小后面 用 if 太多判断
int sum = l.first + l.second + r.first + r.second;
double s1 = l.first, s2 = r.first;
double d1 = l.second, d2 = r.second;
double s = max(0.0, s1 - s2 - d2); // 两个子树拼接后,的最小的单核时间
// 返回父树的,其单核时间 = s + 根节点
// 双核时间 = 两个子树的总时间 - 两个子树最后剩下的单核时间
return {s + root->val, sum - s};
}
double minimalExecTime(TreeNode* root) {
auto res = dfs(root);
return res.first + res.second / 2.0;
}
};