基环树专题总结
基环树,顾名思义是一棵树中只含一个环,而有关基环树的题便大多与这个环有关。
例如原本树上(dp)因为在基环树上会有后效性,所以断环后再进行(dp)等等,下面便来总结下做基环树题的常见套路。
求环
所有关于基环树的题没有不需要求环的,下面是我求环的板子,因为这样的方法显然不会遍历到所有点,所以需要额外单独(dfs)一次来标记已经考虑过的节点。
ll dfs2(ll x,ll fa)
{
ll i,j;
if(book[x]){rt=x;return 1;}
book[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
ll to=a[i].to;
if(i==fa) continue; //按边来判断! 毕竟两个点组成环也是可以的
ll pd=dfs2(to,i^1);
if(pd)
{
if(pd==1)
{
sta[++top]=x;
color[x]=1;
if(x!=rt) return 1;
}
return 2;
}
}
return 0;
}
基环树上DP
T1 「ZJOI2008」骑士
对于环上每个点对应的子树做一遍“没有上司的舞会”,然后将环变成链,规定第(1)个节点必选还是必不选做两遍(DP)即可。
#include<bits/stdc++.h>
#define ll long long
#define N 2100000
#define M 4400000
using namespace std;
ll n,val[N],cnt=2,ans;
ll vis[N],book[N],head[N];
struct note{
ll to,nxt;
}a[M];
void add(ll x,ll y)
{
a[cnt].to=y;
a[cnt].nxt=head[x];
head[x]=cnt++;
}
void dfs1(ll x,ll fa)
{
ll i,j;
vis[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
ll to=a[i].to;
if(vis[to]||to==fa) continue;
dfs1(to,x);
}
}
ll sta[N],top,rt,color[N];
ll dfs2(ll x,ll fa)
{
ll i,j;
if(book[x]){rt=x;return 1;}
book[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
ll to=a[i].to;
if(i==fa) continue; //按边来判断! 毕竟两个点组成环也是可以的
ll pd=dfs2(to,i^1);
if(pd)
{
if(pd==1)
{
sta[++top]=x;
color[x]=1;
if(x!=rt) return 1;
}
return 2;
}
}
return 0;
}
ll dp1[N][2],dp2[N][2];
void calc(ll x,ll fa)
{
ll i,j;
dp1[x][0]=0;dp1[x][1]=val[x];
for(i=head[x];i;i=a[i].nxt)
{
ll to=a[i].to;
if(to==fa||color[to]) continue;
calc(to,x);
dp1[x][1]+=dp1[to][0];
dp1[x][0]+=max(dp1[to][1],dp1[to][0]);
}
}
ll solve(ll pd)
{
ll i,j;
dp2[1][1]=(pd?dp1[sta[1]][1]:0);
dp2[1][0]=dp1[sta[1]][0];
for(i=2;i<=top;i++)
{
dp2[i][0]=dp1[sta[i]][0]+max(dp2[i-1][1],dp2[i-1][0]);
dp2[i][1]=dp2[i-1][0]+dp1[sta[i]][1];
}
return pd?dp2[top][0]:max(dp2[top][1],dp2[top][0]);
}
int main()
{
ll i,j,x;
scanf("%lld",&n);
for(i=1;i<=n;i++)
{
scanf("%lld%lld",&val[i],&x);
add(i,x),add(x,i);
}
ans=0;
for(i=1;i<=n;i++)
{
if(vis[i]) continue;
dfs1(i,0);top=0;
dfs2(i,0);
for(j=1;j<=top;j++) calc(sta[j],0);
ans+=max(solve(0),solve(1));
}
printf("%lld
",ans);
return 0;
}
T2 创世纪
又是一道类似上司的舞会的题,同样也是断边后两遍(dp),也有两种写法,第一种先断,第二种后断。
#include<bits/stdc++.h>
#define ll long long
#define N 1500000
#define M 3000000
using namespace std;
int n,head[N],cnt=2,ans,book[N];
int arr[N],vis[N],c[N],rt,js,top;
struct note{
int to,nxt,w;
}a[M];
void add(int x,int y,int z)
{
a[cnt].to=y;
a[cnt].w=z;
a[cnt].nxt=head[x];
head[x]=cnt++;
}
void predfs(int x,int fa)
{
int i;
book[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
int to=a[i].to;
if(to==fa||book[to]) continue;
predfs(to,x);
}
}
int dfs(int x,int fa)
{
int i;
if(vis[x]==1)
{
vis[x]=2;arr[++top]=x;c[x]=1;
return 1;
}
vis[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
int to=a[i].to;
if(i==fa) continue;
int now=dfs(to,i^1);
if(now)
{
if(vis[x]!=2)
{
arr[++top]=x;c[x]=1;
return 1;
}
else
{
return 0;
}
}
}
return 0;
}
int dp[N][2],used[N],B[N];//0为没选,1为选 的 最多数目。
void Dp1(int x,int fa,int lim)
{
int i,maxn=-1e9-7,pd=0,id,pd2=0;
dp[x][0]=0;dp[x][1]=1;
for(i=head[x];i;i=a[i].nxt)
{
int to=a[i].to;
if(i==fa||i==lim||(i==(lim^1))) continue;
pd2=1;
Dp1(to,i^1,lim);
dp[x][0]+=max(dp[to][1],dp[to][0]);
if(dp[to][0]>=dp[to][1])
{
dp[x][1]+=dp[to][0];
pd=1;
}
else
{
dp[x][1]+=dp[to][1];
if(dp[to][0]-dp[to][1]>maxn)
{
maxn=dp[to][0]-dp[to][1];
id=to;
}
}
//dp[x][1]=max(dp[x][0])
}
if(pd==0&&pd2==1)
{
dp[x][1]-=dp[id][1];
dp[x][1]+=dp[id][0];
}
else if(pd2==0)
{
dp[x][1]=-1e9;
}
}
void Dp2(int x,int fa,int lim)
{
int i,maxn=-1e9,pd=0,id,pd2=0;
dp[x][0]=0;dp[x][1]=1;
for(i=head[x];i;i=a[i].nxt)
{
int to=a[i].to;
if(i==fa||i==lim||(i==(lim^1))) continue;
pd2=1;
Dp2(to,i^1,lim);
dp[x][0]=dp[x][0]+max(dp[to][1],dp[to][0]);
if(B[x])
{
dp[x][1]=dp[x][1]+max(dp[to][1],dp[to][0]);
pd=1;
continue;
}
if(dp[to][0]>=dp[to][1])
{
dp[x][1]+=dp[to][0];
pd=1;
}
else
{
dp[x][1]+=dp[to][1];
if(dp[to][0]-dp[to][1]>maxn)
{
maxn=dp[to][0]-dp[to][1];
id=to;
}
}
}
if(pd==0&&pd2==1&&!B[x])
{
dp[x][1]-=dp[id][1];
dp[x][1]+=dp[id][0];
}
else if(pd2==0&&!B[x])
{
dp[x][1]=-1e9;
}
else if(pd2==0&&B[x])
{
dp[x][1]=1;
}
}
int solve(int x)
{
int i,res=0;
predfs(x,0);top=0;
// printf("%d
",top);
dfs(x,0);
// for(i=1;i<=top;i++) printf("%d ",arr[i]);
// printf("
");
rt=arr[1];
for(i=head[rt];i;i=a[i].nxt)
{
if(a[i].w==1)
{
js=i;
break;
}
}
//
Dp1(rt,0,js);
// printf("#:%d %d
",dp[rt][1],dp[rt][0]);
res=max(dp[rt][0],dp[rt][1]);
B[a[js].to]=1;
Dp2(rt,0,js);
B[a[js].to]=0;
// printf("#:%d %d
",dp[rt][1],dp[rt][0]);
res=max(res,dp[rt][0]);
// printf("#:%d
",res);
return res;
}
int main()
{
int i,x;
scanf("%d",&n);
for(i=1;i<=n;i++)
{
scanf("%d",&x);
add(i,x,1);
add(x,i,0);
}
for(i=1;i<=n;i++)
if(!book[i])
ans+=solve(i);
printf("%d
",ans);
return 0;
}
#include <bits/stdc++.h>
#define N 1100000
using namespace std;
const int inf=1e9;
int n,fa[N];
int f[N],nxt[N],data[N],num,a[N],cnt,g[N][2],dp[N][2];
bool tag[N],pag[N];
inline void add(int x,int y){ nxt[++num]=f[x]; f[x]=num; data[num]=y; }
inline void get_line(int x,int y){
while(x!=y){
a[++cnt]=x; x=fa[x];
}
a[++cnt]=y;
}
inline void dfs(int x){
tag[x]=1;
int y,pp=0,res1=0,minn=inf;
for(int i=f[x];i;i=nxt[i]){
y=data[i]; if(pag[y]==1)continue;
dfs(y);
g[x][0]+=max(g[y][0],g[y][1]);
if(g[y][0]>g[y][1])pp=1;
res1+=g[y][1]; minn=min(minn,g[y][1]-g[y][0]);
}
if(pp==1)g[x][1]=g[x][0]+1;
else if(minn!=inf)g[x][1]=res1-minn+1;
// cout<<x<<" "<<g[x][0]<<" "<<g[x][1]<<endl;
}
int solve(int x){
int y=x,xx=x,res=0; cnt=0;
while(pag[x]==0){
pag[x]=1; y=x; x=fa[x];
}
while(xx!=x)pag[xx]=0,xx=fa[xx];
get_line(x,y);
for(int i=1;i<=cnt;i++){ dfs(a[i]); }
dp[1][1]=g[a[1]][1]; dp[1][0]=-inf;
for(int i=2;i<=cnt;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1])+g[a[i]][0];
dp[i][1]=max(dp[i-1][1]+g[a[i]][1],dp[i-1][0]+g[a[i]][0]+1);
}
res=max(dp[cnt][0]+g[a[1]][0]+1-g[a[1]][1],dp[cnt][1]);
dp[1][1]=-inf; dp[1][0]=g[a[1]][0];
for(int i=2;i<=cnt;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1])+g[a[i]][0];
dp[i][1]=max(dp[i-1][1]+g[a[i]][1],dp[i-1][0]+g[a[i]][0]+1);
}
res=max(res,max(dp[cnt][1],dp[cnt][0]));
return max(res,0);
}
int main(){
// freopen("test.in","r",stdin);
cin>>n;
for(int i=1;i<=n;i++){
scanf("%d",&fa[i]);
add(fa[i],i);
}
int anss=0;
for(int i=1;i<=n;i++){
if(tag[i]==0){
anss+=solve(i);
}
}
cout<<anss;
}
基环树直径
有这一类题需要 算基环树的直径,求法是先找到环上每个点子树内的直径取最大值,同时保存以环上节点为端点的链的最长值,然后便在环上求经过环的路径最大值,使用单调队列便可轻松解决,然后和之前的最大值取(max)后就是答案。
T1「IOI2008」Islands
求每棵基环树的直径之和。
#include<bits/stdc++.h>
#define ll long long
#define N 2100000
#define M 4200000
#define pb push_back
#define pf push_front
#define popb pop_back
#define popf pop_front
using namespace std;
ll n,head[N],cnt=2,ans;
struct note{
ll to,nxt,w;
}a[M];
void add(ll x,ll y,ll z)
{
a[cnt].to=y;
a[cnt].w=z;
a[cnt].nxt=head[x];
head[x]=cnt++;
}
ll st,tot,vis[N],arr[N],s[N],color[N],book[N];
ll dfs(ll x,ll fa)
{
ll i;
if(vis[x]==1)
{
vis[x]=2; arr[++tot]=x; color[x]=1;
return 1;
}
vis[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
ll to=a[i].to;
if(i==fa) continue;
ll now=dfs(to,i^1);
if(now)
{
if(vis[x]!=2)
{
arr[++tot]=x; color[x]=1; s[tot]=s[tot-1]+a[i].w;
return 1;
}
else
{
s[st]=s[tot]+a[i].w;
return 0;
}
}
}
return 0;
}
ll dp[N],len[N],b[N],res;
void Dp(ll x,ll fa)
{
ll i;
for(i=head[x];i;i=a[i].nxt)
{
ll to=a[i].to;
if(to==fa||color[to]) continue;
Dp(to,x);
res=max(res,dp[x]+dp[to]+a[i].w);
dp[x]=max(dp[x],dp[to]+a[i].w);
}
}
void dfs0(ll x,ll fa)
{
ll i;
book[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
ll to=a[i].to;
if(to==fa||book[to]) continue;
dfs0(to,x);
}
}
ll solve(ll x)
{
st=tot+1;res=0;
ll i;
dfs0(x,0);
dfs(x,0);
ll num=tot-st+1;
for(i=st;i<=tot;i++)
{
Dp(arr[i],0);
len[i-st+1]=dp[arr[i]];
}
b[1]=0;b[num+1]=s[st];
for(i=2;i<=num;i++) b[i]=s[st+i-1];s[st]=0;
for(i=num+2;i<=2*num;i++) b[i]=b[i-1]+s[st+i-num-1]-s[st+i-num-2];
for(i=num+1;i<=2*num;i++) len[i]=len[i-num];
deque<ll> q;
q.clear();
for(i=1;i<=num*2;i++)
{
while(q.size()&&q.front()<=i-num) q.popf();
if(q.size())
{
res=max(res,len[i]+len[q.front()]+b[i]-b[q.front()]);
}
while(q.size()&&len[q.back()]-b[q.back()]<=len[i]-b[i]) q.popb();
q.pb(i);
}
return res;
}
int main()
{
ll i,x,z;
scanf("%lld",&n);
for(i=1;i<=n;i++)
{
scanf("%lld%lld",&x,&z);
add(i,x,z);add(x,i,z);
}
for(i=1;i<=n;i++)
if(!book[i])
ans+=solve(i);
printf("%lld
",ans);
return 0;
}
发现性质题
T1 「NOIP2018」旅行 加强版
非加强版中的做法是断掉环上的边,然后每次都贪心的跑一边。
加强版中暴力断边肯定行不通了,需要直接发现在环上断那条边最优,可以发现当我们需要断掉环上一条边时,回溯后到达的节点肯定比当前下一个结点更优,所以保存每个环上节点的回溯到达节点,每当将要进入下一个节点时比较一下大小来判断是否断这条边即可。
#include<bits/stdc++.h>
#define ll long long
#define N 300000
#define M 600000
using namespace std;
int n,m,head[N],cnt=2,vis[N];
int st,c[N];
struct note{
int to,nxt;
}a[M];
void add(int x,int y)
{
a[cnt].to=y;
a[cnt].nxt=head[x];
head[x]=cnt++;
}
void dfs(int x)
{
int i;
priority_queue<int> q;
printf("%d ",x);vis[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
int to=a[i].to;
if(vis[to]) continue;
q.push(-to);
}
while(!q.empty())
{
dfs(-q.top());
q.pop();
}
}
int dfs1(int x,int fa)
{
int i;
if(vis[x]==1)
{
vis[x]=2;st=x;c[x]=1;
return 1;
}
vis[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
int to=a[i].to;
if(i==fa) continue;
int now=dfs1(to,i^1);
if(now)
{
if(vis[x]!=2)
{
c[x]=1;
return 1;
}
else
{
return 0;
}
}
}
return 0;
}
int book,nxt[N];
void solve(int x)
{
int i;
printf("%d ",x);
priority_queue<int> q;
vis[x]=1;
for(i=head[x];i;i=a[i].nxt)
{
int to=a[i].to;
if(vis[to]) continue;
q.push(-to);
}
if(!c[x]||(x!=st&&c[x]&&book==0))
{
while(!q.empty())
{
solve(-q.top());
q.pop();
}
}
else
{
if(x==st)
{
while(!q.empty())
{
int now=-q.top();
q.pop();
if(!c[now]) solve(now);
if(c[now]&&book==0)
{
nxt[now]=-q.top();
book=1;
solve(now);
continue;
}
if(c[now]&&book==1)
{
book=0;
if(!vis[now])
{
solve(now);
}
}
}
return;
}
if(book==1)
{
while(!q.empty())
{
int now=-q.top();q.pop();
if(!c[now]) solve(now);
else
{
if(!q.empty()) nxt[now]=-q.top();
else nxt[now]=nxt[x];
if(now<nxt[now]) solve(now);
else continue;
}
}
return;
}
}
}
int main()
{
int i,u,v;
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
if(m==n-1){dfs(1);return 0;}
dfs1(1,0);
memset(vis,0,sizeof(vis));book=0;
solve(1);
return 0;
}