⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。
学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode 周赛的解题报告,一起体会上分之旅。
本文是 LeetCode 上分之旅系列的第 48 篇文章,往期回顾请移步到文章末尾~
https://leetcode.cn/problems/minimum-operations-to-collect-elements/description/
简单模拟题。
预初始化包含 1 − k 1 - k 1−k 元素的集合,根据题意逆向遍历数组并从集合中移除元素,当集合为空时表示已经收集到所有元素,返回 n − i n - i n−i。
class Solution {
fun minOperations(nums: List, k: Int): Int {
val n = nums.size
val set = (1..k).toHashSet()
for (i in n - 1 downTo 0) {
set.remove(nums[i])
if (set.isEmpty()) return n - i
}
return -1
}
}
class Solution:
def minOperations(self, nums, k):
n, nums_set = len(nums), set(range(1, k+1))
for i in range(n-1, -1, -1):
nums_set.discard(nums[i])
if not nums_set:
return n - i
return -1
class Solution {
public:
int minOperations(std::vector& nums, int k) {
int n = nums.size();
unordered_set set;
for (int i = 1; i <= k; ++i) {
set.insert(i);
}
for (int i = n - 1; i >= 0; --i) {
set.erase(nums[i]);
if (set.empty()) {
return n - i;
}
}
return -1;
}
};
function minOperations(nums: number[], k: number): number {
var n = nums.length;
var set = new Set();
for (let i = 1; i <= k; ++i) {
set.add(i);
}
for (let i = n - 1; i >= 0; --i) {
set.delete(nums[i]);
if (set.size === 0) {
return n - i;
}
}
return -1;
};
class Solution {
int minOperations(List nums, int k) {
int n = nums.length;
Set set = Set();
for (int i = 1; i <= k; i++) {
set.add(i);
}
for (int i = n - 1; i >= 0; i--) {
set.remove(nums[i]);
if (set.isEmpty) return n - i;
}
return -1;
}
}
复杂度分析:
https://leetcode.cn/problems/minimum-number-of-operations-to-make-array-empty/description/
题目两种操作的前提是数字相等,因此我们先统计每个元素的出现次数。
从最少次数的目标出发,显然能移除 3 3 3 个就尽量移除 3 3 3 个,再分类讨论:
组合以上讨论:
class Solution {
fun minOperations(nums: IntArray): Int {
val cnts = HashMap()
for (e in nums) {
cnts[e] = cnts.getOrDefault(e, 0) + 1
}
var ret = 0
for ((_, cnt) in cnts) {
if (cnt == 1) return -1
when (cnt % 3) {
0 -> {
ret += cnt / 3
}
1, 2 -> {
ret += cnt / 3 + 1
}
}
}
return ret
}
}
继续挖掘题目特性,对于余数大于 0 0 0 的情况总是 向上取整 ,那么可以简化为:
class Solution {
fun minOperations(nums: IntArray): Int {
val cnts = HashMap()
for (e in nums) {
cnts[e] = cnts.getOrDefault(e, 0) + 1
}
var ret = 0
for ((_, cnt) in cnts) {
if (cnt == 1) return -1
ret += (cnt + 2) / 3 // 向上取整
}
return ret
}
}
class Solution:
def minOperations(self, nums: List[int]) -> int:
cnts = Counter(nums)
ret = 0
for cnt in cnts.values():
if cnt == 1: return -1
ret += (cnt + 2) // 3
return ret
class Solution {
public:
int minOperations(std::vector& nums) {
unordered_map cnts;
for (auto &e : nums) {
cnts[e] += 1;
}
int ret = 0;
for (auto &p: cnts) {
if (p.second == 1) return -1;
ret += (p.second + 2) / 3;
}
return ret;
}
};
function minOperations(nums: number[]): number {
let cnts: Map = new Map();
for (let e of nums) {
cnts.set(e, (cnts.get(e) ?? 0) + 1);
}
let ret = 0;
for (let [_, cnt] of cnts) {
if (cnt == 1) return -1;
ret += Math.ceil(cnt / 3);
}
return ret;
};
class Solution {
int minOperations(List nums) {
Map cnts = {};
for (int e in nums) {
cnts[e] = (cnts[e] ?? 0) + 1;
}
int ret = 0;
for (int cnt in cnts.values) {
if (cnt == 1) return -1;
ret += (cnt + 2) ~/ 3; // 向上取整
}
return ret;
}
}
复杂度分析:
https://leetcode.cn/problems/split-array-into-maximum-number-of-subarrays/description/
一个重要的结论是:当按位与的数量增加时,按位与的结果是非递增的。
题目要求在子数组的按位与的和最小的前提下,让子数组的个数最大。根据上面的结论,显然将数组全部按位与是最小的。
分类讨论:
class Solution {
fun maxSubarrays(nums: IntArray): Int {
val mn = nums.reduce { acc, it -> acc and it }
if (mn > 0) return 1 // 特判
var ret = 0
var cur = Integer.MAX_VALUE
for (i in nums.indices) {
cur = cur and nums[i]
if (cur == 0) {
cur = Integer.MAX_VALUE
ret++
}
}
return ret
}
}
class Solution:
def maxSubarrays(self, nums: List[int]) -> int:
if reduce(iand, nums): return 1
ret, mask = 0, (1 << 20) - 1
cur = mask
for num in nums:
cur &= num
if cur == 0: ret += 1; cur = mask
return ret
class Solution {
public:
int maxSubarrays(vector& nums) {
int mn = nums[0];
for (auto num : nums) mn &= num;
if (mn != 0) return 1;
int ret = 0;
int cur = INT_MAX;
for (int i = 0; i < nums.size(); i++) {
cur &= nums[i];
if (cur == 0) {
cur = INT_MAX;
ret++;
}
}
return ret;
}
};
function maxSubarrays(nums: number[]): number {
const n = nums.length;
let mn = nums.reduce((acc, it) => acc & it);
if (mn > 0) return 1; // 特判
let mask = (1 << 20) - 1
let ret = 0;
let cur = mask;
for (let i = 0; i < n; i++) {
cur = cur & nums[i];
if (cur === 0) {
cur = mask;
ret++;
}
}
return ret;
};
class Solution {
int maxSubarrays(List nums) {
var mn = nums.reduce((acc, it) => acc & it);
if (mn > 0) return 1; // 特判
var mask = (1 << 20) - 1;
var ret = 0;
var cur = mask;
for (var i = 0; i < nums.length; i++) {
cur = cur & nums[i];
if (cur == 0) {
cur = mask;
ret++;
}
}
return ret;
}
}
复杂度分析:
https://leetcode.cn/problems/maximum-number-of-k-divisible-components/
初步分析:
思考实现:
在保证问题有解的情况下,树上的每个节点要么是单独的连通分量,要么与邻居组成连通分量。那么,这就是典型的「连或不连」和「连哪个」动态规划思维。
如果节点 A A A 的价值能够被 K K K 整除,那么节点 A A A 能作为单独的连通分量吗?
不一定,例如 K = 3 K = 3 K=3 且树为 1 − 3 − 5 1 - 3 - 5 1−3−5 的情况,连通分量只能为 1 1 1,因为 3 3 3 左右子树都不能构造合法的连通块,因此需要与 3 3 3 连接才行。
那么,节点 A A A 应该与谁相连呢?对于节点 A A A 的某个子树 T r e e i Tree_i Treei 来说,存在 2 2 2 种情况:
当节点 A A A 与所有子树的剩余值组合后,再加上当前节点的价值,如果能够构造出 K K K 的整数倍时,说明找到一个新的连通块,并且不需要和上一级节点组合。否则,则进入不能整除的条件,继续和上一级节点组合。
class Solution {
fun maxKDivisibleComponents(n: Int, edges: Array, values: IntArray, k: Int): Int {
// 建图
val graph = Array(n) { LinkedList() }
for ((u, v) in edges) {
graph[u].add(v)
graph[v].add(u)
}
// DFS
fun dfs(i: Int, pre: Int): IntArray {
var ret = intArrayOf(0, values[i])
for (to in graph[i]) {
if (to == pre) continue
val (childCnt, childLeft) = dfs(to, i)
ret[0] += childCnt
ret[1] += childLeft
}
if (ret[1] % k == 0) {
ret[0] += 1
ret[1] = 0
}
return ret
}
return dfs(0, -1)[0]
}
}
class Solution:
def maxKDivisibleComponents(self, n, edges, values, k):
# 建图
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
# DFS
def dfs(i, pre):
ret = [0, values[i]]
for to in graph[i]:
if to == pre: continue
childCnt, childLeft = dfs(to, i)
ret[0] += childCnt
ret[1] += childLeft
if ret[1] % k == 0:
ret[0] += 1
ret[1] = 0
return ret
return dfs(0, -1)[0]
class Solution {
public:
int maxKDivisibleComponents(int n, vector>& edges, vector& values, int k) {
// 建图
vector> graph(n);
for (auto& edge : edges) {
int u = edge[0];
int v = edge[1];
graph[u].push_back(v);
graph[v].push_back(u);
}
// DFS
function(int, int)> dfs = [&](int i, int pre) -> vector {
vector ret(2, 0);
ret[1] = values[i];
for (int to : graph[i]) {
if (to == pre) continue;
vector child = dfs(to, i);
ret[0] += child[0];
ret[1] += child[1];
}
if (ret[1] % k == 0) {
ret[0] += 1;
ret[1] = 0;
}
return ret;
};
return dfs(0, -1)[0];
}
};
function maxKDivisibleComponents(n: number, edges: number[][], values: number[], k: number): number {
// 建图
let graph = Array(n).fill(0).map(() => []);
for (const [u, v] of edges) {
graph[u].push(v);
graph[v].push(u);
}
// DFS
let dfs = (i: number, pre: number): number[] => {
let ret = [0, values[i]];
for (let to of graph[i]) {
if (to === pre) continue;
let [childCnt, childLeft] = dfs(to, i);
ret[0] += childCnt;
ret[1] += childLeft;
}
if (ret[1] % k === 0) {
ret[0] += 1;
ret[1] = 0;
}
return ret;
};
return dfs(0, -1)[0];
};
class Solution {
int maxKDivisibleComponents(int n, List> edges, List values, int k) {
// 建图
List> graph = List.generate(n, (_) => []);
for (final edge in edges) {
int u = edge[0];
int v = edge[1];
graph[u].add(v);
graph[v].add(u);
}
// DFS
List dfs(int i, int pre) {
List ret = [0, values[i]];
for (int to in graph[i]) {
if (to == pre) continue;
List child = dfs(to, i);
ret[0] += child[0];
ret[1] += child[1];
}
if (ret[1] % k == 0) {
ret[0] += 1;
ret[1] = 0;
}
return ret;
}
return dfs(0, -1)[0];
}
}
复杂度分析:
推荐阅读
LeetCode 上分之旅系列往期回顾:
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~