【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集)

news2024/12/22 20:32:30

文章目录

  • 前期知识
  • 例题
    • 337. 打家劫舍 III
  • 相关练习题目
    • 没有上司的舞会 https://www.luogu.com.cn/problem/P1352
    • 1377. T 秒后青蛙的位置 https://leetcode.cn/problems/frog-position-after-t-seconds/⭐⭐⭐
      • 解法1:BFS
        • 优化代码
      • 解法2——自顶向下dfs
      • 解法3——自底向上dfs
    • 2646. 最小化旅行的价格总和 https://leetcode.cn/problems/minimize-the-total-price-of-the-trips/solution/lei-si-da-jia-jie-she-iii-pythonjavacgo-4k3wq/⭐⭐⭐⭐⭐
      • 解法1——暴力dfs每条路径
      • 解法2——Tarjan 离线 LCA + 树上差分
    • 补充题目:2467. 树上最大得分和路径⭐⭐⭐

前期知识

最大独立集 需要从图中选择尽量多的点,使得这些点互不相邻。

在这里插入图片描述

例题

337. 打家劫舍 III

337. 打家劫舍 III

在这里插入图片描述
用一个数组 int[] = {a, b} 来接收每个节点返回的结果。返回值{a,b} a表示没选当前节点的最大值,b表示选了当前节点的最大值。

使用后序遍历dfs。 (发现树形 DP 基本都是后序 dfs

在这里插入图片描述

class Solution {
    public int rob(TreeNode root) {
        int[] res = dfs(root);
        return Math.max(res[0], res[1]);
    }

    public int[] dfs(TreeNode root) {
        // 返回值{a,b} a表示没选当前节点的最大值,b表示选了当前节点的最大值
        if (root == null) return new int[]{0, 0};
        int[] l = dfs(root.left), r = dfs(root.right);
        int a = Math.max(l[0], l[1]) + Math.max(r[0], r[1]), b = root.val + l[0] + r[0];
        return new int[]{a, b};
    }
}

相关练习题目

没有上司的舞会 https://www.luogu.com.cn/problem/P1352

P1352 没有上司的舞会

在这里插入图片描述
这道题目相当于每个节点可能会有若干个子节点的 337. 打家劫舍 III 。

因为是 ACM 模式,所以代码处理输入很冗长。

AC代码如下:

import java.util.*;

public class Main {
    static List<Integer>[] childs;	// 记录每个节点的子节点列表
    static int[] r;					// 记录每个节点的快乐指数

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        r = new int[n + 1];
        for (int i = 1; i <= n; ++i) r[i] = scanner.nextInt();
        childs = new ArrayList[n + 1];
        Arrays.setAll(childs, e -> new ArrayList());
        Set<Integer> s = new HashSet<>();	// 记录哪些节点没有父节点,把他们统一放在0节点之下
        for (int i = 1; i <= n; ++i) s.add(i);
        for (int i = 2; i <= n; ++i) {
            int l = scanner.nextInt(), k = scanner.nextInt();
            childs[k].add(l);
            s.remove(l);
        }
        for (int v: s) childs[0].add(v);    // 所有没有父节点的节点放在0节点之下
        int[] res = dfs(0);
        System.out.println(Math.max(res[0], res[1]));
        return;
    }

    public static int[] dfs(int rootId) {
        int a = 0, b = r[rootId];		// a表示不选当前节点,b表示选当前节点
        for (int i: childs[rootId]) {   // 枚举所有子节点
            int[] res = dfs(i);
            a += Math.max(res[0], res[1]);	// 不选当前节点,所以子节点选不选都可以
            b += res[0];					// 选当前节点,所以子节点只能不选
        }
        return new int[]{a, b};
    }
}

从代码模板来看,这里使用 for 循环来枚举当前节点的所有子节点。

1377. T 秒后青蛙的位置 https://leetcode.cn/problems/frog-position-after-t-seconds/⭐⭐⭐

1377. T 秒后青蛙的位置

在这里插入图片描述

解法1:BFS

从题意来看,每次青蛙都会往下走一层,很像层序遍历,所以可以是用 bfs 来模拟整个过程。

在 bfs 的过程中,如果当前节点的概率为 p,他有 k 个子节点,那么它的所有子节点的概率就是 p / k,即在分母上乘了 k。
在这个过程中,我们可以记录各个节点的概率分母。

如果当前节点没有子节点,那么它的概率不变,再重新加入队列中。

