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

news2024/11/26 15:32:34

⭐️ 本文已收录到 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.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<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 = 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<Int, Int>()
        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<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 = 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<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) - 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<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) 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 <cnt, left>
        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<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.coloradmin.cn/o/1053255.html

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

相关文章

软件测试/测试开发丨python 多态与super 学习笔记

本文为霍格沃兹测试开发学社学员学习笔记分享 原文链接&#xff1a;https://ceshiren.com/t/topic/26828 python 多态与super 多态的概念 多态&#xff1a;Polymorphism 同名方法呈现多种行为 多态的表现 号 加法&#xff1a;数字 数字拼接&#xff1a;字符串 字符串合…

2023年10月腾讯云优惠活动汇总:腾讯云最新优惠、代金券整理

腾讯云作为国内领先的云服务提供商&#xff0c;致力于为用户提供优质、稳定的云服务。为了更好地满足用户需求&#xff0c;腾讯云推出了各种优惠活动。本文将给大家分享腾讯云最新优惠活动&#xff0c;帮助用户充分利用腾讯云提供的优惠。 一、腾讯云优惠券领取【点此领取】 腾…

北京互联网公司、外企、国企大盘点

今天来盘点北京的泛互联网公司。涵盖了综合类、外企类、硬件类、电商类、游戏类、娱乐类、生活类、工具类、人工智能类、金融类、教育类、招聘类、汽车类、外包类、信息化类以及国企央企类等多个领域。 综合类 字节跳动 美团 百度 阿里巴巴 腾讯 网易 外企类 微软 谷歌 亚马逊…

CSP-J第二轮试题-2021年-4题

文章目录 参考&#xff1a;总结 [CSP-J 2021] 小熊的果篮题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 样例 #3样例输入 #3样例输出 #3 提示答案1答案2答案3 现场真题注意事项 参考&#xff1a; https://www.luogu.com.cn/problem/P…

Leetcode 50.Pow(x,n)

实现 pow(x, n) &#xff0c;即计算 x 的整数 n 次幂函数&#xff08;即&#xff0c;xn &#xff09;。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000示例 2&#xff1a; 输入&#xff1a;x 2.10000, n 3 输出&#xff1a;9.26100示例…

Java大数 -- BigInteger类

在java语言中&#xff0c;每一种整数类型都有自己的上限和下限&#xff0c;如果要想对非常大的整数做运算&#xff0c;就需要使用BigInteger类。特别在做算法题传入一个数值型的字符串时。 1.包 import java.math.*; 2.构造方法 public BigInteger(String str){...} BigInte…

Go_原子操作和锁

原子操作和锁 本文先探究并发问题&#xff0c;再探究锁和原子操作解决问题的方式&#xff0c;最后进行对比。 并发问题 首先&#xff0c;我们看一下程序 num该程序表面看上去一步就可以运行完成&#xff0c;但是实际上&#xff0c;在计算机中是分三步运行的&#xff0c;如下…

让大脑自由

前言 作者写这本书的目的是什么&#xff1f; 教会我们如何让大脑更好地为自己工作。 1 大脑的运行机制是怎样的&#xff1f; 大脑的基本运行机制是神经元之间通过突触传递信息&#xff0c;神经元的兴奋和抑制状态决定了神经网络的运行和信息处理&#xff0c;神经网络可以通过…

[架构之路-226]:信息系统建模 - 实体关系图、数据流图、数据字典、流程图

目录 一、信息系统建模概述 二、常见建模工具 2.1 实体关系图 -- “实体》类” 》关注实体&#xff0c;对象 2.2 数据流图 -- 数据流动 -- 关注数据本身 2.3 业务-流程图 -- 活动步骤 -- 关注动作、活动 三、建模工具的比较 3.1 数据流图与业务流程图的区别 3.2 数据、…

Linux高性能服务器编程 学习笔记 第九章 IO复用

IO复用使程序能同时监听多个文件描述符&#xff0c;这可以提高程序的性能&#xff0c;通常网络程序在以下情况需要使用IO复用&#xff1a; 1.客户端进程需要同时处理多个socket。 2.客户端进程需要同时处理用户输入和网络连接。 3.TCP服务器要同时处理监听socket和连接socket…

网络-Ajax

