知识点整理——图上的环(判环、求解最小环)

前言

近日刷图论题遇到了多道求解最小环的例题,由于方法众多,用法不尽相同,数次被此所困扰。在互联网上寻找良久,却没能发现什么系统性的整理,所以便有了此文。

求解此类问题:

给出一张图,输出图中最小环的大小。定义最小环为:由 \(k(k \ge 3)\) 个点构成的最小的简单环。

三元环相关模板

定义

很显然,三元环也属于简单环,所以简单环里面的算法依旧是可以使用的。

性质:若一张有向完全图存在环,则一定存在三元环。

完全图输出三元环上元素

有向图 无向图

时间复杂度为 \(\mathcal O(N^2)\)

详解

这部分的代码来自这一道题目,题目大意为:给出一张有向完全图,输出任意一个三元环上的全部元素。

下方的代码同样适用于非完全图和无向图(但是我个人没有测试过)。

bool Solve() {
	int n; cin >> n;
	vector<vector<int> > a(n + 1, vector<int> (n + 1));
	for (int i = 1; i <= n; ++ i) {
		for (int j = 1; j <= n; ++ j) {
			char x; cin >> x;
			if (x == '1') a[i][j] = 1;
		}
	}
	
	vector<int> vis(n + 1);
	function<void(int, int)> dfs = [&] (int x, int fa) {
		vis[x] = 1;
		for (int y = 1; y <= n; ++ y) {
			if (a[x][y] == 0) continue;
			if (a[y][fa] == 1) {
				cout << fa << " " << x << " " << y;
				exit(0);
			}
			if (!vis[y]) dfs(y, x); // 这一步的if判断很关键
		}
	};
	for (int i = 1; i <= n; ++ i) {
		if (!vis[i]) dfs(i, -1);
	}
	cout << -1;
	return 0;
}

最小环相关模板

输出图上最小环大小(其一): \(\tt flody\)

应用极为广泛的做法,本质是求出最短路后暴力枚举。泛用性高、代码短,唯一的缺点是时间复杂度较高,为 \(\mathcal O(N^3)\)

有向图 无向图
判断是否存在 输出数量 输出大小 输出环上元素
最小环
简单环
详解

这部分的代码来自这一道题目,题目大意为:给出一张无向图,求解该图最小环的大小。

int flody(int n) {
    for (int i = 1; i <= n; ++ i) {
        for (int j = 1; j <= n; ++ j) {
            val[i][j] = dis[i][j]; // 记录最初的边权值
        }
    }
    int ans = 0x3f3f3f3f;
    for (int k = 1; k <= n; ++ k) {
        for (int i = 1; i < k; ++ i) { // 注意这里是没有等于号的
            for (int j = 1; j < i; ++ j) {
                ans = min(ans, dis[i][j] + val[i][k] + val[k][j]);
            }
        }
    for (int i = 1; i <= n; ++ i) { // 往下是标准的flody
        for (int j = 1; j <= n; ++ j) {
            dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
            }
        }
    }
    return ans;
}

输出图上最小环大小(其二): \(\tt bfs\)

复杂度 \(\mathcal O(N^2)\)

有向图 无向图
判断是否存在 输出数量 输出大小 输出环上元素
最小环
简单环
详解

这部分的代码同样来自这一道题目,题目大意为:给出一张无向图,求解该图最小环的大小。

本质也是求解最短路。

auto bfs = [&] (int s) {
    queue<int> q; q.push(s);
    dis[s] = 0;
    fa[s] = -1;
    while (q.size()) {
        auto x = q.front(); q.pop();
        for (auto y : ver[x]) {
            if (y == fa[x]) continue;
            if (dis[y] == -1) {
                dis[y] = dis[x] + 1;
                fa[y] = x;
                q.push(y);
            }
            else ans = min(ans, dis[x] + dis[y] + 1);
        }
    }
};
for (int i = 1; i <= n; ++ i) {
    fill(dis + 1, dis + 1 + n, -1);
    bfs(i);
}
cout << ans;

简单环相关模板

输出图上简单环数量:状压 \(\tt dp\)

例题地址,复杂度 \(\mathcal O(M*2^N)\)

有向图 无向图
有边权
无边权
判断是否存在 输出数量 输出环上元素 输出大小
简单环
最小环

输出图上任意一个简单环: \(\tt dfs\)

本方法在本质上是使用了 \(\tt dfs\) 序加以处理,与 \(\tt tarjan\) 相仿。

有向图 无向图
判断是否存在 输出数量 输出大小 输出环上元素
最小环
简单环
详解

这部分的代码来自这一道题目,处理过后的题目大意为:给出一张无向图,输出任意一个大小 \(\le K\) 的简单环上的全部元素。

注意限定:是简单环而不是最小环

时间仓促,直接引用做题时的截图为例,在下图中,最小环为 \(2-6-5-2\) ,而使用 \(\tt dfs\) 会输出 \(2-3-4-5-6-2\) 这个简单环。

截图

虽然这个做法不能找到最小环,但是由于其优秀的复杂度,在某些题目的解题过程中是必不可少的。

