基环树 DP:学习笔记

news/2025/2/13 19:48:33/文章来源:https://www.cnblogs.com/jerrycyx/p/18714327

总述

定义

基环树,是一个 \(N\) 个点和 \(N\) 条边的连通图,特征是图中有且仅有一个环。特别的,如果不连通且每个连通块的点数和边数都相等,那么这就是一个基环树森林。

基环树 DP,顾名思义,就是在一个基环树上 DP,或是 DP 的结构类似基环树。相对于正常的树型 DP,一般来说基环树 DP 难度更大,代码更加复杂。

处理方法

基环树 DP 一般做的第一件事是找环,方法很多,如拓扑排序等,下面附上我一般用的方法(栈):

bool vst[N],in_cyc[N];
int sta[N],top;
int cyc[N],cyc_idx;
void Find_cyc(int x,int fa)
{if(vst[x]){cyc[++cyc_idx]=x;in_cyc[x]=true;while(sta[top]!=x){cyc[++cyc_idx]=sta[top];in_cyc[sta[top]]=true;sta[top--]=0;}return;}vst[x]=true;sta[++top]=x;for(int i=head[x];i;i=edge[i].nxt){int y=edge[i].to;if(y==fa) continue;Find_cyc(y,x);if(in_cyc[y]) break;}sta[top--]=x;return;
}

基环树 DP 由于其比正常树多了一条边的特性,可以用以下两个视角看待:

一棵树和一条额外边

把基环树看作一棵树和一条额外的边(横叉边或后向边都有可能),在处理的时候像正常的树一样跑树型 DP,在额外边所连接的两点处做特判等特殊处理。

在这个视角下,基环树长这样:

一个广义“根节点”和若干子树

把基环树的环看作一个广义的“根节点”,许多子树挂在这个环上。环上每个点都可以作为一个单独的根节点计算出各自子树的答案,最后再对整个环统一进行计算。

在这个视角下,一般会把基环树画成这样一个形态:

(是哪个天才说长得像细菌的?)

或者把环压平,像这样:

例题

P1453 城市环路

简化题意:求树上带权最大独立集(其中任意两点都没有边相连)

分别考虑用两种视角来做这道题:

视角#1:树带额外边

容易发现如果没有环,这道题和没有上司的舞会一模一样,而这条额外边只是多了一条限制而已。

首先直接遍历树,找到这条额外边所连接的两点,设其为 \(x\)\(y\)

强制 \(x\) 不能选,跑一遍普通树型 DP);再强制 \(y\) 不能选,跑一遍普通树型 DP。两次 DP 取最大值即为答案,因为这样保证了 \(x\)\(y\) 至少有一个没有选,所以不可能两个都选。

视角#2:环带若干子树

首先找出环,对环挂着的每一个子树单独跑一边树型 DP,求出根节点选或不选时的最大价值。

然后对环上所有点跑环状 DP。具体来说,设 \(f_{0/1,i,0/1}\) 表示第一个点选/不选且第 \(i\) 个点选/不选时,前 \(i\) 个点的最大价值。答案即为 \(\max\{f_{0,c,0},f_{0,c,1},f_{1,c,0}\}\)\(c\) 表示环上点数),同样保证了第 \(1\) 个点和第 \(c\) 个点不能同时选。

(其实从本质上看,这两种方法是一样的。)

P4381 [IOI 2008] Island

简化题意:求基环树的直径。

基环树的直径定义为:树上最长链的长度。

这个题似乎只能用视角二。

把基环树上路径分成两种,不经过环的和经过环的。

不经过环的直接就是正常求树的直径,随便写写就行。

经过环的分为三段:从一棵子树开始,环上走一段,在一棵子树结束。

预处理出每棵子树的高度 \(h_i\),然后答案就是:

\[\max_{i \neq j}\{ h_i+h_j+dis(i,j) \} \]

前缀和搞一下可以转成:

\[\max_{i<j}\{h_i+h_j+dis_j-dis_i\} = \max_{i<j}\{h_i-dis_i + h_j+dis_j\} \]

然后有两种方法处理环上求解上述式子,这两种都建议掌握,特别是后一种虽然比较麻烦但是更为通用。

法一:拆分两种路径

