P7624 [AHOI2021初中组] 地铁
知识点:二分答案,差分约束
Link:Luogu
之前一直没怎么学差分约束,学到许多。
但最大的收获是学到了答案是一个区间的二分答案写法嘻嘻。
简述
有一个 \(n\) 个节点的简单环,满足相邻两节点的距离为正整数。给定 \(m\) 个限制条件,第 \(i\) 个限制条件的形式是如下形式之一:
- 环上顺时针由节点 \(s_i\) 到节点 \(t_i\) 的距离不小于一个给定的值 \(L_i\)。
- 环上顺时针由节点 \(s_i\) 到节点 \(t_i\) 的距离不大于一个给定的值 \(L_i\)。
求满足上述限制条件的简单环的总长度共有多少种合法取值。如果有无穷种取值输出
-1
,保证至少有一种可能的方案。
\(3\le n\le 500\),\(1\le m\le 500\),\(1\le L_i\le 10^9\)。
1S,256MB。
分析
先考虑如果已知环长 \(\operatorname{length}\),如何判断是否存在一组合法的方案使得限制满足。考虑差分约束。先套路地断环成链,断开边 \(n\rightarrow 1\)。设 \(\operatorname{dis}_i\) 表示从节点 1 到节点 \(i\) 的最短路长度,考虑差分约束:
-
对于相邻节点的限制,有:
\[\begin{aligned} \operatorname{dis}_i &\le \operatorname{dis}_{i+1} - 1\\ \operatorname{dis}_{n}&\le \operatorname{dis}_{1} + \operatorname{length} - 1 \end{aligned}\] -
对于限制类型 1,有:
\[\begin{aligned} \operatorname{dis}_{s_i} &\le \operatorname{dis}_{t_i} - L_i &(s_i < t_i)\\ \operatorname{dis}_{s_i} &\le \operatorname{dis}_{t_i} + \operatorname{length} - L_i &(s_i > t_i) \end{aligned}\] -
对于限制类型 2,有:
\[\begin{aligned} \operatorname{dis}_{t_i} &\le \operatorname{dis}_{s_i} + L_i &(s_i < t_i)\\ \operatorname{dis}_{t_i} &\le \operatorname{dis}_{s_i} - \operatorname{length} + L_i &(s_i > t_i) \end{aligned}\]
数据范围较小直接 Bellman-Ford 判负环即可。
在此基础上考虑如何求 \(\operatorname{length}\)。对于某个不合法的 \(\operatorname{length}\),我们考察图中某个负环的情况:
- 负环的权值和 \(s\) 可以表示成:\(s = k\times \operatorname{length} + \sum L_i <0\) 的形式。
- 保证保证至少有一种可能的方案,则 \(k\not= 0\),否则没有合法的 \(\operatorname{length}\)。
- 则我们可以通过调整 \(\operatorname{length}\) 的取值使得该负环消失。具体地,若 \(k>0\) 则令 \(\operatorname{length}\) 增大,否则减小。
- 由于所有负环的权值和均可以表示成这种形式,则形成负环的原因是 \(\operatorname{length}\) 过大或过小,则最终 \(\operatorname{length}\) 的取值范围一定是一段连续区间。
于是我们考虑二分答案来确定 \(\operatorname{length}\) 的上界和下界,对所有枚举到的 \(mid\) 都进行一次差分约束算法求负环,并按照上述方式进行调整即可。如果求得的上界过大,说明解有无穷种取值。
总复杂度 \(O(nm\log L)\) 级别。
实现上的细节
-
枚举上界的二分答案:
LL l, r; for (l = 0, r = kInf; l < r; ) { LL mid = (l + r + 1) >> 1ll; //这里是 +1,否则 TLE。 int ret = Check(mid); if (ret == 0) l = mid; //合法的上界 else if (ret == 1) l = mid + 1; else r = mid - 1; } maxsum = l; //上界
-
枚举下界的二分答案:
LL l, r; for (l = 0, r = kInf; l < r; ) { LL mid = (l + r) >> 1ll; //这里没有 +1,否则 TLE。 int ret = Check(mid); if (ret == 0) r = mid; //合法的下界 else if (ret == 1) l = mid + 1; else r = mid - 1; } minsum = l; //下界
-
Bellman-Ford 求负环并寻找负环的方法:
对于每个节点 \(v\),记录发生松弛的边 \(\operatorname{pre_v}\)。如果经过 \(n-1\) 轮松弛后边 \((u, v, w)\) 还能对 \(v\) 进行松弛,则说明存在某个能够到达节点 \(u\) 的负环(\(u\) 不一定在负环中)。为了找到负环中的点,我们可以从 \(u\) 开始连续跳 \(n\) 次 \(\operatorname{pre}_u\),则一定可以到达某个位于负环中的点,在该负环内转圈圈即可。
代码
//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <algorithm>
#define LL long long
const int kN = 510;
const int kM = kN << 2;
const LL kInf = 1e12 + 2077;
const LL kBigInf = 9e18 + 2077;
//=============================================================
int n, m;
LL minsum, maxsum, dis[kN], pre[kN];
int edgenum, u[kM], v[kM], k[kM], w[kM];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0';
return f * w;
}
void Add(int u_, int v_, int k_, int w_) {
u[++ edgenum] = u_;
v[edgenum] = v_;
k[edgenum] = k_;
w[edgenum] = w_;
}
void Init() {
n = read(), m = read();
for (int i = 1; i < n; ++ i) Add(i + 1, i, 0, -1);
Add(1, n, 1, -1);
for (int i = 1; i <= m; ++ i) {
int type = read(), s = read(), t = read(), l = read();
if (type == 1) {
if (s < t) Add(t, s, 0, -l);
else Add(t, s, 1, -l);
} else {
if (s < t) Add(s, t, 0, l);
else Add(s, t, -1, l);
}
}
}
int Check(LL sum_) {
for (int i = 1; i <= n; ++ i) dis[i] = kBigInf;
dis[1] = 0;
for (int j = 1; j <= n; ++ j) {
for (int i = 1; i <= edgenum; ++ i) {
int u_ = u[i], v_ = v[i], k_ = k[i], w_ = w[i];
if (dis[v_] > dis[u_] + k_ * sum_ + w_) {
dis[v_] = dis[u_] + k_ * sum_ + w_;
pre[v_] = i;
}
}
}
for (int i = 1; i <= edgenum; ++ i) {
int u_ = u[i], v_ = v[i], k_ = k[i], w_ = w[i];
if (dis[v_] > dis[u_] + k_ * sum_ + w_) {
for (int i = 1; i <= n; ++ i) u_ = u[pre[u_]];
int sumk = k[pre[u_]];
for (int y_ = u[pre[u_]]; y_ != u_; y_ = u[pre[y_]]) sumk += k[pre[y_]];
return sumk > 0 ? 1 : -1;
}
}
return 0;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
Init();
LL l, r;
for (l = 0, r = kInf; l < r; ) {
LL mid = (l + r + 1) >> 1ll;
int ret = Check(mid);
if (ret == 0) l = mid;
else if (ret == 1) l = mid + 1;
else r = mid - 1;
}
maxsum = l;
if (maxsum >= kInf) {
printf("-1\n");
return 0;
}
for (l = 0, r = kInf; l < r; ) {
LL mid = (l + r) >> 1ll;
int ret = Check(mid);
if (ret == 0) r = mid;
else if (ret == 1) l = mid + 1;
else r = mid - 1;
}
minsum = l;
printf("%lld\n", maxsum - minsum + 1);
return 0;
}