LeetCode第193场周赛(Weekly Contest 193)解题报告

Nice,兄滴们,第一次进入前 100,可喜可贺,继续努力吧,奥力给

第一题:前缀和

第二题:排序 + 贪心

第三题:二分

第四题:树上倍增 + DP

详细题解如下。


1.一维数组的动态和

         AC代码(C++)

2. 不同整数的最少数目

         AC代码(C++)

3.制作 m 束花所需的最少天数

         AC代码(C++)

4.树节点的第 K 个祖先

         AC代码(C++)


LeetCode第 193场周赛地址:

https://leetcode-cn.com/contest/weekly-contest-193/


1.一维数组的动态和

题目链接

https://leetcode-cn.com/problems/running-sum-of-1d-array/

题意

给你一个数组 nums 。数组「动态和」的计算公式为:runningSum[i] = sum(nums[0]…nums[i]) 。

请返回 nums 的动态和。

示例 1:

输入:nums = [1,2,3,4]
输出:[1,3,6,10]
解释:动态和计算过程为 [1, 1+2, 1+2+3, 1+2+3+4] 。

示例 2:

输入:nums = [1,1,1,1,1]
输出:[1,2,3,4,5]
解释:动态和计算过程为 [1, 1+1, 1+1+1, 1+1+1+1, 1+1+1+1+1] 。

示例 3:

输入:nums = [3,1,2,10,1]
输出:[3,4,6,16,17]

提示:

  • 1 <= nums.length <= 1000
  • -10^6 <= nums[i] <= 10^6

解题思路

送分题,其实就是前缀和而已,O(N) 复杂度解决。

AC代码(C++)

class Solution {
public:
    vector<int> runningSum(vector<int>& nums) {
        vector<int> ans;
        int cur = 0;
        for(auto c : nums)
        {
            cur += c;
            ans.push_back(cur);
        }
        return ans;
    }
};

2. 不同整数的最少数目

题目链接

https://leetcode-cn.com/problems/least-number-of-unique-integers-after-k-removals/

题意

给你一个整数数组 arr 和一个整数 k 。现需要从数组中恰好移除 k 个元素,请找出移除后数组中不同整数的最少数目。

示例 1:

输入:arr = [5,5,4], k = 1
输出:1
解释:移除 1 个 4 ,数组中只剩下 5 一种整数。

示例 2:

输入:arr = [4,3,1,1,3,3,2], k = 3
输出:2
解释:先移除 4、2 ,然后再移除两个 1 中的任意 1 个或者三个 3 中的任意 1 个,最后剩下 1 和 3 两种整数。

提示:

  • 1 <= arr.length <= 10^5
  • 1 <= arr[i] <= 10^9
  • 0 <= k <= arr.length

解题思路

思维题,题目是,原本有不同的数字,然后每个数字可能出现若干次。现在,给我们可以删去部分数(k),即可以删除 k 个数。最后我们要,不同的数字个数 最少。

那么,我们希望,删除 k 个数的时候,尽可能把 不同种类的数字 多删一点儿,比如说,示例 2

总共是,1 的出现个数为 2,2 的出现个数为 1,3 的出现个数为 3,4 的出现个数为 1。原本有,4 种不同的数,现在我们可以删除 3 个数,那么我们希望删除 3 个数种,尽可能多删除数字的种类多。即,我们删除 2 和 4(这样子,只用花费 2 就可以删除了两种数),然后剩下的 1 无论删除 1 还是 3 都无法把种类减少。

因此,我们希望删除的时候,先将 出现次数 少的 数字删除,这样子就可以花费最小的代价,删除最多的数字种类。

所以,我们需要统计每个数的出现次数,注意,此时我们不需要关心到底是 哪个数 出现了几次。

我们关心的,只是 每个数字的出现次数,还是比如 实例 2,我们只关心出现 1 次的数字种类有 2 个,出现 2 次的数字种类有 1 个,出现 3 次的熟悉种类有 1 个.

根据数据范围,n 最大是 100000,那么一个数出现次数,最多也就是 100000。

那么我们用一个数组 cnts,cnts[ i ] 表示,出现次数为 i 的数字种类 数。

