0
点赞
收藏
分享

微信扫一扫

class079 树型dp-下【算法】


class079 树型dp-下【算法】

算法讲解079【必备】树型dp-下

class079 树型dp-下【算法】_数组

code1 2477. 到达首都的最少油耗

// 到达首都的最少油耗
// 给你一棵 n 个节点的树(一个无向、连通、无环图)
// 每个节点表示一个城市,编号从 0 到 n - 1 ,且恰好有 n - 1 条路
// 0 是首都。给你一个二维整数数组 roads
// 其中 roads[i] = [ai, bi] ,表示城市 ai 和 bi 之间有一条 双向路
// 每个城市里有一个代表,他们都要去首都参加一个会议
// 每座城市里有一辆车。给你一个整数 seats 表示每辆车里面座位的数目
// 城市里的代表可以选择乘坐所在城市的车,或者乘坐其他城市的车
// 相邻城市之间一辆车的油耗是一升汽油
// 请你返回到达首都最少需要多少升汽油
// 测试链接 : https://leetcode.cn/problems/minimum-fuel-cost-to-report-to-the-capital/

以x为头
孩子的最少油耗+孩子的人数/seats向上取整

package class079;

import java.util.ArrayList;

// 到达首都的最少油耗
// 给你一棵 n 个节点的树(一个无向、连通、无环图)
// 每个节点表示一个城市,编号从 0 到 n - 1 ,且恰好有 n - 1 条路
// 0 是首都。给你一个二维整数数组 roads
// 其中 roads[i] = [ai, bi] ,表示城市 ai 和 bi 之间有一条 双向路
// 每个城市里有一个代表,他们都要去首都参加一个会议
// 每座城市里有一辆车。给你一个整数 seats 表示每辆车里面座位的数目
// 城市里的代表可以选择乘坐所在城市的车,或者乘坐其他城市的车
// 相邻城市之间一辆车的油耗是一升汽油
// 请你返回到达首都最少需要多少升汽油
// 测试链接 : https://leetcode.cn/problems/minimum-fuel-cost-to-report-to-the-capital/
public class Code01_MinimumFuelCost {

	public static long minimumFuelCost(int[][] roads, int seats) {
		int n = roads.length + 1;
		ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
		for (int i = 0; i < n; i++) {
			graph.add(new ArrayList<>());
		}
		for (int[] r : roads) {
			graph.get(r[0]).add(r[1]);
			graph.get(r[1]).add(r[0]);
		}
		int[] size = new int[n];
		long[] cost = new long[n];
		f(graph, seats, 0, -1, size, cost);
		return cost[0];
	}

	// 根据图,当前来到u,u的父节点是p
	// 遍历完成后,请填好size[u]、cost[u]
	public static void f(ArrayList<ArrayList<Integer>> graph, int seats, int u, int p, int[] size, long[] cost) {
		size[u] = 1;
		for (int v : graph.get(u)) {
			if (v != p) {
				f(graph, seats, v, u, size, cost);

				size[u] += size[v];
				cost[u] += cost[v];
				// a/b向上取整,可以写成(a+b-1)/b
				// (size[v]+seats-1) / seats = size[v] / seats 向上取整
				cost[u] += (size[v] + seats - 1) / seats;
			}
		}
	}

}

code2 2246. 相邻字符不同的最长路径

// 相邻字符不同的最长路径
// 给你一棵 树(即一个连通、无向、无环图),根节点是节点 0
// 这棵树由编号从 0 到 n - 1 的 n 个节点组成
// 用下标从 0 开始、长度为 n 的数组 parent 来表示这棵树
// 其中 parent[i] 是节点 i 的父节点
// 由于节点 0 是根节点,所以 parent[0] == -1
// 另给你一个字符串 s ,长度也是 n ,其中 s[i] 表示分配给节点 i 的字符
// 请你找出路径上任意一对相邻节点都没有分配到相同字符的 最长路径
// 并返回该路径的长度
// 测试链接 : https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/

以x为头的树
不要x:孩子的最长路径的最大值
要x:和x不同的耗子的最长路径+次长路径
返回[包含头结点的最长路径,最长路径]
叶子结点返回(1,1)

package class079;

import java.util.ArrayList;

