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

⭐️ 本文已收录到 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 1 - k 1k 元素的集合,根据题意逆向遍历数组并从集合中移除元素,当集合为空时表示已经收集到所有元素,返回 n − i n - i ni

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(n) O(n) 线性遍历;
  • 空间复杂度: O ( k ) O(k) O(k) 散列表空间。

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

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

题解(贪心)

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

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

  • 如果出现次数为 1 1 1,那么一定无解,返回 − 1 -1 1
  • 如果出现次数能够被 3 3 3 整除,那么操作 c n t / 3 cnt / 3 cnt/3 次是最优的;
  • 如果出现次数除 3 3 3 1 1 1,那么把 1 1 1 3 3 3 拆出来合并为 4,操作 c n t / 3 + 1 cnt / 3 + 1 cnt/3+1 次是最优的;
  • 如果出现次数除 3 3 3 2 2 2,那么剩下的 2 2 2 操作 1 1 1 次,即操作 c n t / 3 + 1 cnt / 3 + 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 0 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) O(n) 线性遍历
  • 空间复杂度: O ( n ) O(n) O(n) 计数空间。

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

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

题解(思维题)

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

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

分类讨论:

  • 如果整体按位于的结果不为 0 0 0,那么就不可能存在分割数组的方法使得按位与的和更小,直接返回 1 1 1
  • 否则,问题就变成分割数组的最大个数,使得每个子数组按位与为 0 0 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(n) O(n) 线性遍历;
  • 空间复杂度: O ( 1 ) O(1) O(1) 仅使用常量级别空间。

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

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

问题分析

初步分析:

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

思考实现:

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

  • 思考「连或不连」:

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

不一定,例如 K = 3 K = 3 K=3 且树为 1 − 3 − 5 1 - 3 - 5 135 的情况,连通分量只能为 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 种情况:

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

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

题解(DFS)

  • 定义 DFS 函数并返回两个数值:<子树构造的连通分量, 剩余值>;
  • 任意选择一个节点为根节点走一遍 DFS,最终返回 d f s ( 0 , − 1 ) [ 0 ] dfs(0,-1)[0] 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 ) O(n) O(n) 每个节点访问 1 1 1 次;
  • 空间复杂度: O ( n ) O(n) O(n) 图空间。

推荐阅读

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

点击、拖拉拽,BI系统让业务掌握数据分析主动权

在今天的商业环境中&#xff0c;数据分析已经成为企业获取竞争优势的关键因素之一。然而&#xff0c;许多企业在面对复杂的数据分析工具时&#xff0c;却常常感到困扰。这些工具往往需要专业的技术人员操作&#xff0c;而且界面复杂&#xff0c;难以理解和使用。对业务人员来说…

阿里云 Oss 权限控制

前言 最近公司的私有 Oss 服务满了&#xff0c;且 Oss 地址需要设置权限&#xff0c;只有当前系统的登录用户才能访问 Oss 下载地址。一开始想着用 Nginx 做个转发来着&#xff0c;Nginx 每当检测当前请求包含特定的 Oss 地址就转发到我们的统一鉴权接口上去&#xff0c;但是紧…

picoctf_2018_shellcode

picoctf_2018_shellcode Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments32位&#xff0c;啥都没开 这个看着挺大的&#xff0c;直接来个ROPchain&#xff0c;…

Redis缓存穿透、击穿和雪崩

面试高频 服务的高可用问题&#xff01; 在这里我们不会详细的区分析解决方案的底层&#xff01; Redis缓存概念 Redis缓存的使用&#xff0c;极大的提升了应用程序的性能和效率&#xff0c;特别是数据查询方面。但同时&#xff0c;它也带来了一些问题。其中&#xff0c;最要…

Android stdio的Gradle菜单栏无内容问题的解决方法

右边Gradle菜单栏里没有Tasks选项内容的问题 正常情况↓ 如果这个问题如果无法解决的话&#xff0c;Gradle打包就只能通过控制台输入命令来解决&#xff0c;但这无疑是把简单问题复杂化了&#xff0c;我们来看看怎么解决这个问题吧。 这里有几个方法提供&#xff0c;可以自行选…

排序篇(四)----归并排序

排序篇(四)----归并排序 1.归并(递归) 基本思想&#xff1a; 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide andConquer&#xff09;的一个非常典型的应用。将已有序的子序列合并&#xff0c;得到…

leetCode 376.摆动序列 动态规划 + 图解 + 状态转移

376. 摆动序列 - 力扣&#xff08;LeetCode&#xff09; 如果连续数字之间的差严格地在正数和负数之间交替&#xff0c;则数字序列称为 摆动序列 。第一个差&#xff08;如果存在的话&#xff09;可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。 例如…

微信小程序引入字体在部分机型失效不兼容解决办法