所以我们需要统计同一个数字的出现次数:我们可以进行排序,然后相邻就是相同数,这样子就可以统计同一个数字的出现次数。(从而也可以得到不同数字的 种类数 ans)

然后,我们对于 cnts[ i ],枚举 i(从小到大,先删除 出现次数少的,这样子的花销 会小)。如果不为 0,说明出现 i 次的数字种类有,那么我们希望 k 尽可能删除完

那么此时,出现 i 次的数字种类,我们可以删除 d 种,d = min(k / i, cnts[ i ]),因为同一个数出现 i 次,我们需要删除完 i 次,才能减少一种。

那么此时 k -= d * i,如果 k < 0 ,说明无法删除 d 种,那就结束了(因为后面更大 i,更加删除不了)

如果可以删,那么 此时答案 ans -= d,因为减少了 d 种

AC代码(C++)

const int MAXN = 1e5 + 50;
class Solution {
public:
    int cnts[MAXN];
    int findLeastNumOfUniqueInts(vector<int>& arr, int k) {
        sort(arr.begin(), arr.end());
        int n = arr.size();
        int cur = 1;
        int ans = 0;
        for(int i = 1;i < n; ++i)
        {
            if(arr[i] == arr[i - 1]) ++cur;
            else
            {
                ++cnts[cur];
                ++ans;
                cur = 1;
            }
        }
        ++cnts[cur]; ++ans;

        for(int i = 1;i < MAXN; ++i)
        {
            if(cnts[i] != 0)
            {
                int d = min(k / i, cnts[i]);
                k -= d * i;
                if(k < 0) break;
                ans -= d;

            }
        }
        return ans;
    }
};

3.制作 m 束花所需的最少天数

题目链接

https://leetcode-cn.com/problems/minimum-number-of-days-to-make-m-bouquets/

题意

给你一个整数数组 bloomDay,以及两个整数 m 和 k 。

现需要制作 m 束花。制作花束时,需要使用花园中 相邻的 k 朵花 。

花园中有 n 朵花,第 i 朵花会在 bloomDay[i] 时盛开,恰好 可以用于 一束 花中。

请你返回从花园中摘 m 束花需要等待的最少的天数。如果不能摘到 m 束花则返回 -1 。

示例 1:

输入:bloomDay = [1,10,3,10,2], m = 3, k = 1
输出:3
解释:让我们一起观察这三天的花开过程,x 表示花开,而 _ 表示花还未开。
现在需要制作 3 束花,每束只需要 1 朵。
1 天后:[x, _, _, _, _]   // 只能制作 1 束花
2 天后:[x, _, _, _, x]   // 只能制作 2 束花
3 天后:[x, _, x, _, x]   // 可以制作 3 束花,答案为 3

示例 2:

输入:bloomDay = [1,10,3,10,2], m = 3, k = 2
输出:-1
解释:要制作 3 束花,每束需要 2 朵花,也就是一共需要 6 朵花。而花园中只有 5 朵花,无法满足制作要求,返回 -1 。

示例 3:

输入:bloomDay = [7,7,7,7,12,7,7], m = 2, k = 3
输出:12
解释:要制作 2 束花,每束需要 3 朵。
花园在 7 天后和 12 天后的情况如下:
7 天后:[x, x, x, x, _, x, x]
可以用前 3 朵盛开的花制作第一束花。但不能使用后 3 朵盛开的花,因为它们不相邻。
12 天后:[x, x, x, x, x, x, x]
显然,我们可以用不同的方式制作两束花。

提示:

  • bloomDay.length == n
  • 1 <= n <= 10^5
  • 1 <= bloomDay[i] <= 10^9
  • 1 <= m <= 10^6
  • 1 <= k <= n

解题分析

一开始拿到的题目,想到的就是,直接枚举答案,那么这样子的话,时间复杂度是 O(1e9 * n),肯定会超时的。

但是,我们可以对答案,进行二分查找。

二分查找,主要就是,通过将区间答案,不断的缩小一般,通过判断,那么这道题,可以这样子做吗?

