力扣1489-找到最小生成树里的关键边和伪关键边

  |  

第 194 场周赛 D 题

  • 枚举所有边分类
    • 必出现在 MST
    • 可以出现在 MST
    • 不会出现在 MST
  • 分类算法
    • step1: 删除边,跑 Krudkal,如果不连通(返回-1)或者 MST 变大,为必选边
    • step2: 选该边再跑 Krudkal,若 MST 变大,为必不选边,否则为可选边

优化:

  • 在 Kruskal 基础上,将相同权的边放在一起考虑
  • Trajan算法: 无向图的桥
  • 连通分量缩点技巧

$1 题目

给你一个 n 个点的带权无向连通图,节点编号为 0 到 n-1 ,同时还有一个数组 edges ,其中 edges[i] = [fromi, toi, weighti] 表示在 fromi 和 toi 节点之间有一条带权无向边。最小生成树 (MST) 是给定图中边的一个子集,它连接了所有节点且没有环,而且这些边的权值和最小。

请你找到给定图中最小生成树的所有关键边和伪关键边。如果从图中删去某条边,会导致最小生成树的权值和增加,那么我们就说它是一条关键边。伪关键边则是可能会出现在某些最小生成树中但不会出现在所有最小生成树中的边。

请注意,你可以分别以任意顺序返回关键边的下标和伪关键边的下标。

提示:

1
2
3
4
5
6
2 <= n <= 100
1 <= edges.length <= min(200, n * (n - 1) / 2)
edges[i].length == 3
0 <= fromi < toi < n
1 <= weighti <= 1000
所有 (fromi, toi) 数对都是互不相同的。

示例 1:
输入:n = 5, edges = [[0,1,1],[1,2,1],[2,3,2],[0,3,2],[0,4,3],[3,4,3],[1,4,6]]
输出:[[0,1],[2,3,4,5]]
解释:上图描述了给定图。
下图是所有的最小生成树。
注意到第 0 条边和第 1 条边出现在了所有最小生成树中,所以它们是关键边,我们将这两个下标作为输出的第一个列表。
边 2,3,4 和 5 是所有 MST 的剩余边,所以它们是伪关键边。我们将它们作为输出的第二个列表。

示例 2 :
输入:n = 4, edges = [[0,1,1],[1,2,1],[2,3,1],[0,3,1]]
输出:[[],[0,1,2,3]]
解释:可以观察到 4 条边都有相同的权值,任选它们中的 3 条可以形成一棵 MST 。所以 4 条边都是伪关键边。

$2 题解

算法: Kruskal 对边分类

  • 枚举所有边分类
    • 必出现在 MST
    • 可以出现在 MST
    • 不会出现在 MST
  • 分类算法
    • step1: 删除边,跑 MST,如果不连通(返回-1)或者 MST 变大,为必选边
    • step2: 选该边再跑 MST,若 MST 变大,为必不选边,否则为可选边

Kruskal 对边排序只需要做一次。

时间复杂度为 $O(E^{2})$。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class UnionFindSet
{
public:
UnionFindSet(int N=0)
{
this -> N = N;
init();
}

void merge(int x, int y)
{
x = _find(x);
y = _find(y);
if(x == y)
return;
if(_rank[x] < _rank[y])
_father[x] = y;
else
{
_father[y] = x;
if(_rank[x] == _rank[y])
++_rank[x];
}
}

bool same(int x, int y)
{
return _find(x) == _find(y);
}

void init()
{
_father.assign(N, 0);
_rank.assign(N, 0);
for(int i = 0; i < N; ++i)
_father[i] = i;
}

private:
int N;
vector<int> _father;
vector<int> _rank;

int _find(int x)
{
if(_father[x] == x)
return x;
return _father[x] = _find(_father[x]);
}
};

struct Edge
{
int u, v, w, id;
Edge(){}
Edge(int u, int v, int w, int id):u(u),v(v),w(w),id(id){}
};

struct Cmp
{
bool operator()(const Edge& e1, const Edge& e2) const
{
return e1.w < e2.w;
}
};

class Solution {
public:
vector<vector<int>> findCriticalAndPseudoCriticalEdges(int n, vector<vector<int>>& edges) {
N = n;
unionfindset = UnionFindSet(n);
int m = edges.size();
for(int i = 0; i < m; ++i)
es.emplace_back(edges[i][0], edges[i][1], edges[i][2], i);
sort(es.begin(), es.end(), Cmp());
vector<vector<int>> result(2);
int MST = kruskal(0, 0, -1);
for(int i = 0; i < m; ++i)
{
unionfindset.init();
int mst1 = kruskal(0, 0, i);
if(mst1 == -1 || mst1 > MST)
{
result[0].push_back(i);
continue;
}
unionfindset.init();
unionfindset.merge(edges[i][0], edges[i][1]);
int mst2 = kruskal(1, edges[i][2], -1);
if(mst2 <= MST)
result[1].push_back(i);
}
return result;
}

private:
UnionFindSet unionfindset;
vector<Edge> es;
int N;

int kruskal(int chosen, int start_cost, int delete_id)
{
int cnt = chosen;
int cost = start_cost;
// 边数小于 n - 1,不连通,返回 -1
for(const Edge &e: es)
{
int u = e.u, v = e.v, w = e.w, id = e.id;
if(id == delete_id)
continue;
if(unionfindset.same(u, v))
continue;
cost += w;
unionfindset.merge(u, v);
++cnt;
if(cnt == N - 1)
break;
}
if(cnt < N - 1)
return -1;
return cost;
}
};

