基环树和笛卡尔树

基环树

基环树是由 \(n\) 个点及 \(n\) 条边组成的连通图,比树多一条边。当然,如果不保证连通,有n个节点、n条边的无向图也有可能是一个基环树森林。

有向基环树又分为内向基环树(每个点入度为1),外向基环树(每个点出度为1)。可以形象地理解为前者是从外指向环的,后者是从环向外指出的。

对于有关基环树的问题,一般有两种解决方式:
1. 把环抽出来,这样整个图就变成一个环上面挂了几个子树的样子了,然后对子树进行操作,将信息合并到环上的节点,最后就能把一个图上的问题降到环上处理。
2. 将环断开一条边,然后当作普通树处理。

找环

dfs找环:

void find_circle(int u)
{
	dfn[u]=++idx;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa[u]) continue;
		if(dfn[v])
		{
			if(dfn[v]<dfn[u]) continue;
			a[++tot]=v;
			for(;u!=v;v=fa[v]) a[++tot]=fa[v];
		}
		else fa[v]=u, find_circle(v);
	}
}

拓扑排序找环:

for(int i=1;i<=n;i++)
{
	cin>>v[i];
	e[v[i]].push_back(i);//vectorli记录的是每个节点的儿子 
	in[v[i]]++;
}
for(int i=1;i<=n;i++)
{
	if(!in[i]) q.push(i);
}
while(q.size())
{
	int u=q.front();
	q.pop();
	if(!--in[v[u]]) q.push(v[u]);
}
for(int i=1;i<=n;i++)
{
	if(in[i]) 
	{
		//此时的i即为环上的点 
	}
}

例题:[ZJOI2008] 骑士

考虑将环断开一条边(端点为 \(x,y\)),那么就变成一个普通树形dp了,设 \(dp[i][0/1]\) 表示i节点选或不选的子树最大值,转移很显然。考虑断开的这两条变不能都选,那么从x和y分别跑一次dp,答案即为 \(max(dp[x][0],dp[y][0])\)

值得注意的一点是这道题会形成基环树森林,所以要从每个点开始都找一遍环,然后处理并累计答案。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+5;
int n, a[maxn], f[maxn], dp[maxn][2], vis[maxn], siz[maxn], flag, ans, x, y, tot;
int head[maxn], edgenum=1;
struct edge{
    int next;
    int to;
}edge[maxn<<1];
inline void add(int from,int to)
{
    edge[++edgenum].next=head[from];
    edge[edgenum].to=to;
    head[from]=edgenum;
}
inline void dfs(int u,int fa)
{
	vis[u]=1;
	siz[++tot]=u;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa) continue;
		if(!vis[v]) dfs(v, u);
		else if(vis[v]&&!flag)
		{
			flag=1;
			x=u, y=v;//要断的那条边的两个端点 
		}
	}
}
inline void dfs1(int u,int fa)
{
	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==fa) continue;
		dfs1(v, u);
		dp[u][0]+=max(dp[v][0], dp[v][1]);
		dp[u][1]+=dp[v][0];
	}
}
inline void solve()
{
	if(!flag)//没有基环树 
	{
		int rt=siz[1];
		dfs1(rt, 0);
		ans+=max(dp[rt][0], dp[rt][1]);
		return ; 
	}
	for(int i=head[x];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==y)//断环 
		{
			edge[i].to=0;
			edge[i^1].to=0;
			break;
		} 
	}
	dfs1(x, -1);
	int maxx=dp[x][0];
	dfs1(y, -1);
	maxx=max(maxx, dp[y][0]);
	ans+=maxx;
} 
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i]>>f[i];
		add(f[i], i);
		add(i, f[i]);
	}
	for(int i=1;i<=n;i++)
	{
		if(!vis[i])
		{
			tot=0;
			flag=0;
			dfs(i, 0);
			solve();
		}
	}
	cout<<ans;
	return 0;
}

练1:[NOIP 2018 提高组] 旅行

m=n-1的60分很好拿。然后考虑环的部分,枚举断边,然后再跑dfs,输出最小字典序即可。

放一个dfs:

bool dfs(int u,int fa)
{
	if(!flag)
	{
		if(u>ans[cnt]) return 1;
		if(u<ans[cnt]) flag=1;
	}
	vis[u]=1;
	ans[cnt++]=u;
	for(int i=0;i<e[u].size();i++)
	{
		int v=e[u][i];
		if(v==fa||vis[v]||mp[u][v]==0) continue;
		if(dfs(v, u)) return 1;
	}
	return 0;
}

练2:[IOI 2008] Island

一开始想的环上断一条边做发现复杂度过高,于是果断把问题降到环上,发现只需要求出所有基环树的直径之和即可。对于一棵基环树,它的直径要么经过环,要么不经过。对于后者,设 \(f_u\) 表示从环上节点u到它的子树中任意节点的最大值, \(g_u\) 表示它的子树的直径。那么后者的答案即为 \(max(f[i]+f[j]+dis[i][j])\),考虑优化,处理出环上的前缀和,那么答案就是 \(max(f[i]-s[i]+f[j]+s[j])\),那么只需要记录下最大的 \(f[i]-s[i]\)\(f[j]+s[j]\)即可。但是在 \(s[j]-s[i]<0\) 时,答案为 \(max(f[i]-s[i]+f[j]+s[j]+sum)\)(sum是环上所有边的和),取最大值即可。(其实我觉得我对这句话的理解还不是很深刻)

