树上dp的一些整理(长期施工中)
这一部分应该算是树上问题的入门 通过dfs以及转移方程解决问题
树就相当于没有环的图 最顶端的点叫做根节点 一般树由上往下长,根节点向下分成若干个子节点。最底下的子节点称为叶子节点。树有很多非常好的性质值得学习。
在解决树的问题的时候 如果没有规定root 我们就以1为root,在存图的时候,一般存双向边比较方便。下文中所有边均存入名为adj的vector中,即邻接表。
1.基础问题:子树大小、子树最值等
在解决这些问题的时候 我们发现他们有个共同特点 就是父节点的信息能由子节点推算出来
也就是说,父亲节点的子树大小,其实就等于它的所有子节点的子树大小相加再+1。我们可以利用dfs,先递归到叶子节点,再一层一层向上推,此时跑一遍的时间复杂度为O(n)
以下为求dfs求子树大小 最值同理 将转移方程改变为sum[u]=max(sum[u],sum[v])即可

int sum[MAXN]; void dfs_sum(int u,int fa){ sum[u]=1;//规定初始的值为1 for(auto &v:adj[u]){ if(v==fa) continue;//如果是父亲节点 就不搜 dfs_sum(u,v);//向下递归 sum[u]+=sum[v];//转移 } }
稍微难一点的是求树上最长路径 在求路径的时候要维护最大值和次大值

ll dp[2][MAXN] void dfs(int u,int fa){ for(auto &[v,w]:adj[u]){ if(v==fa) continue; dfs(v,u); if(dp[0][v]+w>dp[0][u) dp[1][u]=dp[0][u],dp[0][u]=dp[0][v]+w; else if(dp[0][v]+w>dp[1][u]) dp[1][u]=dp[0][v]+w; //更新最大值和次大值 } ans=max(ans,dp[0][u]+dp[1][u]); }
2.双向遍历
双向遍历就是子节点信息由父亲节点得到,比如经典问题,求树的中心 树上某点到其他任意点的最大值最小化。我们不仅需要知道向下的值 还需要知道向上的值

vector<pair<int,int> > adj[MAXN]; ll up1[MAXN],up2[MAXN],down[MAXN]; void dfs_down(int u,int fa) { up1[u]=0,up2[u]=0; for(auto [v,w]:adj[u]) { //往下寻找下面的最大值 if(v==fa) continue; dfs_down(v,u); if(up1[v]+w>=up1[u]) { //更新最大的 up2[u]=up1[u]; up1[u]=up1[v]+w; } else if(up1[v]+w>=up2[u]) { //更新第二大的 up2[u]=up1[v]+w; } } } void dfs_up(int u,int fa) { //往上寻找上面(子树外)的最大值 //用父节点的值更新子节点 for(auto [v,w]:adj[u]) { if(v==fa) continue; if(w+up1[v]==up1[u]) { //最大的直线在一条树上 //用次长子树更新 down[v]=max(up2[u]+w,down[u]+w);//取次长的子树+w与u最长的路径+w的最大值 } else { down[v]=max(up1[u]+w,down[u]+w); } dfs_up(v,u); } }
待补充
A Problem - 1324F - Codeforces
题意: 选择一些连通子树 让白子-黑子 的数量最大化
思路: 上下两遍扫描 处理出子树的最大值 然后再处理往上的最大值

#include<bits/stdc++.h> #define close std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0) using namespace std; typedef long long ll; const ll MAXN = 3e5+7; const ll mod =1e9+7; const ll inf =0x3f3f3f3f; const ll INF =0x3f3f3f3f3f3f3f3f; //如果 cnt1>cnt2 就加上子树的贡献 int dp[MAXN]; //dp[i]表示以i为根的树 可以存在的最大值 //转移方程 : 向下:如果子节点的>0 就加上 // 向上: 如果父节点的子节点>0 就取最大值 否则取加上和不加上的最大值 vector<int> adj[MAXN]; void dfs_down(int u,int fa){ for(auto v:adj[u]){ if(v==fa)continue; dfs_down(v,u); if(dp[v]>0) dp[u]+=dp[v]; } } void dfs_up(int u,int fa){ for(auto v:adj[u]){ if(v==fa) continue; if(dp[v]>0) dp[v]=max(dp[u],dp[v]); else dp[v]=max(dp[v],dp[u]+dp[v]); dfs_up(v,u); } } void solve(){ int n;cin>>n; for(int i=1;i<=n;i++) { int a;cin>>a; if(a==0) dp[i]=-1; else dp[i]=1; } for(int i=1;i<n;i++){ int u,v;cin>>u>>v; adj[u].push_back(v); adj[v].push_back(u); } dfs_down(1,-1); dfs_up(1,-1); for(int i=1;i<=n;i++) cout<<dp[i]<<" "; } int main(){ solve(); }
题意: 这题与A比较相似 但是是处理删除后的最大值并且使删除的操作数量更少 难度更大一点
思路:dp[i][1]为以i为根节点的子树中 删除最少的点能达到的最大值
dp[i][0]为最小步数
只进行一次搜索就可以完成
如果>0 肯定要加上 <0 肯定要切割
如果等于0 已经操作次>1 就删掉 这样赚 否则加上
最后的时候 因为建树的时候是以1为节点 如果以其他为节点的时候 还要多删一下 因此操作数+1

#include<bits/stdc++.h> #define close std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0) using namespace std; typedef long long ll; const ll MAXN = 4e5+7; const ll mod =1e9+7; const ll inf =0x3f3f3f3f; const ll INF =0x3f3f3f3f3f3f3f3f; #define int long long vector<int > adj[MAXN]; int dp[MAXN][2];//dp[i][0]表示 包含i的i子树 操作次数 dp[i][1]表示操作后的最大权值和 int a[MAXN]; void dfs(int u,int fa){ dp[u][0]=0,dp[u][1]=a[u]; for(auto &v:adj[u]){ if(v==fa) continue; dfs(v,u); if(dp[v][1]<0){ dp[u][0]+=1; } else if(dp[v][1]>0){ dp[u][0]+=dp[v][0]; dp[u][1]+=dp[v][1]; } else{//=0的情况 如果v的操作数>1 删掉 否则加上去 if(dp[v][0]>=1){ dp[u][0]+=1; } else{ dp[u][0]+=dp[v][0]; dp[u][1]+=dp[v][1]; } } } // if(adj[u].size()==1) } void solve(){ int n;cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<n;i++){ int u,v;cin>>u>>v; adj[u].push_back(v); adj[v].push_back(u); } dfs(1,-1); int maxs=dp[1][1],maxt=dp[1][0]; for(int i=2;i<=n;i++){ if(dp[i][1]>maxs||(dp[i][1]==maxs&&dp[i][0]+1<maxt)){//更新最小操作数和最大权值和 maxs=dp[i][1]; maxt=dp[i][0]+1; } } // for(int i=1;i<=n;i++){ // cout<<"# "<<i<<" : "<<" t:"<<dp[i][0]<<" x:"<<dp[i][1]<<"\n"; // } // if(maxs==dp[1][1]&&maxt==dp[1][0]){//如果不是在1这个点取到的 操作数+1 cout<<maxs<<" "<<maxt; // } // else cout<<maxs<<" "<<maxt+1; } signed main(){ solve(); }
题意:最多可使用一次传送门 算出最短距离
思路:赛场上是把所有点搞到一起 然后用lca算距离 一直re 赛后学长提醒才发现树有环了 寄
不过想想这道题还是比较简单的 两个点相通有两个方法 1使用传送门 2不使用传送门
使用的情况 算出每个点到传送点的最大距离 然后距离相加 (或者建虚点 dij也行
不使用的情况 直接d[u]+d[v]-d[lca(u,v)] 取最小即可

#include<bits/stdc++.h> #define close std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0) using namespace std; typedef long long ll; const ll MAXN = 2e5+7; const ll mod =1e9+7; const ll inf =0x3f3f3f3f; const ll INF =0x3f3f3f3f3f3f3f3f; #define int long long int vis[MAXN],f[MAXN]; vector<pair<int,ll> > adj[MAXN]; int par[MAXN][20],dep[MAXN];ll dis[MAXN],d[MAXN]; void dfs(int u,int fa){ dep[u]=dep[fa]+1; par[u][0]=fa; for(int i=1;i<20;++i){ par[u][i]=par[par[u][i-1]][i-1]; } for(auto &i:adj[u]){ int v=i.first; if(v==fa) continue; dfs(v,u); } } int getLCA(int u,int v){ if(dep[u]<dep[v]) swap(u,v); for(int i=19;i>=0;--i){ if(dep[par[u][i]]>=dep[v]) u=par[u][i]; } if(u==v) return u; for(int i=19;i>=0;i--){ if(par[u][i]!=par[v][i]){ u=par[u][i]; v=par[v][i]; } } return par[u][0]; } void dfs_down(int u,int fa){ for(auto &i:adj[u]){ int v=i.first,w=i.second; if(v==fa) continue; dfs_down(v,u); //if(f[v]) dis[u]=0; dis[u]=min(dis[u],dis[v]+w); } } void dfs_up(int u,int fa){ for(auto &i:adj[u]){ int v=i.first,w=i.second; if(v==fa) continue; // if(f[u]) dis[v]=0; dis[v]=min(dis[v],dis[u]+w); dfs_up(v,u); } } void dfs_sum(int u,int fa){ for(auto &i:adj[u]){ int v=i.first,w=i.second; if(v==fa) continue; d[v]=d[u]+w; dfs_sum(v,u); } } void solve(){ int m,n;cin>>n>>m; int root; memset(dis,0x3f3f3f3f,sizeof(dis)); memset(d,0x3f3f3f3f,sizeof(d)); for(int i=0;i<m;i++){ int a;cin>>a; f[a]=1; if(i==0) root=a; dis[a]=0; } for(int i=1;i<n;i++){ int u,v,w;cin>>u>>v>>w; adj[u].push_back({v,w}); adj[v].push_back({u,w}); } d[root]=0; dfs_sum(root,-1); dfs(root,-1); dfs_down(root,-1); dfs_up(root,-1); int q;cin>>q; while(q--){ int u,v;cin>>u>>v; int ans=d[u]+d[v]-2*d[getLCA(u,v)]; ans=min(ans,dis[u]+dis[v]); cout<<ans<<"\n"; } } signed main(){ close; solve(); }
D Problem - 1363C - Codeforces
题意:每人轮流取一个点 谁先取到x谁输
思路:如果只有一个点 先手输 如果两个点 后手输 剩下判断奇偶性即可

#include<bits/stdc++.h> #define close std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0) using namespace std; typedef long long ll; const ll MAXN = 3e5+7; const ll mod =1e9+7; const ll inf =0x3f3f3f3f; const ll INF =0x3f3f3f3f3f3f3f3f; vector<int> adj[MAXN]; int IN[MAXN]; int size[MAXN]; void dfs(int u,int fa){ size[u]=1; for(auto v:adj[u]){ if(v==fa) continue; size[u]+=size[v]; } } void solve(){ int n,x;cin>>n>>x; for(int i=1;i<=n;i++) size[i]=0,IN[i]=0; for(int i=1;i<n;i++){ int u,v;cin>>u>>v; adj[u].push_back(v); adj[v].push_back(u); IN[u]++; IN[v]++; } if(IN[x]==1||IN[x]==0){ cout<<"Ayush"<<"\n"; return; } // dfs(1,-1); int maxs=0; if(n%2==0){ cout<<"Ayush\n"; } else cout<<"Ashish\n"; } int main(){ int t;cin>>t; while(t--) solve(); }
E Problem - 1404B - Codeforces
题意:A和B各在一个点上 分别一步可以走da与db A先走 如果A能追上B A胜 否则B
思路:首先如果B在A的一次控制范围 即d(a,b)<=da A必胜
如果da*2>=树的直径 那么只要A站直径中间 B逃不走
如果da*2>=db da可以慢慢逼近db //这里还不会证明
写起来简单
F M-Monster Hunter_第 45 届国际大学生程序设计竞赛(ICPC)亚洲区域赛(南京) (nowcoder.com)

#include<bits/stdc++.h> #define close std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0) using namespace std; typedef long long ll; const ll MAXN = 3e3+7; const ll mod = 1e9+7; const ll inf = 0x3f3f3f3f; #define int ll int dp[MAXN][MAXN][2],a[MAXN];// i为顶点的子树 已经用了j个 此时用/不用 减少的最大贡献 vector<int> adj[MAXN]; int n; int ans=0; int sum[MAXN]; int sz[MAXN]; void dfs_sum(int u,int fa) { sum[u]=a[u]; sz[u]=1; for(auto &v:adj[u]) { sum[u]+=a[v]; dfs_sum(v,u); } ans+=sum[u]; } void dfs(int u,int fa) { dp[u][1][1]=sum[u]; for(auto &v:adj[u]) { dfs(v,u); for(int i=sz[u]; i>=0; i--) { //枚举父节点删了几个 for(int j=sz[v];j>=0;j--){ if(j>0) dp[u][j+i][0]=max(dp[u][j+i][0],dp[u][i][0]+max(dp[v][j][0],dp[v][j][1]+a[v])); else dp[u][j+i][0]=max(dp[u][j+i][0],dp[u][i][0]+dp[v][j][0]); if(j>0&&i>0) dp[u][j+i][1]=max(dp[u][j+i][1],dp[u][i][1]+max(dp[v][j][0],dp[v][j][1])); else if(i>0) dp[u][j+i][1]=max(dp[u][j+i][1],dp[u][i][1]+dp[v][j][0]); } } sz[u]+=sz[v]; } // cout<<"delet : u "<<u<<"\n"; // for(int i=0;i<=n;i++) cout<<"i = "<<i<<" "<<dp[u][i][0]<<" "<<dp[u][i][1]<<"\n"; } void solve() { cin>>n; for(int i=1; i<=n; i++) { adj[i].clear(); sum[i]=0; for(int j=0; j<=n; j++) dp[i][j][0]=dp[i][j][1]=0; } ans=0; for(int i=2; i<=n; i++) { int u; cin>>u; adj[u].push_back(i); } for(int i=1; i<=n; i++) cin>>a[i]; dfs_sum(1,0); dfs(1,0); for(int i=0; i<=n; i++) { cout<<ans-max(dp[1][i][0],dp[1][i][1])<<" "; } cout<<"\n"; } signed main() { int t; cin>>t; while(t--) solve(); }

#include<bits/stdc++.h> #define close std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0) using namespace std; typedef long long ll; const ll MAXN = 4e5+7; const ll mod = 1e9+7; const ll inf = 0x3f3f3f3f; #define int ll int a[MAXN],b[MAXN]; vector<int> adj[MAXN]; int dp[MAXN],sum[MAXN]; void dfs(int u,int fa) { dp[u]=a[u]; int tmp=0; int max_1=0; int max_2=0; for(auto v:adj[u]) { if(v==fa) continue; sum[u]+=a[v]; dfs(v,u); tmp=max(tmp,a[v]); if(b[v]==3) { if(a[v]>a[max_1]) { max_2=max_1; max_1=v; } else if(a[v]>a[max_2]) { max_2=v; } } dp[u]+=dp[v]; dp[u]-=a[v]; } int temp=dp[u]; for(auto &v:adj[u]){ if(v==fa) continue; int k=a[v]; for(auto &it:adj[v]){ if(it==u) continue; k+=dp[it]-a[it]; } if(v==max_1) dp[u]=max({temp+tmp,temp-dp[v]+a[v]+k+a[max_2],dp[u]}); else dp[u]=max({temp+tmp,temp-dp[v]+a[v]+k+a[max_1],dp[u]}); } dp[u]=max(dp[u],temp+tmp); } void solve() { int n; cin>>n; for(int i=1; i<=n; i++) cin>>a[i]; for(int i=1; i<=n; i++) { cin>>b[i]; if(b[i]==2) b[i]=1; adj[i].clear(); dp[i]=0; sum[i]=0; } for(int i=1; i<n; i++) { int u,v; cin>>u>>v; adj[u].push_back(v); adj[v].push_back(u); } sum[0]=inf; dfs(1,0); cout<<dp[1]<<'\n'; } signed main() { close; int t; cin>>t; while(t--) solve(); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话