- 负环与差分约束
- 1. 基本概念、方法
- 1.1 负环
- 1.1.1 spfa 判负环/正环
- 1.1.2 tarjan+缩点 判断正环/负环
- 1.1.3 拓扑排序 判断正环/负环
- 1.2 差分约束
- 1.1 负环
- 2. 例题
- 2.1 负环/正环判定
- 2.1.1 spfa判断负环/正环
- 2.1.2 tarjan求scc+缩点判断正环/负环
- 2.1.3 拓扑排序判断正环/负环
- 2.2 差分约束
- 2.2.1 spfa差分约束
- 2.2.2 tarjan求scc + 缩点 + dp 差分约束
- 2.2.3 拓扑排序 差分约束
- 2.1 负环/正环判定
- 1. 基本概念、方法
负环与差分约束
1. 基本概念、方法
1.1 负环
1.1.1 spfa 判负环/正环
适用条件: 边权有正有负有零
判负环: 如果存在负环,那么spfa将一直跑不出结果,因此只需要考虑如果两个点之间有n个点,那么由抽屉原理,必然存在负环.常用的方法为spfa判断负环,该方法在一般的图中,复杂度为O(km),但理论时间复杂度为O(nm)
判正环: 判断正环的思路相反:在i和j之间跑最长路,一旦i和j之间的点的数目大于等于n,认为出现正环
技巧:
1.技巧1:有时候判断负环容易超时,因为一个要让两个点间的点数大于等于n的时候比较费时,所以可以去记录一下当前进入队列的点的总数count,一旦这个总数比较大的时候.比如这个点数count>=2n时,我们认为很大概率存在负环;
2.技巧2:把队列换成栈,一旦存在负环,那么使用栈来处理能够更快得到一个负环
spfa算法明确:
- 如果spfa只要求最短路,那么一开始要把所有点距离都初始化为0x3f,把源点放入队列,做标记,源点距离dist[s] = 0
- 如果spfa只要判负环,那么需要把所有点距离都初始化为0x3f, 同时所有点都放入队列。但这样求出的dis数组数值不对,只能表示相对关系
- 如果spfa既要求最短路,又要判负环,那么需要把所有点初始化为0x3f,同时所有点都放入队列,然后把源点做标记,源点距离dist[s] = 0
1.1.2 tarjan+缩点 判断正环/负环
适用条件: 边权全部>=0(或全部<=0)
判负环/正环: tarjan跑scc,然后缩点,判断每个超级点内是否存在大于0(小于0)的边,如果存在说明存在正环(负环)。
1.1.3 拓扑排序 判断正环/负环
适用条件: 边权全部>0(或全部<0)
判断正环/负环: 跑拓扑排序算法,如果最后拓扑序列内数目==n,那么有解,无正环/负环,否则存在正环/负环。
1.2 差分约束
差分约束问题就是求解一组不等式。当题目给定的条件可以转化为不等式组的时候就是求解差分约束。当求最小值,跑最长路;求最大值,跑最短路。同时一旦发现正(负)环那么无解。
差分约束的步骤:
- 根据题目条件,建图。求最小值,跑最长路,就转化为:xi>=xj+c,然后add(j, i, c);求最大值,跑最短路,就转化为xi<=xj+c,然后add(j, i, c)。
- 按照题目要求、边权情况,选择不同算法跑最短(长)路,由此导致了判断负(正)环的方法不同(见1.1):
① 如果边权有正有零有负,那么选择spfa来求最短(长)路,时间复杂度为O(km),且同时使用spfa判断负(正)环
② 如果边权都大于等于0(都小于等于0),那么选择tarjan求scc + 缩点 + dp求最长路(最短路),且同时使用tarjan判断正环(负环)
③ 如果边权都大于0,拓扑排序+dp求最长(短)路,同时直接拓扑排序判断正环(负环)。
2. 例题
2.1 负环/正环判定
2.1.1 spfa判断负环/正环
acwing904虫洞
判断图中是否存在负环
#include
using namespace std;
int const N = 5e2 + 10, M = 3e6 + 10;
int e[M], ne[M], w[M], idx, h[N], t, n, m, wi, dist[N], cnt[N], st[N];
// 建邻接表
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
// spfa求负环(正环)
bool spfa() {
queue q;
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
// 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
// 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
for (int i = 1; i <= n; i ++ ) {
st[i] = true;
q.push(i);
}
// dist[0] = 0, st[0] = 1, q.push(0); 如果希望能够正确求出dis数组,那么还需要加上这个代码
while (q.size()) {
int t = q.front(); // 取队首
q.pop(); // 出队首
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1; // 更新边数
if (cnt[j] >= n) return true; // 如果j点到源点的边数大于等于n
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
cin >> t;
while (t--) {
cin >> n >> m >> wi;
idx = 0;
memset(h, -1, sizeof h);
for (int i = 1, a, b, c; i <= m; ++i) {
scanf("%d %d %d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
for (int i = 1, a, b, c; i <= wi; ++i) {
scanf("%d %d %d", &a, &b, &c);
add(a, b, -c);
}
if (spfa()) printf("YES\n");
else printf("NO\n");
}
return 0;
}
acwing361观光奶牛
给定一张L个点、P条边的有向图,每个点都有一个权值f[i],每条边都有一个权值t[i]。求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。输出这个最大值。
点数N~1e3, 边数M~5e3
/*
本题是最大比率环+01分数规划
方法为二分 + 构图跑负环
即枚举二分枚举答案,然后根据这个mid来重新构图:把每个点和这个点对应的一条出边对应起来作为环上的一条边,
具体操作就是当对t点的所有出边进行更新的时候,原来的边权w[i],变为mid*w[i] - f[t]。这样把点权放到每个出边的边权上。
然后跑spfa判断是否存在负环即可
*/
#include
using namespace std;
int const N = 1e3 + 10, M = 5e5 + 10;
double const eps = 1e-8;
int e[M], ne[M], w[M], idx, h[N], n, m, cnt[N], st[N], f[N];
double dist[N];
// 建邻接表
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
// spfa求负环(正环)
bool spfa(double mid) {
queue q;
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
// 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
// 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
for (int i = 1; i <= n; i ++ ) {
st[i] = true;
q.push(i);
}
// dist[0] = 0, st[0] = 1, q.push(0); 如果希望能够正确求出dis
while (q.size()) {
int t = q.front(); // 取队首
q.pop(); // 出队首
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i] * mid - f[t]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
dist[j] = dist[t] + w[i] * mid - f[t]; // 边权发生改变
cnt[j] = cnt[t] + 1; // 更新边数
if (cnt[j] >= n) return true; // 如果j点到源点的边数大于等于n
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; ++i) scanf("%d", &f[i]);
for (int i = 1, a, b, c; i <= m; ++i) {
scanf("%d %d %d", &a, &b, &c);
add(a, b, c);
}
double l = 0, r = 1e9;
while (r - l > eps) {
double mid = (l + r) / 2;
if (spfa(mid)) l = mid;
else r = mid;
}
printf("%.2lf", l);
return 0;
}
acwing1165单词环
我们有 n 个字符串,每个字符串都是由 a∼z 的小写英文字母组成的。如果字符串 A 的结尾两个字符刚好与字符串 B 的开头两个字符相匹配,那么我们称 A 与 B 能够相连(注意:A 能与 B 相连不代表 B 能与 A 相连)。我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个环串(一个串首尾相连也算),我们想要使这个环串的平均长度最大。
n~1e5
/* 如果把每个单词当成一个点那么建图的时候会超时,因此把两个字母当成一个点,那么只有676个点
单词的长度当成边权,然后01分数规划处理
处理的时候边权变为w[i]-mid*f[i],然后判断是否存在正环,存在则mid太小 */
#include
using namespace std;
int const N = 27 * 27, M = 1e5 + 10;
double const eps = 1e-4;
int e[M], ne[M], idx, h[N], n, cnt[N], st[N], tt, rr, q[677];
double dist[N], w[M];
char s[1001];
// 建邻接表
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
// spfa求负环(正环)
bool spfa(double mid) {
stack q;
memset(dist, -0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
// 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
// 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
for (int i = 1; i <= 676; i ++ ) {
st[i] = true;
q.push(i);
}
// dist[0] = 0, st[0] = 1, q.push(0); 如果希望能够正确求出dis数组,那么还需要加上这个代码
while (q.size()) {
int t = q.top(); // 取队首
q.pop(); // 出队首
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[t] + w[i] - mid) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
dist[j] = dist[t] + w[i] - mid;
cnt[j] = cnt[t] + 1; // 更新边数
if (cnt[j] >= 676) return true; // 如果j点到源点的边数大于等于n
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
while (cin >> n && n != 0) {
idx = 0;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; ++i) {
scanf("%s", s);
int len = strlen(s);
if (len < 2) continue;
int a = (s[0] - 'a') * 26 + (s[1] - 'a' + 1);
int b = (s[len - 2] - 'a') * 26 + (s[len - 1] - 'a' + 1);
add(a, b, len);
}
double l = 0, r = 1e3;
while (r - l > eps) {
double mid = (l + r) / 2;
if (spfa(mid)) l = mid;
else r = mid;
}
if (fabs(l) < eps) printf("No solution\n");
else printf("%lf\n", l);
}
return 0;
}
2.1.2 tarjan求scc+缩点判断正环/负环
// 缩点建图(顺便判断是否有解)
bool success = true;
for (int i = 1; i <= n + 1; i ++ ) {
for (int j = h1[i]; ~j; j = ne[j]) {
int k = e[j];
int a = scc[i], b = scc[k];
if (a == b) {
if (w[j] > 0) {
success = false; // 存在正环
break;
}
}
else add(a, b, w[j], h2);
}
if (!success) break;
}
2.1.3 拓扑排序判断正环/负环
判断是否能够构成拓扑序列,能的话说明没有正环/负环,否则有。
2.2 差分约束
2.2.1 spfa差分约束
acwing362区间
给定 n 个区间 [ai,bi]和 n 个整数 ci。你需要构造一个整数集合 Z,使得∀i∈[1,n],Z 中满足ai≤x≤bi的整数 x 不少于 ci 个。求这样的整数集合 Z 最少包含多少个数。
n~5e4, ai,bi~5e4
/*
本题是考察差分约束
本题需要从0~50000中选出尽量少的整数,使得区间[ai, bi]内都有至少ci个数字被选
这里提供的条件为:
1.s[bi] - s[ai-1] >= ci
2.s[k] - s[k - 1] >= 0
3.s[k - 1] - s[k] >= -1
因此,我们需要-1~50000这50002个整数分别作为图中的节点
但是我们可以把整体向上加一,即把0~50001作为节点
*/
#include
using namespace std;
const int N = 50010, M = N * 3;
int n;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], 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 ++ ;
}
// spfa求负环(正环)
bool spfa() {
queue q;
memset(dist, -0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
// 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
// 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
for (int i = 1; i <= n; i ++ ) {
st[i] = true;
q.push(i);
}
dist[0] = 0, st[0] = 1, q.push(0); // 如果希望能够正确求出dis数组,那么还需要加上这个代码
while (q.size()) {
int t = q.front(); // 取队首
q.pop(); // 出队首
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[t] + w[i]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1; // 更新边数
if (cnt[j] >= N) return true; // 如果j点到源点的边数大于等于n
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
scanf("%d", &n);
memset(h, -1, sizeof h);
// 读入n个点
while (n -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
a ++, b ++; // 整体加一
add(a - 1, b, c); // 加边
}
// 把1~50001加边
for (int i = 1; i <= 50001; i ++ )
{
add(i - 1, i, 0);
add(i, i - 1, -1);
}
// 跑最长路
spfa();
printf("%d\n", dist[50001]);
return 0;
}
acwing1170排队布局
N头奶牛站成一排,有M1对关系和M2对关系。M1对关系希望A和B至多相隔L,M2对关系希望A和B至少相隔D。输出一个整数,如果不存在满足要求的方案,输出-1;如果 1 号奶牛和 N 号奶牛间的距离可以任意大,输出-2;否则,输出在满足所有要求的情况下,1 号奶牛和 N 号奶牛间可能的最大距离。
/*本题求解最大值,就是跑最短路,得到i<=j+c的关系建图,
然后spfa跑最短路,由于要求1号点到n号点的最短距离,所以直接把1号点作为源点
如果存在负环,那么输出-1;
如果不存在负环,但dis[n]=0x3f3f3f3f,那么-2
否则,输出dis[n]
本题需要注意的是,由于1号点可能为孤立点,因此如果直接把1号点放入队列,其他不放入队列,
那么可能判不出负环,因此需要把所有的点都放入队列。*/
#include
using namespace std;
const int N = 1010, M = 10000 + 10000 + 1000 + 10, INF = 0x3f3f3f3f;
int n, m1, m2;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int q[N], 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 ++ ;
}
// spfa求负环(正环)
bool spfa() {
queue q;
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
// 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
// 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
for (int i = 1; i <= n; i ++ ) {
st[i] = true;
q.push(i);
}
dist[1] = 0;
while (q.size()) {
int t = q.front(); // 取队首
q.pop(); // 出队首
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1; // 更新边数
if (cnt[j] >= N) return true; // 如果j点到源点的边数大于等于n
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
scanf("%d%d%d", &n, &m1, &m2);
memset(h, -1, sizeof h);
for (int i = 1; i < n; i ++ ) add(i + 1, i, 0);
while (m1 -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
if (a > b) swap(a, b);
add(a, b, c);
}
while (m2 -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
if (a > b) swap(a, b);
add(b, a, -c);
}
if (spfa()) printf("-1");
else {
if (dist[n] == 0x3f3f3f3f) printf("-2\n");
else printf("%d", dist[n]);
}
return 0;
}
acwing393雇佣收银员
24小时需要的收银员数目为r[0], r[1], ..., r[23]
有N个申请人,每个申请人可以工作8小时,问最少需要雇佣多少个收银员才能保证24小时不断营业
N~1e3
/*假设在第i小时开始工作的人有num[i]个,我们选择其中的xi个,那么有0<=xi<=num[i]
对于第i小时,需要r[i]个人,而能够在这个时刻工作的人有xi-7+xi-6+...+xi,要满足xi-7+xi-6+...+xi>=r[i]
则,整理上面式子得到:
记s为xi的前缀和
1. si>=si-1
2. si01>=si-num[i]
3. si>=si-8+r[i], i >=8
4. si>=s16+i+ r[i] - s24
那么我们去枚举s24,一旦发现当前s24的值建出来的图不存在正环,说明存在最小值。
同时,s24是定值,因此还需要添加s24>=c, s24<=c*/
#include
using namespace std;
int const N = 25, M = 24 * 5;
int e[M], ne[M], w[M], idx, h[N], t, n, wi, dist[N], cnt[N], st[N];
int num[N], r[N];
// 建邻接表
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void build(int s24) {
memset(h, -1, sizeof h);
idx = 0;
add(0, 24, s24), add(24, 0, -s24);
for (int i = 1; i <= 24; ++i) {
add(i - 1, i, 0), add(i, i - 1, -num[i]);
if (i >= 8) add(i - 8, i, r[i]);
if (i <= 7) add(16 + i, i, r[i] - s24);
}
}
// spfa求负环(正环)
bool spfa() {
queue q;
memset(dist, -0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
// 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
// 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
for (int i = 0; i <= 24; i ++ ) {
st[i] = true;
q.push(i);
}
// dist[0] = 0, st[0] = 1, q.push(0); 如果希望能够正确求出dis数组,那么还需要加上这个代码
while (q.size()) {
int t = q.front(); // 取队首
q.pop(); // 出队首
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[t] + w[i]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1; // 更新边数
if (cnt[j] >= 25) return true; // 如果j点到源点的边数大于等于n
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
cin >> t;
while (t--) {
memset(num, 0, sizeof num);
for (int i = 1; i <= 24; ++i) scanf("%d", &r[i]);
cin >> n;
for (int i = 1, k; i <= n; ++i) {
scanf("%d", &k);
k++;
num[k] ++;
}
bool success = false;
for (int i = 0; i <= n; ++i) {
build(i);
if (!spfa()) {
cout << i << endl;
success = true;
break;
}
}
if (!success) cout << "No Solution\n";
}
return 0;
}
2.2.2 tarjan求scc + 缩点 + dp 差分约束
acwing1169糖果
幼儿园里有 N 个小朋友,老师现在想要给这些小朋友们分配糖果,要求每个小朋友都要分到糖果。老师需要满足小朋友们的 K 个要求。老师想知道他至少需要准备多少个糖果。
要求有5种:
如果 X=1.表示第 A 个小朋友分到的糖果必须和第 B 个小朋友分到的糖果一样多。
如果 X=2,表示第 A 个小朋友分到的糖果必须少于第 B 个小朋友分到的糖果。
如果 X=3,表示第 A 个小朋友分到的糖果必须不少于第 B 个小朋友分到的糖果。
如果 X=4,表示第 A 个小朋友分到的糖果必须多于第 B 个小朋友分到的糖果。
如果 X=5,表示第 A 个小朋友分到的糖果必须不多于第 B 个小朋友分到的糖果。
N~1e5, K~1e5, 1 <=A, B <= N
/*
原来的思路是建图后,做差分约束,跑spfa,一旦发现出现正环那么无解,否则求出最长距离,然后累加,这种方法时间卡在spfa上,
spfa有可能跑出O(nm)的时间导致超时
由于数据比较特殊,只有0和1两种,那么可以换一个方法:
对于每一个环,它一定是属于scc,而只要出现1条边权为1的边那么就是出现正环,所有我们可以缩点后,判断每个scc内部是否出现
边权为1的边,一旦出现就是正环,无解;如果没有出现,那么有解,求完scc后缩点,然后按照缩点的逆序(拓扑序)进行dp,求出
最长链dis,然后答案就是每个超级点内点的个数*这个点的最长距离的累加值。
*/
#include
using namespace std;
typedef long long LL;
int const N = 1e5 + 10, M = 6e5 + 10;
// dfn记录每个点的时间戳,low记录每个点的回溯值,scc[i]=x表示i在标号为x的强连通分量里,stk维护一个栈,sccnum记录强连通分量的个数
int dfn[N], low[N], scc[N], stk[N], sccnum, top, timestamp;
int h1[N], h2[N], e[M], ne[M], idx, w[M];
int n, m;
int scc_count[N];
int dis[N];
// a->b有一条边
void add(int a, int b, int c, int h[])
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// tarjan算法求强连通分量
void tarjan(int root, int h[])
{
if (dfn[root]) return; // 时间戳不为0,返回
dfn[root] = low[root] = ++timestamp; // 记录当前点的时间戳和回溯值,初始化二者相同,而后dfn[root]>=low[root]
stk[++top] = root; // 把根放入栈内
for (int i = h[root]; i != -1; i = ne[i]) // 遍历每一个与根节点相邻的点
{
int j = e[i]; // 与i相邻的点为j
if (!dfn[j]) // j点没有访问过
{
tarjan(j, h); // 继续dfs,得到所有以j点为根的子树内所有的low和dfn
low[root] = min(low[root], low[j]); // 根的low是其子树中low最小的那个
}
else if (!scc[j]) // 如果j这个点还在栈内(在栈内的话不属于任何一个scc),同时一个栈内的点在一个scc内
{
low[root] = min(low[root], dfn[j]); // low代表所能到达的最小的时间戳
}
}
// 如果root的后代不能找到更浅的节点(更小的时间戳)
if (low[root] == dfn[root]) // 只有某个强连通分量的根节点的low和dfn才会相同
{
sccnum++;
while (1) // 出栈直到栈空
{
int x = stk[top--];
scc[x] = sccnum;
if (x == root) break;
}
}
}
int main()
{
cin >> n >> m;
memset(h1, -1, sizeof h1);
memset(h2, -1, sizeof h2);
// 建图
for (int i = 0, x, a, b; i < m; ++i) {
scanf("%d %d %d", &x, &a, &b);
if (x == 1) add(a, b, 0, h1), add(b, a, 0, h1);
else if (x == 2) add(a, b, 1, h1);
else if (x == 3) add(b, a, 0, h1);
else if (x == 4) add(b, a, 1, h1);
else if (x == 5) add(a, b, 0, h1);
}
// tarjan求scc
for (int i = 1; i <= n; ++i)
if (!dfn[i]) tarjan(i, h1);
// 计算每个强连通分量内点的个数
for (int i = 1; i <= n; ++i) scc_count[scc[i]] ++;
// 缩点建图(顺便判断是否有解)
bool success = true;
for (int i = 1; i <= n; i ++ ) {
for (int j = h1[i]; ~j; j = ne[j]) {
int k = e[j];
int a = scc[i], b = scc[k];
if (a == b) {
if (w[j] > 0) {
success = false;
break;
}
}
else add(a, b, w[j], h2);
}
if (!success) break;
}
// 做dp求最长路
if (!success) puts("-1");
else {
for (int i = sccnum; i; i--) dis[i] = 1;
for (int i = sccnum; i; i -- ) {
for (int j = h2[i]; ~j; j = ne[j]) {
int k = e[j];
dis[k] = max(dis[k], dis[i] + w[j]);
}
}
// 求答案
LL res = 0;
for (int i = 1; i <= sccnum; i ++ ) res += (LL)dis[i] * scc_count[i];
printf("%lld\n", res);
}
return 0;
}
2.2.3 拓扑排序 差分约束
acwing1192奖金
公司按照每个人的贡献给每个人发奖金。奖金存在M对关系,每对关系为a,b,表示a的奖金比b高。每位员工工资最少为100元,问最少需要发多少奖金。
/*
本题是差分约束的简化版,形成的边只有正权边
如果存在正环那么无解,换言之,如果不存在拓扑序则无解,因此可以使用拓扑排序来判断
如果有解,求出拓扑序后,直接按照拓扑序更新最长路即可
*/
#include
using namespace std;
int const N = 1e4 + 10, M = 2e4 + 10;
int n, m;
int din[N], dis[N];
int e[M], ne[M], h[N], idx;
vector ans;
// 拓扑排序
bool topsort()
{
queue q;
for (int i = 1; i <= n; ++i)
if (!din[i]) q.push(i);
while (q.size())
{
auto t = q.front();
q.pop();
ans.push_back(t);
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
din[j]--;
if (!din[j]) q.push(j);
}
}
return ans.size() == n;
}
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int main()
{
// 建图
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; ++i)
{
int a, b;
scanf("%d %d", &a, &b);
add(b, a);
din[a] ++;
}
// 拓扑排序判断是否有解
if (!topsort())
{
printf("Poor Xed\n");
return 0;
}
// 按照拓扑排序更新最长路
for (int i = 1; i <= n; ++i) dis[i] = 100;
for (int i = 0; i < n; ++i)
{
int t = ans[i];
for (int j = h[t]; ~j; j = ne[j])
{
int k = e[j];
dis[k] = max(dis[k], dis[t] + 1);
}
}
// 计算答案
int ans = 0;
for (int i = 1; i <= n; ++i) ans += dis[i];
cout << ans << endl;
return 0;
}