// 相邻字符不同的最长路径
// 给你一棵 树(即一个连通、无向、无环图),根节点是节点 0
// 这棵树由编号从 0 到 n - 1 的 n 个节点组成
// 用下标从 0 开始、长度为 n 的数组 parent 来表示这棵树
// 其中 parent[i] 是节点 i 的父节点
// 由于节点 0 是根节点,所以 parent[0] == -1
// 另给你一个字符串 s ,长度也是 n ,其中 s[i] 表示分配给节点 i 的字符
// 请你找出路径上任意一对相邻节点都没有分配到相同字符的 最长路径
// 并返回该路径的长度
// 测试链接 : https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/
public class Code02_LongestPathWithDifferentAdjacent {

	public static int longestPath(int[] parent, String str) {
		int n = parent.length;
		char[] s = str.toCharArray();
		ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
		for (int i = 0; i < n; i++) {
			graph.add(new ArrayList<>());
		}
		for (int i = 1; i < n; i++) {
			graph.get(parent[i]).add(i);
		}
		return f(s, graph, 0).maxPath;
	}

	public static class Info {
		public int maxPathFromHead; // 一定要从头节点出发的情况下,相邻字符不等的最长路径长度
		public int maxPath; // 整棵树上,相邻字符不等的最长路径长度

		public Info(int a, int b) {
			maxPathFromHead = a;
			maxPath = b;
		}
	}

	public static Info f(char[] s, ArrayList<ArrayList<Integer>> graph, int u) {
		if (graph.get(u).isEmpty()) {
			// u节点是叶
			return new Info(1, 1);
		}
		int max1 = 0; // 下方最长链
		int max2 = 0; // 下方次长链
		int maxPath = 1;
		for (int v : graph.get(u)) {
			Info nextInfo = f(s, graph, v);
			maxPath = Math.max(maxPath, nextInfo.maxPath);
			if (s[u] != s[v]) {
				if (nextInfo.maxPathFromHead > max1) {
					max2 = max1;
					max1 = nextInfo.maxPathFromHead;
				} else if (nextInfo.maxPathFromHead > max2) {
					max2 = nextInfo.maxPathFromHead;
				}
			}
		}
		int maxPathFromHead = max1 + 1;
		maxPath = Math.max(maxPath, max1 + max2 + 1);
		return new Info(maxPathFromHead, maxPath);
	}

}

code3 2458. 移除子树后的二叉树高度

// 移除子树后的二叉树高度
// 给你一棵 二叉树 的根节点 root ,树中有 n 个节点
// 每个节点都可以被分配一个从 1 到 n 且互不相同的值
// 另给你一个长度为 m 的数组 queries
// 你必须在树上执行 m 个 独立 的查询,其中第 i 个查询你需要执行以下操作:
// 从树中 移除 以 queries[i] 的值作为根节点的子树
// 题目所用测试用例保证 queries[i] 不等于根节点的值
// 返回一个长度为 m 的数组 answer
// 其中 answer[i] 是执行第 i 个查询后树的高度
// 注意:
// 查询之间是独立的,所以在每个查询执行后,树会回到其初始状态
// 树的高度是从根到树中某个节点的 最长简单路径中的边数
// 测试链接 : https://leetcode.cn/problems/height-of-binary-tree-after-subtree-removal-queries/

dfn序号+size
每一个结点的深度

package class079;

// 移除子树后的二叉树高度
// 给你一棵 二叉树 的根节点 root ,树中有 n 个节点
// 每个节点都可以被分配一个从 1 到 n 且互不相同的值
// 另给你一个长度为 m 的数组 queries
// 你必须在树上执行 m 个 独立 的查询,其中第 i 个查询你需要执行以下操作:
// 从树中 移除 以 queries[i] 的值作为根节点的子树
// 题目所用测试用例保证 queries[i] 不等于根节点的值
// 返回一个长度为 m 的数组 answer
// 其中 answer[i] 是执行第 i 个查询后树的高度
// 注意:
// 查询之间是独立的,所以在每个查询执行后,树会回到其初始状态
// 树的高度是从根到树中某个节点的 最长简单路径中的边数
// 测试链接 : https://leetcode.cn/problems/height-of-binary-tree-after-subtree-removal-queries/
public class Code03_HeightRemovalQueries {

	// 不要提交这个类
	public static class TreeNode {
		public int val;
		public TreeNode left;
		public TreeNode right;
	}

	// 提交如下的方法
	public static final int MAXN = 100010;

	// 下标为节点的值
	public static int[] dfn = new int[MAXN];

	// 下标为dfn序号
	public static int[] deep = new int[MAXN];

	// 下标为dfn序号
	public static int[] size = new int[MAXN];

