理论基础:
内向基环树就是每个联通块有且仅有一个环,并且出度为1的有向图,每一个内向基环树都是由联通环和指向联通环的树枝组成。而且基环可以只有两个节点构成。
Leetcode - 2127:参加会议的最多员工数
题目:
一个公司准备组织一场会议,邀请名单上有 n
位员工。公司准备了一张 圆形 的桌子,可以坐下 任意数目 的员工。
员工编号为 0
到 n - 1
。每位员工都有一位 喜欢 的员工,每位员工 当且仅当 他被安排在喜欢员工的旁边,他才会参加会议。每位员工喜欢的员工 不会 是他自己。
给你一个下标从 0 开始的整数数组 favorite
,其中 favorite[i]
表示第 i
位员工喜欢的员工。请你返回参加会议的 最多员工数目 。
示例 1:
输入:favorite = [2,2,1,2] 输出:3 解释: 上图展示了公司邀请员工 0,1 和 2 参加会议以及他们在圆桌上的座位。 没办法邀请所有员工参与会议,因为员工 2 没办法同时坐在 0,1 和 3 员工的旁边。 注意,公司也可以邀请员工 1,2 和 3 参加会议。 所以最多参加会议的员工数目为 3 。
笔记:
这是一道标准的基环图题目,用到了拓扑排序和分类讨论的方法:
首先明确题目的要求,让我们求出参加会议的最多人数,我们就可以将提供的favorite数组转变成一个有向图,这样我们就可以想到这是一个内向基环图,为什么呢?我们可以这么想:首先我们假想图中存在一个基环,那么基环内的所有节点必定是互相连接,不可能出现出边连接非基环内的节点,接着我们向图中加入非基环边,那么这些边一定是存在连接基环的边的。
知道这是一个内向基环图之后,我们就可以思考参加会议的人数,这个参加会议的人的可能性有三种,一种是在基环上的所有节点,这些人可以参加会议,一种是当基环节点数为2时,这是参加会议的人数为基环上的两个点加上这两点的树枝长度。
所以经过上面的分析我们要判断的情况有两种,一种是存在一个或者多个基环的图,我们需要选择节点数最多的那个基环,一种是树枝加两节点基环的总结点数 。所以就是在这两种情况系选择节点数最多的那个。
对于第一种情况,求图中基环的大小,我们可以采用拓扑排序,将树枝节点和基环分隔开,求出所有基环的大小进行比较得出最大基环大小。
第二种情况:我们需要进行判断如果当前基环的大小为2,那么我们就将该情况参加会议人数加上这两点的树枝大小,最后对这两种情况进行比较。
class Solution {
public:int maximumInvitations(vector<int> &favorite) {int n = favorite.size();vector<int> deg(n);for (int f: favorite) {deg[f]++; // 统计基环树每个节点的入度}vector<vector<int>> rg(n); // 反图queue<int> q;for (int i = 0; i < n; i++) {if (deg[i] == 0) {q.push(i);}}while (!q.empty()) { // 拓扑排序,剪掉图上所有树枝int x = q.front();q.pop();int y = favorite[x]; // x 只有一条出边rg[y].push_back(x);if (--deg[y] == 0) {q.push(y);}}// 通过反图 rg 寻找树枝上最深的链,求树枝的大小function<int(int)> rdfs = [&](int x) -> int {int max_depth = 1;for (int son: rg[x]) {max_depth = max(max_depth, rdfs(son) + 1);}return max_depth;};int max_ring_size = 0, sum_chain_size = 0;for (int i = 0; i < n; i++) {if (deg[i] == 0) continue; // 树枝节点直接跳过// 遍历基环上的点deg[i] = 0; // 将基环上的点的入度标记为 0,避免重复访问int ring_size = 1; // 基环长度// 求当前遍历节点i所在的基环的大小for (int x = favorite[i]; x != i; x = favorite[x]) {deg[x] = 0; // 将基环上的点的入度标记为 0,避免重复访问ring_size++;}if (ring_size == 2) { // 基环长度为 2sum_chain_size += rdfs(i) + rdfs(favorite[i]); // 累加两条最长链的长度} else {max_ring_size = max(max_ring_size, ring_size); // 取所有基环长度的最大值}}return max(max_ring_size, sum_chain_size);}
};作者:灵茶山艾府
链接:https://leetcode.cn/problems/maximum-employees-to-be-invited-to-a-meeting/solutions/1187830/nei-xiang-ji-huan-shu-tuo-bu-pai-xu-fen-c1i1b/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
下面是自己写的版本:
class Solution {
public:int branch_len(vector<vector<int>>& rev, int& x){int n = rev[x].size();int len = 1;for(int i = 0; i < n; i++){// branch_len是遍历当前i节点的树枝,而max的作用是比较所有的树枝选出最大的。len = max(len, branch_len(rev, rev[x][i]) + 1);}return len;}int maximumInvitations(vector<int>& favorite) {int n = favorite.size();// 统计各节点入度:vector<int> indeg(n, 0);for(int i = 0; i < n; i++){indeg[favorite[i]]++;}queue<int> que;for(int i = 0; i < n; i++){if(indeg[i] == 0){que.push(i);}}// 建立一个反图便于从树枝连接基环处向下遍历到底从而求出大小:vector<vector<int>> rev(n);while(!que.empty()){int cur = que.front(); que.pop();int y = favorite[cur];indeg[y]--;rev[y].push_back(cur);if(indeg[y] == 0){que.push(y);}}int res_1 = 0;// 基环大小大于二的情况int res_2 = 0;// 基环大小等于二的情况for(int i = 0; i < n; i++){// 遍历所有节点找到基环,找到后将该点标记为已访问过if(indeg[i] == 0) continue;indeg[i] = 0;int loop_len = 1;// 求基环大小:for(int j = favorite[i]; j != i; j = favorite[j]){loop_len++;indeg[j] = 0;}if(loop_len == 2){// 当基环大小等于二时,可邀请的人可以是这两个基环节点所连接的边// 那如果基环节点链接了多个树枝呢?// 只能选择最长的链:这两个节点喜欢的节点都是那个基环节点,但基环节点左右左右两个位置可以坐人,左边已经坐了基环节点喜欢的节点,那么右边只能坐一个。res_1 += branch_len(rev, i) + branch_len(rev, favorite[i]);}else{res_2 = max(res_2, loop_len);}}return max(res_1, res_2);}
};
Leetcode - 684:冗余连接
题目:
树可以看成是一个连通且 无环 的 无向 图。
给定往一棵 n
个节点 (节点值 1~n
) 的树中添加一条边后的图。添加的边的两个顶点包含在 1
到 n
中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n
的二维数组 edges
,edges[i] = [ai, bi]
表示图中在 ai
和 bi
之间存在一条边。
请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n
个节点的树。如果有多个答案,则返回数组 edges
中最后出现的那个。
示例 1:
输入: edges = [[1,2], [1,3], [2,3]] 输出: [2,3]
笔记:
这道题需要在并查集模板上进行一定的修改:在将当前遍历到的两点进行合并加入到并查集的过程中我们需要习判断一下当前两个点是否在同一个连通分量中,也就是两点是否为同一根节点,如果两者属于同一连通分量那么这两点的连线边就可以去掉,不影响所有点相连。如果两点不在同一分量那么就将两点加入到并查集中。
class Solution {
public:// 并查集模板:vector<int> father;void init(int n){father = vector<int>(n + 1, 0);for(int i = 1; i <= n; i++){father[i] = i;}}int find(int x){return x == father[x] ? x : father[x] = find(father[x]);}void join(int u, int v){u = find(u);v = find(v);if(u == v){return;}father[v] = u;};vector<int> findRedundantConnection(vector<vector<int>>& edges) {init(edges.size());vector<int> res;for(int i = 0; i < edges.size(); i++){int x = edges[i][0];int y = edges[i][1];// 检查当前两点是否为同一联通分量:if(find(x) != find(y)){join(x,y);}else{res = edges[i];}}return res;}
};
Leetcode - 2359:找到里给定两个节点最近的距离:
题目:
给你一个 n
个节点的 有向图 ,节点编号为 0
到 n - 1
,每个节点 至多 有一条出边。
有向图用大小为 n
下标从 0 开始的数组 edges
表示,表示节点 i
有一条有向边指向 edges[i]
。如果节点 i
没有出边,那么 edges[i] == -1
。
同时给你两个节点 node1
和 node2
。
请你返回一个从 node1
和 node2
都能到达节点的编号,使节点 node1
和节点 node2
到这个节点的距离 较大值最小化。如果有多个答案,请返回 最小 的节点编号。如果答案不存在,返回 -1
。
注意 edges
可能包含环。
示例 1:
输入:edges = [2,2,3,-1], node1 = 0, node2 = 1 输出:2 解释:从节点 0 到节点 2 的距离为 1 ,从节点 1 到节点 2 的距离为 1 。 两个距离的较大值为 1 。我们无法得到一个比 1 更小的较大值,所以我们返回节点 2 。
笔记:
这道题的思路就是求出node1和node2到各个点的距离,然后找出这些距离中的最小值。
class Solution {
public:// 求出node1和node2到各个点的距离vector<int> find_len(vector<int>& edges, int x){int n = edges.size();int d = 0;vector<int> len(n, n);// 遍历的要求:该节点不为最后的节点并且该节点未被访问过while(x >= 0 && len[x] == n){len[x] = d++;x = edges[x];}return len;}int closestMeetingNode(vector<int>& edges, int node1, int node2) {int n = edges.size();int res = n;int ans = -1;vector<int> a(n, n);a = find_len(edges, node1);vector<int> b(n, n);b = find_len(edges, node2);for(int i = 0; i < n; i++){int d = max(a[i], b[i]);if(d < res){res = d;ans = i;}}return ans;}
};
Leetcode - 2360:图中的最长环:
题目:
给你一个 n
个节点的 有向图 ,节点编号为 0
到 n - 1
,其中每个节点 至多 有一条出边。
图用一个大小为 n
下标从 0 开始的数组 edges
表示,节点 i
到节点 edges[i]
之间有一条有向边。如果节点 i
没有出边,那么 edges[i] == -1
。
请你返回图中的 最长 环,如果没有任何环,请返回 -1
。
一个环指的是起点和终点是 同一个 节点的路径。
示例 1:
输入:edges = [3,3,4,2,3] 输出去:3 解释:图中的最长环是:2 -> 4 -> 3 -> 2 。 这个环的长度为 3 ,所以返回 3 。
笔记:
这道题就是参加会议的最多员工数那道题的简易版本,也就是去除了考虑基环为2的座位问题,只考虑大小>= 2的基环的大小。
思路是:利用拓扑排序去除掉非基环的树枝部分,然后循环遍历所有点找到所有的基环,通过比较得出最大的那个基环。
这道题需要注意的一个点是:当edges[i] = -1时的处理情况:没有出边就代表必定不能成环所以我们直接忽略这条边。
class Solution {
public:int longestCycle(vector<int>& edges) {int n = edges.size();vector<int> indeg(n, 0);// 计算每个节点的入度for(int i = 0; i < n; i++){if (edges[i] != -1) { // 忽略指向 -1 的边indeg[edges[i]]++;}}queue<int> que;// 找出所有入度为0的节点for(int i = 0; i < n; i++){if(indeg[i] == 0){que.push(i);}}// 拓扑排序去除所有入度为0的节点while(!que.empty()){int cur = que.front(); que.pop();int y = edges[cur];if (y != -1) { // 忽略指向 -1 的边indeg[y]--;if(indeg[y] == 0){que.push(y);}}}int res = -1; // 用 -1 表示没有环的情况// 寻找剩余的节点中的最长环for(int i = 0; i < n; i++){if(indeg[i] == 0) continue; // 跳过已处理的节点int len = 0;for(int j = i; indeg[j] > 0; j = edges[j]) {indeg[j] = 0; // 标记节点为已访问len++;}res = max(res, len);}return res;}
};
Leetcode - 2876:有向图访问计数:
题目:
现有一个有向图,其中包含 n
个节点,节点编号从 0
到 n - 1
。此外,该图还包含了 n
条有向边。
给你一个下标从 0 开始的数组 edges
,其中 edges[i]
表示存在一条从节点 i
到节点 edges[i]
的边。
想象在图上发生以下过程:
- 你从节点
x
开始,通过边访问其他节点,直到你在 此过程 中再次访问到之前已经访问过的节点。
返回数组 answer
作为答案,其中 answer[i]
表示如果从节点 i
开始执行该过程,你可以访问到的不同节点数。
示例 1:
输入:edges = [1,2,0,0] 输出:[3,3,3,4] 解释:从每个节点开始执行该过程,记录如下: - 从节点 0 开始,访问节点 0 -> 1 -> 2 -> 0 。访问的不同节点数是 3 。 - 从节点 1 开始,访问节点 1 -> 2 -> 0 -> 1 。访问的不同节点数是 3 。 - 从节点 2 开始,访问节点 2 -> 0 -> 1 -> 2 。访问的不同节点数是 3 。 - 从节点 3 开始,访问节点 3 -> 0 -> 1 -> 2 -> 0 。访问的不同节点数是 4 。
笔记:
这道题的难点和不同点在于当球树枝上的节点的访问计数时,我们会发现比较难处理,因为在参加会议的最多员工数拿到题目中,我们可以通过拓扑排序之后找到基环并通过基环的大小锁定需要求的熟知的根节点然后从根节点开始处理便可以求得这树枝的大小,而这道题我们无法定位到根节点。
若是在环上的节点我们可以直接遍历整个环求出计数,但在树枝上的点我们需要求出其深度还要求出与其相连的基环的大小:
本题的关键就在于我们需要处理的就这两种情况:
1、节点在树枝上不在基环上,这种情况计数为在树枝上的深度加上基环的大小。
2、节点在基环上,这种情况计数为基环的大小。
由于edges是内向基环图,所以我们需要建立一个返图方便我们从基环上的点向外dfs拓展,在这里我们在遍历反图时的基础深度设置为基环的大小方便我们处理,
对于多个基环的理解:我一开始是认为多个基环不就是一个基环么,但需要注意的是如果是两个基环通过一个节点数为2的基环相连接那么这是一个基环,如果不是节点数为2的基环连接那么就是两个独立的基环。
class Solution {
public:vector<int> countVisitedNodes(vector<int> &g) {int n = g.size();vector<vector<int>> rg(n); // 反图vector<int> deg(n);for (int x = 0; x < n; x++) {int y = g[x];rg[y].push_back(x);deg[y]++;}// 拓扑排序,剪掉 g 上的所有树枝// 拓扑排序后,deg 值为 1 的点必定在基环上,为 0 的点必定在树枝上queue<int> q;for (int i = 0; i < n; i++) {if (deg[i] == 0) {q.push(i);}}while (!q.empty()) {int x = q.front();q.pop();int y = g[x];if (--deg[y] == 0) {q.push(y);}}vector<int> ans(n, 0);// 在反图上遍历树枝:x:基环上的点也就是树枝的根:function<void(int, int)> rdfs = [&](int x, int depth) {ans[x] = depth;for (int y: rg[x]) {if (deg[y] == 0) { // 树枝上的点在拓扑排序后,入度均为 0rdfs(y, depth + 1);}}};// 关键代码:for (int i = 0; i < n; i++) {if (deg[i] <= 0) {continue;}vector<int> ring;for (int x = i;; x = g[x]) {deg[x] = -1; // 将基环上的点的入度标记为 -1,避免重复访问// 存储当前遍历的基环的所有节点:ring.push_back(x); // 收集在基环上的点if (g[x] == i) {break;}}// 对当前遍历的基环上的点通过反图向外dfs获取对应树枝的深度:for (int x: ring) {rdfs(x, ring.size()); // 为方便计算,以 ring.size() 作为初始深度}}return ans;}
};作者:灵茶山艾府
链接:https://leetcode.cn/problems/count-visited-nodes-in-a-directed-graph/solutions/2464852/nei-xiang-ji-huan-shu-pythonjavacgo-by-e-zrzh/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
class Solution {
public:// 求各节点的深度,树枝起始深度为基环的大小void brank_len(vector<int>& ans, vector<int>& indeg, vector<vector<int>>& rev, int x, int dep){int n = rev[x].size();ans[x] = dep;for(int i = 0; i < n; i++){brank_len(ans, indeg, rev, rev[x][i], dep + 1);}}vector<int> countVisitedNodes(vector<int>& edges) {int n = edges.size();vector<int> ans(n, 0);vector<int> indeg(n, 0);// 拓扑排序模板:for(int i = 0; i < n; i++){indeg[edges[i]]++;}queue<int> que;for(int i = 0; i < n; i++){if(indeg[i] == 0){que.push(i);}}vector<vector<int>> rev(n);// 建立反图while(!que.empty()){int cur = que.front(); que.pop();int y = edges[cur];rev[y].push_back(cur);if(--indeg[y] == 0){que.push(y);}}for(int i = 0; i < n; i++){if(indeg[i] == 0) continue;vector<int> ring;indeg[i] = 0;ring.push_back(i);for(int j = edges[i]; j != i; j = edges[j]){indeg[j] = 0;ring.push_back(j);}for(int k = 0; k < ring.size(); k++){brank_len(ans, indeg, rev, ring[k], ring.size());}}return ans;}
};