可以的,因为对于一个区间 [l, r],如果对于 mid,我们如果 mid 可以满足要求,那么此时答案,应该在 右区间(因为要找最小值),如果 mid 不满足要求,那么此时答案,应该在 左区间。从而可以将 区间不断的划分。

当我们得到 mid ,要去判断满不满足,其实就是,去判断,在 花开的那个时间数组中,连续的部分,(可能是多段),能不能做成 m 朵话即可。

其实很简单,我们直接贪心,也就是,在一个 连续部分中,我们看能连续 分成 k 的几次,这样子就作出了对应的几朵花。然后可能是多段连续的。

比如说,示例 3,当是 7 天的时候,数组满足的应该是[x, x, x, x, _, x, x],此时 k = 3,那么连续的有两段,第一段最多 一朵花,第二段无法。所以最后只能做一朵花。

那么这样子,时间复杂度就是 O(n * log2(1e9)) = O(n * 32) 不会超时。

做法就是,直接对天数 二分查找,然后每一次去判断 能不能做出 m 朵,从而缩小区间。

AC代码(C++)

class Solution {
public:
    
    bool check(vector<int>& bd, int n, int m, int k, int val)
    {
        int lianxu =  0;
        for(int i = 0;i < n; ++i)
        {
            if(bd[i] <= val)
            {
                ++lianxu;
                if(lianxu == k)
                {
                    --m;
                    lianxu = 0;
                }
            }
            else 
            {
                lianxu = 0;
            }
        }
        
        return m <= 0;
    }
    
    int minDays(vector<int>& bd, int m, int k) {
        int n = bd.size();
        if(m * k > n) return -1;
        int l = 1, r = 1e9;
        while(l < r)
        {
            int mid = (r - l) / 2 + l;
            if(check(bd, n, m, k, mid)) r = mid;
            else l = mid + 1;
        }
        return l;
    }
};

4.树节点的第 K 个祖先

题目链接

https://leetcode-cn.com/problems/kth-ancestor-of-a-tree-node/

题意

给你一棵树,树上有 n 个节点,按从 0 到 n-1 编号。树以父节点数组的形式给出,其中 parent[i] 是节点 i 的父节点。树的根节点是编号为 0 的节点。

请你设计并实现 getKthAncestor(int node, int k) 函数,函数返回节点 node 的第 k 个祖先节点。如果不存在这样的祖先节点,返回 -1 。

树节点的第 k 个祖先节点是从该节点到根节点路径上的第 k 个节点。

示例 1:

输入:
["TreeAncestor","getKthAncestor","getKthAncestor","getKthAncestor"]
[[7,[-1,0,0,1,1,2,2]],[3,1],[5,2],[6,3]]

输出:
[null,1,0,-1]

解释:
TreeAncestor treeAncestor = new TreeAncestor(7, [-1, 0, 0, 1, 1, 2, 2]);

treeAncestor.getKthAncestor(3, 1);  // 返回 1 ,它是 3 的父节点
treeAncestor.getKthAncestor(5, 2);  // 返回 0 ,它是 5 的祖父节点
treeAncestor.getKthAncestor(6, 3);  // 返回 -1 因为不存在满足要求的祖先节点

提示:

  • 1 <= k <= n <= 5*10^4
  • parent[0] == -1 表示编号为 0 的节点是根节点。
  • 对于所有的 0 < i < n ,0 <= parent[i] < n 总成立
  • 0 <= node < n
  • 至多查询 5*10^4 次

 

解题分析

一开始的想法,就是,遇到每个点,直接暴力求其父节点,然后其父节点再求父节点....,一直求到当前点的 第 k 个祖先节点。

但是,会超时,其实分析时间复杂度,就可以知道,查看某一个点的 第 k 个祖先,那么就需要时间复杂度是 O(k),那么总共可能要查询 5e4 次,那么总的时间复杂度为 O(5e4 * k),就会超时了。

那么我们换个角度,我们每一次找父节点,都是一层一层的找,那么此时就很慢。有没有方法可以快速找呢?

有的,就是一个方法:树上倍增