同时因为环上两点间有两条路径,所以还要反过来更新一下答案(\(L\) 表示环总长):

\[\max_{i<j}\{h_i+h_j+L-(dis_j-dis_i)\} = L+ \max_{i<j}\{h_i+dis_i + h_j-dis_j\} \]

两部分分别算一下,取个最大值就是答案。

法二:单调队列限制距离

把环从某处断开,复制一份接到末尾,这条新链上任意一条点数不超过原环点数的子区间都可以表示原环上的一段距离。

然后我们可以枚举 \(j\)。不断右移 \(j\) 的过程中 \(h_j+dis_j\) 确定,通过单调队列找到定长区间 \([j-s+1,j)\)\(s\) 表示原环点数)内最大的 \(h_i-dis_i\)。两者相加即可更新答案。

P1399 [NOI2013] 快餐店

其实我觉得这应该是个紫题的

点名表扬这篇题解,把这道题所需结论讲正确且讲清楚了。引用一下:

我们做出答案点的最短路生成树,发现答案点也是该生成树上到其它点距离的最大值最小的点(对于该生成树的答案),于是可以考虑枚举生成树并求生成树的答案。接着又发现枚举生成树求出的答案值不会小于原图的答案值,于是算法正确性成立。

结论:本题答案为删去环上任意一条边后所得树直径的最小值的一般。不是基环树直径的一半!

本题仍然使用视角二。

虽然不能求基环树直径,但是仍然需要先找环,算出每个子树各自的直径和深度。

首先这些直径是无论如何切环都不会变的,设其中的最大值为 \(mxsub\)。然后假定环上有 \(k\) 个点,且以 \(1,2,3,\dots k\) 的顺序排列,设各个子树的深度为 \(h_i\)

接着可以发现,上述法二刚好满足我们的需求:其框定的一块长度为 \(k\) 的区域恰好就相当于将环从区域两端点处断开。那么我们根据之前的结论可得此时答案应该是为:

\[\max_{l \le i<j \le l+k-1}\{(h_i-dis_i) + (h_j+dis_j)\} \]

而我们要在均摊 \(O(1)\) 的时间复杂度下求解这个,容易想到两个单调队列分别维护 \(h_i-dis_i\)\(h_j+dis_j\) 的值。然后你就会发现:这种做法无法判断 \(i \neq j\)

一般在这种情况下,常用的处理方法都是记录一个次大值以作备用,但是很快你就会愤怒地发现:单调队列不!能!维!护!次大值(大佬们如果知道方法麻烦告诉我一声,感激不尽)。

上面提到的题解里也发现了这个问题,他使用了一种变异的单调队列,其实是将在线转为了离线处理(普通单调队列是在线的)。本蒟蒻爆调 \(4\) 小时未能战胜,最终放弃转而采用温柔体贴的线段树,\(25\) 行就解决了,甚至比那个变异单调队列还短。

(然后这就引申处一个结论:维护次大值的时候,只有不重合的区间才可以直接合并。所以理论上 ST 表也不能直接这样求解次大值)

记每种切法(即每段区间)这样得到的答案的最小值为 \(ans\),总答案即为 \(\max(mxsub,ans)\)

代码在下面,有空可以参考以下,没空就算了。

