算法学习——图论(三)

目录

一、并查集基础概念

1、并查集的作用

2、并查集模板

二、寻找图中是否存在路径(并查集)

三、冗余连接(并查集)

四、冗余连接II(并查集)

五、最小生成树

1、prim算法(维护点集)

2、kruskal算法(维护边集)

六、课程表II(拓扑排序)


一、并查集基础概念

1、并查集的作用

  • 并:将一个元素添加到一个集合中
  • 查:查找元素所在的集合

2、并查集模板

int n; // 节点数量
vector<int> father(n, 0);
void init() // 初始化
{
	// 最开始时,每个节点的根都是自己
	for (int i = 0; i < n; i++)
	{
		father[i] = i;
	}
}
int find(int u) // 寻找一个节点的根
{
	if (u == father[u])
	{
		return u; // 自己就是根,直接返回
	}
	else
	{
		return father[u] = find(father[u]); // 自己不是根,层层递归并把寻找路径压缩
	}
}
bool isSame(int u, int v) // 判断两个节点是否在同一根下
{
	u = find(u);
	v = find(v);
	return u == v;
}
void join(int u, int v) // 合并操作
{
	u = find(u);
	v = find(v);
	if (u == v)
	{
		return; // 已经是同一根下,无需合并
	}
	father[v] = u; // 把 v 的根置为 u
}

二、寻找图中是否存在路径(并查集)

        原题力扣1971        . - 力扣(LeetCode)

        等价于求起点和终点是否在同一个图中,即同一个根下

int find(vector<int>& father, int u)
{
	if (u == father[u])
	{
		return u;
	}
	else
	{
		return father[u] = find(father, father[u]);
	}
}
bool isSame(vector<int>& father, int u, int v)
{
	u = find(father, u);
	v = find(father, v);
	return u == v;
}
void join(vector<int>& father, int u, int v)
{
	u = find(father, u);
	v = find(father, v);
	if (u == v)
	{
		return;
	}
	father[v] = u;
}
bool validPath(int n, vector<vector<int>>& edges, int source, int destination)
{
	vector<int> father(n, 0);
	for (int i = 0; i < n; i++)
	{
		father[i] = i;
	}
	for (const auto& edge : edges)
	{
		join(father, edge[0], edge[1]);
	}
	return isSame(father, source, destination);
}

三、冗余连接(并查集)

        原题力扣684        . - 力扣(LeetCode)

        模板代码冗长且是一摸一样的,略过

vector<int> findRedundantConnection(vector<vector<int>>& edges)
{
	int n = edges.size();
	vector<int> father(n + 1, 0);
	for (int i = 0; i <= n; i++)
	{
		father[i] = i;
	}
	for (const auto& edge : edges)
	{
		if (isSame(father, edge[0], edge[1]))
		{
			return edge; // 两个节点已经在同一根下,说明是冗余的,可以删除
		}
		else
		{
			join(father, edge[0], edge[1]);
		}
	}
	return {};
}

四、冗余连接II(并查集)

        原题力扣685        力扣. - 力扣(LeetCode)

        参考网站        代码随想录

// 找到构成环的边,即要删除的边
vector<int> removeEdge(vector<int>& father, vector<vector<int>>& edges)
{
	for (const auto& edge : edges)
	{
		if (isSame(father, edge[0], edge[1])) // 构成环了,此时就是要删除的边
		{
			return edge;
		}
		else
		{
			join(father, edge[0], edge[1]);
		}
	}
	return {};
}
// 判断删除边后是否为一颗树
bool isTree(vector<int>& father, vector<vector<int>>& edges, int toDelete) 
{
	for (int i = 0; i < edges.size(); i++)
	{
		if (i == toDelete) // 跳过要删除的边
		{
			continue;
		}
		if (isSame(father, edges[i][0], edges[i][1])) // 构成环了,一定不是树
		{
			return false;
		}
		join(father, edges[i][0], edges[i][1]);
	}
	return true;
}
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges)
{
	int n = edges.size();
	vector<int> father(n + 1, 0);
	for (int i = 0; i <= n; i++)
	{
		father[i] = i;
	}
	vector<int> inDegree(n + 1, 0); // 记录所有节点的入度
	for (const auto& edge : edges)
	{
		inDegree[edge[1]]++;
	}
	vector<int> inEqualTwo; // 记录使节点入度为 2 的边
	for (int i = n - 1; i >= 0; i--) // 注意倒序,因为要输出最后的边
	{
		if (inDegree[edges[i][1]] == 2)
		{
			inEqualTwo.push_back(i);
		}
	}
	// 存在使节点入度为 2 的边,要删除的边必定在此中
	if (inEqualTwo.size() > 0)
	{
		// 如果删除后仍然是树,则是答案
		if (isTree(father, edges, inEqualTwo[0]))
		{
			return edges[inEqualTwo[0]];
		}
		else
		{
			return edges[inEqualTwo[1]];
		}
	}
	// 不存在入度为 2 的边,说明情况同冗余连接I
	return removeEdge(father, edges);
}

五、最小生成树

        原题卡码53        53. 寻宝(第七期模拟笔试)