function<void(int, int)> dfs = [&] (int x, int f) {
    for (auto y : ver[x]) {
        if (y == f) continue;
        if (dis[y] == -1) {
            dis[y] = dis[x] + 1;
            fa[y] = x;
            dfs(y, x);
        }
        else if (dis[y] < dis[x] && dis[x] - dis[y] <= k - 1) { // 遇到了更小的时间戳
            cout << dis[x] - dis[y] + 1 << endl; // 输出简单环的大小
            int pre = x;
            cout << pre << " "; // 输出环上元素
            while (pre != y) {
                pre = fa[pre];
                cout << pre << " ";
            }
            exit(0);
        }
    }
};
dis[1] = 0;
dfs(1, -1);

输出有向图简单环大小

方法1:\(\tt dfs\) 染色

这部分的代码来自这一道题目,处理过后的题目大意为:给出一个基环内向森林,输出全部简单环的大小。这里有一点要补充的内容,即本题其实是一张无向图,但是

标准的 \(\mathcal O(N+M)\)\(\tt dfs\) ,借助深度数组 dis[] 计算。

有向图 无向图
判断是否存在 输出数量 输出大小 输出环上元素
最小环
简单环
vector<int> vis(n + 1), dis(n + 1), ring;
function<void(int)> dfs = [&] (int x) {
	vis[x] = 1;
	for (auto y : ver[x]) {
		if (vis[y] == 0) {
			dis[y] = dis[x] + 1;
			dfs(y);
		}
		else if (vis[y] == 1) {
			ring.push_back(dis[x] - dis[y] + 1);
		}
	}
	vis[x] = 2;
};
for (int i = 1; i <= n; ++ i) {
	if (!vis[i]) dfs(i);
}

for (auto it : ring) {
	cout << it << " ";
}

环相关模板

判断图上是否存在环: \(\tt topsort\)

有向图与无向图都可以处理,但是判断条件略有不同。

有向图 无向图
有边权
无边权
判断是否存在 输出大小 输出环上元素
简单环
最小环

判断有向图是否存在环:\(\tt dfs\) 染色

初始时所有点颜色均为 \(0\) ,开始对这个点进行 \(\tt dfs\) 前将其染为 \(1\) ,当结束对这个点的 \(\tt dfs\) 时将其染为 \(2\) 。当在 \(\tt dfs\) 的过程中遇到 \(1\) 时说明存在环。

有向图 无向图
有边权 ?
无边权 ?
判断是否存在 输出大小 输出环上元素 输出数量
简单环
最小环
? ?
function<void(int)> dfs(int x) {
    vis[x] = 1;
    for (auto y : ver[x]) {
        if (vis[y] == 0) dfs(y); //如果未被搜索过
        else if (vis[y] == 1) ++ ans; //如果已经被搜索过,说明找到了一个环
    }
    vis[x] = 2;
};
for (int i = 1; i <= n; ++ i) if (vis[i] == 0) {
    dfs(i);
}
cout << ans;

判断无向图是否存在环:\(\tt dsu\)

\(\tt dsu\) 判断新连接的两个点是否具有同一祖先(直接运行 same 函数)。

有向图 无向图
有边权
无边权
判断是否存在 输出大小 输出环上元素
简单环
最小环
? ?

输出有向图环上元素:\(\tt tarjan\)

有向图 无向图
有边权
无边权
判断是否存在 输出数量 输出环上元素 输出大小
简单环
最小环
namespace SCC { // 在有向图中将强连通分量缩点后重建图
    vector<PII> ver[N];
    int time[N], time_cnt, upper[N];
    int color[N], color_cnt;
    stack<int> S; int v[N];
    
    void clear(int n) {
        for (int i = 1; i <= n; ++ i) {
            ver[i].clear();
            v[i] = time[i] = upper[i] = color[i] = 0;
        }
        time_cnt = color_cnt = 0;
        while (!S.empty()) S.pop();
    }
    void add(int x, int y, int w) { ver[x].push_back({y, w}); }
    void tarjan(int x) {
        time[x] = upper[x] = ++ time_cnt;
        S.push(x); v[x] = 1; // v数组用于记录x点此时是否在S中
        for (auto [y, w] : ver[x]) {
            if (!time[y]) {
                tarjan(y);
                upper[x] = min(upper[x], upper[y]);
            }
            else if (v[y] == 1) upper[x] = min(upper[x], time[y]);
        }
        if (upper[x] == time[x]) {
            int pre = 0; ++ color_cnt; // colorCnt代表强连通分量的数量
            do {
                pre = S.top(); S.pop();
                v[pre] = 0;
                color[pre] = color_cnt; // 给相同强连通分量内的点染色
            } while (pre != x);
        }
    }
    void solve(int n, function<void(int, int, int)> add) {
        for (int i = 1; i <= n; ++ i) { // 若原图不连通
            if (time[i] == 0) tarjan(i);
        }
        //基于已染的颜色和附加条件重建图
        for (int x = 1; x <= n; ++ x) {
            for (auto [y, w] : ver[x]) {
                int X = color[x], Y = color[y];
                if (X != Y) add(X, Y, w); //【这里很容易写错】
            }
        }
    }
} // namespace SCC

来点例题

抽屉原理+最小环+性质特判 在求解最小环这块属于模板题,使用 \(\tt flody\)\(\tt bfs\) 均可求解。

判简单环+输出简单环+构造 使用 \(\tt dfs\) 查找并输出简单环。

posted @ 2023-03-02 12:00  hh2048  阅读(849)  评论(0编辑  收藏  举报