基础知识 负环 :环上权值之和是负数
求负环的常用方法 基于SPFA
统计每个点入队次数,如果某个点入队n次,则说明存在负环(完全等价于Bellman-Ford算法)
统计当前每个点的最短路中所包含的边数,如果某点的最短路包含的边数大于等于n,则说明存在负环
玄学结论:当所有点的入队次数超过2n时,就认为图中有很大肯存在负环
例题 虫洞 原题链接
农夫约翰在巡视他的众多农场时,发现了很多令人惊叹的虫洞。
虫洞非常奇特,它可以看作是一条 单向 路径,通过它可以使你回到过去的某个时刻(相对于你进入虫洞之前)。
农夫约翰的每个农场中包含 N 片田地,M 条路径(双向)以及 W 个虫洞。
现在农夫约翰希望能够从农场中的某片田地出发,经过一些路径和虫洞回到过去,并在他的出发时刻之前赶到他的出发地。
他希望能够看到出发之前的自己。
请你判断一下约翰能否做到这一点。
下面我们将给你提供约翰拥有的农场数量 F,以及每个农场的完整信息。
已知走过任何一条路径所花费的时间都不超过 10000 秒,任何虫洞将他带回的时间都不会超过 10000 秒。
输入格式
第一行包含整数 F,表示约翰共有 F 个农场。
对于每个农场,第一行包含三个整数 N,M,W。
接下来 M 行,每行包含三个整数 S,E,T,表示田地 S 和 E 之间存在一条路径,经过这条路径所花的时间为 T。
再接下来 W 行,每行包含三个整数 S,E,T,表示存在一条从田地 S 走到田地 E 的虫洞,走过这条虫洞,可以回到 T 秒之前。
输出格式
输出共 F行,每行输出一个结果。
如果约翰能够在出发时刻之前回到出发地,则输出 YES,否则输出 NO。
数据范围
$1≤F≤5$ $1≤N≤500,$ $1≤M≤2500,$ $1≤W≤200,$ $1≤T≤10000,$ $1≤S,E≤N$
输入样例 2 3 3 1 1 2 2 1 3 4 2 3 1 3 1 3 3 2 1 1 2 3 2 3 4 3 1 8
输出样例
题意 负环板题
思路 跑一遍spfa
代码 #include <bits/stdc++.h> using namespace std;const int N = 510 , M = 5210 ;int n, m1, m2;int h[N], ne[M], e[M], w[M], idx;int dist[N];int cnt[N];bool st[N];void add (int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ; } bool spfa () { memset (dist, 0 , sizeof dist); memset (cnt, 0 , sizeof cnt); memset (st, 0 , sizeof st); queue<int > q; for (int i = 1 ; i <= n; i ++ ) { q.push (i); st[i] = true ; } while (q.size ()) { int t = q.front (); q.pop (); st[t] = false ; for (int i = h[t]; ~i; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; cnt[j] = cnt[t] + 1 ; if (cnt[j] >= n) return true ; if (!st[j]) { q.push (j); st[j] = true ; } } } } return false ; } int main () { ios::sync_with_stdio (false ); cin.tie (0 ), cout.tie (0 ); int T; cin >> T; while (T -- ) { cin >> n >> m1 >> m2; memset (h, -1 , sizeof h); idx = 0 ; for (int i = 0 ; i < m1; i ++ ) { int a, b, c; cin >> a >> b >> c; add (a, b, c), add (b, a, c); } for (int i = 0 ; i < m2; i ++ ) { int a, b, c; cin >> a >> b >> c; add (a, b, -c); } if (spfa ()) puts ("YES" ); else puts ("NO" ); } }
观光奶牛 原题链接
给定一张 L 个点、P 条边的有向图,每个点都有一个权值 f[i],每条边都有一个权值 t[i]。
求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。
输出这个最大值。
注意:数据保证至少存在一个环。
输入格式
第一行包含两个整数 L 和 P。
接下来 L 行每行一个整数,表示 f[i]。
再接下来 P 行,每行三个整数 a,b,t[i],表示点 a 和 b 之间存在一条边,边的权值为 t[i]。
输出格式
输出一个数表示结果,保留两位小数。
数据范围
$2≤L≤1000,$ $2≤P≤5000,$ $1≤f[i],t[i]≤1000$
输入样例 5 7 30 10 10 5 10 1 2 3 2 3 2 3 4 5 3 5 2 4 5 5 5 1 3 5 2 2
输出样例
题意 给出点权和边权,求一个环使得点权之和除以边权之和最大
思路 求比值最大的问题 -> ==01分数规划== -> 二分
首先确定边界值,答案一定在[0, 1000)
之间
每次取中点mid,如果图中存在一个环使得比值大于mid,则答案在mid和r之间,反之亦然
现在问题变成了如何判断是否存在一个比值大于mid的环?
判断:$\frac{\sum f_i}{\sum t_i}>Mid$ 化简:$\sum f_i-Mid*\sum t_i>0$
在环上的点权我们可以任意加到边权上,对环不会有影响,假设我们将所有点权加到出边上,化简:$\sum (f_i-Mid*t_i)>0$
现在将边权全看成$f_i-Mid*t_i$
问题转换成了:图中是否存在一个正环
求最长路即可
代码 #include <bits/stdc++.h> using namespace std;const int N = 1010 , M = 5010 ;int n, m;int wf[N];int h[N], e[M], ne[M], wt[M], idx;double dist[N];int cnt[N];bool st[N];void add (int a, int b, int c) { e[idx] = b, wt[idx] = c, ne[idx] = h[a], h[a] = idx ++ ; } bool check (double mid) { memset (dist, 0 , sizeof dist); memset (st, 0 , sizeof st); memset (cnt, 0 , sizeof cnt); queue<int > q; for (int i = 1 ; i <= n; i ++ ) { q.push (i); st[i] = true ; } while (q.size ()) { int t = q.front (); q.pop (); st[t] = false ; for (int i = h[t]; ~i; i = ne[i]) { int j = e[i]; if (dist[j] < dist[t] + wf[t] - mid * wt[i]) { dist[j] = dist[t] + wf[t] - mid * wt[i]; cnt[j] = cnt[t] + 1 ; if (cnt[j] >= n) return true ; if (!st[j]) { q.push (j); st[j] = true ; } } } } return false ; } int main () { cin >> n >> m; for (int i = 1 ; i <= n; i ++ ) cin >> wf[i]; memset (h, -1 , sizeof h); for (int j = 0 ; j < m; j ++ ) { int a, b, c; cin >> a >> b >> c; add (a, b, c); } double l = 0 , r = 1e6 ; while (r - l > 1e-4 ) { double mid = (l + r) / 2 ; if (check (mid)) l = mid; else r = mid; } printf ("%.2lf\n" , l); }
单词环 原题链接
我们有 n 个字符串,每个字符串都是由 a∼z 的小写英文字母组成的。
如果字符串 A 的结尾两个字符刚好与字符串 B 的开头两个字符相匹配,那么我们称 A 与 B 能够相连(注意:A 能与 B 相连不代表 B 能与 A 相连)。
我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个环串(一个串首尾相连也算),我们想要使这个环串的平均长度最大。
如下例: 第一个串能与第二个串相连,第二个串能与第三个串相连,第三个串能与第一个串相连,我们按照此顺序相连,便形成了一个环串,长度为 5+7+10=22(重复部分算两次),总共使用了 3 个串,所以平均长度是 223≈7.33。
输入格式
本题有多组数据。
每组数据的第一行,一个整数 n,表示字符串数量;
接下来 n 行,每行一个长度小于等于 1000 的字符串。
读入以 n=0 结束。
输出格式
若不存在环串,输出”No solution”,否则输出最长的环串的平均长度。
只要答案与标准答案的差不超过 0.01,就视为答案正确。
数据范围
$1≤n≤105$
输入样例 3 intercommunicational alkylbenzenesulfonate tetraiodophenolphthalein 0
输出样例
题意 n个字符串,如果a的末尾两个字符和b的开头相同则能相连,求能构成的环的平均长度最大值
思路 把每个字符串的首位两个字母看做一个点,比如说样例可以这样建图: 答案就变成了求$\frac{\sum w_i}{\sum 1}$的最大值
答案在(0, 1000)
之内,二分做,类似观光奶牛
最终判断式为:$\sum w_i - Mid*\sum 1>0$
判断有没有解直接把 Mid = 0
代入即可,因为如果等于0都不能满足的话大于0就更不会满足了
于是成功TLE,用一下玄学优化
代码 #include <bits/stdc++.h> using namespace std;const int N = 700 , M = 100010 ;int n;int h[N], e[M], ne[M], w[M], idx;double dist[N];int cnt[N];bool st[N];void add (int a, int b, int c) { e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++ ; } bool check (double mid) { memset (st, 0 , sizeof st); memset (cnt, 0 , sizeof cnt); queue<int > q; for (int i = 0 ; i < 676 ; i ++ ) { q.push (i); st[i] = true ; } int count = 0 ; while (q.size ()) { int t = q.front (); q.pop (); st[t] = false ; for (int i = h[t]; ~i; i = ne[i]) { int j = e[i]; if (dist[j] < dist[t] + w[i] - mid) { dist[j] = dist[t] + w[i] - mid; cnt[j] = cnt[t] + 1 ; if (++ count > 10000 ) return true ; if (cnt[j] >= N) return true ; if (!st[j]) { q.push (j); st[j] = true ; } } } } return false ; } int main () { char str[1010 ]; while (scanf ("%d" , &n), n) { memset (h, -1 , sizeof h); idx = 0 ; for (int i = 0 ; i < n; i ++ ) { cin >> str; int len = strlen (str); if (len >= 2 ) { int left = (str[0 ] - 'a' ) * 26 + str[1 ] - 'a' ; int right = (str[len - 2 ] - 'a' ) * 26 + str[len - 1 ] - 'a' ; add (left, right, len); } } if (!check (0 )) puts ("No solution" ); else { double l = 0 , r = 1000 ; while (r - l > 1e-4 ) { double mid = (l + r) / 2 ; if (check (mid)) l = mid; else r = mid; } printf ("%lf\n" , r); } } }