这是图中很基本的问题,很多图的问题可以转化为求图中的最大环或最长链。
例如Leetcode 5970. 参加会议的最多员工数,等价于求有向图最长环,和长度为2的环加上其外链。
有向图
最大环
有多种方法:
- 一种是先用拓扑排序将外链去掉,再dfs每一个环
- 另一种是从某一点出发,记录途径的点,如果遇到已经访问过的点,说明找到了环的入口。减去起始点到入口的距离,就是环的长度。
- 还有一种有并查集,对于
x->y
,如果x
和y
同属于一个集合,说明形成了一个环。
求最小环的方式是类似的。
假设favorite[i]=v
表示从i
到i
有一条边,这里采用第二种方法
int DirectMaxCycle(vector<int>& favorite) {
int n = favorite.size();
vector<bool> vis(n, false);
int max_cycle = 0;
for(int i = 0;i < n;i++) {
if(vis[i]) continue;
int cur = i;
vector<int> cycle;
while(!vis[cur]) {
vis[cur] = true;
cycle.push_back(cur);
cur = favorite[cur];
}
for(int j = 0;j < cycle.size();j++) {
if(cycle[j] == cur) {
int len = cycle.size() - j;
if(len > max_cycle) max_cycle = len;
break;
}
}
}
return max_cycle;
}
有向无环图:最长链
这里有一个很重要的问题,有环怎么办?
有环的情况下,求最长链是没有意义的。要么保证无环,要么是求连接到环上的链的长度。
例如求连接到环上的链的长度,需要从入度为0的节点开始,递推计算,于是采用拓扑序。
int TopologicalSort(vector<int>& favorite) {
int n = favorite.size();
vector<bool> vis(n, false);
vector<int>in(n, 0);
vector<int>dp(n, 1);
queue<int> q;
for(int i = 0;i < n;i++) in[favorite[i]]++;
for(int i = 0;i < n;i++) {
if(in[i] == 0) q.push(i);
}
while(!q.empty()) {
int cur = q.front();
q.pop();
// cout << cur << " ";
dp[favorite[cur]] = max(dp[favorite[cur]], dp[cur] + 1);
if(--in[favorite[cur]] == 0) q.push(favorite[cur]);
}
// dp[i] 表示到达i的最长链的长度
int two_point_sum = 0; // 题目相关部分
for(int i = 0;i < n;i++) {
if(i == favorite[favorite[i]]) two_point_sum += dp[i];
}
return two_point_sum;
}
无向图
最大环
和有向图类似,略
无向无环图:最长链
因为是无环图,求最长链也就是求树的直径
- 也可以和有向图一样,拓扑序+dp
- 还有一种有趣的方法,两次dfs。可以证明,从任一点出发,dfs能走到的最远点一定是"直径"的一个端点,然后从这个端点出发,dfs得到另一个端点。
例如Leetcode310最小数高度,等价于求树的直径
第一次dfs找到一个端点,再从这个端点出发dfs找到另一个端点,最后在写个dfs得到路径
class Solution {
public:
static const int maxn = 20000+10;
vector<int>graph[maxn];
bool vis[maxn];
int end[2], max_dis=-1;
void dfs(int s, int dis, int flag) {
vis[s] = true;
if(dis >= max_dis) {max_dis = dis; end[flag] = s;}
for(int i = 0; i < graph[s].size(); i++) {
int t = graph[s][i];
if(!vis[t]) {
dfs(t, dis+1, flag);
}
}
}
vector<int>ans;
void path_dfs(int s, int dis, vector<int>& path) {
if(s == end[1]) {
int n = path.size();
// cout << "path: ";
// for(int i = 0; i < n; i++) {
// cout << path[i] << " ";
// }
// cout << endl;
if(n%2 == 0) ans = {path[n/2-1], path[n/2]};
else ans = {path[n/2]};
return;
}
vis[s] = true;
for(int i = 0; i < graph[s].size(); i++) {
int t = graph[s][i];
if(!vis[t]) {
path.push_back(t);
path_dfs(t, dis+1, path);
path.pop_back();
}
}
}
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
for(auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
graph[edge[1]].push_back(edge[0]);
}
memset(vis, 0, sizeof(vis));
dfs(0, 0, 0); // end[0] is rightmost node
memset(vis, 0, sizeof(vis));
dfs(end[0], 0, 1); // end[1] is leftmost node
// cout << end[0] << " " << end[1] << endl;
vector<int>path = {end[0]};
memset(vis, 0, sizeof(vis));
path_dfs(end[0], 0, path);
return ans;
}
};
也可以双BFS写法,而且相比前面DFS,BFS可以在求最远点的时候得到路径
int bfs(int s){ // 返回距s的最远点
memset(vis, 0, sizeof(vis));
queue<int>q;
q.push(s);
vis[s] = true;
int u;
while(!q.empty()){
u = q.front();
q.pop();
for(int i = 0; i < graph[u].size(); i++){
int v = graph[u][i];
if(!vis[v]){
vis[v] = true;
q.push(v);
}
}
}
return u;
}
int pre[maxn];
vector<int> path_bfs(int s) { // 返回s到end的路径
memset(vis, 0, sizeof(vis));
memset(pre, -1, sizeof(pre));
queue<int>q;
q.push(s);
vis[s] = true;
int u;
while(!q.empty()){
u = q.front();
q.pop();
for(int i = 0; i < graph[u].size(); i++){
int v = graph[u][i];
if(!vis[v]){
vis[v] = true;
q.push(v);
pre[v] = u;
}
}
}
vector<int>path;
while(u != -1){
path.push_back(u);
u = pre[u];
}
return path;
}
注意
有环图中,双dfs/bfs这种方法是错误的,很容易找到反例:
图片来自The time complexity of finding the diameter of a graph
上述方法得到的结果可能是4,而实际是5。