图论:负环 / 01分数规划 / 差分约束 知识整理(附题)

负环:

如何在一个图中求负环?

通常都会用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~
此题的解法是:

  1. 理所当然的 观察出公式所具有的二分性质,如果二分中点 mid 满足 ∑ f i / ∑ t i > m i d ∑f_i/∑t_i > mid fi/ti>mid,说明答案 ans 可以更大,所以 mid 变大右移;反之左移
  2. 将公式推导转化, ∑ 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 fitimid>0(边权点权累计肯定是大于零的,所以不用变号)
  3. 将点权下放到连出去的边上,显然不会影响原来的路径权值统计;原来的边权为 t i ti ti,现在变为 f i − t i ∗ m i d fi - ti*mid fitimid,在这样的新路径上,公式 ∑ f i − ∑ t i ∗ m i d > 0 ∑f_i-∑t_i*mid > 0 fitimid>0 可以转化为 ∑ ( f i − t i ∗ m i d ) > 0 ∑{(fi-ti*mid)}>0 (fitimid)>0

在这里插入图片描述

由此你能惊奇地发现, ∑ ( f i − t i ∗ m i d ) > 0 ∑{(fi-ti*mid)}>0 (fitimid)>0 相当于统计新路径上边权之和是否大于0,而题目又要求路径在环上,所以就相当于统计环上的边权之和是否大于0

统计环上的边权之和是否大于0 == 判断环是否为正环

所以用spfa跑 最长路(因为判断正环,判断负环跑最短路)

此题的二分条件即:
跑最长路的情况下若存在环,倒推公式得, ∑ ( f i − t i ∗ m i d ) > 0 ∑{(fi-ti*mid)}>0 (fitimid)>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<=StSs1<=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的方法不限于:

  1. 不建立源点(推荐),把所有点初始入队就相当于建立源点了(一些数据在从源点更新到其他点时会特别慢)
  2. 队列改栈的方法(慎用),改栈在判断 正/负权回路 时效果优秀,但一旦不存在 正/负权回路 ,数据又比较大,需要输出答案时效果就很差了
  3. 定义一个数记录spfa跑的时间(慎用),当超过一定值强行认为存在 正/负权回路 。显然只是经验上的认为,并没有优化效果
  4. 在加边的时候就判断 是否存在自环 ,导致 正/负权回路 的产生,存在就直接否了无需在spfa里跑,自环多起来spfa很容易就会退化成 O ( n m ) O(nm) O(nm) 的时间复杂度
  5. 改队列为 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 SbiSai1>=ci

同时前缀和本身也满足 S i − S i − 1 > = 0 S_i - S_{i-1}>=0 SiSi1>=0 S i − S i − 1 < = 1 Si - S_{i-1}<=1 SiSi1<=1 S i S_i Si多且最多多1)

三条约束公式:

  1. S b i > = S a i − 1 + c i S_{bi}>=S_{ai-1}+c_i Sbi>=Sai1+ci a i − 1 a_{i-1} ai1 b i b_i bi连一条权值为 c i c_i ci 的边)
  2. S i > = S i − 1 + 0 S_i >=S_{i-1}+0 Si>=Si1+0 (同)
  3. S i − 1 > = S i − 1 S_{i-1}>=S_i -1 Si1>=Si1(注意 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
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

—————————————————————————————————————

差分约束帧素钛棒辣,我逐渐理解一切

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值