最小环的几种解法(并查集、删边)

本文介绍了三种算法解决寻找有向图中最小环路长度的问题,包括并查集、带权路径压缩并查集及基于入度等于出度的环检测方法,通过实例详细解释了每种算法的实现过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目: https://www.luogu.org/problemnew/show/P2661
题解:
原理:
1.如果有两个点祖先节点相同,那么就可以构成一个环,长度为两个点到祖先节点长度之和+1。
2.新加入的一条边的两个端点在并查集中同祖先,则一定成环 。

注意:图中不一定有一个环,即不一定是一个连通图,可能有好几个连通分量。

解法一:并查集+找爹函数传地址变量记录深度

#include<bits/stdc++.h>
using namespace std;
int n,f,i,k,cnt=0,ans=0x3f3f3f3f;//重要初始化 
int fa[200005];
inline int find(int x,int &cnt)//找爹函数改进版 
{//cnt要用地址来操作,因为是在递归中计数用的
    cnt++;//层数更新 
	if(fa[x]==x) return x;
	else return find(fa[x],cnt);
} 
int main()
{
	cin>>n;
	for(i=1;i<=n;i++) fa[i]=i;//并查集的初始化
	for(i=1;i<=n;i++)
	{
		cin>>f;//在本题中,这条路径是i->f 
		cnt=0;//重要的初始化 
		if(find(f,cnt)==i)//存在环路,可能=不止有一个环路 
		{
			ans=min(cnt,ans);//维护最小的环的长度 
			//在使用min或max时,要注意维护变量的初始化 
		}
		else
		  fa[i]=f;
	 } 
	 cout<<ans<<endl;
	 return 0;
}

在这里插入图片描述
解法二:带权路径并查集

#include<bits/stdc++.h>
using namespace std;
int f[200002],d[200002],n,minn,last;
int find(int x){//带权路径并查集模板 
	if(f[x]==x) return x;
	else
	{
		int last=f[x];//记录父节点 
		f[x]=find(f[x]);//更新祖先(开国太祖)节点 
		d[x]+=d[last];//更新路径长 
		return f[x];//不理解可以手推一遍并查集 
	}
}
void check(int a,int b){
	int x=find(a),y=find(b);
	if(x==y) minn=min(minn,d[a]+d[b]+1);
//新加入的一条边的两个端点在并查集中同祖先,则一定成环 
//d[]保存该点到其祖先(开国太祖)的路径长度 
	else{
		f[x]=y;//合并 
		d[a]=d[b]+1;//更新路径长度 
	}
	
}
int main(){
	int i,t;
	cin>>n;
	for(i=1;i<=n;i++) f[i]=i;//并查集初始化
	minn=0x7777777;//重要初始化
	for(i=1;i<=n;i++){
		cin>>t;
		check(i,t);
	 } 
    cout<<minn<<endl;
	return 0;	
}

在这里插入图片描述
在这里插入图片描述
解法三:
原理:环中元素的入度等于出度。

#include<bits/stdc++.h>
using namespace std;
#define N 200005
#define INF 200005
int to[N],indegree[N];//记录入度 
bool visit[N];
int n;
void delzero(){
	bool flag=1;
	for(int i=1;i<=n;i++){//仅仅是一轮删除 
		if(indegree[i]==0&&!visit[i]){
//以i为起点的边未被访问过,且i点入度为0,即这是一条废边 
			flag=0;//标志着还有边可以删 
			visit[i]=1;//标记删除的点,在search()中不会被访问 
			indegree[to[i]]--;//删除一条边 
		}
	}
	if(flag) return;//没有废边可以删除了 
	else delzero();//进行下一轮删除 
}
int ans=INF;//重要初始化
void search(int start,int now,int step){
	if(start==now){ans=min(ans,step);return;}//设置返回条件
	visit[now]=1;//标记 
	search(start,to[now],step+1); //利用递归遍历环 
} 
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>to[i];
		indegree[to[i]]++;//计算入度 
	}
	delzero();
	for(int i=1;i<=n;i++){
		if(!visit[i]){//剪枝 
			visit[i]=1;//标记,认为一个连通块中至多有一个环 
			search(i,to[i],1);//遍历所有环 
		}
	}
	cout<<ans<<endl;
	return 0;
}

在这里插入图片描述
在这里插入图片描述

