基环树和笛卡尔树
基环树
就是比平常的树多一条边,有 \(n\) 条边,也就有一个环在里面。
基本思想就是断环,跑树形 \(dp\),或者用拓扑排序判环去跑环形 \(dp\)。
树的直径
今天才了解到的,用两遍 \(dfs\) 跑。
首先第一遍找到离根节点最远的节点 \(u_1\),
然后再从 \(u_1\) 找到离它最远的节点 \(u_2\),那么树链 \((u_1,u_2)\) 就是树的直径。
在后面很有用。
#3437. [ZJOI2008]骑士
类似于树形 dp 的思路(没有上司的舞会)。
考虑状态转移方程:
\[dp[u][1]=\sum dp[v][0]+a[u]
\]
\[dp[u][0]=\sum \max(dp[v][0],dp[v][1])
\]
其中 \(v\) 表示 \(u\) 的子节点。
然后基环树的思路是先找环,找到环的根之后跑树形 dp。
void dfs(int u)
{
vis[u]=1;
dp[u][0]=0,dp[u][1]=a[u];
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v!=root)
{
dfs(v);
dp[u][0]+=max(dp[v][0],dp[v][1]);
dp[u][1]+=dp[v][0];
}
else dp[v][1]=-1145141919;
}
}
void find(int x)
{
vis[x]=1;
root=x;
while(!vis[fa[root]])
{
root=fa[root];
vis[root]=1;
}
dfs(root);
int tmp=max(dp[root][0],dp[root][1]);
vis[root]=1;
root=fa[root];
dfs(root);
ans+=max(tmp,max(dp[root][1],dp[root][0]));
}
#P1203. 「NOIP2018」旅行
P3533 [POI2012] RAN-Rendezvous
妈呀,打了一下午成小孩了。
调了一万次甚至动用科技,最后发现树剖炸了、
题目给出一个基环内向树森林,我们首先预处理出每个点所属环上的根节点 \(cir\) 是谁,再对分别对每棵树进行树剖。
然后通过 \(dfs\) 计算每个环的编号 \(id\) 和长度 \(len\)。
考虑两个点的状态进行分类讨论:
- 两点位于不同的基环树中
通过找环的时候给每个环进行编号,查询两点在环上根节点所属环是否一样进行判断;
- 两点位于同一基环树的同一子树中
意思是两点的 \(cir\) 相同,我们可以直接查询 \(lca(a,b)\) 作为答案;
- 两点位于同一基环树不同子树中
比较两个 \(cir\) 作为相遇点,哪个更符合要求即可。
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
register int s=0,w=1;
register char c=getchar();
while(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9')
{
s=(s<<1)+(s<<3)+(c^48);c=getchar();
}return s;
}
int n,m;
const int N=5e5+1;
vector<int>e[N],uu[N];
int indeg[N];
int q[N*2],tt;
int cir[N];
struct HPD{
int siz[N],dep[N],son[N],fa[N];
int id[N],tim=0,top[N];
void dfs1(int u,int fat)
{
siz[u]=1,fa[u]=fat;
if(!indeg[u]) dep[u]=dep[fat]+1;
cir[u]=cir[fat];
for(int v:uu[u])
{
if(v==fat||indeg[v]) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int t)
{
top[u]=t;
if(son[u]) dfs2(son[u],t);
for(int v:uu[u])
{
if(v==fa[u]||indeg[v]||v==son[u]) continue;
dfs2(v,v);
}
}
int lca(int u,int v)
{
while(top[u]!=top[v])
{
if(dep[top[u]]>=dep[top[v]]) u=fa[top[u]];
else v=fa[top[v]];
}
return dep[u]<dep[v]?u:v;
}
}hpd;
bool vis[N];
int len[N],pos[N];
int id[N],cur;
void dfs(int u,int le)
{
len[u]=le,vis[u]=1,pos[u]=le;
for(int v:e[u])
{
if(vis[v]||!indeg[v]) continue;
id[v]=id[u];
dfs(v,le+1);
len[u]=len[v];
}
}
inline bool cmp(int x1,int y,int x2,int y2)
{
if(max(x1,y)!=max(x2,y2)) return max(x1,y)<max(x2,y2);
if(min(x1,y)!=min(x2,y2)) return min(x1,y)<min(x2,y2);
return x1>=y;
}
int main()
{
n=read(),m=read();
register int v;
for(int i=1;i<=n;i++)
{
v=read();
e[i].push_back(v);
indeg[v]++;
uu[v].push_back(i);
}
for(int i=1;i<=n;i++)
{
if(!indeg[i]) q[++tt]=i;
}
register int now;
while(tt!=0)
{
now=q[tt];tt--;
v=e[now][0];
indeg[v]--;
if(!indeg[v]) q[++tt]=v;
}
for(int i=1;i<=n;i++)
{
if(indeg[i])
{
cir[i]=i;
hpd.dfs1(i,i);
hpd.dfs2(i,i);
if(!vis[i]) id[i]=++cur,dfs(i,1);
}
}
register int a,b;
while(m--)
{
a=read(),b=read();
if(id[cir[a]]!=id[cir[b]])
{
printf("-1 -1\n");continue;
}
// cout<<cir[a]<<" "<<cir[b]<<endl;
if(cir[a]==cir[b])
{
int lc=hpd.lca(a,b);
// cout<<lc<<endl;
printf("%d %d\n",hpd.dep[a]-hpd.dep[lc],hpd.dep[b]-hpd.dep[lc]);
}
else
{
int cira=pos[cir[a]],cirb=pos[cir[b]];
int mod=len[cir[a]];
// cout<<cira<<" "<<cirb<<endl;
int t1=hpd.dep[a]+(cirb-cira+mod)%mod;
int t2=hpd.dep[b]+(cira-cirb+mod)%mod;
if(cmp(hpd.dep[a],t2,t1,hpd.dep[b])) printf("%d %d\n",hpd.dep[a],t2);
else printf("%d %d\n",t1,hpd.dep[b]);
}
}
}
#3441. 创世纪
奇妙贪心?!
- 考虑叶子节点:
它本身不会被选,但是它的父亲要被选。
所以直接打上 \(vis\) 然后跳到父亲的父亲。
如果一个基环树的某个环上节点挂了一棵子树,那么这棵树必然会被本次操作全部分解。
- 考虑环:
上述操作结束之后必然只剩下环(或者不剩)
剩下环的条件是这个基环树本身就是个环(因为这是个基环树森林)
环的贡献是环长除以二 \(len>>1\)。
然后就结束了。