python 基础知识点(蓝桥杯python科目个人复习计划47)

今日复习内容:树形DP

自上而下的树形动态规划(DP)是一种解决树形结构问题的有效方法。在树形结构中,每个节点通常有0个或多个子节点,而且节点之间存在着特定的父子关系。树形DP的目标是通过叶子节点逐步向上计算,根据根节点的最优解或者其他需要的信息。

1.问题建模

首先需要将问题转化为树形结构,确定节点之间的父子关系以及每个节点的属性和目标。

确定问题的子问题定义,即每个节点需要解决的子问题是什么。

2.状态定义

定义动态规划状态,通常与问题的子问题有关。

状态是每个节点的某种属性,也可以是节点之间的关系或者路径。

3.状态转移方程

根据子问题的定义和状态的定义,建立状态转移方程。

状态转移方程描述了从子问题到父问题的转移过程,通常使用递归或者迭代的方式进行定义。

4.自底而上计算

从叶子节点开始计算,逐步向上计算父节点的状态,直到算出根节点的状态或者所需的信息。

5.结果输出

根据问题的具体要求,从根节点的状态或者其他节点的状态中获取所需的信息或者最优解。

6.优化技巧

可以利用记忆化搜索或者动态规划数组来优化计算过程,避免重复计算子问题。

如果树的结构较大,可以考虑剪枝或者其他优化策略来减少计算复杂度。


最大独立集问题是在给定的图中寻找一个最大的顶点集合,使得集合中的任意两个顶点都不相邻(即没有边连接)。

来举个例子,我用的是贪心算法。

问题描述:假设有一个无向图,其顶点集合为{1,2,3,4,5,6},边集合为{(1,2),(1,3),(2,3),(2,4),(3,5),(4,5),(4,6)},我们的目标是找到这个图中的一个最大独立集。

一种可能的最大独立集是{1,4,5},因为这个集合中的任意两个顶点都不相邻。

我们可使用贪心算法来解决这个问题。贪心算法的基本思路是每次选择一个顶点加入独立集中,并移除该顶点的所有相邻顶点。通过反复执行这个过程,直到所有顶点都被考虑到为止。

在这个示例中,我们可以按照以下步骤来构建最大独立集。

1.选择顶点1加入独立集,移除与顶点1相邻的2和3;

2.选择顶点4加入独立集,移除与顶点4相邻的2和5;

3.选择顶点5加入独立集,移除与顶点5相邻的3和4;

4.所得的独立集是{1,4,5}。


接下来做几个题:

例题1:蓝桥舞会

题目描述:

蓝桥公司一共有n名员工,编号分别为1 - n。

他们之间的关系就像一棵以董事长为根的树,父节点就是子节点的直接上司。

每个员工有一个快乐指数ai。

现蓝桥董事会决定举办一场蓝桥舞会让员工们在工作之余享受美好时光,不过对于每个员工,他们都不愿意与自己的直接上司一起参会。

董事会希望舞会的所有参会员工的快乐指数总和最大,请你求出这个最大值。

输入描述:

输入的第一行是一个整数n,表示蓝桥公司的员工数。

第二行包含n个整数,分别表示第i个员工的快乐指数ai。

接下来n - 1行每行包含两个整数u和v,表示v是u的直接上司。

1 <= u,v,ai <= n <= 10^5

输出描述:

输出一个整数,表示答案。

参考答案:

n = int(input())
happy = [0] + list(map(int, input().split()))
tree = [[] for i in range(n + 1)]
fa = [0] * (n + 1)
for i in range(n - 1):
    u, v = map(int, input().split())
    tree[v].append(u)
    fa[u] = 1  # 我是u!我有爹!
dp = [[0] * 2 for _ in range(n + 1)]


# 直接上司不行,但是间接上司可以,所以并不代表选了这层,下下一层就不能选了。
def dfs(i):
    dp[i][0] = 0
    dp[i][1] = happy[i]  # 首先,宣誓本节点的值。

    if tree[i] == []:  # 爆搜到底根。
        return
    for j in tree[i]:  # 对子节点的最优子结构进行累加。
        dfs(j)

        dp[i][0] += max(dp[j][0], dp[j][1])  # 不选,那么后面都可以选。
        dp[i][1] += dp[j][0]  # 选了,他的子节点不能选了。最优子结构锁定在dp[j][0]