class Solution {
    public double frogPosition(int n, int[][] edges, int t, int target) {
        // 建树
        List<Integer>[] g = new ArrayList[n + 1];
        Arrays.setAll(g, e -> new ArrayList());
        for (int[] edge: edges) {
            int x = edge[0], y = edge[1];
            g[x].add(y);
            g[y].add(x);
        }
        boolean[] st = new boolean[n + 1];
        // 由于某些样例在计算概率分母时会溢出int,因此使用long
        Queue<long[]> q = new LinkedList();
        q.offer(new long[]{1, 1});   // 将第一个节点放入队列
        st[1] = true;
        for (int i = 0; i <= t; ++i) {
            int sz = q.size();
            for (int j = 0; j < sz; ++j) {
                long[] cur = q.poll();
                // 如果达到了目标位置且时间正确,返回答案
                if (cur[0] == target && i == t) return 1.0 / cur[1];
                List<Integer> children = g[(int)cur[0]];
                boolean f = false;          // 记录它是否有子节点
                int cnt = 0;                // 记录它有几个子节点
                for (int child: children) {
                    if (!st[child]) ++cnt;
                }
                for (int child: children) {
                    if (!st[child]) {
                        q.offer(new long[]{child, cur[1] * cnt});    // 将子节点加入队列,概率的分母更新为cnt[1]*cnt
                        st[child] = true;
                        f = true;           // 说明有子节点
                    }
                }
                // 如果当前节点没有子节点,那么它还会待在当前位置
                if (!f) q.offer(cur);
            }
        }
        return 0;
    }
}

优化代码

优化的点包括:

  • 修改了 return 结果的判断条件
  • 删去了 cnt 统计每个节点的子节点个数,因为子节点数目就是g[x].size() - 1
  • 给起始节点1增加一个父节点0,可以避免写多余判断
class Solution {
    public double frogPosition(int n, int[][] edges, int t, int target) {
        // 建树
        List<Integer>[] g = new ArrayList[n + 1];
        Arrays.setAll(g, e -> new ArrayList());
        g[1].add(0);		// 小技巧
        for (int[] edge: edges) {
            int x = edge[0], y = edge[1];
            g[x].add(y);
            g[y].add(x);
        }
        boolean[] st = new boolean[n + 1];
        // 由于某些样例在计算概率分母时会溢出int,因此使用long
        Queue<long[]> q = new LinkedList();
        q.offer(new long[]{1, 1});   // 将第一个节点放入队列
        st[1] = true;
        for (int i = 0; i <= t; ++i) {
            int sz = q.size();
            for (int j = 0; j < sz; ++j) {
                long[] cur = q.poll();
                // 如果达到了目标位置且时间正确,返回答案
                if (cur[0] == target) {
                    if (i == t || g[(int)cur[0]].size() == 1) return 1.0 / cur[1];
                    return 0;
                }
                List<Integer> children = g[(int)cur[0]];
                int cnt = children.size() - 1;                      // 记录它有几个子节点
                for (int child: children) {
                    if (!st[child]) {
                        q.offer(new long[]{child, cur[1] * cnt});    // 将子节点加入队列,概率的分母更新为cnt[1]*cnt
                        st[child] = true;
                    }
                }
            }
        }
        return 0;
    }
}

解法2——自顶向下dfs

整体思路与 bfs 类似。

class Solution {
    double ans;

    public double frogPosition(int n, int[][] edges, int t, int target) {
        // 建树
        List<Integer>[] g = new ArrayList[n + 1];
        Arrays.setAll(g, e -> new ArrayList());
        g[1].add(0);        	// 常用技巧:为了减少额外判断,给1加个父节点0
        for (int[] edge: edges) {
            int x = edge[0], y = edge[1];
            g[x].add(y);
            g[y].add(x);
        }
        dfs(g, target, 1, 0, t, 1);
        return ans;
    }

    public boolean dfs(List<Integer>[] g, int target, int x, int father, int leftTime, long prod) {
        // 如果x == target 且 (正好在t秒或处在叶子节点上)
        if (x == target && (leftTime == 0 || g[x].size() == 1)) {
            ans = 1.0 / prod;
            return true;
        }
        if (x == target || leftTime == 0) return false;
        for (int y: g[x]) {     // 遍历x的儿子y
            if (y != father && dfs(g, target, y, x, leftTime - 1, prod * (g[x].size() - 1))) {
                return true;    // 找到了就不再递归了
            }
        }
        return false;
    }
}

这里学到的重要技巧:

  • 每个节点只会有一个父节点,因此它的子节点数量就是 g[x].size() - 1,不需要单独计算了。
  • g[1].add(0); // 常用技巧:为了减少额外判断,给1加个父节点0
  • dfs 返回 boolean 值,在 if () 中进行 dfs。

解法3——自底向上dfs

