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

这篇具有很好参考价值的文章主要介绍了【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前期知识

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

【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集),算法,算法,树形DP,打家劫舍Ⅲ,DFS,动态规划,树

例题

337. 打家劫舍 III

337. 打家劫舍 III

【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集),算法,算法,树形DP,打家劫舍Ⅲ,DFS,动态规划,树
用一个数组 int[] = {a, b} 来接收每个节点返回的结果。返回值{a,b} a表示没选当前节点的最大值,b表示选了当前节点的最大值。

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

【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集),算法,算法,树形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 没有上司的舞会

【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集),算法,算法,树形DP,打家劫舍Ⅲ,DFS,动态规划,树
这道题目相当于每个节点可能会有若干个子节点的 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 秒后青蛙的位置

【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集),算法,算法,树形DP,打家劫舍Ⅲ,DFS,动态规划,树

解法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. 最小化旅行的价格总和

【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集),算法,算法,树形DP,打家劫舍Ⅲ,DFS,动态规划,树

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

【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集),算法,算法,树形DP,打家劫舍Ⅲ,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. 树上最大得分和路径

【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集),算法,算法,树形DP,打家劫舍Ⅲ,DFS,动态规划,树
这道题目更多的是练习树的 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文章来源地址https://www.toymoban.com/news/detail-520922.html

到了这里,关于【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 力扣hot100 打家劫舍 DP 滚动数组

    Problem: 198. 打家劫舍 👨‍🏫 参考地址 时间复杂度: O ( n ) O(n) O ( n ) 空间复杂度: O ( n ) O(n) O ( n ) 空间复杂度: O ( 1 ) O(1) O ( 1 )

    2024年01月19日
    浏览(42)
  • 【动态规划】简单多状态dp问题(1)打家劫舍问题

    打家劫舍问题 传送门:面试题 17.16. 按摩师 题目: 1.1 题目解析 越难的dp问题,看示例只能起到了解题目的效果,一般推不出啥普遍的规律,所以接下来就是我们的算法原理,通过动归的思想去理解,才会豁然开朗! 1.2 算法原理 1.2.1 状态表示 我们需要通过经验 + 题目要求去

    2024年02月12日
    浏览(43)
  • 算法刷题Day 48 打家劫舍+打家劫舍II+打家劫舍III

    分成两段来处理:比如说输入的长度是n(0~n-1),就分成[0, n - 1)和[1, n)两部分 写一个辅助函数,返回两个状态,抢或者不抢能得到的最大收获。

    2024年02月16日
    浏览(36)
  • 算法训练第四十八天|198.打家劫舍、213.打家劫舍II、337.打家劫舍III

    题目链接:198.打家劫舍 参考:https://programmercarl.com/0198.%E6%89%93%E5%AE%B6%E5%8A%AB%E8%88%8D.html 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入

    2023年04月16日
    浏览(40)
  • 【动态规划】12简单多状态dp问题_打家劫舍II_C++ (medium)

    题目链接:leetcode打家劫舍II 目录 题目解析: 算法原理 1.状态表示 2.状态转移方程 3.初始化 4.填表顺序 5.返回值 编写代码 题目让我们求 在不触动警报装置的情况下  ,能够偷窃到的最高金额。 由题可得: 第一个房屋和最后一个房屋是紧挨着的 如果两间相邻的房屋在同一晚

    2024年02月02日
    浏览(48)
  • 【算法|动态规划No.10】leetcode LCR 089. 打家劫舍 & LCR 090. 打家劫舍 II

    个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 🍔本专栏旨在提高自己算法能力的同时,记录一下自己的学习过程,希望对大家有所帮助 🍓希望我们一起努力、成长,共同进步。

    2024年01月20日
    浏览(44)
  • 算法第十八天-打家劫舍Ⅱ

    [打家劫舍Ⅱ]是说两个相邻的房间不能同时偷,并且首尾两个房间是相邻的(不能同时偷首尾房间) 明显是基于[打家劫舍Ⅰ]做的升级。[打家劫舍Ⅰ]也是说两个相邻的房间不能同时偷,但是首尾房间不是相邻的(可以同时偷首尾房间) 所以,我们先从[打家劫舍Ⅰ]开始说起。

    2024年01月17日
    浏览(43)
  • Day 42 算法记录|动态规划 09 (打家劫舍)

    1.dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。 2.dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); 3.初始化,dp[0] 和 dp[1],dp[0] 一定是 nums[0],dp[1] = max(nums[0], nums[1]); 3.遍历顺序,dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历! 进一步对滚动数组

    2024年02月15日
    浏览(35)
  • 【LeetCode题目详解】第九章 动态规划part09 198.打家劫舍 213.打家劫舍II 337.打家劫舍III(day48补)

    你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统, 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动

    2024年02月09日
    浏览(46)
  • 代码随想录刷题第48天|LeetCode198打家劫舍、LeetCode213打家劫舍II、LeetCode337打家劫舍III

    1、LeetCode198打家劫舍 题目链接:198、打家劫舍 1、dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i] 。 2、递推公式: 如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ; 如果不偷第i房间,那么dp[i] = dp[i - 1]; 然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1

    2024年02月08日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包