<think>嗯,用户想了解蓝桥杯2023年颜色平衡树问题的树上莫队解法。首先,我需要回忆一下这个问题的大致内容。根据提供的引用,题目是给定一棵有n个节点的树,每个节点有颜色,问有多少子树是颜色平衡的,即每种颜色的节点数量相同。 那用户提到的树上莫队算法,通常用于处理树上的查询问题,尤其是子树或路径相关的统计。莫队算法本身是基于分块的离线算法,通过排序查询来优化处理顺序,减少计算量。但树上莫队的实现可能比序列上的更复杂,因为需要考虑树的结构,比如DFS序或者欧拉序来将树结构转化为线性结构。 首先,我需要确定颜色平衡树的条件。每个子树中所有颜色的计数必须相同,比如如果有两种颜色,每种颜色必须有相同的节点数。这意味着对于每个子树,颜色的出现次数的最大公约数必须等于出现次数的最小值,或者所有颜色的出现次数相同。 接下来,树上莫队的应用可能需要将子树查询转化为区间查询。通常,子树可以通过DFS序中的进入和离开时间来表示为一个区间。例如,每个节点u的子树对应DFS序中从in[u]到out[u]的连续区间。这样,子树查询就变成了区间查询,可以用莫队来处理。 但问题在于如何统计每个区间内的颜色计数,并判断是否满足平衡条件。莫队算法在处理每个区间时,需要动态维护颜色的出现次数,并在每次颜色计数变化时检查当前区间是否满足平衡条件。这可能会比较耗时,因为每次调整区间时都要检查所有颜色是否平衡,这在最坏情况下可能导致较高的复杂度。 可能的优化点包括: 1. 预处理所有节点的DFS序,将子树转换为区间。 2. 使用哈希表或数组来记录每个颜色的出现次数。 3. 维护当前区间中不同颜色出现次数的频率,例如使用一个频率数组cnt,其中cnt[k]表示出现k次的颜色数目。当某个颜色的出现次数从c变为c+1时,cnt[c]减少,cnt[c+1]增加。如果所有颜色的出现次数相同,那么应该存在一个k,使得cnt[k]等于颜色总数,并且其他cnt值为0。 4. 需要快速判断当前区间是否满足条件,可能通过维护一个标志变量,记录是否有这样的k存在。 然而,树上莫队的实现可能需要处理块的划分、查询的排序等步骤。此外,莫队算法的时间复杂度通常是O(n√n),这在n较大的情况下可能还是可行的,但需要具体看题目中的数据范围。例如,如果n是1e5级别,可能需要进一步优化,但如果是1e4左右,可能可以接受。 另外,需要处理的问题还有: - 如何高效地判断当前区间是否颜色平衡。直接遍历所有存在的颜色可能会很慢,特别是颜色种类很多的时候。 - 可能需要在每次颜色计数变化时,动态维护最大和最小出现次数,并检查是否所有颜色的出现次数等于某个值。例如,维护max_count和min_count,当max_count == min_count时,可能满足条件,但需要所有颜色的出现次数都相同,这可能需要更复杂的条件判断。 可能的解决方法是,维护当前区间中所有颜色的出现次数的集合,并且当且仅当这个集合中的所有元素相等时才计数。但如何高效地维护这个状态是一个挑战。 另一个思路是,对于每个可能的出现次数k,记录有多少种颜色的出现次数为k。然后,当某个k的计数等于总颜色种类数时,说明所有颜色的出现次数都是k,此时满足条件。因此,可以维护一个数组freq,其中freq[c]表示出现次数为c的颜色数目。当调整颜色计数时,更新freq数组,并检查是否存在某个k使得freq[k]等于当前颜色种类数。 例如,当颜色x的出现次数从c增加到c+1: - 减少freq[c]的值,如果freq[c]变为0,可能需要从可能的k候选集中移除。 - 增加freq[c+1]的值,如果之前不存在,则加入候选集。 然后,检查是否存在k使得freq[k]等于当前颜色总数。这需要维护当前颜色种类数,这可能等于sum(freq[...]),或者需要另外维护一个变量。 这似乎可行,但实现起来需要仔细处理。 另外,需要注意的是,每个子树对应的区间是连续的,因此使用基于DFS序的莫队算法是合适的。同时,莫队算法需要按照块的顺序处理查询,以减少移动指针的次数。 综合以上分析,可能的解题步骤如下: 1. 预处理树的DFS序,得到每个节点的进入时间in[u]和离开时间out[u],将子树转换为区间[in[u], out[u]]。 2. 将所有的子树查询(即每个节点对应的区间)作为离线查询处理,并按照莫队的顺序排序。 3. 使用莫队算法处理这些查询,动态维护颜色出现次数,并统计符合条件的子树数量。 4. 在每次区间调整时,更新颜色计数,并检查当前区间是否满足颜色平衡的条件。如果满足,则计数器加一。 在实现时,需要注意以下几点: - 如何高效维护颜色出现次数和频率统计。 - 如何快速判断当前区间是否满足条件。 - 如何处理多个颜色和较大的数据范围。 可能的优化包括使用哈希表来存储颜色计数,以及维护最大和最小出现次数来快速排除不可能的情况。例如,如果当前区间的颜色出现次数的最大公约数不等于最小值,或者最大值不等于最小值,则可以立即判断不满足条件。 此外,考虑到颜色平衡的条件是所有颜色的出现次数相同,这意味着如果颜色种类为m,则每个颜色的出现次数必须等于总节点数除以m。因此,可以预先检查总节点数是否能被颜色数目整除,这可能是一个快速判断条件。 但这种方法的问题在于,颜色数目m可能随子树的不同而变化,因此需要在处理每个子树时动态计算颜色数目和总节点数。例如,总节点数是区间长度(out[u] - in[u] + 1),颜色数目是当前颜色计数的大小。只有当总节点数能够被颜色数目整除,并且每个颜色的出现次数等于总节点数除以颜色数目时,才满足条件。 因此,在每次调整区间时,需要计算当前颜色数目m和总节点数k,然后检查是否k % m == 0,并且每个颜色的出现次数等于k/m。这可能在每次调整时带来较大的计算量,特别是当颜色数目较多时。 为了高效处理,可以维护两个变量:当前区间的颜色数目m和总节点数k。当k % m != 0时,直接跳过。否则,检查所有颜色的出现次数是否等于k/m。这可能需要遍历所有颜色,但可以通过维护一个标志变量来记录是否所有颜色的出现次数都等于目标值。 例如,维护一个布尔变量valid,当每次颜色计数变化时,检查该颜色的出现次数是否符合目标值(k/m),并且所有颜色都满足这一条件。这可能需要额外的数据结构来跟踪不符合条件的颜色数量,例如维护一个计数器,记录有多少颜色的出现次数不等于目标值。当该计数器为0时,valid为真。 然而,这样的维护可能比较复杂,特别是在颜色数目和出现次数频繁变化的情况下。因此,这可能成为性能瓶颈。 或许,树上莫队并不是最优解,或者需要结合其他优化技巧。例如,可能结合启发式分块,或者利用某些数学性质来减少计算量。 另外,需要参考已有的题解或类似问题的解决方案,看看是否有树上莫队的应用实例。根据用户提供的引用,问题出现在蓝桥杯2023年省赛,可能官方题解或讨论中提到了相关解法,但用户希望了解树上莫队的具体实现。 综上所述,可能的解题思路是: 1. 通过DFS预处理每个节点的in和out时间,将子树转化为区间。 2. 使用莫队算法处理所有子树对应的区间查询。 3. 维护颜色出现次数和频率统计,动态判断当前区间是否满足颜色平衡条件。 4. 统计满足条件的区间数量。 在实现时,需要注意颜色计数的更新、条件判断的效率,以及如何处理大量的查询。可能的代码结构包括: - 预处理DFS序。 - 对查询进行排序。 - 莫队的移动指针函数,处理颜色计数的增减。 - 在每次移动后,检查条件并更新结果。 可能的伪代码如下: ``` 预处理DFS序,得到每个节点u的in[u]和out[u] 将所有子树查询收集为(in[u], out[u])区间 按照莫队的块排序方法对查询排序 初始化颜色计数、当前结果计数器等变量 遍历所有查询,移动左右指针,调整颜色计数: 当增加一个节点颜色时: 旧计数 = count[color] count[color] += 1 freq[旧计数] -= 1 freq[旧计数 + 1] += 1 更新当前颜色数目m和总节点数k 检查是否k % m == 0,且所有颜色的出现次数等于k/m 如果是,结果加一 当减少一个节点颜色时类似处理 最终输出结果 ``` 但具体实现中,如何高效检查条件仍然是关键。可能需要维护额外的变量来跟踪是否符合条件,例如: - total_colors: 当前区间的颜色种类数 - total_nodes: 当前区间的节点数(即r - l + 1) - required_count: 如果存在,应为 total_nodes / total_colors - 维护一个计数器,记录有多少颜色的出现次数不等于required_count 当该计数器为0时,当前区间满足条件。 每次颜色计数变化时,如果required_count存在(即total_nodes能被total_colors整除),则检查该颜色的出现次数是否等于required_count,并更新计数器。否则,直接跳过。 这可能需要每次调整颜色计数时,动态计算total_colors和total_nodes,并判断是否需要更新计数器。 例如,当添加一个颜色c: - total_nodes = r - l + 1 - 如果之前的count[c]为0,则total_colors +=1 - count[c] +=1 - 计算是否total_nodes % total_colors ==0,如果是,则required_count = total_nodes / total_colors - 否则,required_count不存在,当前区间不可能满足条件 - 如果required_count存在: - 如果count[c]之前等于 required_count -1,则现在变为 required_count,因此需要将计数器减一 - 如果count[c]之前不等于 required_count -1,但现在是 required_count,可能需要调整计数器(这可能比较复杂) 这个逻辑似乎容易出错,可能需要更巧妙的处理方式。 另一个思路是,每次区间变化时,记录当前区间的颜色计数,并在每次变化后,检查是否满足条件。但这可能在最坏情况下导致O(n)的检查时间,对于每个查询,这会导致整体复杂度变为O(n^1.5),这在n较大时不可行。 因此,可能需要寻找一种更高效的检查方式,例如维护一个全局变量,记录是否当前区间满足条件,并在每次颜色变化时更新该变量。例如,当颜色c的计数变化时,如果该颜色的计数不符合required_count,则标记为不满足条件。 然而,这需要维护required_count,并且当required_count变化时,可能需要重新检查所有颜色,这在required_count变化时(例如,当total_colors或total_nodes变化时)会导致较高的复杂度。 因此,可能需要限制仅在特定情况下进行条件检查。例如,只有当total_nodes变化时(即区间长度变化时),才重新计算required_count,并检查是否满足条件。而颜色计数的变化可能不会立即触发条件检查,而是在区间调整完成后统一检查。 例如,在莫队处理完一个查询的区间调整后,计算当前total_nodes和total_colors,然后检查是否满足条件,如果满足则计数加一。这样,每个查询只需要在调整完区间后进行一次检查,而不是每次颜色变化时都检查。 这可能减少计算量,但需要正确实现。 假设这样,当处理完一个查询的区间[L, R],对应的子树节点数为k = R-L+1,颜色种类为m。需要检查k是否能被m整除,并且每个颜色的出现次数等于k/m。这可以通过遍历所有颜色的计数来实现,但遍历所有颜色可能耗时,特别是当颜色种类很多时。 因此,这可能只适用于颜色种类较少的情况,否则会导致高时间复杂度。 因此,树上莫队可能并不是最优解,或者需要结合其他优化,比如限制颜色种类数,或者使用位运算等技巧。 或者,可能这个问题有更优的解法,例如基于DFS遍历时维护颜色计数,并结合哈希表来记录每种颜色出现次数的频率,从而在每次回溯时判断当前子树是否满足条件。 例如,在DFS过程中,维护一个哈希表count记录颜色出现次数,以及另一个哈希表freq记录出现次数的频率。当进入一个节点时,增加该颜色的计数,并更新freq。当离开时,减少计数。对于每个子树,当处理完所有子节点后,检查当前freq是否满足所有颜色的出现次数相同。具体来说,如果存在某个频率值,其对应的颜色数目等于总颜色数目,则说明所有颜色的出现次数相同。 这种方法的时间复杂度是O(n),因为每个节点被访问一次,且每次操作是O(1)(假设哈希表操作是平均O(1))。这可能更高效。 然而,用户特别询问的是树上莫队的解法,因此可能需要回到莫队的思路。 总结,可能的实现步骤如下: 1. 预处理DFS序,得到每个节点的in和out时间,将子树转换为区间。 2. 将每个子树对应的区间作为查询,收集所有查询。 3. 按照莫队的块排序对查询进行排序。 4. 初始化颜色计数数组、频率数组等。 5. 遍历每个查询,调整莫队的左右指针,更新颜色计数和频率数组。 6. 对于每个查询,计算当前区间的总节点数和颜色种类数,判断是否满足颜色平衡条件。 7. 统计满足条件的查询数量。 在具体代码实现中,需要注意颜色离散化(如果颜色值较大),以及高效的数据结构来维护计数和频率。 例如,假设颜色用整数表示,可能先对颜色进行离散化处理,转换为连续的整数,以减少内存占用和提高访问速度。 此外,由于每个子树对应一个唯一的节点,因此查询的数量为n,即每个节点对应一个查询。莫队处理n个查询的时间复杂度为O(n√n),这在n=1e5时可能不可行,但对于比赛题目,如果n是1e4级别,可能可以接受。 但根据蓝桥杯的题目,可能n的范围较小,因此树上莫队解法是可行的。 现在,我需要将上述思考整理成结构化的回答,包括解题思路和实现步骤,并生成相关问题。</think>### 蓝桥杯颜色平衡树问题的树上莫队解法 #### 解题思路 颜色平衡树问题要统计树中所有满足**各颜色出现次数相同**的子树数量。采用树上莫队算法需将树结构转化为线性区间,并利用离线查询优化统计过程[^1][^2]。 1. **DFS序预处理** 通过DFS遍历记录每个节点的进入时间$in[u]$和离开时间$out[u]$,将子树映射为区间$[in[u], out[u]]$。例如,根节点1的区间为$[1, n]$。 2. **莫队算法框架** - **查询排序**:将所有子树对应的区间按分块排序,减少指针移动次数。 - **指针移动**:动态维护区间内颜色出现次数,通过增/操作更新计数。 3. **条件判断优化** - 维护哈希表`cnt`记录颜色出现次数,`freq`记录出现次数的频率。 - 当区间长度为$k$且颜色种类为$m$时,若$k \mod m = 0$且所有颜色出现次数为$k/m$,则判定为平衡树。 #### 代码实现(Python示例) ```python import sys from collections import defaultdict import math def main(): sys.setrecursionlimit(1 << 25) n = int(sys.stdin.readline()) color = [0] * (n + 1) parent = [0] * (n + 1) edges = [[] for _ in range(n + 1)] # 读取输入并建树 for i in range(1, n + 1): c, f = map(int, sys.stdin.readline().split()) color[i] = c parent[i] = f if f != 0: edges[f].append(i) # DFS预处理in/out时间 in_time = [0] * (n + 1) out_time = [0] * (n + 1) timestamp = 0 def dfs(u): nonlocal timestamp timestamp += 1 in_time[u] = timestamp for v in edges[u]: dfs(v) out_time[u] = timestamp dfs(1) # 离散化颜色 color_map = {c:i for i, c in enumerate(sorted(set(color[1:])))} color = [color_map[c] for c in color] # 生成查询(每个子树对应一个区间) queries = [] for u in range(1, n + 1): queries.append((in_time[u], out_time[u], u)) # 分块排序 block_size = int(math.sqrt(n)) + 1 queries.sort(key=lambda x: (x[0] // block_size, x[1])) # 莫队初始化 current_cnt = defaultdict(int) current_l = 1 current_r = 0 answer = 0 def add(pos): c = color[pos] current_cnt[c] += 1 def remove(pos): c = color[pos] current_cnt[c] -= 1 if current_cnt[c] == 0: del current_cnt[c] res = 0 for l, r, u in queries: while current_l > l: current_l -= 1 add(current_l) while current_r < r: current_r += 1 add(current_r) while current_l < l: remove(current_l) current_l += 1 while current_r > r: remove(current_r) current_r -= 1 # 判断是否颜色平衡 m = len(current_cnt) k = r - l + 1 if m == 0: continue if k % m != 0: continue target = k // m valid = True for c in current_cnt: if current_cnt[c] != target: valid = False break if valid: res += 1 print(res) if __name__ == "__main__": main() ``` #### 复杂度与优化 - **时间复杂度**: $O(n \sqrt{n})$,取决于分块大小和指针移动次数。 - **空间优化**: 使用离散化减少颜色存储开销,哈希表动态维护计数。 - **剪枝策略**: 当$k \mod m \neq 0$时直接跳过条件判断。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值