for i in range(1, n + 1):
    if fa[i] == 0:
        treeok = i
        dfs(i)  # 没有爸爸的人,就是根节点。
print(max(dp[treeok][0], dp[treeok][1]))


运行结果: 

以下是我对代码的理解:

1.读取输入

n = int(input()):n表示员工的数量

happy = [0] + list(map(int,input().split())):读取每个员工的快乐指数,并存储在happy数组中

tree = [[ ] for i in range(1,n + 1)]:创建了一个空的树状结构tree,用于表示员工之间的关系

fa = [0] * (n + 1):fa数组用于标记每个员工是否有父节点

for i in range(n - 1):循环从第一个员工到倒数第二个员工(因为最后一个员工没有直接上司,不需要处理)

u,v = map(int,input().split()):从输入中读取一对整数u和v,表示v是u的直接上司

tree[v].append(u):将员工u添加到员工v的直接下属列表中,表示员工u是员工v的直接下属

fa[u] = 1:标记员工u有直接上司,因为后面会通过判断fa[u]来确定根节点

dp[[0] * 2 for i in range(n + 1)]:创建一个二维数组,其中每行表示一个节点,每行有两个元素,分别表示不选择该节点和选择该节点时的状态值,其中dp[i][0]表示不选择节点i时的最大快乐指数,dp[i][1]表示选择该节点时的最大快乐指数。

接下来用深度优先搜索dfs(i),用于计算以节点i为根的子树中的最大快乐指数

dp[i][0] = 0:初始化节点i不被选择时的最大快乐指数为0

dp[i][1] = happy[i]:初始化节点i被选择时的最大快乐指数为节点i的快乐指数

if tree[i] == []:检查节点i是否为叶子节点,即没有子节点的情况

return:如果是叶子节点,函数直接返回,因为叶子节点的最大快乐指数就是节点本身的快乐指数

for j in tree[i]:遍历节点i的所有子节点j

dfs(j):递归调用dfs函数,计算以子节点j为根的子树中的最大快乐数

dp[i][0] += max(dp[j][0],dp[j][1]):更新不选择节点i时的最大快乐指数(如果不选择节点i,则可以选择其子节点中的最大快乐指数)

dp[i][1] += dp[j][0]:更新选择节点i时的最大快乐指数(如果选择节点i,则不能选择其子节点,所以最大快乐指数是节点i的快乐指数其所有子节点的不被选择时的最大快乐指数)

for i in range(1,n + 1):遍历所有员工编号,从1到n

if fa[i] == 0:检查是否有直接上司,即是否为根节点,如果fa[i] 为0,表示没有直接上司,说明员工i是根节点

treeok = i:将根节点的编号保存在变量treeok中

dfs(i):调用深度优先搜索函数,计算以根节点i为根的子树的最大快乐指数

最后,输出整个树的最大快乐指数,,即根节点的最大快乐指数。

通过max(dp[treeok][0],dp[treeok][1]):取选择和不选择时的最大值。


例题2:生命之树

题目描述:

在x森林里,上帝创造了生命之树。

他在每棵树的每个节点(叶子也称为一个节点)上,都标了一个整数,代表这个点的和谐值。

上帝要在这棵树内选出一个非空节点集s,使得s中的任意两个点a,b,都存在一个点列a,v1,v2,...,vk,b,使得这个点列中的每个点都是s里的元素,且序列中相邻两个点间有一条边相连。

在这个前提下,上帝要使得s中的点所对应的整数的和尽量大。

这个最大的和就是上帝给生命之树的评分。

经过atm的努力,他已经知道了上帝给每棵树每个节点上的整数。但由于atm不善于计算,他不知第怎样有效地求评分。他需要你为他写一个程序来计算一棵树的分数。

输入描述:

第一行一个整数n表示这棵树有n个节点。

第二行n个整数,一次表示每个节点的评分,接下来n - 1行,每行两个整数u,v,表示存在一条u到v的边。由于这是一棵树,所有不存在环。