文章目录 前言一、Ajax优点&#xff1a;缺点&#xff1a; 二、使用步骤XNLHttpRequest对象完整代码 总结 前言 本文主要记录Ajax技术的简介&#xff0c;以及用法。 一、Ajax Ajax是一组用于在Web浏览器和Web服务器之间进行异步通信的Web开发技术。 它代表着Asynchronous Java…

如果在 Mac 上的 Safari 浏览器中无法打开网站

使用网络管理员提供的信息更改代理设置。个人建议DNS解析&#xff0c;设置多个例如114.114.114.114 8.8.8.8 8.8.4.4 如果打不开网站&#xff0c;请尝试这些建议。 在 Mac 上的 Safari 浏览器 App 中&#xff0c;检查页面无法打开时出现的信息。 这可能会建议解决问题的…

第 114 场 LeetCode 双周赛题解

A 收集元素的最少操作次数 模拟: 反序遍历数组&#xff0c;用一个集合存当前遍历过的不超过 k k k 的正数 class Solution { public:int minOperations(vector<int> &nums, int k) {unordered_set<int> vis;int n nums.size();int i n - 1;for (;; i--) {if…

nodejs+vue活鲜物流监控系统elementui

第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;技术背景 5 3.2.2经济可行性 6 3.2.3操作可行性&#xff1a; 6 3.3 项目设计目标与原则 6 3.4系统流程分析 7 3.4.1操作流程 7 3.4.2添加信息流程 8 3.4.3删除信息流程 9 第4章 系统设计 11 …

蓝桥等考Python组别十级003

第一部分&#xff1a;选择题 1、Python L10 &#xff08;15分&#xff09; 已知s Pencil&#xff0c;下列说法正确的是&#xff08; &#xff09;。 s[0]对应的字符是Ps[1]对应的字符是ns[-1]对应的字符是is[3]对应的字符是e 正确答案&#xff1a;A 2、Python L10 &am…

【OpenCV-Torch-dlib-ubuntu】Vm虚拟机linux环境摄像头调用方法与dilb模型探究

前言 随着金秋时节的来临&#xff0c;国庆和中秋的双重喜庆汇聚成一片温暖的节日氛围。在这个美好的时刻&#xff0c;我们有幸共同迎来一次长达8天的假期&#xff0c;为心灵充电&#xff0c;为身体放松&#xff0c;为未来充实自己。今年的国庆不仅仅是家国团聚的时刻&#xff…

SSL/TLS介绍以及wireshark抓包TLS Handshake报文

文章目录 1.概念1.1 SSL/TLS发展历史1.2 TLS两个阶段1.3 TLS报文头 2.TLS Handshake2.1 Handshake具体过程2.1.1 单向认证和双向认证2.1.2 复用TLS协商结果Session Identifier&#xff08;会话标识符&#xff09;Session Ticket&#xff08;会话票据&#xff09; 2.2 Handshake…

实验篇——根据群体经纬度提取环境数据(数据降维)

实验篇——根据群体经纬度提取环境数据&#xff08;数据降维&#xff09; 文章目录 前言一、先导二、R语言实现2.1. 分气温、降水、光照、风速、蒸汽压划分数据集2.2. 对每个数据集降维处理2.2.1. 气温2.2.2. 降水2.2.2. 光照2.2.3. 风速2.2.4. 蒸汽压2.2.5.定义一个函数&#…

【小沐学前端】Node.js实现UDP和Protobuf 通信(protobuf.js)

文章目录 1、简介1.1 node1.2 Protobuf 2、下载和安装2.1 node2.2 Protobuf 3、node 代码示例3.1 HTTP3.2 UDP单播3.4 UDP广播 4、Protobuf 代码示例4.1 例子:awesome.proto 结语 1、简介 1.1 node Node.js 是一个开源的、跨平台的 JavaScript 运行时环境。 Node.js 是一个开源…

基于 SpringBoot+Vue 的教室人事档案管理系统

1 简介 教师人事档案管理系统利用信息的合理管理&#xff0c;动态的、高效的、安全的实现了教师的各种需求&#xff0c;改变了传统的网上查看方式&#xff0c;使教师可以足不出户的在线查看最适合自己个人档案、奖惩信息、档案变动、培训报名或者新闻资讯。 1、教师后台功能模…