	public static int[] maxl = new int[MAXN];

	public static int[] maxr = new int[MAXN];

	public static int dfnCnt;

	public static int[] treeQueries(TreeNode root, int[] queries) {
		dfnCnt = 0;
		f(root, 0);
		for (int i = 1; i <= dfnCnt; i++) {
			maxl[i] = Math.max(maxl[i - 1], deep[i]);
		}
		maxr[dfnCnt + 1] = 0;
		for (int i = dfnCnt; i >= 1; i--) {
			maxr[i] = Math.max(maxr[i + 1], deep[i]);
		}
		int m = queries.length;
		int[] ans = new int[m];
		for (int i = 0; i < m; i++) {
			int leftMax = maxl[dfn[queries[i]] - 1];
			int rightMax = maxr[dfn[queries[i]] + size[dfn[queries[i]]]];
			ans[i] = Math.max(leftMax, rightMax);
		}
		return ans;
	}

	// 来到x节点,从头节点到x节点经过了k条边
	public static void f(TreeNode x, int k) {
		int i = ++dfnCnt;
		dfn[x.val] = i;
		deep[i] = k;
		size[i] = 1;
		if (x.left != null) {
			f(x.left, k + 1);
			size[i] += size[dfn[x.left.val]];
		}
		if (x.right != null) {
			f(x.right, k + 1);
			size[i] += size[dfn[x.right.val]];
		}
	}

}

code4 2322. 从树中删除边的最小分数

// 从树中删除边的最小分数
// 存在一棵无向连通树,树中有编号从0到n-1的n个节点,以及n-1条边
// 给你一个下标从0开始的整数数组nums长度为n,其中nums[i]表示第i个节点的值
// 另给你一个二维整数数组edges长度为n-1
// 其中 edges[i] = [ai, bi] 表示树中存在一条位于节点 ai 和 bi 之间的边
// 删除树中两条不同的边以形成三个连通组件,对于一种删除边方案,定义如下步骤以计算其分数:
// 分别获取三个组件每个组件中所有节点值的异或值
// 最大 异或值和 最小 异或值的 差值 就是这种删除边方案的分数
// 返回可能的最小分数
// 测试链接 : https://leetcode.cn/problems/minimum-score-after-removals-on-a-tree/

暴力枚举两条边
如果x,y
y>x,y是x的子树
sum1=sumY
sum2=sumX^sumY
sum3=sum0^sumX
y>x,y不是x的子树
sum1=sumY
sum2=sumX
sum3=sum0^ sumX^sumY

package class079;

import java.util.ArrayList;
import java.util.Arrays;

// 从树中删除边的最小分数
// 存在一棵无向连通树,树中有编号从0到n-1的n个节点,以及n-1条边
// 给你一个下标从0开始的整数数组nums长度为n,其中nums[i]表示第i个节点的值
// 另给你一个二维整数数组edges长度为n-1
// 其中 edges[i] = [ai, bi] 表示树中存在一条位于节点 ai 和 bi 之间的边
// 删除树中两条不同的边以形成三个连通组件,对于一种删除边方案,定义如下步骤以计算其分数:
// 分别获取三个组件每个组件中所有节点值的异或值
// 最大 异或值和 最小 异或值的 差值 就是这种删除边方案的分数
// 返回可能的最小分数
// 测试链接 : https://leetcode.cn/problems/minimum-score-after-removals-on-a-tree/
public class Code04_MinimumScoreAfterRemovals {

	public static int MAXN = 1001;

	// 下标为原始节点编号
	public static int[] dfn = new int[MAXN];

	// 下标为dfn序号
	public static int[] xor = new int[MAXN];

	// 下标为dfn序号
	public static int[] size = new int[MAXN];

	public static int dfnCnt;