其中,0 < n <= 10^5,每个节点的评分的绝对值不超过10^6.

输出描述:

输出一行一个数,表示上帝给这棵树的分数。

参考答案:

import sys
sys.setrecursionlimit(100000)
n=int(input())
aList=[0] + [int(i) for i in input().split()]
tree=[[]for i in range(n+1)]
root_max=[0 for i in range(n+1)]
for i in range(n-1):
    m,n=map(int,input().split())
    tree[m].append(n)
    tree[n].append(m)
def dfs(dad,son):
    root_max[son]=aList[son]
    for i  in tree[son]:
        if i !=dad:
            root_max[i]=dfs(son,i)
            if root_max[i]>0:
                root_max[son]+=root_max[i]
    return root_max[son]
dfs(0,1)
print(max(root_max))

以下是我自己的思路:

sys.setrecursionlimit(100000):这行代码设置了递归的最大深度设置,防止递归调用栈溢出

n = int(input()):输入整数n,表示树的节点数

alist = [0] + [int(i) for i in input().split()]:输入每个节点的评分,将它们储存在列表alist中。这里使用列表推导式和split()方法将输入的评分转换为整数列表,并在列表开头添加一个占位符0

tree = [[ ] for i in range(n + 1)]:创建一个空的列表,用于存储树的邻节点,其中,tree[i]存储与节点i相邻的节点列表。

root_max = [0 for i in range(n + 1)]:创建一个长度为n + 1的列表,用于存储以每个节点为根的子树的最大评分

接下来构建树的邻接表表示,其中每个节点的邻居节点存储在列表中。

for i in range(n - 1):这是一个循环,遍历了n - 1次,因为一棵树的边数总是比节点数少一个

“一棵树的边数总是比节点数少一个”:

这句话的意思是在一个无向树中,边的数量总数比节点的数量少一个。这是因为树 是一个连通且无环的图,每个节点都与其他节点通过边相连,但是没有形成环路。考虑到树的性质,可以通过归纳法来得出这个结论。

基础情况:当只有一个节点时,边数为0,符合条件。

归纳假设:假设对于有n个节点的树,边的数量总是n - 1。

归纳步骤:考虑一棵含义n + 1个节点的树。在这棵树中选择任意一个节点作为根节点,将树分成以该节点为根的子树和其他子树。此时,原树中包含n + 1个节点,将根节点和其余的子树看作独立的节点,则按归纳假设,每个子树都含有k个节点,其中1 <= k <= n。根据归纳假设,每个子树的边数为k - 1,总共有n个子树,因此总边数为n。

归纳结论:对于含有n+1个节点数,边的数量也是n。

因此,一棵树的边数总是比节点数少一个。

m,n = map(int,input().split()):从输入中获取一条边的两个节点m和n

tree[m].append(n):将节点n添加到节点m的邻居列表中,表示节点m与节点n之间存在一条边

tree[n].append(m):同样,将节点m添加到n的邻居列表中,因为树的边是双向的

通过这个循环,根据输入的边信息,成功构建了树的邻接表表示。这样,后续在DFS中,可以方便地遍历每个节点的邻居节点,实现相应的计算。

def dfs(dad,son):定义了一个递归的DFS函数,用于计算以son为根的子树的最大评分

root_max[son] = alist[son]:初始化以节点son为根的子树的最大评分为节点son的评分

for i in tree[son]:遍历节点son旁边的所有邻居节点i

if i != dad:检查当前节点i是否为父节点,避免向上回溯。

root_max[i]  = dfs(son,i):递归调用dfs函数计算以节点i为根的子树的最大评分,并且将结果存储在root_max[i]中。

if root_max[i] > 0:如果子树的最大评分大于0,则将其累加到当前节点son的评分中

return root_max[son]:返回以节点son为根的子树的最大评分

dfs(0,1):调用DFS函数,计算以节点1为根的整棵树的最大评分

print(max(root_max)):输出整棵树的最大评分,即取root_max列表中的最大值。


OK,这个知识点我学了好久,暂时只会这些。

下一篇继续!

  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值