LeetCode 周赛上分之旅 #48 一道简单的树上动态规划问题

news/发布时间2024/8/25 6:21:12

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode 周赛的解题报告,一起体会上分之旅。

本文是 LeetCode 上分之旅系列的第 48 篇文章,往期回顾请移步到文章末尾~

LeetCode 双周赛 114

T1. 收集元素的最少操作次数(Easy)

  • 标签:模拟、散列表

T2. 使数组为空的最少操作次数(Medium)

  • 标签:贪心、散列表

T3. 将数组分割成最多数目的子数组(Medium)

  • 标签:思维、位运算

T4. 可以被 K 整除连通块的最大数目(Hard)

  • 标签:树上 DP


T1. 收集元素的最少操作次数(Easy)

https://leetcode.cn/problems/minimum-operations-to-collect-elements/description/

题解(散列表)

简单模拟题。

预初始化包含 $1 - k$ 元素的集合,根据题意逆向遍历数组并从集合中移除元素,当集合为空时表示已经收集到所有元素,返回 $n - i$。

class Solution {fun minOperations(nums: List<Int>, k: Int): Int {val n = nums.sizeval 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 - ireturn -1
class Solution {
public:int minOperations(std::vector<int>& nums, int k) {int n = nums.size();unordered_set<int> 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<number>();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<int> nums, int k) {int n = nums.length;Set<int> set = Set<int>();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;}
}

复杂度分析:

  • 时间复杂度:$O(n)$ 线性遍历;
  • 空间复杂度:$O(k)$ 散列表空间。

T2. 使数组为空的最少操作次数(Medium)

https://leetcode.cn/problems/minimum-number-of-operations-to-make-array-empty/description/

题解(贪心)

题目两种操作的前提是数字相等,因此我们先统计每个元素的出现次数。

从最少次数的目标出发,显然能移除 $3$ 个就尽量移除 $3$ 个,再分类讨论:

  • 如果出现次数为 $1$,那么一定无解,返回 $-1$;
  • 如果出现次数能够被 $3$ 整除,那么操作 $cnt / 3$ 次是最优的;
  • 如果出现次数除 $3$ 余 $1$,那么把 $1$ 个 $3$ 拆出来合并为 4,操作 $cnt / 3 + 1$ 次是最优的;
  • 如果出现次数除 $3$ 余 $2$,那么剩下的 $2$ 操作 $1$ 次,即操作 $cnt / 3 + 1$ 次是最优的。

组合以上讨论:

class Solution {fun minOperations(nums: IntArray): Int {val cnts = HashMap<Int, Int>()for (e in nums) {cnts[e] = cnts.getOrDefault(e, 0) + 1}var ret = 0for ((_, cnt) in cnts) {if (cnt == 1) return -1when (cnt % 3) {0 -> {ret += cnt / 3}1, 2 -> {ret += cnt / 3 + 1}}}return ret}
}

继续挖掘题目特性,对于余数大于 $0$ 的情况总是 向上取整 ,那么可以简化为:

class Solution {fun minOperations(nums: IntArray): Int {val cnts = HashMap<Int, Int>()for (e in nums) {cnts[e] = cnts.getOrDefault(e, 0) + 1}var ret = 0for ((_, cnt) in cnts) {if (cnt == 1) return -1ret += (cnt + 2) / 3 // 向上取整}return ret}
}
class Solution:def minOperations(self, nums: List[int]) -> int:cnts = Counter(nums)ret = 0for cnt in cnts.values():if cnt == 1: return -1ret += (cnt + 2) // 3return ret
class Solution {
public:int minOperations(std::vector<int>& nums) {unordered_map<int, int> 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<number, number> = new Map<number, number>();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<int> nums) {Map<int, int> 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;}
}

复杂度分析:

  • 时间复杂度:$O(n)$ 线性遍历
  • 空间复杂度:$O(n)$ 计数空间。

T3. 将数组分割成最多数目的子数组(Medium)

https://leetcode.cn/problems/split-array-into-maximum-number-of-subarrays/description/

题解(思维题)

一个重要的结论是:当按位与的数量增加时,按位与的结果是非递增的。

题目要求在子数组的按位与的和最小的前提下,让子数组的个数最大。根据上面的结论,显然将数组全部按位与是最小的。

分类讨论:

