树形DP题目

本文介绍了树形动态规划的基本概念,展示了在处理具有树结构的问题时如何运用DFS和状态转移方程。通过实例分析了没有上司的舞会、打家劫舍III和在树上执行操作后的最大分数等LeetCode题目,强调了理解节点依赖关系和状态选择的重要性。
摘要由CSDN通过智能技术生成

什么是树形DP

顾名思义,树形DP就是在某些题目中要求的树结构上使用DP的思想。
树是有n个节点,n-1条边的无向图,且是无环的,联通的,又因为是无向图,所以两个节点间存在着相互的联通关系,有时需要加以判断
当DP建立在依赖关系上时,就可以使用树形DP来解决问题。

树形DP模板

void dfs(u,fa,other): //u为当前节点,fa为其父节点,other为其他参数
    if special
        do  sth 
    for each v (存在 u->v)
        if v=fa continue //因为会与父亲也存在联通关系,所以特判
        dfs(v,u) //因为需要依靠子树的结果来推导自身,所以先继续深入子树
        do dp //进行dp运算

树形DP题目

P1352 没有上司的舞会 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

根据网友们所说,这是一道树形DP的经典题目,通过它可以初见端倪,做完后确实如此。
通过分析题意,很容易将树构建出来。当树构建完毕后,不难想到,一个上司的状态会影响其手下所有员工的状态(当上司来时,其子树所有节点均不能来;当上司不来时,其子树所有节点可以来,也可以不来,对应两种状态,我们这时可以取两种状态的最大值,保证最优)
有了以上分析,我们就可以尝试推敲状态转移方程,如下:
首先分析第一个状态,即当前节点(在编写dp函数时,关注于树上单个节点,也就是最小子问题,切勿写着写着迷糊的过早思考全局)来参加舞会的状态。如果当前节点来参加舞会,那么其子树中所有节点都不能来参加,于是自然地得出以下公式:
f [ 1 ] [ i ] = ∑ 0 c h i l d r e n . s i z e f [ 0 ] [ v ] + v a l u e [ i ] f[1][i] = \sum_{0}^{children.size} f[0][v] + value[i] f[1][i]=0children.sizef[0][v]+value[i]
我们用一个二维数组来保存每个节点的dp结果,此处有一个小技巧,我们可以将大小较小的那一维度尽可能放在前面,使得计算机可以更快的运行代码。规定0为不参会,1为参会,children为当前节点的子节点数组,value为树中节点权值数组。
然后同样分析得出当前节点参会的状态:
f [ 0 ] [ i ] = ∑ 0 c h i l d r e n . s i z e m a x ( f [ 0 ] [ v ] , f [ 1 ] [ v ] ) f[0][i] = \sum_{0}^{children.size} max(f[0][v],f[1][v]) f[0][i]=0children.sizemax(f[0][v],f[1][v])
至此,全部的两个状态就分析完毕了。在计算dp结果的过程中,可能会遇到已经计算过的节点,例如两个不同节点的邻居(在一条链上的中点)相当于会重复遇到,所以我们再额外创建一个bool类型的数组来判断即可。
代码如下:

package main

import "fmt"

//洛谷的Go版本低于1.18,用不了泛型……
//func max[T int](nums ...T) T {
//	var max T
//	max = nums[0]
//	for _, v := range nums {
//		if max < v {
//			max = v
//		}
//	}
//	return max
//}

func max(nums ...int) int {
	var max int
	max = nums[0]
	for _, v := range nums {
		if max < v {
			max = v
		}
	}
	return max
}

func main() {
	var treeSize int
	var input int
	_, err := fmt.Scanf("%d", &treeSize)
	if err != nil {
		fmt.Print("Input error")
	}
	dpTable := make([][]int, 2)
	for i := range dpTable {
		dpTable[i] = make([]int, treeSize+1)
	}
	hasFather := make([]bool, treeSize+1)
	isVisited := make([]bool, treeSize+1)
	happyNumber := make([]int, treeSize+1)
	tree := make([][]int, treeSize+1)
	rootNode := 0
	for i := 0; i <= treeSize; i++ {
		fmt.Scanln(&input)
		happyNumber[i] = input
	}
	for i := 0; i < treeSize-1; i++ {
		var employee, employer int
		fmt.Scanln(&employee, &employer)
		hasFather[employee] = true
		tree[employer] = append(tree[employer], employee)
	}
	var treeDP func(node int)
	treeDP = func(node int) {
		isVisited[node] = true
		dpTable[1][node] = happyNumber[node]
		for _, v := range tree[node] {
			if isVisited[v] {
				continue
			}
			treeDP(v)
			dpTable[0][node] += max(dpTable[0][v], dpTable[1][v])
			dpTable[1][node] += dpTable[0][v]
		}
	}
	for i := 1; i < len(hasFather); i++ {
		if !hasFather[i] {
			rootNode = i
			break
		}
	}
	treeDP(rootNode)
	fmt.Print(max(dpTable[0][rootNode], dpTable[1][rootNode]))
}

337. 打家劫舍 III - 力扣(LeetCode)

