姗姗来迟的总结
1. 什么是基环树
一般的,对于一个 N N N 个点, N N N 条边的连通无向图而言(也可称之为在一颗树上多了一条边,即非树边),我们就可以把它称之为基环树
就像这样:
基环树问题的解决方法和普通的树的问题的解决方法差不多,无非就是几步走:
首先,这一类问题的答案分为两种:环对答案作出了贡献和环没对答案作出贡献
对于情况 1 1 1,就是一个正常的树上的问题,而对于情况 2 2 2,我们要走一下的流程
首先,找到环,然后,按照题目要求,拆掉环上的一条边,然后,整棵基环树就成了一颗普通的树,求得另一个可行答案后,与情况 1 1 1 所得到的答案进行比较即可
很简单,不是吗
2. 关于找环
找环方法其实还不少,这里简单介绍两种:
2.1. Tarjan 找环
众所周知,Tarjan 可以用来缩点,那么,我们考虑在基环树上缩点。显然,环会被缩成一个大点,我们只需要将大点里面的小点找出来即可,这非常容易实现,因为每一个点所对应的大点编号都放在 Tarjan 中的scc
数组里面,无需赘述
2.2. 并查集找环
正常来说,我们把树上的点塞入并查集时,是不会出现“有一组节点是在同一集合里面”的情况,但是,基环树由于有一条非树边,所出现这种情况,那么此时,我们就可以以其中一个点作为起点进行搜索(注意,这两个节点所连接的边时不能走的),用栈来维护搜素路径上的点,进入节点时塞入,回溯时弹出,直到遇到了另一个节点为止
3. 一些例题
3.1. 骑士
这就是一道经典的基环树最大带权独立点集问题,与没有上司的舞会高度相似.
首先,我们需要找到环上的一条边 ( x , y ) (x,y) (x,y),并将这条边拆掉,那么,问题就变成了没有上司的舞会
可是, ( x , y ) (x,y) (x,y) 毕竟也是一条边, x , y x,y x,y 至少有一个不选,而拆环后无法考虑这个情况,因此,我们还要将其分为两种情况:强制不选 x x x 和强制不选 y y y
所以,需要跑两次 DP,分别考虑这两种情况
另外注意:这道题可能会出现重边,而有重边的基环树就相当于一颗普通的树,需要特判
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define re register
#define int long long
using namespace std;
const int N=2000005;
int n,m,x[N],y[N];
int a[N];
int ver[N],edge[N],Next[N],head[N],len;
void add(int x,int y,int z){
ver[++len]=y,edge[len]=z,Next[len]=head[x],head[x]=len;
}
int fa[N];
int xx,yy;
void make(int x){
for(re int i=1;i<=x;i++){
fa[i]=i;
}
}
int find(int x){
if(x==fa[x]){
return x;
}
return fa[x]=find(fa[x]);
}//并查集
bool flag[N];
int dp[N][2];
int ans;
void dfs(int x,int No,int kkk){
//用(kkk,No)代表那条被拆掉的边
flag[x]=1;
if(x==No){//强制不选的点需要单独初始化
dp[x][0]=dp[x][1]=0;
}else{
dp[x][0]=0,dp[x][1]=a[x];
}
for(re int i=head[x];i;i=Next[i]){
int y=ver[i],z=edge[i];
if(y==No&&x==kkk&&z==1){//如果遍历到被拆掉的边,则跳过,除非它是重边
continue;
}
if(!flag[y]){
dfs(y,No,kkk);
dp[x][0]+=max(dp[y][0],dp[y][1]);
dp[x][1]+=dp[y][0];
}
}
}
int read(){
int a=1,b=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
a=ch=='-'?-1:1,ch=getchar();
}
while(ch>='0'&&ch<='9'){
b=(b<<1)+(b<<3)+ch-'0',ch=getchar();
}
return a*b;
}
signed main(){
n=read(),make(n);
for(re int i=1;i<=n;i++){
a[i]=read(),y[i]=read();
x[i]=i;
}
//玄学(?)方法特判重边:2代表重边
for(re int i=1;i<=n;i++){
if(x[i]==y[y[i]]){
add(x[i],y[i],2);
add(y[i],x[i],2);
}else{
add(x[i],y[i],1);
add(y[i],x[i],1);
}
}
for(re int i=1;i<=n;i++){
int xx=find(x[i]),yy=find(y[i]);
if(xx^yy){
fa[xx]=yy;
}else{//找到了非树边
xx=x[i],yy=y[i];
dfs(yy,xx,yy);//第一次DP——强制不选xx
int ans1=max(dp[yy][0],dp[yy][1]);
for(re int i=1;i<=n;i++){
flag[i]=0;
}
dfs(xx,yy,xx);//第二次DP——强制不选yy
ans+=max(max(dp[xx][0],dp[xx][1]),ans1);
for(re int i=1;i<=n;i++){
flag[i]=0;
}
}
}
printf("%lld",ans);
return 0;
}
3.2. Island
一句话题意:求基环树森林的直径和
解决了单颗基环树的直径问题,这道题也就迎刃而解了
显然,分为两种情况讨论
- 直径经过了环
- 直径没经过环
对于情况 1 1 1,我们只需要以环上的每一个节点作为根节点跑一次DP,然后比最大值(注意:此时的子树不应包含除根节点以外的环上节点)
对于情况
2
2
2,考虑:对于两个环上节点
i
,
j
i,j
i,j 而言,设dp[i],dp[j]
分别表示以
i
,
j
i,j
i,j 为根节点的子树的直径,dis[i][j]
表示从
i
i
i 到
j
j
j 的最长距离,且若直径经过了这两节点,则直径为
d
p
[
i
]
+
d
i
s
[
i
]
[
j
]
+
d
p
[
j
]
dp[\ i\ ]+dis[\ i\ ][\ j\ ]+dp[\ j\ ]
dp[ i ]+dis[ i ][ j ]+dp[ j ]
.若我们设pre[i]
为环上以某一结点为起点,到第
i
i
i 节点的前缀和,sum
表示环上的权值总和,那么,上述式子可以改写为:
max
(
p
r
e
[
j
]
−
p
r
e
[
i
]
,
s
u
m
−
p
r
e
[
j
]
+
p
r
e
[
i
]
)
+
d
p
[
i
]
+
d
p
[
j
]
\max(pre[\ j\ ]-pre[\ i\ ],sum-pre[\ j\ ]+pre[\ i\ ])+dp[\ i\ ]+dp[\ j\ ]
max(pre[ j ]−pre[ i ],sum−pre[ j ]+pre[ i ])+dp[ i ]+dp[ j ],更进一步的,则有:
max
(
d
p
[
i
]
−
p
r
e
[
i
]
+
d
p
[
j
]
+
p
r
e
[
j
]
,
s
u
m
+
p
r
e
[
i
]
+
d
p
[
i
]
+
d
p
[
j
]
−
p
r
e
[
j
]
)
\max(dp[\ i\ ]-pre[\ i\ ]+dp[\ j\ ]+pre[\ j\ ],sum+pre[\ i\ ]+dp[\ i\ ]+dp[\ j\ ]-pre[\ j\ ])
max(dp[ i ]−pre[ i ]+dp[ j ]+pre[ j ],sum+pre[ i ]+dp[ i ]+dp[ j ]−pre[ j ])
则通过环上的直径的值为:
a n s = max { max ( d p [ i ] − p r e [ i ] + d p [ j ] + p r e [ j ] , s u m + p r e [ i ] + d p [ i ] + d p [ j ] − p r e [ j ] ) } ans=\max\{\max(dp[\ i\ ]-pre[\ i\ ]+dp[\ j\ ]+pre[\ j\ ],sum+pre[\ i\ ]+dp[\ i\ ]+dp[\ j\ ]-pre[\ j\ ])\} ans=max{max(dp[ i ]−pre[ i ]+dp[ j ]+pre[ j ],sum+pre[ i ]+dp[ i ]+dp[ j ]−pre[ j ])}
此时,若直接枚举
i
,
j
i,j
i,j,则时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),但是,我们可以用单调队列维护dp[j]+pre[j]
和dp[j]-pre[j]
的最大值,这样,可以将时间复杂度降为
O
(
n
)
O(n)
O(n)
同样注意:这道题可能有重边
#include<cstdio>
#include<algorithm>
#define int long long
using namespace std;
const int N=4000005;
int n,x[N],y[N],z[N];
int ver[N],edge[N],Next[N],head[N],len;
void add(int x,int y,int z){
ver[++len]=y,edge[len]=z,Next[len]=head[x],head[x]=len;
}
int fa[N];
void make(int x){
for(int i=1;i<=x;i++){
fa[i]=i;
}
}
int find(int x){
if(x==fa[x]){
return x;
}
return fa[x]=find(fa[x]);
}
int xx,yy;
int Ring[N],Ring_len,Ring_z,Ring_OK;
int Ring_z_pre[N];
bool Is_Ring[N];
void Find_Ring(int x,int fa){//找环
Ring[++Ring_len]=x;
if(x==yy){
Ring_OK=1;
return ;
}
for(int i=head[x];i;i=Next[i]){
int y=ver[i],z=edge[i];
if(y^fa){
Ring_z+=z;
Ring_z_pre[Ring_len+1]=Ring_z_pre[Ring_len]+z;
Find_Ring(y,x);
if(Ring_OK){
return ;
}
Ring_z-=z;
}
}
Ring_len--;
}
int dp[N][3];
int Stop_x,Stop_y;
void DP(int x,int fa){//求单棵树的直径
int zzz=0;
for(int i=head[x];i;i=Next[i]){
int y=ver[i],z=edge[i];
if(y==fa||Is_Ring[y]){//不能回溯,不能去环上跑DP
continue;
}
if(x==Stop_x&&y==Stop_y){//重边特判:找到权值最大的一条边,所以不急着DP
zzz=max(zzz,z);
continue;
}
DP(y,x);
dp[x][2]=max(dp[x][2],dp[y][2]);
int tot=dp[y][0]+z;
if(dp[x][0]<tot){
dp[x][1]=dp[x][0];
dp[x][0]=tot;
}else if(dp[x][1]<tot){
dp[x][1]=tot;
}
}
if(x==Stop_x){//特判
for(int i=head[x];i;i=Next[i]){
int y=ver[i];
if(y==Stop_y){
int z=zzz;
DP(y,x);
dp[x][2]=max(dp[x][2],dp[y][2]);
int tot=dp[y][0]+z;
if(dp[x][0]<tot){
dp[x][1]=dp[x][0];
dp[x][0]=tot;
}else if(dp[x][1]<tot){
dp[x][1]=tot;
}
break;
}
}
}
dp[x][2]=max(dp[x][2],dp[x][0]+dp[x][1]);
}
int ans,sum;
int q[N],Head=1,Tail;
int q1[N],Head1=1,Tail1;
signed main(){
scanf("%lld",&n);
make(n);
for(int i=1;i<=n;i++){
scanf("%lld%lld",&y[i],&z[i]);
x[i]=i;
add(x[i],y[i],z[i]),add(y[i],x[i],z[i]);
}
for(int kkk=1;kkk<=n;kkk++){
int tot_x=find(x[kkk]),tot_y=find(y[kkk]);//并查集判环
if(tot_x==tot_y){
xx=x[kkk],yy=y[kkk];
Ring_z+=z[kkk];
}else{
fa[tot_x]=fa[tot_y];
}
if(!xx){
continue;
}
Find_Ring(xx,yy);//判环
if(!Ring_len){//特判:重边
Stop_x=xx,Stop_y=yy;
DP(xx,0);
ans+=dp[xx][2];
Ring_z=0;
xx=yy=0;
for(int i=1;i<=n;i++){
dp[i][0]=dp[i][1]=dp[i][2]=0;
}
continue;
}
for(int i=1;i<=Ring_len;i++){
Is_Ring[Ring[i]]=1;
}
for(int i=1;i<=Ring_len;i++){//不经过环的情况
DP(Ring[i],0);
sum=max(sum,dp[Ring[i]][2]);
}
for(int i=Ring_len-1;i>=1;i--){
//单调队列优化DP
while(Head<=Tail&&dp[Ring[q[Tail]]][0]+Ring_z_pre[q[Tail]]<=dp[Ring[i+1]][0]+Ring_z_pre[i+1]){
Tail--;
}
q[++Tail]=i+1;
while(Head1<=Tail1&&dp[Ring[q1[Tail1]]][0]-Ring_z_pre[q1[Tail1]]<=dp[Ring[i+1]][0]-Ring_z_pre[i+1]){
Tail1--;
}
q1[++Tail1]=i+1;
sum=max(sum,max(Ring_z+Ring_z_pre[i]+dp[Ring[i]][0]+dp[Ring[q1[Head1]]][0]-Ring_z_pre[q1[Head1]],
dp[Ring[i]][0]-Ring_z_pre[i]+dp[Ring[q[Head]]][0]+Ring_z_pre[q[Head]]));
}
ans+=sum;
sum=0;
for(int i=1;i<=Ring_len;i++){
dp[Ring[i]][0]=dp[Ring[i]][1]=dp[Ring[i]][2]=0;
Is_Ring[Ring[i]]=0;
Ring_z_pre[i]=0;
Ring[i]=0;
}
Head=Head1=1,Tail=Tail1=0;
Ring_len=Ring_z=Ring_OK=0;
xx=0,yy=0;
//注意这道题的初始化有点多
}
printf("%lld",ans);
return 0;
}
.