然后我觉得这个题最精髓的就在于它的代码,真的很简短,简直是题解区里的一股清流。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+5;
int n, w[maxn], flag, v[maxn], f[maxn], g[maxn], in[maxn], ans;
int solve(int u)
{
	int p=u;
	int sum=w[p], t1=f[p], t2=f[p], ans1=g[p], ans2=-1e9;
	u=v[u];
	while(p!=u)
	{
		in[u]=0;
		ans1=max(ans1, max(g[u], f[u]+sum+t1));
		ans2=max(ans2, f[u]-sum+t2);
		t1=max(t1, f[u]-sum), t2=max(t2, f[u]+sum);
		sum+=w[u];
		u=v[u];
	}
	return max(ans1, ans2+sum);
}
queue<int> q;
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i];
		in[v[i]]++;
	}
	for(int i=1;i<=n;i++)
	{
		if(!in[i]) q.push(i);
	}
	while(q.size())
	{
		int u=q.front();
		q.pop();
		int t=f[u]+w[u];
		g[v[u]]=max(g[v[u]], max(f[v[u]]+t, g[u]));
		f[v[u]]=max(f[v[u]], t);
		if(!--in[v[u]]) q.push(v[u]);
	}
	for(int i=1;i<=n;i++)
	{
		if(in[i]) 
		{
			ans+=solve(i);
		}
	}
	cout<<ans;
	return 0;
} 
/*
11
3 8
7 2
4 2
1 4
1 9
3 4
2 3
5 8
8 3
5 8
10 3
*/

练3:[POI 2012] RAN-Rendezvous

考虑分类讨论。

  • a和b在不同的基环树里
    那么答案肯定为-1。
  • a和b在同一个基环树的同一子树里
    那么答案显然为他们的lca。考虑用倍增求解。
  • a和b在同一基环树的不同子树里
    答案有两种情况,分别为这两个子树的根,根据题意输出即可。

考虑如何判断这三种情况,只需要在拓扑找环时给同一个环上的节点染上相同颜色即可,同时处理处该环上节点的子树的各种信息。还有一个细节是处理环上两点距离dis,请看下面的代码。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+5; 
int n, k, v[maxn], in[maxn], len[maxn], dis[maxn], rt[maxn], f[maxn][20], dep[maxn], col[maxn], idx, vis[maxn];
vector<int> e[maxn];
queue<int> q;
void dfs(int u,int fa,int d,int t)//处理子树 
{
	for(int i=0;i<e[u].size();i++)
	{
		int v=e[u][i];
		if(v==fa||in[v]) continue;
		dep[v]=d+1;
		rt[v]=t;
		dfs(v, u, d+1, t);
	}
}
void dfs1(int u,int idx,int sum)
{
	if(dis[u]) return ;
	col[u]=idx;
	dis[u]=sum;
	len[idx]++;
	dfs1(v[u], idx, sum+1);
}
int lca(int x,int y)
{
	if(dep[x]<dep[y]) swap(x, y);
	for(int i=18;i>=0;i--)
	{
		if(dep[f[x][i]]>=dep[y]) x=f[x][i];
	}
	if(x==y) return x;
	for(int i=18;i>=0;i--)
	{
		if(f[x][i]!=f[y][i]) 
		{
			x=f[x][i], y=f[y][i];
		}
	}
	return f[x][0];
}
bool check(int a,int b,int c,int d)
{
	if(max(a, b)!=max(c, d)) return max(a, b)<max(c, d);
	if(min(a, b)!=min(c, d)) return min(a, b)<min(c, d);
	return a>=b;
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++)
	{
		czhixuyin>>v[i];
		e[v[i]].push_back(i); 
		in[v[i]]++;
		f[i][0]=v[i];
	}
	for(int i=1;i<=n;i++)
	{
		if(!in[i]) q.push(i);
	}
	while(q.size())
	{
		int u=q.front();
		q.pop();
		if(!--in[v[u]]) q.push(v[u]);
	}
	for(int i=1;i<=n;i++)
	{
		if(in[i]) 
		{
			rt[i]=i;
			dfs(i, 0, 0, i);
			if(!col[i])
				dfs1(i, ++idx, 1); 
		}
	}
	for(int i=1;i<=18;i++)
	{
		for(int u=1;u<=n;u++)
		{	
			f[u][i]=f[f[u][i-1]][i-1];
		}
	}
	while(k--)
	{
		int x, y;
		cin>>x>>y;
		if(col[rt[x]]!=col[rt[y]])
		{
			cout<<-1<<" "<<-1<<"\n";
		}
		else if(rt[x]==rt[y])
		{
			int lca_=lca(x, y);
			cout<<dep[x]-dep[lca_]<<" "<<dep[y]-dep[lca_]<<"\n";
		} 
		else
		{
			int a=rt[x], b=rt[y];
			int ans1=dep[x]+(dis[b]-dis[a]+len[col[a]])%len[col[a]], ans2=dep[y]+(dis[a]-dis[b]+len[col[a]])%len[col[a]];
			if(check(dep[x], ans2, ans1, dep[y])) cout<<dep[x]<<" "<<ans2<<"\n";
			else cout<<ans1<<" "<<dep[y]<<"\n";
		}
	}
	return 0;
}