	public static int minimumScore(int[] nums, int[][] edges) {
		int n = nums.length;
		ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
		for (int i = 0; i < n; i++) {
			graph.add(new ArrayList<>());
		}
		for (int[] edge : edges) {
			graph.get(edge[0]).add(edge[1]);
			graph.get(edge[1]).add(edge[0]);
		}
		Arrays.fill(dfn, 0, n, 0);
		dfnCnt = 0;
		f(nums, graph, 0);
		int m = edges.length;
		int ans = Integer.MAX_VALUE;
		for (int i = 0, a, b, pre, pos, sum1, sum2, sum3; i < m; i++) {
			a = Math.max(dfn[edges[i][0]], dfn[edges[i][1]]);
			for (int j = i + 1; j < m; j++) {
				b = Math.max(dfn[edges[j][0]], dfn[edges[j][1]]);
				if (a < b) {
					pre = a;
					pos = b;
				} else {
					pre = b;
					pos = a;
				}
				sum1 = xor[pos];
				// xor[1] : 整棵树的异或和
				// 因为头节点是0,一定拥有最小的dfn序号1
				// f函数调用的时候,也是从0节点开始的
				if (pos < pre + size[pre]) {
					sum2 = xor[pre] ^ xor[pos];
					sum3 = xor[1] ^ xor[pre];
				} else {
					sum2 = xor[pre];
					sum3 = xor[1] ^ sum1 ^ sum2;
				}
				ans = Math.min(ans, Math.max(Math.max(sum1, sum2), sum3) - Math.min(Math.min(sum1, sum2), sum3));
			}
		}
		return ans;
	}

	// 当前来到原始编号u,遍历u的整棵树
	public static void f(int[] nums, ArrayList<ArrayList<Integer>> graph, int u) {
		int i = ++dfnCnt;
		dfn[u] = i;
		xor[i] = nums[u];
		size[i] = 1;
		for (int v : graph.get(u)) {
			if (dfn[v] == 0) {
				f(nums, graph, v);
				xor[i] ^= xor[dfn[v]];
				size[i] += size[dfn[v]];
			}
		}
	}

}

code5 P2014 [CTSC1997] 选课

// 选课
// 在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习
// 在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习
// 现在有 N 门功课,每门课有个学分,每门课有一门或没有直接先修课
// 若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b
// 一个学生要从这些课程里选择 M 门课程学习
// 问他能获得的最大学分是多少
// 测试链接 : https://www.luogu.com.cn/problem/P2014
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

方法1

当前来到i号节点为头的子树
只在i号节点、及其i号节点下方的前j棵子树上挑选节点
一共挑选k个节点,并且保证挑选的节点连成一片
返回最大的累加和
public static int f(int i, int j, int k)

f(i,j-1,k)
以i最后第一个孩子枚举s,1<=s<k
f(i,j-1,k-s)+f(v,v.size,s)

方法2
dp[i][j] : i ~ n+1 范围的节点,选择j个节点一定要形成有效结构的情况下,最大的累加和

不选i及其子树
学i结点
dp[i][j] = Math.max(dp[i + size[i]][j], val[i] + dp[i + 1][j - 1]);

package class079;

// 选课
// 在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习
// 在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习
// 现在有 N 门功课,每门课有个学分,每门课有一门或没有直接先修课
// 若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b
// 一个学生要从这些课程里选择 M 门课程学习
// 问他能获得的最大学分是多少
// 测试链接 : https://www.luogu.com.cn/problem/P2014
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.ArrayList;

// 普通解法,邻接表建图 + 相对好懂的动态规划
// 几乎所有题解都是普通解法的思路,只不过优化了常数时间、做了空间压缩
// 但时间复杂度依然是O(n * 每个节点的孩子平均数量 * m的平方)
public class Code05_CourseSelection1 {

	public static int MAXN = 301;

	public static int[] nums = new int[MAXN];

	public static ArrayList<ArrayList<Integer>> graph;

	static {
		graph = new ArrayList<>();
		for (int i = 0; i < MAXN; i++) {
			graph.add(new ArrayList<>());
		}
	}

	public static int[][][] dp = new int[MAXN][][];

	public static int n, m;

	public static void build(int n) {
		for (int i = 0; i <= n; i++) {
			graph.get(i).clear();
		}
	}

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			// 节点编号从0~n
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval + 1;
			build(n);
			for (int i = 1, pre; i <= n; i++) {
				in.nextToken();
				pre = (int) in.nval;
				graph.get(pre).add(i);
				in.nextToken();
				nums[i] = (int) in.nval;
			}
			out.println(compute());
		}
		out.flush();
		out.close();
		br.close();
	}

	public static int compute() {
		for (int i = 0; i <= n; i++) {
			dp[i] = new int[graph.get(i).size() + 1][m + 1];
		}
		for (int i = 0; i <= n; i++) {
			for (int j = 0; j < dp[i].length; j++) {
				for (int k = 0; k <= m; k++) {
					dp[i][j][k] = -1;
				}
			}
		}
		return f(0, graph.get(0).size(), m);
	}

	// 当前来到i号节点为头的子树
	// 只在i号节点、及其i号节点下方的前j棵子树上挑选节点
	// 一共挑选k个节点,并且保证挑选的节点连成一片
	// 返回最大的累加和
	public static int f(int i, int j, int k) {
		if (k == 0) {
			return 0;
		}
		if (j == 0 || k == 1) {
			return nums[i];
		}
		if (dp[i][j][k] != -1) {
			return dp[i][j][k];
		}
		int ans = f(i, j - 1, k);
		// 第j棵子树头节点v 
		int v = graph.get(i).get(j - 1);
		for (int s = 1; s < k; s++) {
			ans = Math.max(ans, f(i, j - 1, k - s) + f(v, graph.get(v).size(), s));
		}
		dp[i][j][k] = ans;
		return ans;
	}

}

