【算法比赛】2020 力扣杯!Code Your Future 春季全国编程大赛

这个比赛,除了最后一题,其他四题其实没啥难度。

第一题:模拟。

第二题:BFS 或者 DP。

第三题:二分。

第四题:BFS。

第五题:思维 + 树的 DFS。

详细题解如下。


1.拿硬币

       AC代码(C++)

2. 传递信息

       AC代码(方法一、BFS  C++)

       AC代码(方法二、DP  C++)

3.剧情触发时间

       AC代码(C++)

4.最小跳跃次数

       AC代码(C++)

5.二叉树任务调度

       AC代码(C++)


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;
    }
};

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值