  • 如果整体按位于的结果不为 $0$,那么就不可能存在分割数组的方法使得按位与的和更小,直接返回 $1$;
  • 否则,问题就变成分割数组的最大个数,使得每个子数组按位与为 $0$,直接贪心分割就好了。
class Solution {fun maxSubarrays(nums: IntArray): Int {val mn = nums.reduce { acc, it -> acc and it }if (mn > 0) return 1 // 特判var ret = 0var cur = Integer.MAX_VALUEfor (i in nums.indices) {cur = cur and nums[i]if (cur == 0) {cur = Integer.MAX_VALUEret++}}return ret }
}
class Solution:def maxSubarrays(self, nums: List[int]) -> int:if reduce(iand, nums): return 1ret, mask = 0, (1 << 20) - 1cur = maskfor num in nums:cur &= numif cur == 0: ret += 1; cur = maskreturn ret
class Solution {
public:int maxSubarrays(vector<int>& 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) - 1let 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<int> 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;}
}

复杂度分析:

  • 时间复杂度:$O(n)$ 线性遍历;
  • 空间复杂度:$O(1)$ 仅使用常量级别空间。

T4. 可以被 K 整除连通块的最大数目(Hard)

https://leetcode.cn/problems/maximum-number-of-k-divisible-components/

问题分析

初步分析:

  • 问题目标: 求解分割后满足条件的最大连通块数量;
  • 问题条件: 连通块的和能够被 K 整除;
  • 关键信息: 题目保证数据是可以分割的,这是重要的前提。

思考实现:

在保证问题有解的情况下,树上的每个节点要么是单独的连通分量,要么与邻居组成连通分量。那么,这就是典型的「连或不连」和「连哪个」动态规划思维。

  • 思考「连或不连」:

如果节点 $A$ 的价值能够被 $K$ 整除,那么节点 $A$ 能作为单独的连通分量吗?

不一定,例如 $K = 3$ 且树为 $1 - 3 - 5$ 的情况,连通分量只能为 $1$,因为 $3$ 左右子树都不能构造合法的连通块,因此需要与 $3$ 连接才行。

  • 继续思考「连哪个」:

那么,节点 $A$ 应该与谁相连呢?对于节点 $A$ 的某个子树 $Tree_i$ 来说,存在 $2$ 种情况:

  • 能整除:那么子树 $Tree_i$ 不需要和节点 $A$ 相连;
  • 不能整除:那么子树 $Tree_i$ 的剩余值就必须与节点 $A$ 相连,有可能凑出 $K$ 的整除。

当节点 $A$ 与所有子树的剩余值组合后,再加上当前节点的价值,如果能够构造出 $K$ 的整数倍时,说明找到一个新的连通块,并且不需要和上一级节点组合。否则,则进入不能整除的条件,继续和上一级节点组合。

题解(DFS)

  • 定义 DFS 函数并返回两个数值:<子树构造的连通分量, 剩余值>;
  • 任意选择一个节点为根节点走一遍 DFS,最终返回 $dfs(0,-1)[0]$。
class Solution {fun maxKDivisibleComponents(n: Int, edges: Array<IntArray>, values: IntArray, k: Int): Int {// 建图val graph = Array(n) { LinkedList<Int>() }for ((u, v) in edges) {graph[u].add(v)graph[v].add(u)}// DFS <cnt, left>fun dfs(i: Int, pre: Int): IntArray {var ret = intArrayOf(0, values[i])for (to in graph[i]) {if (to == pre) continueval (childCnt, childLeft) = dfs(to, i)ret[0] += childCntret[1] += childLeft}if (ret[1] % k == 0) {ret[0] += 1ret[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 <cnt, left>def dfs(i, pre):ret = [0, values[i]]for to in graph[i]:if to == pre: continuechildCnt, childLeft = dfs(to, i)ret[0] += childCntret[1] += childLeftif ret[1] % k == 0:ret[0] += 1ret[1] = 0return retreturn dfs(0, -1)[0]
class Solution {
public:int maxKDivisibleComponents(int n, vector<vector<int>>& edges, vector<int>& values, int k) {// 建图vector<list<int>> 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 <cnt, left>function<vector<int>(int, int)> dfs = [&](int i, int pre) -> vector<int> {vector<int> ret(2, 0);ret[1] = values[i];for (int to : graph[i]) {if (to == pre) continue;vector<int> 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 <cnt, left>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<List<int>> edges, List<int> values, int k) {// 建图List<List<int>> 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 <cnt, left>List<int> dfs(int i, int pre) {List<int> ret = [0, values[i]];for (int to in graph[i]) {if (to == pre) continue;List<int> 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];}
}

复杂度分析:

  • 时间复杂度:$O(n)$ 每个节点访问 $1$ 次;
  • 空间复杂度:$O(n)$ 图空间。

推荐阅读

LeetCode 上分之旅系列往期回顾:

  • LeetCode 单周赛第 364 场 · 前后缀分解结合单调栈的贡献问题
  • LeetCode 单周赛第 363 场 · 经典二分答案与质因数分解
  • LeetCode 双周赛第 113 场 · 精妙的 O(lgn) 扫描算法与树上 DP 问题
  • LeetCode 双周赛第 112 场 · 计算机科学本质上是数学吗?

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.liansuoyi.cn/news/54706862.html

如若内容造成侵权/违法违规/事实不符,请联系连锁易网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

代码链接与实践截图

include <fcntl.h> include <unistd.h> int main() { int file_desc = open("test.txt", O_RDONLY); if (file_desc < 0) { // 错误处理 } // 进行其他操作... close(file_desc); return 0;} include <fcntl.h> include <unistd.h> int m…

沁恒蓝牙Mesh之中心节点

1. 中心节点示例代码解读 void App_Init(void)vendor_model_cli_init(vnd_models) 传入参数vnd_models的来源及其数据类型模型初始化需要传入一个蓝牙mesh模型实例 vendor_model_cli_init(vnd_models);vendor_model_cli是自定义模型数据类型 blemesh_on_sync() 主要是对Mesh参数…

vue:el-table在resize时报错(element-plus@2.3.12)

一,报错信息: Uncaught runtime errors:ERROR ResizeObserver loop completed with undelivered notifications. at handleError (webpack-internal:///./node_modules/webpack-dev-server/client/overlay.js:299:58) at eval (webpack-internal:///./node_modules/webpac…

2023年中国高校计算机大赛-团队程序设计天梯赛(GPLT)上海理工大学校内选拔赛(同步赛)

A. A Xor B Problem(计数)输入5 1 1 2 2 3输出9说明点击查看代码 #include<bits/stdc++.h> #define IOS ios::sync_with_stdio(false);cin.tie(0),cout.tie(0) #define int long longusing namespace std;const int N = 2e5 + 10;unordered_map<int, int> mp; in…

CS2打开可以听到声音,但黑屏问题?

1.问题 我这里原先是可以启动CS2的,但是后来在CS2中重新调整了分辨率等等,之后由于某种原因又调整了屏幕分辨率,导致后面一进入CS2登录界面,橙色登陆界面就会缩在左上角一小块,并且之后就会陷入黑屏但有声音的状态 2.解决方法 参考链接:https://appuals.com/black-screen…

高级系统架构师学习(九)数据库系统

一、数据库概述 数据库模式 三级模式:外模式:视图 模式(也称为概念模式):数据库表 内模式:物理文件两层映像:两层映像可以保证数据库中的数据具有较高的逻辑独立性和物理独立性。外模式 - 模式映像 模式 - 内模式映像物理独立性:即数据库的内模式发生改变时,应用程序不…

传递函数变换到状态空间

1. 分子为1的传递函数 例: \[G(s)=\frac{1}{s^3+a_2s^2+a_1s+a_0} \]首先写成输入输出关系: \[(s^3+a_2s^2+a_1s+a_0)Y(s)=U(s) \]对应的微分方程: \[\dddot{y}(t)+a_2\ddot y(t)+a_1\dot y(t)+a_0y(t)=u(t)\\ \dddot{y}(t)=-a_2\ddot y(t)-a_1\dot y(t)-a_0y(t)+u(t)\\ \]令…

-bash: ifconfig: 未找到命令

#abao { background-color: rgba(245, 245, 245, 1); padding: 10px; text-align: center; width: 800px } 命令: yum -y install ifconfig如果返回为:没有可用软件包 ifconfig。错误:无须任何处理则输入:yum search ifconfig返回:==================== 匹配:ifconfig ==…

SOC芯片架构技术分析(三)

SOC芯片架构技术分析(三) 3.1 汽车:汽车平台未来需要高算力 1)汽车半导体涵盖了汽车芯片、功率器件、传感器等重要电子零部件。汽车的计算芯片包括传统的MCU芯片和SoC芯片。 MCU芯片一般包含CPU一个处理器单元;而汽车SoC一般包含多个处理单元。 2)ECU(Electronic Contro…

python文件操作

在python,使用open函数,可以打开一个已经存在的文件,或者创建一个新文件 open(文件路径,访问模式)f = open(test.txt, w)读文件:f1 = open(test1.txt,r)content = f1.read()print(content)readLines返回一个列表,可以遍历 f1 = open(test1.txt,r)content = f1.readlines(…

MySQL索引的认识

MySQL表的所有记录,是存储在磁盘中的。 当根据非索引字段进行查询时,MySQL 通常需要执行全表扫描,以查找满足查询条件的记录。全表扫描意味着 MySQL 必须逐一检查表中的每一行,以确定哪些行符合查询条件。 全表扫描会导致磁盘 I/O 次数增加,因为 MySQL 需要读取整个表的数…

Java集合框架(部分)

类图List:有序可重复集合 特点 1.元素允许重复 2.元素有序(元素的顺序就是插入时的顺序) 3.元素可以通过索引来访问或者设置 { ArrayLIst:底层为数组,查询速度快,增删慢 LinkedList:底层是链表,查询速度慢,增删快 两者的优缺点,:效率高,线程不安全 } Set:无序不可…

Go每日一库之172:go-prompt

简介 受python提示工具包的启发,在Go中构建强大的交互式提示 一、代码示例 package mainimport ("fmt""github.com/c-bata/go-prompt" )func completer(d prompt.Document) []prompt.Suggest {s := []prompt.Suggest{{Text: "users", Descripti…

SpringCloud

目录Springcloud介绍注册中心(Eureka)背景注册中心案例总结负载均衡(Ribbon)测试使用负载均衡RibbonRibbon负载均衡流程Ribbon的IRule常见负载均衡策略Ribbon的使用方法远程调用(Open Feign)Feign使用连接池注册中心(Nacos)测试配置集群命名空间Nacos非临时实例配置中心(Nacos)…

RedisInsight安装及使用

前言 RedisInsight 是一个直观高效的 Redis GUI 管理工具,它可以对 Redis 的内存、连接数、命中率以及正常运行时间进行监控,并且可以在界面上使用 CLI 和连接的 Redis 进行交互(RedisInsight 内置对 Redis 模块支持)。 RedisInsight 提供的功能:唯一支持 Redis Cluster 的…

函数基础和函数参数

第一部分:函数基础 函数的作用意义:1.为了更好地管理代码,可能对应的代码块需要重复多次使用,所以通过一个函数封装起来,便于下次直接调用2.方法实际上是通过函数实现的 例1:# type() # 内置函数 def lis():li=[1,2,3]li.append(4)li.pop(2) # 指定删除# print(li) # …

06. 系统滴答定时器

一、SysTick定时器简介SysTick,即系统滴答定时器,是属于 CM3 内核中的一个外设,内嵌在 NVIC 中。系统定时器是一个 24bit 的向下递减的计数器,SysTick 的时钟源自 HCLK。当计数值减到 0 时,将从 RELOAD 寄存器中自动重装载定时初值,开始新一轮计数。只要不把它在 SysTick…

【Qt6】列表模型——树形列表

QStandardItemModel 类作为标准模型,主打“类型通用”,前一篇水文中,老周还没提到树形结构的列表,本篇咱们就好好探讨一下这货。 还是老办法,咱们先做示例,然后再聊知识点。下面这个例子,使用 QTreeView 组件来显示数据,使用的列表模型比较简单,只有一列。#include &l…

QT QPixmap QImage内存泄漏

无论是在代码中还是在UI中设置icon都会产生内存泄漏 大概看了下,好像是QPixmap的data_ptr的引用计数,到不了1/0(查看引用计数,释放后,理论上应回到1) 试了下,仅以下两种方式不会产生内存泄漏: 1、从 XPM加载: img = QPixmap(result); //result为 static const cha…
推荐文章