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;
}
posted @ 2023-02-10 15:23  Luckyblock  阅读(61)  评论(0编辑  收藏  举报