负环的习题和应用

\(\text{update 2024/6/9: }\) 文中提到了速度较快的 \(\text{DFS-SPFA}\),虽然速度较好,但是容易被卡,例如模板!

大家好,我是Weekoder!

今天要讲的是负环的一些习题与负环在题目中的应用。

一共有 \(3\) 个例题,分别为:

\(\color{#52C41A}\texttt{P2136}\)\(\color{#9D3DCF}\texttt{P2868}\)\(\color{#9D3DCF}\texttt{P3199}\)。(点击有 \(\text{Link}\)

\(\color{#52C41A}\texttt{P2136}\) 拉近距离

题面

在小明和小红的生活中,有 \(N\) 个关键的节点。有 \(M\) 个事件,记为一个三元组 \((S_i,T_i,W_i)\),表示从节点 \(S_i\) 有一个事件可以转移到 \(T_i\),事件的效果就是使他们之间的距离减少 \(W_i\)

这些节点构成了一个网络,其中节点 \(1\)\(N\) 是特殊的,节点 \(1\) 代表小明,节点 \(N\) 代表小红,其他代表进展的阶段。所有事件可以自由选择是否进行,但每次只能进行当前节点邻接的。请你帮他们写一个程序,计算出他们之间可能的最短距离。如果这个距离可以无限缩小,输出Forever love

分析

可以把题意理解为一个人在一条路径上不断往另一个人的方向走。

注意到题目中说距离会减少 \(W_i\),那么边权即为负。正常来说,跑一遍 SPFA 就可以了,答案是 \(dis_n\)。但是注意到不仅是小明可以去找小红,小红也可以去找小明,于是做两遍 SPFA,分别为 \(\text{spfa(1)}\)\(\text{spfa(n)}\),最后取 \(\min\)\(\texttt{Forever love}\) 就是判断有没有负环,除此之外和 SPFA 判负环没有区别。由于没有负环要输出最短路,所以考虑使用 BFS-SPFA。

\(\text{Code:}\)

#include <bits/stdc++.h>
using namespace std;

const int N = 1e3 + 5;

struct Edge {
	int to, w;
};

int n, m, dis[N], dp[N];
bool vis[N];

vector<Edge> nbr[N];

bool spfa(int s) {
	queue<int> q;
	memset(vis, 0, sizeof vis);
	memset(dis, 0x3f, sizeof dis);
	memset(dp, 0, sizeof dp);
	dis[s] = 0;
	vis[s] = 1;
	q.push(s);
	while (!q.empty()) {
		int cur = q.front(); q.pop();
		vis[cur] = 0;
		for (auto nxt : nbr[cur]) {
			int to = nxt.to, w = nxt.w;
			if (dis[to] > dis[cur] + w) {
				dis[to] = dis[cur] + w;
				dp[to] = dp[cur] + 1;
				if (dp[to] >= n) return 1;
				if (!vis[to]) {
					vis[to] = 1;
					q.push(to);
				}
			}
		}
	}
	return 0;
}

int main() {
	cin >> n >> m;
	for (int i = 1, u, v, w; i <= m; i++) {
		cin >> u >> v >> w;
		nbr[u].emplace_back((Edge){v, -w});
	}
	if (spfa(1)) {
	    cout << "Forever love";
	    return 0;
	}
	int tmp = dis[n];
	if (spfa(n)) {
	    cout << "Forever love";
	    return 0;
	}
	cout << min(dis[1], tmp);
	return 0;
}

\(\color{#9D3DCF}\texttt{P2868}\) Sightseeing Cows G

题面

给你一张 \(n\)\(m\) 边的有向图,第 \(i\) 个点点权为 \(F_i\),第 \(i\) 条边边权为 \(T_i\)

找一个环,设环上的点组成的集合为 \(S\),环的边组成的集合为 \(E\),最大化 \(\dfrac{\sum_{u\in S}F_u}{\sum_{e\in E}T_e}\)

分析

考虑二分答案。

设二分答案 \(mid\),因为要找最大值,所以我们设还有一个更大值。这个值就是 \(\dfrac{\sum_{u\in S}F_u}{\sum_{e\in E}T_e}\),所以假设我们有:\(\dfrac{\sum_{u\in S}F_u}{\sum_{e\in E}T_e}>mid\)。如果成立,就猜大些,否则猜小些。但是这个不好判断,所以我们来推一下式子(在一个环中,点数 \(=\) 边数):

\[\begin{aligned} {} & \dfrac{\sum_{u\in S}F_u}{\sum_{e\in E}T_e}>mid \\ = & \sum_{u\in S}F_u>mid\cdot \sum_{e\in E}T_e \\ = & mid\cdot \sum_{e\in E}T_e-\sum_{u\in S}F_u<0 \\ = & \sum(mid\cdot T_e-F_u)<0 \end{aligned} \]

推到这里,我们发现了一个有趣的结论:若想继续猜大些,则需要满足对于环上的边权之和,有 \(mid\cdot T_e-F_u<0\)。我们把 \(mid\cdot T_e-F_u\) 看作边权,那这不就变成了一个找负环的问题吗?没错,在简单的计算后,我们得到了一个结论:找负环。

在这里,就仅仅需要判断是否有负环并二分答案,推荐使用 DFS-SPFA,用时仅有 \(46\text{ms}\)。对于 BFS-SPFA,也能过,不过时间为 \(83\text{ms}\)

\(\text{Code:}\)

\(\text{DFS-SPFA}\)

#include <bits/stdc++.h>
using namespace std;

const int N = 1e3 + 5;
const double eps = 1e-6;

struct Edge {
    int to;
    double w;
};

int n, m;
double dis[N], f[N];
bool vis[N];

vector<Edge> nbr[N];

bool spfa(int cur, double X) {
    vis[cur] = 1;
    for (auto nxt : nbr[cur]) {
        int to = nxt.to;
        double w = nxt.w * X - f[cur];
        if (dis[to] > dis[cur] + w) {
            dis[to] = dis[cur] + w;
            if (vis[to] || spfa(to, X)) return 1;
        }
    }
    vis[cur] = 0;
    return 0;
}

bool check(double X) {
    memset(vis, 0, sizeof vis);
    memset(dis, 0, sizeof dis);
    for (int i = 1; i <= n; i++)
        if (spfa(i, X))
            return 1;
    return 0;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> f[i];
    double l = 1005, r = -1;
    for (int i = 1, u, v; i <= m; i++) {
        double w;
        cin >> u >> v >> w;
        nbr[u].emplace_back((Edge){v, w});
        l = min(l, w);
        r = max(r, w);
    }
    r++, l--;
    while (r - l > eps) {
        double mid = (l + r) / 2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    printf("%.2lf", l);
    return 0;
}

\(\text{BFS-SPFA}\)

#include <bits/stdc++.h>
using namespace std;

const int N = 1e3 + 5;
const double eps = 1e-6;

struct Edge {
	int to;
	double w;
};

int T, n, m, dp[N];
double dis[N], f[N];
bool vis[N];

vector<Edge> nbr[N];

bool spfa(double X) {
	queue<int> q;
	memset(vis, 0, sizeof vis);
	memset(dis, 0, sizeof dis);
	memset(dp, 0, sizeof dp);
	for (int i = 1; i <= n; i++)
		vis[i] = 1, q.push(i);
	while (!q.empty()) {
		int cur = q.front(); q.pop();
		vis[cur] = 0;
		for (auto nxt : nbr[cur]) {
			int to = nxt.to;
			double w = X * nxt.w - f[cur];
			if (dis[to] > dis[cur] + w) {
				dis[to] = dis[cur] + w;
				dp[to] = dp[cur] + 1;
				if (dp[to] >= n) return 1;
				if (!vis[to]) {
					vis[to] = 1;
					q.push(to);
				}
			}
		}
	}
	return 0;
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> f[i];
	double l = 1005, r = -1;
	for (int i = 1, u, v; i <= m; i++) {
		double w;
		cin >> u >> v >> w;
		nbr[u].emplace_back((Edge){v, w});
		l = min(l, w);
		r = max(r, w);
	}
	double tmp = r + 1;
	r++, l--;
	while (r - l > eps) {
		double mid = (l + r) / 2;
		if (spfa(mid)) l = mid;
		else r = mid;
	}
	printf("%.2lf", l);
	return 0;
}

\(\color{#9D3DCF}\texttt{P3199}\) 最小圈

题面

考虑带权有向图 \(G=(V,E)\) 以及 \(w:E\rightarrow \mathbb{R}\),每条边 \(e=(i,j)\)\(i\neq j\)\(i, j\in V\))的权值定义为 \(w_{i,j}\)。设 \(n=|V|\)

\(c=(c_1,c_2,\cdots,c_k)\)\(c_i\in V\))是 \(G\) 中的一个圈当且仅当 \((c_i,c_{i+1})\)\(1\le i<k\))和 \((c_k,c_1)\) 都在 \(E\) 中。称 \(k\) 为圈 \(c\) 的长度,同时记 \(c_{k+1}=c_1\),并定义圈 \(c=(c_1,c_2,\cdots,c_k)\) 的平均值为

\[\mu(c)= \frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}} \]

\(c\) 上所有边的权值的平均值。设 \(\mu'(G)=\min_c\mu(c)\)\(G\) 中所有圈 \(c\) 的平均值的最小值。

给定图 \(G=(V,E)\) 以及 \(w:E\rightarrow \mathbb{R}\),求出 \(G\) 中所有圈 \(c\) 的平均值的最小值 \(\mu'(G)\)

题面概括

给定一个图,找一个环,最小化

\[\frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}} \]

分析

考虑二分答案。

设二分答案 \(mid\),这次要找最小值,于是设 \(\frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}}<mid\)。继续推公式:

\[\begin{aligned} & \frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}}<mid \\ = & mid\cdot k>\sum\limits_{i=1}^{k}w_{c_i,c_{i+1}} \\ = & \sum\limits_{i=1}^{k}(mid-w_{c_i,c_{i+1}})>0 \\ = & \sum\limits_{i=1}^{k}(w_{c_i,c_{i+1}}-mid)<0 \\ \end{aligned} \]

