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/
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) 复杂度解决。
class Solution {
public:
vector runningSum(vector& nums) {
vector ans;
int cur = 0;
for(auto c : nums)
{
cur += c;
ans.push_back(cur);
}
return ans;
}
};
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 种
const int MAXN = 1e5 + 50;
class Solution {
public:
int cnts[MAXN];
int findLeastNumOfUniqueInts(vector& 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;
}
};
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 朵,从而缩小区间。
class Solution {
public:
bool check(vector& 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& 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;
}
};
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) 了,降低了查询的时间复杂度。
// f[i][j],第 i 个节点的,第 2^j 个祖父节点
const int MAXN = 5e4 + 50;
class TreeAncestor {
public:
int f[MAXN][17]; // 2^16 = 65536 = 6e4
vector G[MAXN]; // 要用 bfs,所以邻接表
TreeAncestor(int n, vector& 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 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);
*/