基環樹
就是比平常的樹多一條邊,有 \(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\)。
然後就結束了。