package class079;

// 选课
// 在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习
// 在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习
// 现在有 N 门功课,每门课有个学分,每门课有一门或没有直接先修课
// 若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b
// 一个学生要从这些课程里选择 M 门课程学习
// 问他能获得的最大学分是多少
// 测试链接 : https://www.luogu.com.cn/problem/P2014
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Arrays;

// 最优解,链式前向星建图 + dfn序的利用 + 巧妙定义下的尝试
// 时间复杂度O(n*m),觉得难可以跳过,这个最优解是非常巧妙和精彩的!
public class Code05_CourseSelection2 {

	public static int MAXN = 301;

	public static int[] nums = new int[MAXN];

	// 链式前向星建图
	public static int edgeCnt;

	public static int[] head = new int[MAXN];

	public static int[] next = new int[MAXN];

	public static int[] to = new int[MAXN];

	// dfn的计数
	public static int dfnCnt;

	// 下标为dfn序号
	public static int[] val = new int[MAXN + 1];

	// 下标为dfn序号
	public static int[] size = new int[MAXN + 1];

	// 动态规划表
	public static int[][] dp = new int[MAXN + 2][MAXN];

	public static int n, m;

	public static void build(int n, int m) {
		edgeCnt = 1;
		dfnCnt = 0;
		Arrays.fill(head, 0, n + 1, 0);
		Arrays.fill(dp[n + 2], 0, m + 1, 0);
	}

	public static void addEdge(int u, int v) {
		next[edgeCnt] = head[u];
		to[edgeCnt] = v;
		head[u] = edgeCnt++;
	}

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval;
			build(n, m);
			for (int i = 1; i <= n; i++) {
				in.nextToken();
				addEdge((int) in.nval, i);
				in.nextToken();
				nums[i] = (int) in.nval;
			}
			out.println(compute());
		}
		out.flush();
		out.close();
		br.close();
	}

	public static int compute() {
		f(0);
		// 节点编号0 ~ n,dfn序号范围1 ~ n+1
		// 接下来的逻辑其实就是01背包!不过经历了很多转化
		// 整体的顺序是根据dfn序来进行的,从大的dfn序,遍历到小的dfn序
		// dp[i][j] : i ~ n+1 范围的节点,选择j个节点一定要形成有效结构的情况下,最大的累加和
		// 怎么定义有效结构?重点!重点!重点!
		// 假设i ~ n+1范围上,目前所有头节点的上方,有一个总的头节点
		// i ~ n+1范围所有节点,选出来j个节点的结构,
		// 挂在这个假想的总头节点之下,是一个连续的结构,没有断开的情况
		// 那么就说,i ~ n+1范围所有节点,选出来j个节点的结构是一个有效结构
		for (int i = n + 1; i >= 2; i--) {
			for (int j = 1; j <= m; j++) {
				dp[i][j] = Math.max(dp[i + size[i]][j], val[i] + dp[i + 1][j - 1]);
			}
		}
		// dp[2][m] : 2 ~ n范围上,选择m个节点一定要形成有效结构的情况下,最大的累加和
		// 最后来到dfn序为1的节点,一定是原始的0号节点
		// 原始0号节点下方一定挂着有效结构
		// 并且和补充的0号节点一定能整体连在一起,没有任何跳跃连接
		// 于是整个问题解决
		return nums[0] + dp[2][m];
	}

	// u这棵子树的节点数返回
	public static int f(int u) {
		int i = ++dfnCnt;
		val[i] = nums[u];
		size[i] = 1;
		for (int ei = head[u], v; ei > 0; ei = next[ei]) {
			v = to[ei];
			size[i] += f(v);
		}
		return size[i];
	}

}

2023-11-26 20:48:06


举报

相关推荐

0 条评论