优化

事实:若 G 中所有边的权不同,那么 MSR 唯一,用 Kruskal 看一遍即可得出这个事实

从小到大枚举边的权值,当前为 x,在 Kruskal 的过程中,已经用权值 < x 的边连接了一些点,形成了若干个连通分量。

枚举权值为 x 的边 e = (u, v):

  • (1) 如果 u, v 属于同一个连通分量,则 e 一定不会被选,即必不选边。
  • (2) 如果 u, v 不属于一个连通分量,则 e 可以被选,但是可选边还是必选边还需要看

建一个新图 NG,该图包含原图 G 中的孤立点,以及连通分量缩点之后的点(用并查集的代表元维护缩点),将满足 (2) 的边连接在一起(连接的是代表元)

这个新图 NG 中的边,都是边权为 x 的边,这些边中

  • 桥边是必选边
  • 非桥边是可选边

因此在新图 NG 中跑一遍 tarjan 将所有桥边返回。这些边是必选边

然后将 NG 中的连通分量缩点,并将新图 NG 清空

对缩点图的处理和更新技巧比较多,见注释。

其中求桥 tarjan 算法是在模板的基础上微调,不返回具体的桥,而是将判定为桥边的边 id 做标记,模板参考:无向图的桥

求桥过程中的 dfn, low, NGbridge,在每次求桥时需要重复使用。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class Solution {
public:
vector<vector<int>> findCriticalAndPseudoCriticalEdges(int n, vector<vector<int>>& edges) {
// 初始化
unionfindset = UnionFindSet(n);
dfn = vector<int>(n);
low = vector<int>(n);
NG = vector<vector<NG_Edge>>(n);
int m = edges.size();
bridge = vector<bool>(m, false);

// mapping[x] := 权值为 x 的边集合
map<int, vector<Edge>> mapping;
for(int i = 0; i < m; ++i)
mapping[edges[i][2]].emplace_back(edges[i][0], edges[i][1], i);

vector<bool> optional(m, false);
vector<Edge> level_es;
for(const auto &item: mapping)
{
const vector<Edge> &es = item.second;
level_es.clear();
for(const Edge &e: es)
{
// u, v 为缩点后的编号
int u = unionfindset.get(e.u);
int v = unionfindset.get(e.v);
if(u == v) continue;
// 建缩点图:在 NG 中将 u, v 连边
optional[e.id] = true;
NG[u].emplace_back(v, e.id);
NG[v].emplace_back(u, e.id);
level_es.emplace_back(u, v, e.id);
}
// 开始处理缩点图的每条边
for(Edge &e: level_es)
{
// 只需要将连边的点的 dfn 初始化
// 如果所有点都初始化,时间复杂度会退化
dfn[e.u] = 0;
dfn[e.v] = 0;
}
// 因为 level_es 中的边可能形成多个连通分量,因此每个点都要尝试找桥
for(Edge &e: level_es)
{
if(dfn[e.u] == 0) // 该连通分量之前没找过
find_bridge(e.u);
if(dfn[e.v] == 0) // 该连通分量之前没找过
find_bridge(e.v);
}
// 处理完缩点图的每条边之后,将缩点图清空
// 并将缩点图中的连通分量缩点
for(Edge &e: level_es)
{
NG[e.u].clear();
NG[e.v].clear();
unionfindset.merge(e.u, e.v);
}
}

vector<vector<int>> result(2);
for(int i = 0; i < m; ++i)
{
if(!optional[i]) continue;
if(bridge[i])
result[0].push_back(i);
else
result[1].push_back(i);
}
return result;
}

private:
UnionFindSet unionfindset;
vector<int> dfn;
vector<int> low;
vector<bool> bridge;
struct Edge
{
int u, v, id;
Edge(int u, int v, int id):u(u),v(v),id(id){}
};
struct NG_Edge
{
int v, id;
NG_Edge(int v, int id):v(v),id(id){}
};
vector<vector<NG_Edge>> NG;

void find_bridge(int S)
{
int dfnid = 0;
dfs(S, -1, dfnid);
}

void dfs(int node, int prev, int& dfnid)
{
if(dfn[node] != 0)
return;
dfn[node] = ++dfnid;
low[node] = dfnid;
int min_low = dfnid;
int cnt = 0;
for(NG_Edge e: NG[node])
{
// (node, son) 这条边
int son = e.v;
if(son == prev)
{
if(++cnt > 1)
min_low = min(min_low, dfn[son]);
continue;
}
dfs(son, node, dfnid);
if(low[son] > dfn[node]) // 找到桥边
bridge[e.id] = true;
min_low = min(min_low, low[son]);
}
low[node] = min_low;
}
};

Share