差分约束概念
如果一个系统由(n)个变量和(m)个约束条件组成,形成(m)个形如(x_i - x_j ≤ k)的不等式(i,j∈[1,n],k为常数),则称其为差分约束系统。亦即,差分约束系统是求解关于一组变量的特殊不等式组的方法。
求解差分约束系统,可以转化成图论的单源最短路径(或最长路径)问题。
观察(x_i - x_j <= c_k),会发现它类似最短路中的三角不等式(dis[v] <= dis[u] + w[u,v]),即(dis[v] - dis[u] <= w[u,v])。因此,以每个变量(x_i)为结点,对于约束条件(x_i - x_j <= c_k),连接一条边((j, i)),边权为(c_k)。我们再增加一个源点(s),(s)与所有定点相连,边权均为(0)。对这个图,以(s)为源点运行Bellman-ford算法(或SPFA算法),最终{(dis[i])}即为一组可行解。
解释:不等式的形式等同于图论问题中的最短路的求解过程,故将差分约束的不等式问题转换为图论问题
求变量的最大值或最小值(求解性问题)
源点要满足的条件:从源点出发,一定可以走到所有的边
结论:如果求的是最小值,则应该求最长路; 如果求的是最大值,则应该求最短路;
问题:如何转化(x_i <= C),其中(c)是一个常数,这类的不等式
方法:建立一个虚拟源点0,然后建立(0->i),长度是(c)的边即可。
以求(x_i)的最大值为例:求所有从(x_i)出发,构成的不等式链(x_i <= x_j+ c_1 <= x_k + c_2 + c_1 <= .... <= x_0 + c_1 + c_2 + ...),其中(x_0)是虚拟源点,初始值是已知的,即可求出(x_i)的一个范围的上界
最终(x_i)的最大值等于所有上界的最小值,原因见下图(类似短板效应)
综上,求变量的最大值(都是(<=)的不等式),即求所有上界中的最小值,即求图上最短路;同理,求变量最大值,即求图上最长路
求最短路时如果图上有负环,那么该变量误解;求最长路时如果图上有正环,则变量无解
求不等式组的可行解(判定性问题)
源点要满足的条件:从源点出发,一定可以走到所有的边
步骤:
- 先将每个不等式(x_i <= x_j + c_k),转化成一条从(x_j)走到(x_i),长度为(c_k)的一条边
- 找一个超级源点,使得该源点一定可以遍历到所有边
- 从源点求一遍单源最短路
结果1:如果存在负环,则原不等式组一定无解
结果2:如果没有负环,则dis[i]就是原不等式组的一个可行解
注: 为何条件要求源点一定要可以走到所有边,为何不是所有点?
每条边都是一个限制条件,差分约束为的是满足所有限制条件,所以必须保证所有边都能走到才能保证满足所有限定条件
如果某些点是孤立点,走到走不到都无所谓,走不到说明对该点没有限制,它的取值是任意而已
SPFA解法
- 求变量的最大值或最小值应用实例
题目描述
分析方法
由于本题求解的为最小值,故由题意可以得到以下方程组
但是,差分约束问题易错点就在于不等关系找的不全面,本题中还有一个容易忽略的要求每个小朋友都要分到糖果。即,如果设(s[i])为小朋友(i)分到的糖果数量,还应有关系(s[i] >= 1)。为了满足差分约束形式的要求,可以设定一个值为0的点(s[0]),则上述不等关系式可写为(s[i] >= s[0] + 1)
之后考虑差分约束转换为图论问题的条件从源点出发,一定可以走到所有的边。显然,(s[0])即可满足要求。故从(s[0])开始进行spfa(因要判断是否有解,即图中是否存在正环),将所有小朋友的糖果数量相加即为最终答案
代码实现
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <stack>
using namespace std;
using LL = long long;
const int N = 1e5 + 10, M = 3e5 + 10;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
stack<int> q;
bool st[N];
int cnt[N], dis[N];
void add(int a, int b, int c)
{
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
bool spfa()
{
q.push(0);
st[0] = true;
while (q.size())
{
int t = q.top();
q.pop();
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int p = e[i];
if (dis[p] < dis[t] + w[i])
{
dis[p] = dis[t] + w[i];
cnt[p] = cnt[t] + 1;
if (cnt[p] >= n + 1) return true;
if (!st[p])
{
q.push(p);
st[p] = true;
}
}
}
}
return false;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
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) add(a, b, 1);
else if (x == 3) add(b, a, 0);
else if (x == 4) add(b, a, 1);
else add(a, b, 0);
}
for (int i = 1; i <= n; ++ i) add(0, i, 1);
if (spfa()) cout << -1 << endl;
else
{
LL sum = 0;
for (int i = 1; i <= n; ++ i) sum += dis[i];
cout << sum << endl;
}
return 0;
}
- 求不等式组的可行解
题目描述
分析方法
设(num[i])表示给定收银员中,开始工作时间为(i)的人数为(num[i])
(s[i])表示(R[0])到(R[i])对应时间段分配收银员的数量为(s[i])
(r[i])表示时间(i)要求的收银员数量为(r[i])
以上说法较为抽象,以样例为例(为了使用前缀和,数据整体向后迁移一位)
s[]是待求值
0, 23, 22, 1, 10 因前缀和转化为 1, 24, 23, 2, 11
num[1] | num[2] | num[3] | num[4] | num[5] | num[6] | num[7] | num[8] | num[9] | num[10] | num[11] | num[12] | num[13] | num[14] | num[15] | num[16] | num[17] | num[18] | num[19] | num[20] | num[21] | num[22] | num[23] | num[24] |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
1 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
r[1] | r[2] | r[3] | r[4] | r[5] | r[6] | r[7] | r[8] | r[9] | r[10] | r[11] | r[12] | r[13] | r[14] | r[15] | r[16] | r[17] | r[18] | r[19] | r[20] | r[21] | r[22] | r[23] | r[24] |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
由题意可得以下不等关系
-
(0 leq s_i - s_{i - 1} leq num[i] , 1 leq i leq 24)
-
(i geq 8 , s_i - s_{i - 8} geq r_i)
(0 < i < 7 , s_i + s_{24} - s_{i + 16} geq r_i)
推导后可得
-
(s_i geq s_{i - 1} + 0)
-
(s_{i - 1} geq s_i - num[i])
-
(i geq 8 , s_i geq s_{i - 8} + r_i)
-
(0 < i < 7 , s_i geq s_{i + 16} - s_{24} + r_i)
前3项均符合差分约束仅包含两个变量的形式要求,但第4项中的存在3个变量,其中,(s_{24})是要求解的值
正确的方法为从小到大遍历(s_{24})的所有可能取值,第一次满足所有不等式要求的值即为答案
此求解过程体现出的即为求不等式组的可行解
代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
const int N = 30, M = 100;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
bool st[N];
int cnt[N], dis[N];
int x[N], s[N], r[N], num[N];
queue<int> q;
void add(int a, int b, int c)
{
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void build(int x)
{
idx = 0;
memset(h, -1, sizeof h);
add(0, 24, x), add(24, 0, -x); // 规定s[24] = x,相当于也是添加的一个限定条件,s[24] = x <=> s[24] >= x && s[24] <= x
// 这里其实没必要判断r[i]是否为0,因为即使为0也不过是一个>=0的限定条件
// for (int i = 1; i <= 24; ++ i)
// if (r[i])
// {
// if (i >= 8) add(i - 8, i, r[i]);
// else add(i + 16, i, r[i] - x);
// }
for (int i = 1; i < 8; ++ i) add(i + 16, i, r[i] - x);
for (int i = 8; i <= 24; ++ i) add(i - 8, i, r[i]);
for (int i = 0; i <= 23; ++ i)
{
add(i, i + 1, 0);
add(i + 1, i, -num[i + 1]);
}
}
bool spfa(int x)
{
build(x); // 将x作为s[24]构造一个图
memset(st, 0, sizeof st);
memset(dis, -0x3f, sizeof dis);
memset(cnt, 0, sizeof cnt);
dis[0] = 0;
q.push(0);
st[0] = true;
while (q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int p = e[i];
if (dis[p] < dis[t] + w[i])
{
dis[p] = dis[t] + w[i];
cnt[p] = cnt[t] + 1;
if (cnt[p] >= 24) return true;
if (!st[p])
{
q.push(p);
st[p] = true;
}
}
}
}
return false;
}
int main()
{
int T;
cin >> T;
while (T --)
{
for (int i = 1; i <= 24; ++ i) cin >> r[i];
cin >> n;
memset(num, 0, sizeof num);
while (n --)
{
int t;
cin >> t;
++ t;
++ num[t];
}
bool flag = false;
// 判断给定s[24]的合法性,从小到大第一次合法的即为答案要求的最小值
for (int i = 0; i <= 1000; ++ i) // 枚举s[24]
if (!spfa(i))
{
cout << i << endl;
flag = true;
break;
}
if (!flag) cout << "No Solution" << endl;
}
return 0;
}
Tarjan强连通分量缩点解法
SPFA在面对不同数据时的实际表现不稳定,为了保险起见可以采用Tarjan强连通分量缩点,复杂度比较稳定
题目描述
为了更好对比SPFA解法和Tarjan强连通分量解法,采用同一道题目进行讲解
算法思路
从宏观来看:
在有向有环图中,由于依赖关系并非线性排列的,存在环路,所以需要采用SPFA判断环路以及求解最值
但对于拓扑图,依赖关系都是单向的,如果按照拓扑序遍历所有点,即可以线性复杂度维护所有点的要求,最终求和即可
从细节上来看:
- 第一步首先需要将有向有环图转化为DAG,使用Tarjan缩点的过程参照之前的写法即可
- 建立一张缩点后的图,此过程应同时完成有无可行性解的验证
通用解决方案为统计每一个scc边权和,若边权和为正则代表存在正环,在本题中即为无解;但在本题中,能够保证边权非负,即只需存在一条正权边即代表存在正环即代表题目无解 - 按照拓扑序遍历(scc编号从大到小)整张图,维护每个点满足要求的值(Tarjan算法能够保证求得的scc编号值越大则对应优先级越高)
此时的遍历是在缩点后的图上进行的,会把一个scc等效为一个点,这样做带来的一个疑惑是一个scc中那么多点对其它scc内的点的更新结果都是一样的吗?为何可以用一个点等效一个scc的所有点
原因在于此时可以保证任意一个scc内边权均为0(若存在非0边权说明无解),因此对于(scc_a)中的点(p_1), (p_2),(scc_b)中的点(q_1),用(p_1)更新(q_1)和用(p_2)更新(q_1)得到的结果是相同的,因此可以用(scc_a)等效其中的所有点 - 遍历缩点后图中所有(scc),累计求和即可(注意求和是需要对所有点求和,需要用(scc)的值*(scc)中点的个数)
代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
#include <stack>
using namespace std;
using LL = long long;
const int N = 1e5 + 10, M = 6 * N; // 题目给定边数最多=2*N,从0出发的边数=N,需要建2次图,共6*N
int n, m;
int h[N], hs[N], e[M], ne[M], w[M], idx;
stack<int> stk;
bool in_stk[N];
int id[N], Size[N], scc_cnt;
int dfn[N], low[N], timestamp;
int dis[N];
void add(int *h, int a, int b, int c)
{
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk.push(u); in_stk[u] = true;
for (int i = h[u]; ~i; i = ne[i])
{
int p = e[i];
if (!dfn[p])
{
tarjan(p);
low[u] = min(low[u], low[p]);
}
else if (in_stk[p]) low[u] = min(low[u], dfn[p]);
}
if (dfn[u] == low[u])
{
int y;
++ scc_cnt;
do {
y = stk.top(); stk.pop();
id[y] = scc_cnt;
in_stk[y] = false;
++ Size[scc_cnt];
} while (y != u);
}
}
int main()
{
memset(h, -1, sizeof h);
memset(hs, -1, sizeof hs);
cin >> n >> m;
// 第一次建图
for (int i = 0; i < m; ++ i)
{
int t, a, b;
cin >> t >> a >> b;
if (t == 1) add(h, a, b, 0), add(h, b, a, 0);
else if (t == 2) add(h, a, b, 1);
else if (t == 3) add(h, b, a, 0);
else if (t == 4) add(h, b, a, 1);
else add(h, a, b, 0);
}
for (int i = 1; i <= n; ++ i) add(h, 0, i, 1);
// for (int i = 0; i <= n; ++ i)
// if (!dfn(i)) tarjan(i);
// 本题中,0号点为超级源点,从该点出发即可走到所有点,因此从0号点开始tarjan即可
tarjan(0);
bool flag = true;
for (int i = 0; i <= n; ++ i)
{
for (int j = h[i]; ~j; j = ne[j])
{
int p = e[j];
int a = id[i], b = id[p];
// 进行有无可行性解的验证,验证同一scc中是否存在正权边
if (a == b)
{
if (w[j] > 0)
{
flag = false;
break;
}
}
else
add(hs, a, b, w[j]); // 所求并非方案数,因此可以建立重边,无需判重
}
if (!flag) break;
}
if (!flag) cout << -1 << endl;
else
{
// 递推求解每个点符合要求的最小值
for (int i = scc_cnt; i >= 1; -- i)
for (int j = hs[i]; ~j; j = ne[j])
{
int p = e[j];
dis[p] = max(dis[p], dis[i] + w[j]);
}
LL sum = 0;
for (int i = 1; i <= scc_cnt; ++ i) sum += (LL)dis[i] * Size[i]; // 这里的点是一个个scc,统计要计算的所有点,因此需要*Size[i]
cout << sum << endl;
}
return 0;
}