【toposort】【基环树】参与会议的最多员工数
参与会议的最多员工数
一个公司准备组织一场会议,邀请名单上有 n 位员工。公司准备了一张 圆形 的桌子,可以坐下 任意数目 的员工。
员工编号为 0 到 n - 1 。每位员工都有一位 喜欢 的员工,每位员工 当且仅当 他被安排在喜欢员工的旁边,他才会参加会议。每位员工喜欢的员工 不会 是他自己。
给你一个下标从 0 开始的整数数组 favorite ,其中 favorite[i] 表示第 i 位员工喜欢的员工。请你返回参加会议的 最多员工数目 。
思路
根据题意,每个人都有一个出度,所以必然成环。
成环分两种情况:
-
二人成环:这种环可以在一个桌子上有任意多个,并且允许有支链连接到环上(链状)。
... --> 1 <--> 2 <-- ... table ... --> 3 <--> 4 <-- ...
-
三及以上人成环:这种环在一个桌子上只能有一个,且不允许任何环外支链。
1 --> 2 --> 3 ^ table │ └-----------┘
所以问题关键是:先检测所有环,根据环长分类讨论。三人以上环只需要统计环长,取最大值即可,可以通过DFS解决。二人成环则还需要统计连接到环上的支链长度,通过DFS不好解决(因为需要提前知道环结构),但可以通过拓扑排序解决,统计所有二人环加上最长支链的总和。最后,取这两个的极大值即可。
反思:首先就没能正确分类讨论,WA了几次才注意到三人环是不兼容的(只能取一个最大三人环,或是取全部二人环)。其次没有想到拓扑排序,为了能够正确的计算最长支链,时间复杂度超过了O(n),被TLE制裁。然后企图用并查集,但结果无法保证正确。
不要想着能够把问题全都塞到一个算法里,如果能够放弃用DFS统计二人环,再写一个拓扑排序,就能过了...
别人的优秀解答:
class Solution {
public:
int maximumInvitations(vector<int>& p) {
int n = p.size();
// detect largest loop
int size3 = 0;
vector<int> vis(n, -1), steps(n, 0);
function<void(int, int, int)> dfs = [&](int x, int id, int step) {
steps[x] = step;
int y = p[x];
if (vis[y] == -1) {
vis[y] = id;
dfs(y, id, step + 1);
} else if (vis[y] == id) {
// found loop, update max size
size3 = max(size3, step - steps[y] + 1);
}
};
for (int i = 0; i < n; i++) if (vis[i] == -1) {
vis[i] = i;
dfs(i, i, 1);
}
// toposort
vector<int> ind(n), subchain(n, 1); // in degree, max subchain length
queue<int> q;
for (int i = 0; i < n; i++) ind[p[i]] += 1;
for (int i = 0; i < n; i++) if (ind[i] == 0) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
subchain[p[u]] = max(subchain[p[u]], subchain[u] + 1);
if (--ind[p[u]] == 0) q.push(p[u]);
}
int size2 = 0;
for (int i = 0; i < n; i++)
// assures it's a 2-loop, update subchain length
if (p[p[i]] == i && p[i] > i) size2 += subchain[i] + subchain[p[i]];
return max(size3, size2);
}
};
拓展
基环树/环套树(pseudo-tree):具有N个节点,N条边的连通图。若不连通,则成为基环树森林(pseudo-forest)。
基环树通常包含两个部分,即环与环上的树枝,故又称环套树。
基环内向树:每个点只有一条出边。
基环外向树:每个点只有一条入边。
此题属于基环内向树森林。
基本处理思路为通过拓扑排序分离树枝与环。
本题的另一种思路即先拓扑排序,再对环长分类讨论,长度3及以上的环只需要统计最大环长,长度2的环则可以通过反图计算最长树枝。
class Solution {
public:
int maximumInvitations(vector<int> &favorite) {
int n = favorite.size();
vector<vector<int>> g(n), rg(n); // rg 为图 g 的反图
vector<int> deg(n); // 图 g 上每个节点的入度
for (int v = 0; v < n; ++v) {
int w = favorite[v];
g[v].emplace_back(w);
rg[w].emplace_back(v);
++deg[w];
}
// 拓扑排序,剪掉图 g 上的所有树枝
queue<int> q;
for (int i = 0; i < n; ++i) {
if (deg[i] == 0) {
q.emplace(i);
}
}
while (!q.empty()) {
int v = q.front();
q.pop();
for (int w : g[v]) {
if (--deg[w] == 0) {
q.emplace(w);
}
}
}
// 寻找图 g 上的基环
vector<int> ring;
vector<int> vis(n);
function<void(int)> dfs = [&](int v) {
vis[v] = true;
ring.emplace_back(v);
for (int w: g[v]) {
if (!vis[w]) {
dfs(w);
}
}
};
// 通过反图 rg 寻找树枝上最深的链
int max_depth = 0;
function<void(int, int, int)> rdfs = [&](int v, int fa, int depth) {
max_depth = max(max_depth, depth);
for (int w: rg[v]) {
if (w != fa) {
rdfs(w, v, depth + 1);
}
}
};
int max_ring_size = 0, sum_list_size = 0;
for (int i = 0; i < n; ++i) {
if (!vis[i] && deg[i]) { // 遍历基环上的点(拓扑排序后入度不为 0)
ring.resize(0);
dfs(i);
int sz = ring.size();
if (sz == 2) { // 基环大小为 2
int v = ring[0], w = ring[1];
max_depth = 0;
rdfs(v, w, 1);
sum_list_size += max_depth; // 累加 v 这一侧的最长链的长度
max_depth = 0;
rdfs(w, v, 1);
sum_list_size += max_depth; // 累加 w 这一侧的最长链的长度
} else {
max_ring_size = max(max_ring_size, sz); // 取所有基环的最大值
}
}
}
return max(max_ring_size, sum_list_size);
}
};