负环:
如何在一个图中求负环?
通常都会用Spfa求解
最坏情况
O
(
n
m
)
O(nm)
O(nm),可以拟造数据卡spfa
但很可惜的是,基本没有更好的求负环方法了,所以求负环的效率是不高的
队列Spfa模板:
bool spfa() {
int hh = 0, tt = 1;
q[0] = 0;//必须要保证能遍历全部点
dist[0] = 0;//判断负环可以不初始化dist,不管dist是什么值都是可以跑出负环的
while (hh != tt)
{
int t = q[hh++];
if (hh == N)hh = 0;//循环队列,因为可能不止跑 n 次
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;//cnt记录走到该点经过了多少条边
if (cnt[j] >= n)return false;
//一旦边数等于或超过点数,说明该路径上必定有两个重复点
//说明死循环了,存在环
if (!st[j]) {
st[j] = true;
q[tt++] = j;
if (tt == N)tt = 0;
}
}
}
}
return true;
}
dfs版Spfa模板:
bool spfa(int x, double mid) {
st[x] = true;//标记该点存在于路径中
for (int i = h[x]; ~i; i = ne[i]) {
int j = e[i];
double w = vv[i] - mid * pp[i];
if (dist[j] < dist[x] + w) { //满足某某限制
if (st[j])return true;//如果重复遍历,显然存在环
else {
dist[j] = dist[x] + w;
if (spfa(j, mid))return true;//搜索
}
}
}
st[x] = false;//回溯现场
return false;
}
模板题:Acwing:虫洞
——————————————————————————————————————————
求负环的引申 —— 01分数规划:
该类问题基本都是:
在求环的基础上,对环路径的 不同权值(边权点权等) 有所限制
——————————————————————————————————————————
Acwing:观光奶liu
显然最终要求的答案 a n s = = ∑ f i / ∑ t i ans==∑f_i/∑t_i ans==∑fi/∑ti( f i fi fi是点权, t i ti ti是边权),跟路径上不同权值之间的关系相关
so~
此题的解法是:
理所当然的观察出公式所具有的二分性质,如果二分中点 mid 满足 ∑ f i / ∑ t i > m i d ∑f_i/∑t_i > mid ∑fi/∑ti>mid,说明答案 ans 可以更大,所以 mid 变大右移;反之左移- 将公式推导转化, ∑ f i / ∑ t i > m i d ∑f_i/∑t_i > mid ∑fi/∑ti>mid = > => => ∑ f i − ∑ t i ∗ m i d > 0 ∑f_i-∑t_i*mid > 0 ∑fi−∑ti∗mid>0(边权点权累计肯定是大于零的,所以不用变号)
- 将点权下放到连出去的边上,显然不会影响原来的路径权值统计;原来的边权为 t i ti ti,现在变为 f i − t i ∗ m i d fi - ti*mid fi−ti∗mid,在这样的新路径上,公式 ∑ f i − ∑ t i ∗ m i d > 0 ∑f_i-∑t_i*mid > 0 ∑fi−∑ti∗mid>0 可以转化为 ∑ ( f i − t i ∗ m i d ) > 0 ∑{(fi-ti*mid)}>0 ∑(fi−ti∗mid)>0
由此你能惊奇地发现, ∑ ( f i − t i ∗ m i d ) > 0 ∑{(fi-ti*mid)}>0 ∑(fi−ti∗mid)>0 相当于统计新路径上边权之和是否大于0,而题目又要求路径在环上,所以就相当于统计环上的边权之和是否大于0
统计环上的边权之和是否大于0 == 判断环是否为正环
所以用spfa跑 最长路(因为判断正环,判断负环跑最短路)
此题的二分条件即:
跑最长路的情况下若存在环,倒推公式得,
∑
(
f
i
−
t
i
∗
m
i
d
)
>
0
∑{(fi-ti*mid)}>0
∑(fi−ti∗mid)>0
=
=
==
==
∑
f
i
/
∑
t
i
>
m
i
d
∑f_i/∑t_i > mid
∑fi/∑ti>mid,故 mid 右移
上代码:
#include<bits/stdc++.h>
#include<unordered_set>
#include<unordered_map>
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define cout_double(a) cout << setiosflags(ios::fixed) << setprecision(a)
#define sca scanf
#define pri printf
#define ul u << 1
#define ur u << 1 | 1
//#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 1010, M = 6010;
int INF = 0x3f3f3f3f, mod = 1e9 + 7;
ll LNF = 1000000000000000;
int n, m, k, T, S;
int h[N], ne[M], e[M], idx;
double dist[N], f[N], w[M];
int cnt[N], q[N];
bool st[N];
void add(int a, int b, int x) {
e[idx] = b, ne[idx] = h[a], w[idx] = x, h[a] = idx++;
}
bool spfa(double mid) {
mem(st, 0);
mem(cnt, 0);
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++)//初始把所有点都放入队列中、
//相当于建立超级源点,保证能遍历到每一个点
q[tt++] = i, st[i] = true;
while (hh != tt)
{
int t = q[hh++];
if (hh == N)hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
//01分数规划:把点权下放到连出去的边上,再找图论算法匹配
if (dist[j] < dist[t] + f[t] - mid * w[i]) { //最长路
//此题只能跑最长路判正环,为什么不能跑最短路判负环呢?
//
//因为根据推导,有正环同等于 有 ∑fi / ∑wi > mid
//所以只要满足图中有 一个 正环,我们就可以说 有 ∑fi / ∑wi > mid
//之后就可以更新mid,往右靠变大
//但如果是判负环呢?
//同等于 ∑fi / ∑wi < mid
//只要满足图中有 一个 负环,我们就可以说 有 ∑fi / ∑wi < mid
//但,我们就可以说这个图没有正环了吗?
//mid 就可以轻易左移了吗?
dist[j] = dist[t] + f[t] - mid * w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n)return true;
//显然是不能的
//这里的只判定了第一次遇到的环,没有把全图的环都跑一遍
//
// 跑出一个负环自然也不能确定有没有正环
// 不能确定 全部 ∑fi / ∑wi < mid
// 记住我们的目的,是要使 mid 尽可能大
//
//而要跑遍全图的环,显然会麻烦了,所以这题跑正环才是便捷的正解
//二分题,定义真的很重要啊,一字一句都要扣出仔细理解
if (!st[j]) {
st[j] = true;
q[tt++] = j;
if (tt == N)tt = 0;
}
}
}
}
return false;
}
int main() {
cinios;
mem(h, -1);
cin >> n >> m;
for (int i = 1; i <= n; i++)cin >> f[i];
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
double l = 0, r = 1010;//注意边界情况,能取到的最大最小值
while (r - l > 1e-4)
{
double mid = (l + r) / 2;
if (spfa(mid))l = mid;//满足情况mid右移
else r = mid;
}
cout_double(2);
cout << l;
return 0;
}
———————————————————————————————————————————
Acwing:单词环
跟First差不多,在建图上有点小操作,把给的字符串的前两位字母和后两位字母相连(哈希一下),这样建的图中的点最多只有26*26个
一样判断下 ∑ t i / ∑ f i ∑t_i/∑f_i ∑ti/∑fi 与 m i d mid mid 的关系,推导出公式尝试二分
注意下不存在解的情况,即二分结果为边界
此题要 改队列为栈 来优化一下spfa找负环的速度
代码:
#include<bits/stdc++.h>
#include<unordered_set>
#include<unordered_map>
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define cout_double(a) cout << setiosflags(ios::fixed) << setprecision(a)
#define sca scanf
#define pri printf
#define ul u << 1
#define ur u << 1 | 1
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 680, M = 100005;
int INF = 0x3f3f3f3f, mod = 1e9 + 7;
ll LNF = 1000000000000000;
int n, m, k, T, S;
int h[N], ne[M], e[M], idx;
double dist[N], wf[M];
int cnt[N], q[N];
bool st[N];
char s[1010];
unordered_map<string, int> ump;
void add(int a, int b, int x) {
e[idx] = b, ne[idx] = h[a], wf[idx] = x, h[a] = idx++;
}
bool spfa(double mid) {
mem(cnt, 0);
int hh = 0, tt = 0;
for (int i = 1; i <= m; i++)// m 是哈希离散后的点数
q[tt++] = i, st[i] = true;
// 一号玄学:
// int count = 0;
//经验值判断,如果在队列里跑了 3 * m 以上个点了,大概率有环
while (hh != tt)
{
//二号玄学:讲队列换成栈,先进先出变成后进先出
//后进先出先更新点,更有可能持续更新找到负环
int t = q[--tt];
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
//01分数规划:把点权下放到连出去的边上,再找图论算法匹配
if (dist[j] < dist[t] + wf[i] - mid) {
//bug —— 明明不是点权,作为边的wf[i] 写成wf[t]
dist[j] = dist[t] + wf[i] - mid;
cnt[j] = cnt[t] + 1;
// 一号玄学:
// if (++count > 3000)return true;
if (cnt[j] >= m)return true;
if (!st[j]) {
st[j] = true;
q[tt++] = j;
}
}
}
}
return false;
}
int main() {
while (cin >> n, n)
{
mem(h, -1);
idx = 0;
m = 0;//初始化不要漏
ump.clear();
for (int i = 1; i <= n; i++)
{
string sl, sr;
sca("%s", s);
int len = strlen(s);
if (len >= 2) { //长度小于 2 显然没法连
sl += s[0], sl += s[1], sr += s[len - 2], sr += s[len - 1];
//只能一个个加,别加反向了
if (!ump.count(sl))ump[sl] = ++m;
if (!ump.count(sr))ump[sr] = ++m;
add(ump[sl], ump[sr], len);
}
}
if (!spfa(0))puts("No solution");//直接判断也行
else {
double l = 0, r = 1010;
while (r - l > 1e-4)
{
double mid = (l + r) / 2;
if (spfa(mid))l = mid;
else r = mid;
}
//if (l < 1e-4)puts("No solution");//显然无环情况会一直往左靠
pri("%f\n", l);
//精度差异oj会自动判断
}
}
return 0;
}
—————————————————————————————————————
洛谷:P1768 天路
- 此题思路与上面的差不多,也是观察推公式。
- 唯一不同的是,该题用队列 Spfa 会超时,所以使用了巧妙的 dfs 版本 Spfa。
#include<bits/stdc++.h>
#include<unordered_map>
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define forr(a,b,c) for(int a=b;a<=c;a++)
#define rfor(a,b,c) for(int a=b;a>=c;a--)
#define oper(a) (operator<(const a& ee)const)
#define endl "\n"
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> PII;
double DNF = 1e17;
const int N = 7010, M = 30010, MM = N;
int INF = 0x3f3f3f3f, mod = 998244353;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D, K;
int h[N], e[M], ne[M], idx;
double vv[M], pp[M];
double dist[N];
bool st[N];
void add(int a, int b, int c, int d) {
e[idx] = b, vv[idx] = c, pp[idx] = d, ne[idx] = h[a], h[a] = idx++;
}
bool spfa(int x, double mid) {
st[x] = true;//标记该点存在于路径中
for (int i = h[x]; ~i; i = ne[i]) {
int j = e[i];
double w = vv[i] - mid * pp[i];
if (x == 0 || dist[j] < dist[x] + w) { //满足某某限制
if (st[j])return true;//如果重复遍历,显然存在环
else {
dist[j] = dist[x] + w;
if (spfa(j, mid))return true;//搜索
}
}
}
st[x] = false;//回溯现场
return false;
}
int main() {
cin >> n >> m;
mem(h, -1);
forr(i, 1, m) {
int a, b, c, d;
sca("%d%d%d%d", &a, &b, &c, &d);
add(a, b, c, d);
}
forr(i, 1, n)add(0, i, 0, 0);//建立虚拟源点
if (!spfa(0, 0))cout << -1;
else {
double l = 0, r = 200;
while (r - l > 1e-3)
{
double mid = (l + r) / 2;
mem(dist, 0);
mem(st, 0);
if (spfa(0, mid))l = mid;
else r = mid;
}
pri("%.1f", l);
}
return 0;
}
/*
*/
—————————————————————————————————————
差分约束:
给与一系列不等式组,对不等式组的求解问题
大部分情况下求解与求正负环密切相关
洛谷:狡猾的 差分约束问题 商人
这个图论差分约束问题比较特殊,通常的约束多用于求 至多至少 问题
但该题求的是正好满足,s月到t月的收入正好为v
故维护一个前缀和数组S, S i Si Si 表示前缀和(一直到 i 月的收入总和)
因此需要满足的约束就变为 v < = S t − S s − 1 < = v v <= St - Ss-_1 <= v v<=St−Ss−1<=v ,建双向反边即可,跑最长路最短路都可以得出答案
数据较小,不需要担心卡spfa的问题
代码(队列spfa):
#include<bits/stdc++.h>
#include<unordered_set>
#include<unordered_map>
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul u << 1
#define ur u << 1 | 1
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 110, M = 4010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int h[N], e[M], ne[M], w[M], idx;
int dist[N], cnt[N], q[N];
bool st[N];
void add(int a, int b, int x) {
e[idx] = b, ne[idx] = h[a], w[idx] = x, h[a] = idx++;
}
bool spfa() {
mem(cnt, 0);
mem(st, 0);
int hh = 0, tt = 0;
for (int i = 0; i <= n; i++)//注意要保证所有边都能遍历到(即所有点入队)(相当于超级源点,但不用建出来)
q[tt++] = i, st[i] = true;
while (hh != tt)
{
int t = q[hh++];
if (hh == N)hh = 0;
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 false;//0 点也存在图中,所以多一条边
if (!st[j]) {
st[j] = true;
q[tt++] = j;
if (tt == N)tt = 0;
}
}
}
}
return true;
}
int main() {
cinios;
cin >> T;
while (T--)
{
cin >> n >> m;
mem(h, -1);
idx = 0;
while (m--)
{
int a, b, x;
cin >> a >> b >> x;
add(a - 1, b, x);//a - 1 有可能为 0,不能忽略 0 点
add(b, a - 1, -x);
}
if (!spfa())cout << "false";
else cout << "true";
cout << '\n';
}
return 0;
}
——————————————————————————————————————————
优化spfa速度的小技巧(对于数据范围比较大,spfa不稳定的差分约束题)
只要出题人想卡spfa,怎么优化都是无济于事
优化spfa的方法不限于:
- 不建立源点(推荐),把所有点初始入队就相当于建立源点了(一些数据在从源点更新到其他点时会特别慢)
- 队列改栈的方法(慎用),改栈在判断 正/负权回路 时效果优秀,但一旦不存在 正/负权回路 ,数据又比较大,需要输出答案时效果就很差了
- 定义一个数记录spfa跑的时间(慎用),当超过一定值强行认为存在 正/负权回路 。显然只是经验上的认为,并没有优化效果
- 在加边的时候就判断 是否存在自环 ,导致 正/负权回路 的产生,存在就直接否了无需在spfa里跑,自环多起来spfa很容易就会退化成 O ( n m ) O(nm) O(nm) 的时间复杂度
- 改队列为 dfs,类似判环的方式,遇到标记过的点就存在环(高效)。
其实还有一些高级的优化方法,但还未学到,日后再补充
spfa是有极限的呐,所以,我不做spfa了!
洛谷:P3275 [SCOI2011]糖果
差分约束解法:
代码:
#include<bits/stdc++.h>
#include<unordered_set>
#include<unordered_map>
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul u << 1
#define ur u << 1 | 1
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 100010, M = 200010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int h[N], e[M], ne[M], w[M], idx;
int dist[N], cnt[N], q[N];
bool st[N];
void add(int a, int b, int x) {
e[idx] = b, ne[idx] = h[a], w[idx] = x, h[a] = idx++;
}
bool spfa() {
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++)//需要保证每个小朋友都有至少一个糖果
q[tt++] = i, st[i] = true, dist[i] = 1;//不建源点,全入队
//若建源点会 TLE ,边的遍历顺序会影响速度
int step = 0;
while (hh != tt)
{
int t = q[hh++];//改成栈不一定就快了,只能说判断 存在 正/负权回路比较快
if (hh == N)hh = 0;
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 false;
if (!st[j]) {
st[j] = true;
q[tt++] = j;
if (tt == N)tt = 0;
}
}
}
}
return true;
}
int main() {
cinios;
cin >> n >> m;
mem(h, -1);
while (m--)
{
int x, a, b;
cin >> x >> a >> b;
if (x == 1)add(a, b, 0), add(b, a, 0);
else if (x == 2) {
if (a == b) { cout << -1; return 0; };//如不特判会 TLE
add(a, b, 1);
}
else if (x == 3)add(b, a, 0);
else if (x == 4) {
if (a == b) { cout << -1; return 0; };//自环会严重退化spfa的时间复杂度
add(b, a, 1);
}
else add(a, b, 0);
}
if (!spfa())cout << -1;
else {
ll sum = 0;//题目没给范围,但要开ll
for (int i = 1; i <= n; i++)sum += dist[i];
cout << sum;
}
return 0;
}
强连通缩点拓扑解法:
此题 正解 应该是 找加边的性质,缩点拓扑求解(保证不会超时)
思路:
对于1、3、5情况,要求至少多少糖果,显然两点连的边权值肯定要尽可能少,满足要求的情况下权值最小为 0
对于只有1、3、5情况建出来的图,即使存在环也是没关系的,环上每个点的糖果数相同即可
所以我们直接把1、3、5图进行一个强连通缩点,tarjan算法就可以轻松完成
这图肯定还不完全,2、4情况完全没加上呢,接下来的操作就是往 旧图 上加边,2、4情况的边权最小就不是 0 了,而是 1(贪心贪得无厌)。自然需要满足的是:
加上的这条边的两个端点所在的连通块必须不同
一旦相同,就代表原本这个连通块的 “平衡” 状态被打破了(即环上每个点的糖果数相同),出现了环中两点差值为 1 的情况。所以肯定满足不了孩子们的需求,只能输出 -1 了
全部2、4都满足情况,我们就对新图进行一个拓扑,每个点维护一个满足所有约束情况下的最小值,累计每个点的糖果数就是我们的答案了。
但如果拓扑的过程中遍历不到所有点,就代表添加2、4边的时候,这些边自身产生了环,导致 入度 减不下去点无法入队。这种情况也是输出 -1
总结以上,优美的代码就诞生了:
#include<bits/stdc++.h>
#include<unordered_set>
#include<unordered_map>
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul u << 1
#define ur u << 1 | 1
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 100010, M = 200010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int h[N], nh[N], e[M], ne[M], w[M], idx;
int dist[N], ru[N];//图的存储、每个人维护糖果数、入度
int dfn[N], low[N], tim;//标准tarjan算法需要的数组
int stk[N], top;
bool v[N];
int scc_cnt, id[N], siz[N];//记录强连通的个数、某点对应在哪个强连通、该强连通内部点数
struct edge
{
int l, r, w;
}ed[M];//记录边
void add(int* h, int a, int b, int x) {
e[idx] = b, ne[idx] = h[a], w[idx] = x, h[a] = idx++;
}
void tarjan(int x) { //模板
dfn[x] = low[x] = ++tim;
stk[++top] = x;
v[x] = true;
for (int i = h[x]; ~i; i = ne[i]) {
int j = e[i];
if (!dfn[j]) {
tarjan(j);
low[x] = min(low[x], low[j]);
}
else if (v[j])low[x] = min(low[x], dfn[j]);
}
if (dfn[x] == low[x]) {
int y;
scc_cnt++;
do
{
y = stk[top--];
v[y] = false;
id[y] = scc_cnt;
siz[scc_cnt]++;
} while (x != y);
}
}
bool top_sort() {
queue<int> q;
for (int i = 1; i <= scc_cnt; i++) { //每个强连通特判入队
dist[i] = 1;//每个人至少有 1 糖果
if (!ru[i])q.push(i);
}
int cnt = 0;
while (q.size())
{
int t = q.front();
q.pop();
cnt += siz[t];//记录总人数
for (int i = nh[t]; ~i; i = ne[i]) {
int j = e[i];
dist[j] = max(dist[j], dist[t] + w[i]);//维护一个最大值,得以满足每一种约束
if (--ru[j] == 0)
q.push(j);
}
}
return cnt == n;//总人数不足显然2、4边有环,不满足条件
}
int main() {
cinios;
cin >> n >> m;
mem(h, -1);//旧图
mem(nh, -1);//新图
for (int i = 1; i <= m; i++) {
int x, a, b;
cin >> x >> a >> b;
if (x == 1)add(h, a, b, 0), add(h, b, a, 0);//情况 1 必定产生自环,建新图时只有两个不在同一连通块的点才建边,所以无需记录
else if (x == 2)ed[k++] = { a,b,1 };
else if (x == 3)add(h, b, a, 0), ed[k++] = { b,a,0 };
else if (x == 4)ed[k++] = { b,a,1 };
else add(h, a, b, 0), ed[k++] = { a,b,0 };
//只有1、3、5先建旧图
//2、4记录下来,3、5也记录是为了在新图累计入度
}
for (int i = 1; i <= n; i++)//tarjan!
if (!dfn[i])tarjan(i);
for (int i = 0; i < k; i++) {
int a = ed[i].l, b = ed[i].r, w = ed[i].w;
if (w && id[a] == id[b]) {
cout << -1;
return 0;//2、4边存在(w==1)且在一个强连通内,满足不了要求
}
else if (id[a] != id[b]) { //否则不在一个块内才建边
add(nh, id[a], id[b], w);
ru[id[b]]++;
//新图建边,边是建在两个连通块之间的!入度记录的也是连通块的
}
}
if (!top_sort())cout << -1;
else {
ll sum = 0;
for (int i = 1; i <= scc_cnt; i++)
sum += (ll)siz[i] * dist[i];//一个强连通内的每个点糖果数都是一样的
cout << sum;
}
return 0;
}
—————————————————————————————————————
Acwing:区间
题意求最少,对应spfa内求最长路(可以当成结论记,反之亦然)
此题的限制为,满足 a i < = x < = b i a_i <= x <= b_i ai<=x<=bi 的整数 x 不少于 c i c_i ci个,可以用前缀和处理下,转化成 S b i − S a i − 1 > = c i S_{bi}-S_{ai-1}>=c_i Sbi−Sai−1>=ci
同时前缀和本身也满足 S i − S i − 1 > = 0 S_i - S_{i-1}>=0 Si−Si−1>=0 与 S i − S i − 1 < = 1 Si - S_{i-1}<=1 Si−Si−1<=1( S i S_i Si多且最多多1)
三条约束公式:
- S b i > = S a i − 1 + c i S_{bi}>=S_{ai-1}+c_i Sbi>=Sai−1+ci( a i − 1 a_{i-1} ai−1 向 b i b_i bi连一条权值为 c i c_i ci 的边)
- S i > = S i − 1 + 0 S_i >=S_{i-1}+0 Si>=Si−1+0 (同)
- S i − 1 > = S i − 1 S_{i-1}>=S_i -1 Si−1>=Si−1(注意 i 的取值范围)
确定完所有约束无遗漏,且满足spfa能遍历到每一个约束条件
此题必定有解,所以无需判环
代码:
#include<bits/stdc++.h>
#include<unordered_set>
#include<unordered_map>
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define cout_double(a) cout << setiosflags(ios::fixed) << setprecision(a)
#define sca scanf
#define pri printf
#define ul u << 1
#define ur u << 1 | 1
//#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 50010, M = 150010;
int INF = 0x3f3f3f3f, mod = 1e9 + 7;
ll LNF = 1000000000000000;
int n, m, k, T, S;
int h[N], ne[M], e[M], w[M], idx;
int dist[N];
int q[N];
bool st[N];
void add(int a, int b, int x) {
e[idx] = b, ne[idx] = h[a], w[idx] = x, h[a] = idx++;
}
void spfa() {
mem(dist, -0x3f);//不是判负环而是求最优解,所以需要dist参照,不能随意赋值
dist[0] = 0;
int hh = 0, tt = 1;
q[0] = 0;//此题加边使得可以从 0 跑遍任意约束
while (hh != tt)
{
int t = q[hh++];
if (hh == N)hh = 0;
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];
if (!st[j]) {
st[j] = true;
q[tt++] = j;
if (tt == N)tt = 0;
}
}
}
}
}
int main() {
cinios;
cin >> n;
mem(h, -1);
for (int i = 1; i <= 50001; i++) {
add(i - 1, i, 0);
add(i, i - 1, -1);
}
for (int i = 0; i < n; i++) {
int a, b, c;
cin >> a >> b >> c;
a++, b++;//整体右移一位,保证从1开始,方便前缀和
add(a - 1, b, c);
}
spfa();
cout << dist[50001];//求最少包含多少个数,取前缀和最后一位即可
return 0;
}
—————————————————————————————————————
Acwing:排队布局
此题约束关系较为显然,仔细推导即可
代码:
#include<bits/stdc++.h>
#include<unordered_set>
#include<unordered_map>
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul u << 1
#define ur u << 1 | 1
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 1010, M = 50010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int h[N], ne[M], e[M], w[M], idx;
//bug —— 捏嘛嘛的e[N],找了半个多小时,真的恶心
int dist[N], cnt[N], q[N];
bool st[N];
void add(int a, int b, int x) {
e[idx] = b, ne[idx] = h[a], w[idx] = x, h[a] = idx++;
}
int spfa()
{
mem(dist, 0x3f);
dist[1] = 0;//顺便求最短路,判负环对dist的值无要求
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++)//判负环要保证全部边(也就是点)都能遍历到
{
q[tt++] = i;
st[i] = true;
}
while (hh != tt)
{
int t = q[hh++];
if (hh == N) hh = 0;
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 -1;//不存在满足方案
if (!st[j])
{
q[tt++] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
if (dist[n] == INF)return -2;//距离可以任意大,因为没遍历到就代表无限制
return dist[n];
}
int main()
{
cinios;
cin >> n >> m >> k;
mem(h, -1);
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
if (a > b) swap(a, b);//固定顺序
add(a, b, c);
}
while (k--)
{
int a, b, c;
cin >> a >> b >> c;
if (a > b) swap(a, b);
add(b, a, -c);
}
for (int i = 2; i <= n; i++) add(i, i - 1, 0);
cout << spfa();
return 0;
}
/*
4 2 1
1 3 10
2 4 20
2 3 3
*/
—————————————————————————————————————
Acwing:雇佣收银员
巨难 较难的一道差分约束题,处理需要用 前缀和化简处理约束关系 外,还要通过 枚举将变量转化成常量
关系推导:
代码:
#include<bits/stdc++.h>
#include<unordered_set>
#include<unordered_map>
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul (u << 1)
#define ur (u << 1 | 1)
#define fx first
#define fy second
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 30, M = 110, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int h[N], ne[M], e[M], w[M], idx;
int dist[N], cnt[N], q[N];
int num[N], r[N];
bool st[N];
void add(int a, int b, int x) {
e[idx] = b, ne[idx] = h[a], w[idx] = x, h[a] = idx++;
}
//维护一个前缀和S,Si代表从0点 —— i点安排的收银员总数
void build(int c) { //c 代表的是前缀和 S24 == c,即最少的收银员数量为 c
mem(h, -1);
idx = 0;//每次枚举都要重建图
for (int i = 1; i <= 24; i++) {
add(i - 1, i, 0);
add(i, i - 1, -num[i]);
}
for (int i = 8; i <= 24; i++)add(i - 8, i, r[i]);
for (int i = 1; i <= 7; i++)add(i + 16, i, -c + r[i]);
//以上建边全都依据纸上推导出的不等式关系
//差分约束题,找到关系且不遗漏才是难点
add(0, 24, c), add(24, 0, -c);
//为了体现 c 这个定值,还得把 c 和点联系在一起,不然会遗漏信息
}
bool spfa(int c) {
build(c);
mem(dist, -0x3f);
mem(cnt, 0);
mem(st, 0);
int hh = 0, tt = 1;
q[0] = 0;
dist[0] = 0;
while (hh != tt)
{
int t = q[hh++];
if (hh == N)hh = 0;
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] > 24)return false;
//包含 0 点
if (!st[j]) {
st[j] = true;
q[tt++] = j;
if (tt == N)tt = 0;
}
}
}
}
return true;
}
int main() {
cinios;
cin >> T;
while (T--)
{
//整体右移,方便处理前缀和
for (int i = 1; i <= 24; i++)cin >> r[i];
mem(num, 0);
cin >> n;
while (n--)
{
cin >> m;
num[m + 1]++;
}
bool success = false;
for (int i = 0; i <= 1000; i++)//观察推导出的不等式
//发现S24可以通过枚举将其变成常量 c
//而且这个枚举从小到大,可以完美解决题目的需求
//“如果可行,最少需要的收银员数量”
if (spfa(i)) {
success = true;
cout << i << '\n';//找到的第一个满足条件必定最小
break;
}
if (!success)cout << "No Solution\n";
//若全部收银员工作都满足不了需求
}
return 0;
}
作者:姬雏今天吃什么
链接:https://www.acwing.com/activity/content/code/content/2207055/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
—————————————————————————————————————
差分约束帧素钛棒辣,我逐渐理解一切