也就是说,我们不是每次都 1 个 1 个向上,而是第一次 向上 1,然后第二次向上 2 ,第三次向上 4 ...,每次向上找一次,就直接 2 倍找。

比如说,我们此时找 第 28 个祖先,那么 28 = 2^4 + 2^3 + 2^2 = 16 + 8 + 4,那么也就是,我们先往上找 4 ,然后 8,然后 16....这样子就相当于找到了 28 个祖先。

这个方法,就是可以利用了二进制。

那么此时我们假设 f[ i ][ j ] 表示,第 i 个节点的 第 2^j 个祖先 是哪个节点。

那么分析以下,f[ i ][ 0 ] = p[ i ],即 i 节点的 2^0 = 1 祖先,其实就是 父节点。

f[ i ][ 1 ],就是 i 节点的 2^1 = 2 个祖先,那么就是 f[ i ][ 0 ] 的再上一个,所以 f[ i ][ 1] = f[  f[i ][0] ][ 0 ]

....

f[ i ][ j ],其实就是 2^j = 2^(j - 1) + 2^(j - 1),所以 f[i][j] = f[f[i][j - 1]][j - 1]

所以,我们利用一个,类似 dp 的方式,预处理,记录了 每个 i 节点的 所有 j 个祖先对应的节点。

我们可以发现,对于每个节点,其实可以是它 上面的 所有节点 转移过来的。

所以我们可以用 dfs 或者 bfs,保证先算上面,才到下面

这里,采用的是 bfs,一层一层,这样子转移就没问题。

那么预处理的时间复杂度,我们其实就是要求出 所有状态,那么状态个数 = n * log(n),也就大概 16n 的复杂度。

 

然后到了最后,我们查询某个节点 node 的第 k 个子节点的时候,我们其实就是将 k 进行二进制分解,比如说 k = 5 = 101,那么此时,相当于先找 node 的 2^0 祖先 (此时又记为 node),然后又继续找 node 的 2^2 祖先。

这样子就可以快速找到,那么找一个 点的 k 祖先,时间复杂度是 O(log k),就不再是 O(k) 了,降低了查询的时间复杂度。

AC代码(C++)

// f[i][j],第 i 个节点的,第 2^j 个祖父节点

const int MAXN = 5e4 + 50;

class TreeAncestor {
public:
    int f[MAXN][17];   // 2^16 = 65536 = 6e4
    vector<int> G[MAXN]; // 要用 bfs,所以邻接表

    TreeAncestor(int n, vector<int>& pa) {
        memset(f, -1, sizeof(f));   // 初始化,所有点的都是 祖先都是 -1
        int root = -1;

        for(int i = 0;i < pa.size(); ++i)
        {
            if(pa[i] == -1) root = i;
            else
            {
                G[pa[i]].push_back(i);
            }
        }
        // BFS 转移
        // f[i][0] = pa[i];
        // f[i][j] = f[f[i][j - 1]][j - 1]
        queue<int> q;
        while(!q.empty()) q.pop();
        q.push(root);
        while(!q.empty())
        {
            int x = q.front();
            q.pop();
            for(int y : G[x])
            {
                f[y][0] = x;  // 0 就是父节点
                for(int i = 1;i < 17; ++i)
                {
                    if(f[y][i - 1] != -1)   // 转移的时候,要上面的那个点的 祖先不是 -1,这样子转移下来,才有效
                        f[y][i] = f[f[y][i - 1]][i - 1];
                }
                q.push(y);
            }
        }

    }
    
    int getKthAncestor(int node, int k) {
        for(int i = 0;i < 17; ++i)
        {
            if((k >> i) & 1) node = f[node][i];   // 对于 k,根据其 二进制中的 1 对应子啊 i 位,然后就可以由 f 来快速求
            if(node == -1) return -1;   // 如果中途有 -1,说明此时这个点的 祖先超出了,那就返回 -1
        }
        return node;
    }
};

/**
 * Your TreeAncestor object will be instantiated and called as such:
 * TreeAncestor* obj = new TreeAncestor(n, parent);
 * int param_1 = obj->getKthAncestor(node,k);
 */

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值