树上dp之换根dp
基本概念:
换根dp是树上dp的一种
我们在什么时候需要用到换根dp呢?
当题目询问的属性,是需要当前结点为根时的属性,这个时候,我们就要使用换根dp
换根dp的基本思路:
假设题目询问的的属性为x
通常我们会进行两次dfs
第一次dfs,我们选取任意一个结点作为给出的无根树的根,对其进行dfs,并求出这个根的x,以及一些其他辅助数组(即节点与其子树的一些属性关系)
第二次dfs,我们记dp[i]为对于结点i而言,节点i作为树的根时,我们要求的属性x
那么,令当前结点为u,其子节点为v,我们需要对推到出dp[u]转移到dp[v]的转移方程,从而成功求出结点v作为根时的属性x,如此递归下去,直到将所有结点的dp值都求出来,我们就可以求得题目的询问答案了
例题1:
题目链接:P3478 [POI2008] STA-Station - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目大意:
给出一棵有n个节点的树,求出这样的一个节点,使得以这个节点为根时,所有节点的深度之和最大
题目分析:由于题目很直白地讲了是要求以节点为根时的属性,那么当然是采用换根dp的方法
由于是深度之间的转移,我们设数组s[i],表示的是以节点i为根的子树的深度之和
我们先选取1号节点为根,进行第一次dfs,预处理出s数组的值以及dp[1]的值
之后我们需要用第二次dfs进行关系转移
我们考察当前节点u以及其子结点v
当根从u转移到v时,以v为根的子树的每个结点的深度都减小了1,也就是总深度减小了s[v]
而对于以u为根的,且排除了v结点的子树而言,他们每个结点的深度都增大了1,也就是总深度增大了n-s[v]
由此,我们可以推到出,当根由u转到v时,总深度从dp[u]变到dp[v]的变化是:增加了n-2*s[v]
即得转移方程:dp[v]=dp[u]+n-2*s[v]
至此,我们只需要用第二次dfs求出每个节点的dp值,最后再从1到n遍历出最大的dp值,其对应的节点编号即为询问答案
实现代码:
#include <cstdio>
#include <iostream>
using namespace std;
// 换根dp
// 用s[i]表示以i为根时其子树的结点个数
// 令u为当前结点
// v为当前结点的子结点
// 则有s[u]=s[v]+1
// 第一次dfs,选取任意一个根,来预处理出所有的s[i]//换根的状态转移:
//dp[i]表示以i为根时,所有结点的深度之和
//令当前根为u,v为其子结点
//则将根由u转到v时
//所有在v的子树上的结点深度都会减少1,那么总深度就减少了s[v]
//所有不在v的子树上的结点的深度都会增加1,那么总深度就增加了n-s[v]
//由此,我们得知,dp[v]=dp[u]+n-2*s[v],此为状态转移方程
//我们第二次dfs来进行这个操作//最后只需遍历dp[i],找到最大的即可
const int N=1E6+10,M=N<<1;
int n;
//链式前向星加边
int to[M],nxt[M],h[N],tot;
void add(int a,int b){to[++tot]=b;nxt[tot]=h[a];h[a]=tot;
}
unsigned long long s[N],dp[N],dep[N];
void dfs1(int f,int u){dep[u]=dep[f]+1;s[u]++;for(int i=h[u],v;v=to[i];i=nxt[i]){if(v==f) continue;dfs1(u,v);s[u]+=s[v];}
}
void dfs2(int f,int u){for(int i=h[u],v;v=to[i];i=nxt[i]){if(v==f) continue;dp[v]=dp[u]+n-2*s[v];dfs2(u,v);}
}
int main(){cin>>n;for(int i=1,a,b;i<n;i++){cin>>a>>b;add(a,b);add(b,a);}dep[0]=-1;dfs1(0,1);for(int i=1;i<=n;i++) dp[1]+=dep[i];dfs2(0,1);unsigned long long max=0;int ans;for(int i=1;i<=n;i++)if(dp[i]>max){max=dp[i];ans=i;}cout<<ans;return 0;
}
例题2:
题目链接:3585 -- 积累度 --- 3585 -- Accumulation Degree (poj.org)
题目大意:定义A[i],表示的是结点i所能流到所有叶子结点的最大流量之和。其中由一个结点流向另一个节点的流量不能大于连接这两个结点的边的权值。
题目分析:
由于流量是由父结点流向子结点,这使得dfs不好处理(不知道一开始要流多少,有可能等于边权,也有可能小于边权)
所以我们选择把流量从子结点向父结点转移,也就是说,我们认为一个子结点v需要的流量为x,那么回去找它的父结点u,如果连接u与v的边权w大于等于x,那么父结点u所需要的流量就增加x;否则,父结点所需要的流量只能增加w(因为它最多只能流w到子结点v上)
由此,我们设出数组s1[i],表示的是i结点所需要的流量大小,由上述可以写出s1数组的转移方程,令当前结点为u,其子结点为v,那么s1[u]=s1[v]>w?w:s1[v]
又由于叶子结点v没有子结点了,那么它所需要的流量就应该初始化为连结到v的边的权值,因为它最多就只能要那么多。
怎么找到叶子结点呢?只要注意到其入度为1就好了
这样,我们就可以选取一个结点作为根进行第一次dfs,预处理出s1数组
这里注意不能选取叶子结点作为第一次dfs的根,因为会丢失掉其s1的值
第二次dfs:
设dp[u],其意义就是A[u],那么我们就需要推导其转移方程
考察当前根结点u转移到其子节点v
dp[u]表示u结点所需要的流量,那么这部分流量可以被分为两部分
一部分是v结点所转移到u的流量,我们记为Q1
另一部分是除了v节点外,其他子节点所需要的流量,我们记为Q2,则有Q2=dp[u]-Q1
考察v结点转移到u的流量:
当v结点所需的流量s1[v]>=w时,(w为边权),v只能转移w给u,此时Q1=w
而当s1[v]<w时,v能转移s1[v]给u,此时Q1=s1[v]
当根由u转到v时,Q2这部分流量就需要转到v上,由于边权w的限制,Q2的转移量可能会发生变化,我们设其转移量为Q2'
当Q2>=w时,只能转移w给v,此时Q2'=w
而当Q2<w时,就转移Q2给v,此时Q2'=dp[u]-Q1
而对于节点v而言,当其成为根后,其所需的流量,一部分来自于Q2的转移,另一部分来自于结点v自身所需的流量s1[v],也就是说,dp[v]=Q2'+s1[v]
至此,我们成功推导出了状态转移方程,只需进行第二次dfs,即可求出所有的dp值,进而求出最大值
细节问题:当v为叶子结点时,由于其所需的s1[v]来自于一开始的初始化,当它成为根后,其实并不需要s1[v]这一部分的贡献,因此,我们要将这个值减去
实现代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 2E5 + 10, M = N << 1;
int to[M], nxt[M], w[M], h[N], tot;//链式前向星
int in[N];//记录入度
int n, T;
//加边
void add(int a, int b, int c) {to[++tot] = b;w[tot] = c;nxt[tot] = h[a];h[a] = tot;in[b]++;
}long long s1[N], dp[N];
//第一次dfs获取s1数组
void dfs1(int f, int u) {for (int i = h[u], v; v = to[i]; i = nxt[i]) {if (f == v) continue;//当v为叶子结点时,dfs其之前,先初始化s1[v]的值为边权if (in[v] == 1) s1[v] = w[i];dfs1(u, v);//dfs完后,子结点向父结点转移s1的值s1[u] += s1[v] <= w[i] ? s1[v] : w[i];}
}
//第二次dfs
void dfs2(int f, int u) {for (int i = h[u], v; v = to[i]; i = nxt[i]) {if (f == v) continue;//当Q1=w[i]时,此时Q2=dp[u]-w[i]if (s1[v] >= w[i]) {//当Q2<w[i]时,此时Q2'=dp[u]-w[i]if (dp[u] - w[i] < w[i]) dp[v] = dp[u] - w[i] + s1[v];//否则Q2'=w[i]else dp[v] = w[i] + s1[v];//叶子结点减去重复贡献if (in[v] == 1) dp[v] -= s1[v];}//当Q1=s1[v]时,此时Q2=dp[u]-s1[v]else { //当Q2<w[i]时,此时Q2'=Q2if (dp[u] - s1[v] < w[i]) dp[v] = dp[u];//否则Q2'=w[i]else dp[v] = w[i] + s1[v];//叶子结点减去重复贡献if (in[v] == 1) dp[v] -= s1[v];}dfs2(u, v);}
}
int main()
{cin >> T;while (T--){//多组数据输入一定要记得初始化for (int i = 1; i <= tot; i++) to[i] = nxt[i] = w[i] = 0;for (int i = 1; i <= n; i++) in[i] = h[i] = s1[i] = dp[i] = 0;tot = 0;cin >> n;for (int i = 1, a, b, c; i < n; i++) {scanf("%d%d%d", &a, &b, &c);add(a, b, c); add(b, a, c);}int root = 1;//找到非叶子结点作为第一次dfs的根for (int i = 1; i <= n; i++)if (in[i] != 1) {root = i; break;}dfs1(0, root);dp[root] = s1[root];dfs2(0, root);long long ans = 0;//找最大值for (int i = 1; i <= n; i++) ans = max(ans, dp[i]);cout << ans << endl;}return 0;
}
相关文章:
树上dp之换根dp
基本概念: 换根dp是树上dp的一种 我们在什么时候需要用到换根dp呢? 当题目询问的属性,是需要当前结点为根时的属性,这个时候,我们就要使用换根dp 换根dp的基本思路: 假设题目询问的的属性为x 通常我们…...
2024/8/13 英语每日一段
Mackey says while Whole Foods has become more homogenized under Amazon, it did enable the store to do what it couldn’t have done independently. “People saw us as too expensive and out of touch with our customers,” he says. “The main thing Whole Foods n…...
Java多线程练习(1)
MultiProcessingExercise package MultiProcessingExercise120240813;public class MultiProcessingExercise {public static void main(String[] args) {/*需求:一共有1000张电影票,可以在两个窗口领取,假设每次领取的时间为3000毫秒,请用多线程模拟卖票过程并打印…...
AI高级肖像动画神器LivePortrait
文章目录 前言一、安装1.1 源码安装1.2 windows一键启动包 二、人像生成2.1 浏览器2.2 输入图像2.3 选择驱动视频2.4 生成2.5 结果 三、动物生成3.1 浏览器3.2 输入图片3.3 选择视频3.4 生成3.5 最终结果 四、软件获取 前言 最近,快手可灵大模型团队、中国科学技术…...
Java反射机制深度解析与实践应用
Java反射机制深度解析与实践应用 引言 Java反射是Java语言提供的一种能力,允许程序在运行时访问、检测和修改其自身的属性和行为。反射机制是Java面向对象编程的一大亮点,也是Java框架和库常用的技术之一。 反射的基本概念 反射的核心是java.lang.re…...
Oracle递归查询层级及路径
一、建表及插入数据 ocation_idlocation_nameparent_location_id1广东省NULL2广州市13深圳市14天河区25番禺区26南山区37宝安区3 建表sql: CREATE TABLE locations (location_id NUMBER PRIMARY KEY,location_name VARCHAR2(100),parent_location_id NUMBER ); I…...
leetcode300. 最长递增子序列,动态规划附状态转移方程
leetcode300. 最长递增子序列 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2…...
C语言:字符串函数strcpy
该函数用于字符串的拷贝。 使用方法如下: #include<stdio.h> #include<string.h>int main() {char str[10];char* str1 "abcd";//strcpy(str, str1);//把str1复制到str,但此函数不安全所以用strcpy_sstrcpy_s(str, 10, str1);/…...
Day16-指针2
数组指针与指针数组 变量指针:指向变量的地址。 数组指针:指向数组的地址。 指针变量:存放其他变量地址的变量。 指针数组:存放数组元素指针的变量。 数组指针 概念:数组指针是指向数组的指针。特点: 先…...
数据结构(5.5_3)——并查集的进一步优化
Find操作的优化(压缩路径) 压缩路径——Find操作,先找到根节点,再将查找路径上所有结点都挂到根结点下 代码: //Find "查"操作优化,先找到根节点,再进行"路径压缩" int Find(int S[], int x) {…...
(回溯) LeetCode 131. 分割回文串
原题链接 一. 题目描述 给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串。返回 s 所有可能的分割方案。 示例 1: 输入:s "aab" 输出:[["a","a","b"],[…...
【Linux】阻塞信号|信号原理|深入理解捕获信号|内核态|用户态|sigaction|可重入函数|volatile|SIGCHILD|万字详解
目录 编辑 一,常见的信号术语 二,信号在内核中的表示 信号标志位 Pending表 Block表 handler表 POSIX.1标准 三,sigset_t 信号集操作函数 sigemptyset sigfillset sigaddset sigdelset sigismember sigprocmask sig…...
基于Linux对 【进程地址空间】的详细讲解
研究背景: ● kernel 2.6.32 ● 32位平台 –❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀-正文开始-❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀– 在学习操作系统中想必大家肯定都见过下面这…...
[python]使用Pandas处理多个Excel文件并汇总数据
在数据分析和处理过程中,经常需要处理多个Excel文件,并将其中的数据进行汇总和分析。本文介绍使用Python的Pandas库来读取多个Excel文件,并汇总不同类型的数据,例如员工工资、工件数量等。 代码示例 以下是一个完整的代码示例&a…...
提升体验:UI设计的可用性原则
在中国,每年都有数十万设计专业毕业生涌入市场,但只有少数能够进入顶尖企业。尽管如此,所有设计师都怀揣着成长和提升的愿望。在评价产品的用户体验时,我们可能会依赖直觉来决定设计方案,或者在寻找改善产品体验的切入…...
x264 编码器 SSIM 算法源码分析
SSIM SSIM(Structural Similarity Index)是一种用于衡量两幅图像之间视觉相似度的指标。它不仅考虑了图像的亮度、对比度和饱和度,还考虑了图像的结构信息。SSIM的值介于-1到1之间,值越接近1表示两幅图像越相似。 SSIM是基于以下三个方面来计算的: 亮度(Luminance):比…...
echarts使图表组件根据屏幕尺寸变更而重新渲染大小
效果图: 通过 window.addEventListener(resize, this.resizeChart); 实现 完整代码: <template><div class"dunBlock"><div class"char2" id"char2" ref"chart"></div></div…...
电脑图片损坏打不开怎么办?能修复吗?
照片和视频是记录和保存现实生活中的事件的最好方式。由于手机储存空间有限,一般我们会把有纪念意义的照片放到电脑上进行保存,但有时难免会遇到照片被损坏打不开的情况,一旦遇到这种情况,先不要急,也不要因为照片打不…...
vue-cli(二)
箭头函数 一般的函数: 这里window是用来调用函数的 function fun(){console.log(this) } window.fun(); 箭头函数: 1、如果只有一个参数,形参的小括号可以省略 2、如果只有一条语句,{}可以省略 完整的写法 let fun2 a>…...
今日头条的账号id在哪里看(网页版)
今日头条的账号id在哪里看(网页版) 1.https://mp.toutiao.com/profile_v4/index2.登录今日头条账号3.设置->头条号ID 1.https://mp.toutiao.com/profile_v4/index 2.登录今日头条账号 3.设置->头条号ID 打开下方链接: https://mp.to…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
React第五十七节 Router中RouterProvider使用详解及注意事项
前言 在 React Router v6.4 中,RouterProvider 是一个核心组件,用于提供基于数据路由(data routers)的新型路由方案。 它替代了传统的 <BrowserRouter>,支持更强大的数据加载和操作功能(如 loader 和…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”
目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...
大数据治理的常见方式
大数据治理的常见方式 大数据治理是确保数据质量、安全性和可用性的系统性方法,以下是几种常见的治理方式: 1. 数据质量管理 核心方法: 数据校验:建立数据校验规则(格式、范围、一致性等)数据清洗&…...
Java数组Arrays操作全攻略
Arrays类的概述 Java中的Arrays类位于java.util包中,提供了一系列静态方法用于操作数组(如排序、搜索、填充、比较等)。这些方法适用于基本类型数组和对象数组。 常用成员方法及代码示例 排序(sort) 对数组进行升序…...
大模型——基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程
基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 下载安装Docker Docker官网:https://www.docker.com/ 自定义Docker安装路径 Docker默认安装在C盘,大小大概2.9G,做这行最忌讳的就是安装软件全装C盘,所以我调整了下安装路径。 新建安装目录:E:\MyS…...