#include<cstdio>
#include<algorithm>
#define LL long long
using namespace std;const int N=1e5+5;
int n;struct Allan{int to,nxt;int val;
}edge[N<<1];
int head[N],idx;
inline void add(int x,int y,int z)
{edge[++idx]={y,head[x],z};head[x]=idx;return;
}bool vst[N],in_cyc[N];
pair<int,int> sta[N]; int top;
pair<int,int> cyc[N]; int cyc_idx;
void Find_cyc(int x,int fa)
{if(vst[x]){cyc[++cyc_idx]={x,0};in_cyc[x]=true;while(sta[top].first!=x){cyc[++cyc_idx]=sta[top];in_cyc[sta[top].first]=true;top--;}cyc[1].second=sta[top].second;reverse(cyc+1,cyc+cyc_idx+1);return;}vst[x]=true;sta[++top]={x,0};for(int i=head[x];i;i=edge[i].nxt){int y=edge[i].to,z=edge[i].val;if(y==fa) continue;sta[top].second=z;Find_cyc(y,x);if(in_cyc[y]) break;}top--;return;
}LL mxdep[N],diam[N];
void Calc_diam(int x,int fa)
{for(int i=head[x];i;i=edge[i].nxt){int y=edge[i].to,z=edge[i].val;if(y==fa || in_cyc[y]) continue;Calc_diam(y,x);diam[x]=max({diam[x],diam[y],mxdep[x]+mxdep[y]+z});mxdep[x]=max(mxdep[x],mxdep[y]+z);}return;
}inline void updsec(pair<LL,int> &src1,pair<LL,int> &src2,pair<LL,int> t)
{if(t>src1) src2=src1,src1=t;else if(t>src2) src2=t;return;
}struct SegmentTree{int l,r;pair<LL,int> dat[2];
}tree[2][N<<3];
inline SegmentTree merge(SegmentTree ls,SegmentTree rs)
{SegmentTree res;res.l=ls.l,res.r=rs.r;res.dat[0]=ls.dat[0],res.dat[1]=ls.dat[1];updsec(res.dat[0],res.dat[1],rs.dat[0]);updsec(res.dat[0],res.dat[1],rs.dat[1]);return res;
}
void Build(bool op,int p,int l,int r,LL src[])
{tree[op][p].l=l,tree[op][p].r=r;if(l==r){tree[op][p].dat[0]={src[l],l};tree[op][p].dat[1]={-1e18,0};return;}int mid=l+r>>1;Build(op,p<<1,l,mid,src);Build(op,p<<1|1,mid+1,r,src);tree[op][p]=merge(tree[op][p<<1],tree[op][p<<1|1]);return;
}
SegmentTree Query(int op,int p,int l,int r)
{if(l<=tree[op][p].l&&tree[op][p].r<=r)return tree[op][p];int mid=tree[op][p].l+tree[op][p].r>>1;SegmentTree res;if(l<=mid&&r>mid) return merge(Query(op,p<<1,l,r),Query(op,p<<1|1,l,r));else if(l<=mid) return Query(op,p<<1,l,r);else return Query(op,p<<1|1,l,r);
}LL ans=1e18;
LL s[N<<1],d[N<<1],a[N<<1],b[N<<1];
void Main_Solve()
{LL mxsub=0;for(int i=1;i<=cyc_idx;i++){mxsub=max(mxsub,diam[cyc[i].first]);s[i]=mxdep[cyc[i].first];d[i]=cyc[i].second;}for(int i=1;i<cyc_idx;i++)s[cyc_idx+i]=s[i],d[cyc_idx+i]=d[i];for(int i=1;i<cyc_idx<<1;i++){d[i]+=d[i-1];a[i]=s[i]+d[i-1],b[i]=s[i]-d[i-1];}Build(0,1,1,(cyc_idx<<1)-1,a);Build(1,1,1,(cyc_idx<<1)-1,b);for(int i=1;i<=cyc_idx;i++){int l=i,r=i+cyc_idx-1;SegmentTree tmpa=Query(0,1,l,r),tmpb=Query(1,1,l,r);LL tot=tmpa.dat[0].second==tmpb.dat[0].second?max(tmpa.dat[0].first+tmpb.dat[1].first,tmpa.dat[1].first+tmpb.dat[0].first):tmpa.dat[0].first+tmpb.dat[0].first;ans=min(ans,tot);}ans=max(ans,mxsub);return;
}int main()
{scanf("%d",&n);for(int i=1;i<=n;i++){int x,y,z; scanf("%d%d%d",&x,&y,&z);add(x,y,z),add(y,x,z);}Find_cyc(1,0);for(int i=1;i<=cyc_idx;i++)Calc_diam(cyc[i].first,0);Main_Solve();printf("%.1lf\n",ans/2.0);return 0;
}

顺便给个双倍经验。

总结

其实我觉得基环树 DP 不能算一种单独的 DP,而只能说是树型 DP 和环状 DP 的缝合怪,另外还可以杂糅上一堆乱七八糟的算法(经常是单调队列)。

基环树 DP 一般只是出题人用来 恶心人 适当增加题目难度的,其如果包含多种 DP,关联性一般也不大。所以缝合出基环树 DP 的算法如果都掌握了的话,基环树 DP 大概可以无师自通。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/883372.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字孪生如何让GIS场景视效瞬间高大上?带你了解鲸孪生中的GIS系统