这个题目和舞会题目基本相同,甚至感觉还更简单一些……
还是相同的思考方式,读题后不难想到,一个房子有两个状态,即偷这个房子和不偷这个房子,那么接下来就是对这两种状态进行状态转移方程的书写:
f [ 0 ] [ i ] = f [ 1 ] [ l e f t ] + f [ 1 ] [ r i g h t ] + r o o t . v a l f[0][i] = f[1][left]+f[1][right]+root.val f[0][i]=f[1][left]+f[1][right]+root.val
同样是二维数组,其中0表示偷当前节点,1表示不偷当前节点,很好理解上面的式子
f [ 1 ] [ i ] = m a x ( f [ 0 ] [ l e f t ] , f [ 1 ] [ l e f t ] ) + m a x ( f [ 0 ] [ r i g h t ] , f [ 1 ] [ r i g h t ] ) f[1][i] = max(f[0][left],f[1][left])+max(f[0][right],f[1][right]) f[1][i]=max(f[0][left],f[1][left])+max(f[0][right],f[1][right])
偷当前节点时,左右子节点可以偷也可以不偷,所以我们取其中的最大值为抉择方案
最后就是结束条件,显而易见,当达到空节点时就返回,那么空节点是没法被偷的,无论是偷还是不偷,拿到的价值都是0,而且对结果也不会产生影响,所以我们遇到空节点时就返回两个0对应两个状态即可。

/**

 * Definition for a binary tree node.

 * type TreeNode struct {

 *     Val int

 *     Left *TreeNode

 *     Right *TreeNode

 * }

 */

func rob(root *TreeNode) int {

    var dpTree func(root *TreeNode) (int, int)

    dpTree = func(root *TreeNode) (int, int) {

        if root == nil {

            return 0, 0

        }

        left_withoutRob, left_rob := dpTree(root.Left)

        right_withoutRob, right_rob := dpTree(root.Right)

        root_withoutRob := max[int](left_rob, left_withoutRob) + max[int](right_rob, right_withoutRob)

        root_rob := left_withoutRob + right_withoutRob + root.Val

        return root_withoutRob, root_rob

    }

    return max[int](dpTree(root))

}

 
 
 

func max[T int](nums ...T) T {

    var max T

    max = nums[0]

    for _, v := range nums {

        if max < v {

            max = v

        }

    }

    return max

}

100118. 在树上执行操作以后得到的最大分数 - 力扣(LeetCode)

这周周赛的新题目,正好第三题是个树形dp,写完以后有新的感悟,记录一下。
题目不难读懂,主要记录一下为什么自己没能独立没有将思路变为代码。
首先最初只想到采取正向看树的方式,很容易就想到一个节点有选与不选两种状态,又因为题目需要求解最大值,所以最后应取两种状态的最大值。
如果选择当前节点,当前节点会变为0,如果这时候想符合题意,那么根节点的树必须都得健康,或者就理解为左右子树也是两个叶子节点(统一打包到一起),这两个叶子节点都不能为0。
如果不选择当前节点,那就简单了,其值应该取当前节点的所有子节点之和,这样肯定最大,都不需要dfs了
但这时候有个问题,如果正向着来的话,从根节点开始,是不知道子树的权值和的,所以需要提前算一遍所有子树的权值和。解决这个问题也很简单,可以正难则反,现在是要加节点,反过来可以找怎么减节点最少,这样用总的权值和减去最少的减值,就是最大值。
减值也是分别选择当前与不选择,截止条件是遇到叶子节点,这时候返回其值就行了,因为想要健康,这棵树肯定不能选择根节点(就他一个)。如果选择当前节点,那么损失值就是此节点的值,如果不选择,那么损失值就是其子树的值

func maximumScoreAfterOperations(edges [][]int, values []int) int64 {
	graph := make([][]int, len(values))
	for _, v := range edges {
		x := v[0]
		y := v[1]
		graph[x] = append(graph[x], y)
		graph[y] = append(graph[y], x)
	}
	graph[0] = append(graph[0], -1)
	var dfs func(cur, father int) int
	dfs = func(cur, father int) int {
		if len(graph[cur]) == 1 {
			return values[cur]
		}
		chooseLoss := values[cur]
		noChoose := 0
		for _, v := range graph[cur] {
			if v != father {
				noChoose += dfs(v, cur)
			}
		}
		return minForTwo[int](chooseLoss, noChoose)
	}
	return int64(getAllSum(values)) - int64(dfs(0, -1))
}

func minForTwo[T int](a, b T) (minValue T) {
	if a > b {
		return b
	} else {
		return a
	}
}

func getAllSum(nums []int) int {
	sum := 0
	for i := 0; i < len(nums); i++ {
		sum += nums[i]
	}
	return sum
}
func maximumScoreAfterOperations(edges [][]int, values []int) int64 {
	graph := make([][]int, len(values))
	for _, v := range edges {
		x := v[0]
		y := v[1]
		graph[x] = append(graph[x], y)
		graph[y] = append(graph[y], x)
	}
	graph[0] = append(graph[0], -1)
	subTreeSum := make([]int, len(values))
	var cal_sum func(cur, father int)
	cal_sum = func(cur, father int) {
		subTreeSum[cur] = values[cur]
		for _, v := range graph[cur] {
			if v != father {
				cal_sum(v, cur)
				subTreeSum[cur] += subTreeSum[v]
			}
		}
	}
	cal_sum(0, -1)
	var dfs func(cur, father int) int
	dfs = func(cur, father int) int {
		if len(graph[cur]) == 1 {
			return 0
		}
		noChoose := 0
		choose := values[cur]
		for _, v := range graph[cur] {
			if v != father {
				noChoose += subTreeSum[v]
				choose += dfs(v, cur)
			}
		}
		return maxForTwo[int](choose, noChoose)
	}
	return int64(dfs(0, -1))
}

func minForTwo[T int](a, b T) (minValue T) {
	if a > b {
		return b
	} else {
		return a
	}
}

func getAllSum(nums []int) int {
	sum := 0
	for i := 0; i < len(nums); i++ {
		sum += nums[i]
	}
	return sum
}

func maxForTwo[T int](a, b T) (maxValue T) {
	if a > b {
		return a
	} else {
		return b
	}
}

未完待续

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值