1、prim算法(维护点集)

        prim算法步骤:

  • 选择距离生成树最近的非树中节点
  • 把节点加入树中
  • 更新非树中节点到生成树的距离(即更新minDist数组)

        时间复杂度:O(n^2),其中 n 为节点个数,适用稠密图(边数量较多)

int minimumSpanningTreePrim()
{
	int v, e;
	cin >> v >> e;
	vector<vector<int>> graph(v + 1, vector<int>(v + 1, 100001));
	int v1, v2, val;
	for (int i = 0; i < e; i++)
	{
		cin >> v1 >> v2 >> val;
		graph[v1][v2] = val;
		graph[v2][v1] = val;
	}
	vector<int> minDist(v + 1, 100001); // 记录非树中节点到生成树的距离
	vector<bool> visited(v + 1, false); // 记录是否遍历过

	for (int i = 1; i < v; i++)
	{
		int cur = -1; // 重置当前选择的节点
		int minVal = INT_MAX; // 重置当前选择的节点的最小距离
		for (int j = 1; j <= v; j++)
		{
			// 找到了更近的节点,选择此节点,并更新参数
			if (!visited[j] && minDist[j] < minVal)
			{
				minVal = minDist[j];
				cur = j;
			}
		}
		// 将节点标记为已遍历
		visited[cur] = true;
		// 更新 minDist 数组
		for (int j = 1; j <= v; j++)
		{
			if (!visited[j] && graph[cur][j] < minDist[j])
			{
				minDist[j] = graph[cur][j];
			}
		}
	}
	// 求最小生成树的路径总和
	int ans = 0;
	for (int i = 2; i <= v; i++)
	{
		ans += minDist[i];
	}
	cout << ans << endl;
	return 0;
}

2、kruskal算法(维护边集)

        kruskal算法步骤:

  • 边的权值排序,因为要优先选最小的边加入到生成树里
  • 遍历排序后的边
    • 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环
    • 否则,加入到最小生成树,并把两个节点加入同一个集合

        时间复杂度:O(nlogn),其中 n 为节点个数,适用稀疏图(边数量较少)

// 表示一条边:左端点,右端点,权值
struct Edge
{
	int l, r, val;
};
int find(vector<int>& father, int u)
{
	if (u == father[u])
	{
		return u;
	}
	else
	{
		return father[u] = find(father, father[u]);
	}
}
void join(vector<int>& father, int u, int v)
{
	u = find(father, u);
	v = find(father, v);
	if (u == v)
	{
		return;
	}
	father[v] = u;
}
int minimumSpanningTreeKruscal()
{
	int v, e;
	cin >> v >> e;
	vector<Edge> edges;
	int v1, v2, val;
	for (int i = 0; i < e; i++)
	{
		cin >> v1 >> v2 >> val;
		edges.push_back({v1,v2,val});
	}
	// 按边权值排序
	sort(edges.begin(), edges.end(), [&](const auto& a, const auto& b) {
		return a.val < b.val;
	});
	int ans = 0;
	vector<int> father(v + 1, -1);
	for (int i = 0; i <= v; i++)
	{
		father[i] = i;
	}
	for (const auto& edge : edges)
	{
		int x = find(father, edge.l);
		int y = find(father, edge.r);
		// 两个节点不是同一根,则加入生成树
		if (x != y)
		{
			ans += edge.val;
			join(father, x, y);
		}
	}
	cout << ans << endl;
	return 0;
}

六、课程表II(拓扑排序)

        原题力扣207        . - 力扣(LeetCode)

        步骤:

  • 找到入度为0的节点,加入结果集
  • 从图中删除该点
  • 循环直至完成

        判断图中有环的方法:最终如果结果集的节点数不等于总节点数,说明有环

vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites)
{
	vector<int> inDegree(numCourses, 0); // 记录各节点入度
	unordered_map<int, vector<int>> mp;  // 记录课程依赖关系,<前置课程,<后置课程>>
	// [ai,bi] 的关系:bi->ai,bi是前置课程,ai是后置课程
	for (const auto& prerequisite : prerequisites)
	{
		inDegree[prerequisite[0]]++; // 后置课程入度增加
		mp[prerequisite[1]].push_back(prerequisite[0]); // 记录依赖关系
	}
	queue<int> que; // 记录入度为 0 的节点
	for (int i = 0; i < numCourses; i++)
	{
		if (inDegree[i] == 0)
		{
			que.push(i); // 入度为 0 的节点说明无前置,可入队
		}
	}
	vector<int> ans;
	while (!que.empty())
	{
		int cur = que.front();
		que.pop();
		ans.push_back(cur); // 当前节点放入结果集中
		vector<int> temp = mp[cur]; // 当前节点的后置节点
		if (temp.size() > 0)
		{
			for (int i = 0; i < temp.size(); i++)
			{
				inDegree[temp[i]]--; // 当前节点的后置节点入度减 1
				if (inDegree[temp[i]] == 0)
				{
					que.push(temp[i]); // 入度为 0 的节点说明无前置,可入队
				}
			}
		}
	}
	// 结果集的大小等于课程总数,说明无环,所有课程都能学
	if (ans.size() == numCourses)
	{
		return ans;
	}
	return {};
}

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值