GIS与数字孪生的结合非常紧密,而山海鲸可视化作为一个数字孪生平台,也将GIS系统整合在了鲸孪生功能之中。 GIS中包含了大量的数据,例如遥感数据、地形数据、倾斜摄影数据等,能够为数字孪生系统提供非常好的补充。同时,传统的GIS系统整体视觉效果相对较差,与其他模型和数据…

2 分支 多个思路

利用分支,你就可以在同一个代码基础上同时处理多个完全没有关联、相互独立的工作。考虑以下场景。 假设你正在改一个 Bug-A,此时已经产生了大量的代码修改,并且离修复完成还有很长一段时间(起码得明天)。此时,有一个着急但简单的 Bug-B 需要你立即完成,并在一个小时内同…

记录一种DAG计数方法与一个配套技巧

记录一种DAG计数方法与一个配套技巧 定义 \(f_S\) 表示集合 \(S\) 中的点构成的合法 DAG 子图的方案数。假设找到 DAG 中一个入度为 \(0\) 的节点 \(x\),那么很明显 \(f_S=\sum_{x}f_{S\setminus \{x\}}\),这明显要算重因为 \(S\setminus \{x\}\) 中也有入度为 \(0\) 的点。 …

野鸡题手写题解整合

浴谷正在蒸蒸日上,专栏区怕是马上要倒闭了。 CF2026F 题 题。题外话:这场有点水平,E 题让我重拾了最大权闭合子图的记忆。 首先考虑没有这个可持久化(只有 \(2,3,4\) 操作)怎么做。\(0/1\) 背包问题,动态维护当前的 dp 数组 \(f_i\) 表示总体积 \(\sum p\) 不超过 \(i\) …

鸿蒙开发:了解@Builder装饰器

@Builder装饰是鸿蒙UI开发中,非常重要的一个装饰器,在实际的开发中,合理且正确的使用,能够让我们的代码更加的简洁前言本文代码案例基于Api13,温馨提示:内容相对来说比较简单,如果您已掌握,略过即可。如果说一个页面中组件有很多,我们都统一写到build函数中,显而易见…

P1020 [NOIP 1999 提高组] 导弹拦截(dilworth)

这道题真的做的我鬼火冒,尤其是这个第二问要用到dilworth但是我看讲解完全不知道他们在讲什么,我看了好久才理解,一个数组至少可以由几个不增子序列覆盖就等于严格单调递增的最长子序列的长度,如果是至少可以由几个严格递减子序列覆盖就等于最长单调不减子序列的长度,然后…

Linux系统介绍

1. Linux介绍 Linux和windows一样也是一个操作系统,但是与windows不同的是,Linux是一套开放源码的代码程序、并且可以自由传播的类unix操作系统软件。 Linux系统主要被应用于服务端、嵌入式开发和个人PC桌面3大领域,一般的WEB项目都是部署在Linux操作系统上。 Linux是一个基…

Linux驱动---按键

文章简述了Input子系统架构,讲解了输入设备驱动开发流程,探讨了按键消抖方法及按键驱动开发要点,为嵌入式开发提供参考。目录一、Input子系统1.1、简介1.2、Input子系统构成1.3、input_dev结构体二、输入设备驱动开发流程2.1、分配和初始化输入设备2.2、注册设备2.3、事件上…

Android studio:flutter开发环境配置

Make Android apps | Flutter 下载flutter,并配置环境变量 右击 此电脑->属性->高级系统设置->环境变量在 用户变量 新建两个变量 变量名:FLUTTER_STORAGE_BASE_URL 变量值:https://storage.flutter-io.cn 变量名:PUB_HOSTED_URL 变量值:https://pub.flutter-io.c…

相机模型(Ⅱ) 相机标定(Camera Models 2 Camera Calibration) 总结

弱透视投影(Weak perspective projection)弱透视投影是计算机视觉和摄影测量中的一种近似投影模型。在这种投影中,假设物体距离相机足够远,使得物体上各点到相机光心的距离近似相等。 从图中可以看到,有一个光心 \(O\),物体平面上的点 \(P、Q、R\) 等通过光线投影到图像平…