class Solution {
    double ans;

    public double frogPosition(int n, int[][] edges, int t, int target) {
        // 建树
        List<Integer>[] g = new ArrayList[n + 1];
        Arrays.setAll(g, e -> new ArrayList());
        g[1].add(0);        // 为了减少额外判断,给1加个父节点0
        for (int[] edge: edges) {
            int x = edge[0], y = edge[1];
            g[x].add(y);
            g[y].add(x);
        }
        long prod = dfs(g, target, 1, 0, t);
        return prod == 0? prod: 1.0 / prod;
    }

    public long dfs(List<Integer>[] g, int target, int x, int father, int leftTime) {
        // 找到结果的两种情况
        if (leftTime == 0) return x == target? 1: 0;
        if (x == target) return g[x].size() == 1? 1: 0;
        // 遍历 x 的儿子 y
        for (int y: g[x]) {
            if (y != father) {
                long prod = dfs(g, target, y, x, leftTime - 1);
                if (prod != 0) return prod * (g[x].size() - 1);
            }
        }
        return 0;
    }
}

自底向上的优点在于,只有找到了 target 之后才会做乘法计算 prod。

2646. 最小化旅行的价格总和 https://leetcode.cn/problems/minimize-the-total-price-of-the-trips/solution/lei-si-da-jia-jie-she-iii-pythonjavacgo-4k3wq/⭐⭐⭐⭐⭐

2646. 最小化旅行的价格总和

在这里插入图片描述

解法1——暴力dfs每条路径

在这里插入图片描述

核心思路是遍历每条路径,修改 price 数组,然后将问题转换成 337. 打家劫舍 III 。

class Solution {
    List<Integer>[] g;
    int[] price, cnt;
    int end;

    public int minimumTotalPrice(int n, int[][] edges, int[] price, int[][] trips) {
        g = new ArrayList[n];
        Arrays.setAll(g, e -> new ArrayList());
        for (int[] edge: edges) {
            int x = edge[0], y = edge[1];
            g[x].add(y);
            g[y].add(x);
        }
        this.price = price;
        cnt = new int[n];       // 记录每个节点在路径中出现的次数
        for (int[] trip: trips) {
            end = trip[1];
            op(trip[0], -1);    // 用dfs找到这个trip路径上的点   
        }

        int[] p = dfs(0, -1);
        return Math.min(p[0], p[1]);
    }

    public boolean op(int x, int father) {
        if (x == end) {     // 找到了end
            ++cnt[x];
            return true;
        }
        for (int y: g[x]) {
            if (y != father && op(y, x)) {
                ++cnt[x];
                return true;
            }
        }
        return false;       // 没找到
    }

    // 返回值 :[不减半,减半]
    public int[] dfs(int x, int father) {
        int notHalve = price[x] * cnt[x], halve = notHalve / 2;
        for (int y: g[x]) {
            if (y != father) {
                int[] p = dfs(y, x);
                notHalve += Math.min(p[0], p[1]);   // 当前不减半,那前面的节点随意
                halve += p[0];      // 当前减半,那前面的节点只能不减半
            }
        }
        return new int[]{notHalve, halve};
    }
}

解法2——Tarjan 离线 LCA + 树上差分

TODO
https://leetcode.cn/problems/minimize-the-total-price-of-the-trips/solution/lei-si-da-jia-jie-she-iii-pythonjavacgo-4k3wq/

补充题目:2467. 树上最大得分和路径⭐⭐⭐

2467. 树上最大得分和路径

在这里插入图片描述
这道题目更多的是练习树的 dfs 技巧,与 dp 关系不大。

使用两个 dfs ,
第一个 dfs 用于计算 bob 达到各点的时间,为计算 alice 路径上的价值做准备。
第二个 dfs 用于枚举 alice 到达各个叶子节点的路径,在达到叶子节点时更新答案。

class Solution {
    List<Integer>[] g;
    int[] bobTime, amount;
    int ans = Integer.MIN_VALUE;

    public int mostProfitablePath(int[][] edges, int bob, int[] amount) {
        int n = edges.length + 1;
        g = new ArrayList[n];
        Arrays.setAll(g, e -> new ArrayList());
        for (int[] edge: edges) {
            int x = edge[0], y =edge[1];
            g[x].add(y);
            g[y].add(x);
        }
        g[0].add(-1);   // 常用技巧,给0节点加一个父节点-1,可以少写多余判断

        this.amount = amount;
        bobTime = new int[n];           // 记录bob到达该节点的时间
        Arrays.fill(bobTime, Integer.MAX_VALUE);    // 初始化为bob都到不了
        dfsBob(bob, 0, -1);
        dfsAlice(0, 0, -1, 0);
        return ans;
    }