笛卡尔树

笛卡尔树是一种二叉树,每一个节点由一个键值二元组 (k,w) 构成。要求 k 满足二叉搜索树的性质,而 w 满足堆的性质。如果笛卡尔树的 k,w 键值确定,且 k 互不相同,w 也互不相同,那么这棵笛卡尔树的结构是唯一的。如下图:

用栈构建笛卡尔树

我们考虑将元素按下标顺序依次插入到当前的笛卡尔树中。那么每次我们插入的元素必然在这棵树的右链(右链:即从根节点一直往右子树走,经过的节点形成的链)的末端。于是我们执行这样一个过程,从下往上比较右链节点与当前节点 u 的 w,如果找到了一个右链上的节点 x 满足 \(w_x<w_u\),就把 u 接到 x 的右儿子上,而 x 原本的右子树就变成 u 的左子树。

图中红框部分就是我们始终维护的右链:

代码:

for(int i=1;i<=n;i++)
{
	int k=top;
	while(k>0&&w[stk[k]]>w[i]) k--;
	if(k) rs[stk[k]]=i;
	if(k<top) ls[i]=stk[k+1];
	stk[++k]=i;
	top=k;
}

练1:[TJOI2011] 树的序

发现这个题刚好是插入顺序满足小根堆,元素的值按照搜索树性质,相当于两者反过来了。所以我们就反过来建树,即把原数列从小到大排序,记录下原来的位置id,建树时判断id的大小。因为是数列,元素的值都不超过n,最后直接前序输出下标即可。

练2:[hdu6305]RMQ Similar Sequence

发现两个序列 RMQ 相似当且仅当他们的笛卡尔树同构。考虑根据给出的A序列把该笛卡尔树建出来,然后算贡献。因为 \(b_i\) 在 0 到 1 之间,故 \(b_i\) 的期望值为 1/2 ,所以 b 序列的和的期望值为 n/2。
对于笛卡尔树的每一棵子树,若用 \(sz[i]\) 表示以 i 为根节点的子树的大小,则满足其根节点是子树的最大值的概率为 \(1/sz[i]\) 。那么总共的概率就是 $\prod_{i=1}^nsz_i $。答案即为两者相乘。

练3:[洛谷 P6453]PERIODNI

神仙题。考虑将格子分割成树。从最底部开始为根,自下而上分成二叉树,就长这样子,分治建树即可:

如果不能分成恰好两棵子树,其实没有影响,把某两个放到一起先当成一个节点下一次分开来就好了,没>有必要特别判这个问题。

建树代码
inline int build(int l,int r)
{
	if(l>r) return 0;
	int minn=1e9, p=0;
	for(int i=l;i<=r;i++)
	{
		if(a[i]<minn)
		{
			minn=a[i];
			p=i;
		}
	}
	int lid=build(l, p-1), rid=build(p+1, r);
	h[lid]=a[lid]-a[p], h[rid]=a[rid]-a[p];
	w[p]=r-l+1;
	ls[p]=lid, rs[p]=rid;
	return p;
}

\(dp[u][i]\) 表示以u为根的子树里放i个数字的方案数,显然最后的答案为 \(dp[rt][k]\),但因为他的根是个矩形不好转移,考虑再设一个dp状态 \(dp1[u][i]\) 表示以u为根的子树(除了u这个矩形)的方案数,显然 \(dp1[u][i]=\sum_{i=1}^{m}\sum_{j=0}^{i} dp[ls[u]][j]*dp[rs[u]][i-j]\)。所以dp的转移方程即为:\(dp[u][i]=\sum_{i=1}^m\sum_{j=0}^iC_{h_u}^{i-j}*C_{w_u-j}^{i-j}*(i-j)!*dp1[u][j]\)

大小为 n×m 的棋盘,放入 k 个棋子,互不攻击的方案数为 \(C_n^k*C_m^k*k!\)

可以说是dp的转移是分为根和子树两部分的,dp1的转移是分为左右两个子树两部分的。

练4:[hdu4125]Moles

板子题,注意到题目里说的是按照给出的序列的下标为堆键值,序列值为二叉搜索树的键值排,那不就跟练1是一样的嘛,然后再跑一个kmp就好了。

警钟撅烂:char数组清空要从0开始清!!!不能只清长度

posted @ 2025-04-26 17:43  zhouyiran2011  阅读(34)  评论(0)    收藏  举报