于是我们又得到了:将边权看作原边权 \(-mid\),判负环。

这个题需要用 DFS-SPFA,否则会 \(\color{#052242}\texttt{TLE}\)\(50\text{pts}\)

\(\text{Code:}\)

#include <bits/stdc++.h>
using namespace std;

const int N = 3e3 + 5;
const double eps = 1e-9;

struct Edge {
    int to;
    double w;
};

int n, m;
double dis[N];
bool vis[N];

vector<Edge> nbr[N];

bool spfa(int cur, double X) {
    vis[cur] = 1;
    for (auto nxt : nbr[cur]) {
        int to = nxt.to;
        double w = nxt.w - X;
        if (dis[to] > dis[cur] + w) {
            dis[to] = dis[cur] + w;
            if (vis[to] || spfa(to, X)) return 1;
        }
    }
    vis[cur] = 0;
    return 0;
}

bool check(double X) {
    memset(dis, 0, sizeof dis);
    memset(vis, 0, sizeof vis);
    for (int i = 1; i <= n; i++)
        if (spfa(i, X)) return 1;
    return 0;
}
int main() {
    cin >> n >> m;
    double l = 1e8, r = -1e8;
    for (int i = 1, u, v; i <= m; i++) {
        double w;
        cin >> u >> v >> w;
        nbr[u].emplace_back((Edge){v, w});
        l = min(l, w);
        r = max(r, w);
    }
    r++, l--;
    while (r - l > eps) {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    printf("%.8lf", r);
    return 0;
}

小结

以上就是关于负环的三个习题的内容。负环是一个判定型的算法,适合搭配二分答案等算法,发挥更大的用处。今天的习题都有一些难度,需要巩固练习。

再见!

posted @ 2024-06-09 23:16  Weekoder  阅读(8)  评论(0编辑  收藏  举报