    // 当前节点、当前节点的时间、当前节点的父节点
    public boolean dfsBob(int x, int time, int father) {
        // 只有到达了目的地才设置时间
        if (x == 0) {
            bobTime[x] = time;
            return true;
        }
        for (int y: g[x]) {
            if (y != father && dfsBob(y, time + 1, x)) {
                bobTime[x] = time;
                return true;
            }
        }
        return false;
    }

    public void dfsAlice(int x, int time, int father, int score) {
        // 计算当前节点的值
        int curVal = 0;
        if (time == bobTime[x]) curVal = amount[x] / 2;
        else if (time < bobTime[x]) curVal = amount[x];     // a早到
        // 到达了叶子节点,使用该条路径的值更新答案
        if (g[x].size() == 1) {
            ans = Math.max(ans, score + curVal);
            return;
        }
        // dfs过程
        for (int y: g[x]) {
            if (y != father) {
                dfsAlice(y, time + 1, x, score + curVal);
            }
        }
        return;
    }
}

注意:!
在 bob dfs 的过程中,不是经过的所有节点都需要被设置时间的,而是只有达到了 0 节点路径上的节点才需要被设置时间
因此一个常用技巧是让 dfs 返回一个 boolean 值来记录是否达到了 target

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

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

相关文章

Buildroot 系统设置开机密码登录-迅为RK3588开发板

首先对开发板进行上电&#xff0c;开发板正常启动后&#xff0c;使用命令“vi /etc/inittab”对文件进行修改&#xff0c;如 下图所示&#xff1a; 设置为密码登陆时配置如下图&#xff08;注意将 ttyS0 修改为 ttyFIQ0&#xff09;&#xff1a; 修改完&#xff0c;保存退出&a…

GLM: General Language Model Pretrainingwith Autoregressive Blank Infilling翻译理解

GPT&#xff08;autoregressive&#xff09;模型是一个自回归模型&#xff0c;利用left-to-right语言模型&#xff0c;由于不是双向attention 机制&#xff0c;因此不能再NLU任务中&#xff0c;获取充分的上下文信息&#xff0c;BERT类似自编码&#xff08;autoencoding&#x…

SQL 优化(四):合理使用 join

在工作的时候经常听到的一句话就是&#xff0c;“这条 SQL 因为 join 了很多表&#xff0c;导致查询速度比较慢”&#xff0c;可以从侧面反映出&#xff0c;join语句对性能的影响是比较大的&#xff0c;而且大部分人不知道如何进行优化。这篇文章我们来讲讲join的执行过程&…

PC C++ SDK 全局函数、防录制功能、下载器、播放器

本文档提供了使用 CSDK 的操作步骤及代码示例&#xff0c;通过本文您可以快速了解如何使用 SDK 提供的功能。您也可以通过 Demo 中的示例进行了解和自有业务开发。 SDK 名词含义及功能说明 参见 plv-player-def.h SDK 全局函数 设置日志&#xff0c;日志过滤项。设置观众信息…

ENVI遥感影像处理—水体提取

2 .水体提取 &#xff08;1&#xff09;导入经过大气校正后的影像FLAASH_result.dat。 &#xff08;2&#xff09;选择工具箱中ToolBox——Band Ratio——Band Math&#xff0c;输入(float(b1)-float(b2))/(float(b1)float(b2))&#xff0c;点击Add to List&#xff0c;选中公…

R3LIVE环境搭建

一、安装ros、livox sdk、livox_ros_driver 安装方法[参考] 二、CGAL和pcl_viewer sudo apt-get install libcgal-dev pcl-tools 三、opencv&#xff08;≥3.3&#xff09; 3.1 命令检查 OpenCV 版本&#xff0c;如果 openCV 版本低于 OpenCV-3.3, 更新openCV版本为3.3.1、3…

Kubernetes 多集群管理工具 - Kuboard

Kuboard 是Kubernetes 多集群管理工具&#xff0c;是一个界面化的web网站&#xff0c;使用起来非常方便。在Kuboard中可以导入集群&#xff0c;在kuboard上可以完成很多的运维工作&#xff0c;比如创建命名空间、创建标签、运行服务、修改pod数量等等。 一&#xff1a;kuboard…

【JavaEE初阶】CSS

摄影分享~ 文章目录 一.CSS基本规范1. CSS基本语法规范2.CSS选择器 二.CSS常用属性1. 字体属性2.文本属性3.背景属性4.圆角矩形5.元素的显示模式块级元素行内元素 6.盒子模型边框内边距外边距 7.弹性布局 一.CSS基本规范 层叠样式表。(Cascading Style Sheets) CSS 能够对网页…