写小程序页面&#xff0c;美工作图用了特殊字体 引入代码&#xff1a; font-face {font-family: huxiaobo;src: url("https://xxxxxxxx.top/assets/fonts/huxiaobonanshenti.woff") } .font-loaded {font-family: "huxiaobo"; } 上线后发现部分安卓机型不…

209. 长度最小的子数组(滑动窗口)

一、题目 209. 长度最小的子数组 - 力扣&#xff08;LeetCode&#xff09; 二、代码 class Solution { public:int minSubArrayLen(int target, vector<int>& nums) {int left 0, right 0;int sum nums[right];int MinLength INT_MAX;while (left <nums.siz…

计算机竞赛 深度学习火车票识别系统

文章目录 0 前言1 课题意义课题难点&#xff1a; 2 实现方法2.1 图像预处理2.2 字符分割2.3 字符识别部分实现代码 3 实现效果4 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 图像识别 火车票识别系统 该项目较为新颖&#xff0c;适…

54、数组--模拟

LCR 146. 螺旋遍历二维数组 给定一个二维数组 array&#xff0c;请返回「螺旋遍历」该数组的结果。 螺旋遍历&#xff1a;从左上角开始&#xff0c;按照 向右、向下、向左、向上 的顺序 依次 提取元素&#xff0c;然后再进入内部一层重复相同的步骤&#xff0c;直到提取完所有…

(vue3)create-vue 组合式APIsetup、ref、watch,通信

优势&#xff1a; 更易维护&#xff1a;组合式api&#xff0c;更好的TS支持 之前是选项式api&#xff0c;现在是组合式&#xff0c;把同功能的api集合式管理 复用功能封装成一整个函数 更快的速度 更小的体积 更优的数据响应式&#xff1a;Proxy create-vue 新的脚手架工…

【VIM】VIm-plug插件

如何查找需要的插件 https://github.com/mhinz/vim-startify https://github.com/vim-airline/vim-airline https://github.com/Yggdroot/indentLine github.com/w0ng/vim-hybrid github.com/altercationi/vim-colors-solarized guithub.com/morhetz/gruvbox github.com/sc…

PHP8的静态变量和方法-PHP8知识详解

我们在上一课程讲到了public、private、protected这3个关键字&#xff0c;今天我们来讲解static关键字&#xff0c;明天再讲解final关键字。 如果不想通过创建对象来调用变量或方法&#xff0c;则可以将该变量或方法创建为静态变量或方法&#xff0c;也就是在变量或方法的前面…

什么是博弈论?

什么是博弈&#xff1f;字面描述中&#xff0c;博弈由两个字构成&#xff1a;博 和 弈。博弈是一种双方&#xff08;多方&#xff09;的对抗&#xff08;比赛&#xff09;&#xff0c;对抗总是在一定的规则下进行&#xff0c;参与者必然会考虑应用相应的策略&#xff08;计谋&a…

YOLOv5-PTQ量化部署

目录 前言一、PTQ量化浅析二、YOLOv5模型训练1. 项目的克隆和必要的环境依赖1.1 项目克隆1.2 项目代码结构整体介绍1.3 环境安装 2. 数据集和预训练权重的准备2.1 数据集2.2 预训练权重准备 3. 训练模型3.1 修改数据配置文件3.2 修改模型配置文件3.3 训练模型3.4 mAP测试 三、Y…

ElementUI之增删改及表单验证

⭐⭐本文章收录与ElementUI原创专栏&#xff1a;ElementUI专栏 ⭐⭐ ElementUI的官网&#xff1a;ElementUI官网 目录 一.前言 二.使用ElementUI完成增删改 2.1 后台代码 2.2 前端代码 三.使用ElementUI完成表单验证 一.前言 本章是继上一篇的基础之上在做完善&#xff0…

java Spring Boot按日期 限制大小分文件记录日志

上文 java Spring Boot 将日志写入文件中记录 中 我们实现另一个将控制台日志写入到 项目本地文件的效果 但是 这里有个问题 比如 我项目是个大体量的企业项目 每天会有一百万用户访问 那我每天的日志都记载同一个文件上 那不跟没记没什么区别吗&#xff1f; 东西怎么找&#x…

Windows 安装CMake

CMake 简介 CMake是一个开源的、跨平台的自动化构建系統&#xff0c;用來管理软件构建的过程。 其用途主要包括&#xff1a; 1. 跨平台编译&#xff1a;CMake支援Windows&#xff0c;Mac OS&#xff0c;Linux等多种操作系統&#xff0c;且支援多数主流编译器如GCC&#xff0…

雷达编程实战之功耗优化技术(低功耗)

本篇文章以xWRL6432为例&#xff0c;首先介绍了芯片内部的电源管理框架&#xff0c;在产品业务处理流程的不同阶段&#xff0c;我们可以对不同电源域进行相应的开/关来降低功耗。然后介绍了不同的硬件电源参考设计对芯片功耗的影响&#xff0c;又着重介绍了线性调频脉冲相关参数…