【零基础入门学习Python---Python中Web开发基础之快速入门实践】

&#x1f680; 零基础入门学习Python&#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜…

springboot医院自助服务系统-计算机毕设 附源码74853

springboot医院自助服务系统 目 录 摘要 1 绪论 1.1研究意义 1.2研究背景 1.3springboot框架介绍 1.3论文结构与章节安排 2 医院自助服务系统系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据流程 3.3.2 业务流程 2.3 系统功能分析 2.3.1 功能性分析 2.3.2 …

五、Eureka服务注册、续约、剔除、下线源码分析

Eureka 概念的理解 1 服务的注册 当项目启动时&#xff08;eureka 的客户端&#xff09;&#xff0c;就会向 eureka-server 发送自己的元数据&#xff08;原始数据&#xff09;&#xff08;运行的 ip&#xff0c;端口 port&#xff0c;健康的状态监控等&#xff0c;因为使用的…

ICLR 2023 | Self-Consistency: Google超简单方法改善大模型推理能力

大家好&#xff0c;我是HxShine。 今天分享一篇Google Research, Brain Team的一篇文章&#xff0c;SELF-CONSISTENCY IMPROVES CHAIN OF THOUGHT REASONING IN LANGUAGE MODELS[1]&#xff1a;利用自洽性提高语言模型中的思维链推理效果 这篇文章方法非常简单但是效果非常好…

vite配置指定浏览器打开-2023年7月3日

vue3vitevscode-2023年7月3日 官方demo环境下 官方demo环境下 找到vite.config.js增加如下代码 server:{open: {"process.env.BROWSER":C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe}}

docker安装RocketMQ(附填坑经验connect to <172.17.0.3:10909> failed)

目录 一、docker部署RocketMQ1、简易说明2、docker拉取RocketMQ镜像\RocketMQ控制台3、获取RocketMQ配置文件4、RocketMQ配置文件描述5、docker启动RocketMQ6、进入RocketMQ控制台 二、填坑经验错误一: connect to <172.17.0.3:10909> failed错误二: maybe your broker m…

C++静态库与动态库

什么是库 库是写好的现有的&#xff0c;成熟的&#xff0c;可以复用的代码。现实中每个程序都要依赖很多基础的底层库&#xff0c;不可能每个人的代码都从零开始&#xff0c;因此库的存在意义非同寻常。 本质上来说库是一种可执行代码的二进制形式&#xff0c;可以被操作系统…

英伟达新SOTA可对未知物体进行6D追踪和3D重建

物体可以在整个视频中自由移动&#xff0c;甚至经历严重的遮挡。英伟达的方法在目标上与物体级 SLAM 的先前工作类似&#xff0c;但放松了许多常见的假设&#xff0c;从而能够处理遮挡、反射、缺乏视觉纹理和几何线索以及突然的物体运动。 英伟达方法的关键在于在线姿态图优化…

J2EE自定义mvc【框架配置及功能】

目录 一、配置步骤 二、配置框架前三步 导入相应的jar 导入相应的Class 导入xml文件 三、优化基本操作&#xff08;增删改&#xff09; 1、基础优化 编写实体类 编写BookDao类 优化BookDao JUnit测试 2、后台优化 3、前端优化 一、配置步骤 将框架打成jar包&…

SQL Server SQL语句

在很多情况下&#xff0c;可以用CREATE TABLE语句创建数据表、使用ALTER TABLE语句修改表结构、使用DROP TABLE语句删除表&#xff1b; 可以使用CREATE DATABASE创建数据库、ALTER DATABASE修改文件或文件组、DROP DATABASE语句删除数据库&#xff1b; 1、数据定义语句&#x…

web安全php基础_php变量命名及其作用域

php变量命名规则 php变量命名规则 变量以 $ 符号开始&#xff0c;后面跟着变量的名称变量名必须以字母或者下划线字符开始变量名只能包含字母数字字符以及下划线&#xff08;A-z、0-9 和 _ &#xff09;变量名不能包含空格变量名是区分大小写的&#xff08;$y 和 $Y 是两个不…

戴尔笔记本开机输入密码后黑屏只有鼠标,没有桌面的解决参考办法

戴尔笔记本开机输入密码后黑屏只有鼠标&#xff0c;没有桌面的解决参考办法 网络常用方法方法一&#xff1a;cmd启动资源管理器方法二&#xff1a;进入安全模式 以上两个方法我的电脑无效&#xff0c;因此我怀疑是启动项的问题更改启动项 网络常用方法 方法一&#xff1a;cmd启…