4 月 27 日测试题解

4 月 27 日测试题解

最短路专场

T1 \({\color{green}{\text{100pts}}}\text{/100pts}\)

题意

给出 \(m\) 个变量与 \(n\) 个约束,每个约束形如以下三种中的一种;

  • \(x_i - x_j \le w\)
  • \(x_i - x_j \ge w\)
  • \(x_i - x_j = w\)

\(q\) 个询问,每个询问为形如 \((x_i, x_j)\) 的二元组,要求 \(\max(x_i - x_j)\) 的值,当可以取到正无穷时,输出 \(\texttt{Infinity}\)

对于 \(30\%\) 的数据,\(n, m \le 5\)
对于 \(100\%\) 的数据,\(m \le 200\)\(n \le 10^4\)\(q \le 10^6\)\(|w| \le 10^4\)

思路

\(\text{100pts}\)

这道题是差分约束的裸题。对于每个约束,我们通过手段将其归约到同一种类型,即形如 \(x \le y + C\) 的形式,然后联想到 SSSP 中松弛操作的数学基础:三角不等式。对于一条边 \(e = (u, v, w)\),我们有 \(dis_v \le dis_u + w\)

我们可以考虑建出图,对于每个形如上边一般形式的约束,建一条 \(y \to x\),权值为 \(C\) 的边,然后在上边跑最短路求出一组合法解。但此时,由于我们求的是最短路,得到的解事实上是满足约束条件的最大的一组解,于是满足了题目中的所有条件。

对于题目中的三种约束,我们有:

  • \(x_i - x_j \le w \Leftrightarrow x_i \le x_j + w\)
  • \(x_i - x_j \ge w \Leftrightarrow x_j \le x_i + (-w)\)
  • \(x_i - x_j = w \Leftrightarrow x_i \le x_j + w, x_j \le x_i + (-w)\)

题目中保证给出的约束条件不会相互冲突,于是出现可以取 \(+\infty\) 的情况只可能是题目中的第二种约束或两变量间没有约束的情况,特判一下即可。

可以添加一个超级源来跑,不过这道题要求的值较为特殊,对于一组询问 \((x_i, x_j)\),其答案应为以 \(x_j\) 为源点的 \(dis_{x_i}\) 值。

代码

\(\text{100pts}\)

#include <bits/stdc++.h>

using i64 = long long;

struct Graph {
  static constexpr i64 INF = 1e18;
  struct Edge {
    int u, v;
    i64 w;
    Edge(int _u, int _v, i64 _w) : u(_u), v(_v), w(_w) {}
  };
  int n;
  std::vector<Edge> edges;
  std::vector<std::vector<int>> e;
  
  Graph(int _n) : n(_n + 1) {
    e.resize(n);
    return;
  }
  
  void insert(int u, int v, int w) {
    edges.emplace_back(u, v, w);
    e[u].push_back(edges.size() - 1);
    return;
  }
  
  void add(int u, int v, int w) {
    insert(u, v, w), insert(v, u, w);
    return;
  }
  
  std::vector<i64> spfa(int s) {
    std::vector<i64> dis(n, INF);
    std::vector<bool> vis(n, false);
    std::queue<int> q;
    
    dis[s] = 0;
    q.push(s);
    while (!q.empty()) {
      int u = q.front();
      vis[u] = false;
      q.pop();
      for (auto i : e[u]) {
        int v = edges[i].v, w = edges[i].w;
        if (dis[v] > dis[u] + w) {
          dis[v] = dis[u] + w;
          if (!vis[v]) {
            vis[v] = true, q.push(v);
          }
        }
      }
    }
    
    return dis;
  }
};

int main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(0);
  
  constexpr i64 INF = 1e18;
 
  int n, m;
  std::cin >> n >> m;
  
  Graph g(n);
  for (int i = 0; i < m; i++) {
    int opt, u, v, w;
    std::cin >> opt >> u >> v >> w;
    if (opt == 0) {
      g.insert(v, u, w);
    } else if (opt == 1) {
      g.insert(u, v, -w);
    } else {
      g.insert(v, u, w);
      g.insert(u, v, -w);
    }
  }
  
  int q;
  std::cin >> q;
  
  std::vector<std::vector<i64>> dis(n + 1);
  for (int i = 1; i <= n; i++) {
    dis[i] = g.spfa(i);
  }
  
  while (q--) {
    int u, v;
    std::cin >> u >> v;
    if (dis[v][u] == INF) {
      std::cout << "Infinity\n";
    } else {
      std::cout << dis[v][u] << "\n";
    }
  }
  
  return 0;
}

T2 \({\color{green}{\text{100pts}}}\text{/100pts}\)

题意

有连接 \(n\) 个路口的 \(m\) 条双向公路,每个路口有一盏红绿灯,红绿灯各自的时长由 \(r\)\(g\) 两个序列分别给出,你可以在任意时间到达一个路口,但只有当前路口为绿灯时,你才可以离开。

给出源点和汇点,求从源到汇的最短时间。

对于 \(30\%\) 的数据,\(n \le 10\)\(m \le 20\)
对于 \(100\%\) 的数据,\(n \le 2.5 \times 10^5\)\(m \le 5 \times 10^5\)\(r_i, g_i \le 10^4\)\(w \le 10^6\)

思路

\(\text{100pts}\)

这道题就是普通 SSSP 问题的一个变体。

在松弛时,考虑当前松弛的节点处于红灯还是绿灯。如果是红灯的话,就补上一个等待时间即可。其他的就跟普通的 SSSP 一样了。

当然,你也可以拆点跑分层图,这样就不用讨论了。

这图的规模比较大,最好使用 Dijkstra,你要是能用 SPFA 卡过去也行。

代码

\(\text{100pts}\)

#include <bits/stdc++.h>

using i64 = long long;

namespace fastIO {
template<typename T>
void read(T &x) {
  x = 0;
  T f = 1;
  char c = getchar();
  while (!std::isdigit(c)) {
    if (c == '-') { f = -1; }
    c = getchar();
  }
  while (std::isdigit(c)) {
    x = 10 * x + c - '0';
    c = getchar();
  }
  x *= f;
  return;
}

template<typename T, typename ...arg>
void read(T &x, arg &...args) {
  read(x), read(args...);
  return;
}
}

struct Graph {
  struct Edge {
    int u, v;
    i64 w;
    Edge(int _u, int _v, i64 _w) : u(_u), v(_v), w(_w) {}
  };
  int n;
  std::vector<Edge> edges;
  std::vector<int> r, g;
  std::vector<std::vector<int>> e;
  
  Graph(int _n) : n(_n + 1) {
    e.resize(n);
    r.resize(n), g.resize(n);
    return;
  }
  
  void insert(int u, int v, int w) {
    edges.emplace_back(u, v, w);
    e[u].push_back(edges.size() - 1);
    return;
  }
  
  void add(int u, int v, int w) {
    insert(u, v, w), insert(v, u, w);
    return;
  }
  
  std::vector<i64> dijkstra(int s, int t) {
    std::vector<i64> dis(n, 1e18);
    std::vector<bool> vis(n, false);
    std::priority_queue<
      std::pair<int, int>,
      std::vector<std::pair<int, int>>,
      std::greater<std::pair<int, int>>
    > pq;
    
    dis[s] = 0, pq.emplace(0, s);
    
    while (!pq.empty()) {
      int u = pq.top().second;
      pq.pop();
      if (vis[u]) {
        continue;
      }
      vis[u] = true;
      for (auto i : e[u]) {
        int v = edges[i].v;
        i64 w = edges[i].w, cur = dis[u] + w;
        
        if (v != t && r[v] + g[v] && cur % (r[v] + g[v]) <= r[v]) {
          cur += (r[v] - cur % (r[v] + g[v]));  
        }
        
        if (dis[v] > cur) {
          dis[v] = cur;
          pq.emplace(dis[v], v);
        }
      }
    }
    
    return dis;
  }
};

int main() {
  int n, m, s, t;
  fastIO::read(n, m, s, t);
  
  Graph g(n);
  for (int i = 1; i <= n; i++) {
    fastIO::read(g.r[i], g.g[i]);
  }
  
  for (int i = 0; i < m; i++) {
    int u, v;
    i64 w;
    fastIO::read(u, v, w);
    g.add(u, v, w);
  }
  
  g.add(0, s, 0);
  auto dis = g.dijkstra(0, t);
  
  printf("%lld\n", dis[t]);
  
  return 0;
}

T3 \({\color{orange}{\text{80pts}}}\text{/100pts}\)

题意

给出一张 DAG,每个点的出边权值相同,你可以任意改变任意一个点所有出边的权值,还需要回答以下问题:

  1. 该图的最长路;
  2. 能影响该图最长路的点的个数;
  3. 能影响该图最长路的所有点。

有三问,但是没有 SPJ,全部回答正确才得分。

\(n \le 10^5\)

思路

\(\text{50pts}\)

这部分数据保证 \(n \le 1000\)

求最长路是简单的,用拓扑跑一遍即可,我们重在解决第二三问。

这部分的数据范围在暗示你写 \(O(n^2)\) 的算法,于是你考虑枚举每个点,减小它出边的权值,然后再跑一遍拓扑来检验最长路是否有变短即可。若令 \(n\)\(m\) 同级,则时间复杂度为 \(O(n^2)\)

\(\text{100pts}\)

最长路的求法当然不变不然你给我写个更优的出来

不难看出,第二问实际上求的是所有最长路都经过的点,证明是真的很显然,所以就不给了。

于是你把所有最长路上的店与边抠出来塞到一个新图里,当然这新图也是个 DAG,然后再跑 BFS 求出每条路径经过每个点的次数,若一个点被标记过的次数等于最长路数量,那它就是关键的。

当然也可以把它抠出来改成一个无向图跑割点,本质是一样的。

时间复杂度 \(O(n + m)\) 带超大常数。

代码

\(\text{100pts}\)

/**
 * @file    
 * @author  ForgotDream
 * @brief   
 * @date    2023-04-28
 */
#include <bits/stdc++.h>

using i64 = long long;
using i64u = unsigned long long;
using f80 = long double;

namespace Helper {
void useFileInuput() {
  #ifndef ONLINE_JUDGE
  freopen("tmp.in", "r", stdin);
  freopen("tmp.out", "w", stdout);
  #endif
  return;
}

void debug(const std::string &s) {
  std::clog << s << "\n";
  return;
}
}

struct Graph {
  static constexpr int INF = 1e9;
  struct Edge {
    int u, v, w;
    Edge(int _u = 0, int _v = 0, int _w = 0) : u(_u), v(_v), w(_w) {}
  };
  int n;
  std::vector<Edge> edges;
  std::vector<std::vector<int>> e;
  std::vector<int> in;

  Graph(int _n) : n(_n) {
    e.resize(n);
    in.resize(n);
    return;
  }

  void add(int u, int v, int w) {
    edges.emplace_back(u, v, w);
    e[u].push_back(edges.size() - 1);
    return;
  }

  std::vector<int> topoSort() {
    std::vector<int> dis(n, -INF);
    std::queue<int> q;

    q.push(0), dis[0] = 0;
    while (!q.empty()) {
      int u = q.front();
      q.pop();
      for (auto i : e[u]) {
        int v = edges[i].v, w = edges[i].w;
        if (!--in[v]) {
          q.push(v);
        }
        dis[v] = std::max(dis[v], dis[u] + w);
      }
    }

    return dis;
  }

  void addIn(int u) {
    in[u]++;
    return;
  }

  std::vector<int> solve() {
    std::vector<int> res, dfn(n), low(n);
    int clk = 0;

    std::function<void(int, int)> tarjan = [&](int u, int rt) {
      low[u] = dfn[u] = ++clk;
      int child = 0;

      for (auto i : e[u]) {
        int v = edges[i].v;
        if (!dfn[v]) {
          tarjan(v, rt);
          low[u] = std::min(low[u], low[v]);
          child += (u == rt);
          if (low[v] >= dfn[u] && u != rt) {
            res.push_back(u);
          }
        } else {
          low[u] = std::min(low[u], dfn[v]);
        }
      }

      if (child >= 2 && u == rt) {
        res.push_back(u);
      }

      return;
    };

    tarjan(0, 0);

    return res;
  }
  
};

signed main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);

  int n;
  std::cin >> n;

  Graph g(n + 2), rev(n + 2);
  // rev is the reverse graph of the original graph.
  // it's used to build another graph which only contains the special vertexes and edges
  // on the longest path of the original graph.
  std::vector to(n + 2, std::vector<int>());
  std::vector<int> t(n + 1);
  for (int i = 1; i <= n; i++) {
    int c;
    std::cin >> t[i] >> c;
    if (c == 0) {
      g.add(0, i, 0), rev.add(i, 0, 0);
      g.addIn(i);
    } else {
      while (c--) {
        int v;
        std::cin >> v;
        to[v].push_back(i);
        g.addIn(i);
      }
    }
    g.add(i, n + 1, t[i]), rev.add(n + 1, i, t[i]);
    g.addIn(n + 1);
  }

  for (int i = 1; i <= n; i++) {
    for (auto j : to[i]) {
      g.add(i, j, t[i]), rev.add(j, i, t[i]);
    }
  }

  auto dis = g.topoSort();

  Graph rst(n + 2);  // the graph mentioned above.
  std::queue<int> q;

  q.push(n + 1);
  while (!q.empty()) {
    int u = q.front();
    q.pop();
    for (auto i : rev.e[u]) {
      int v = rev.edges[i].v, w = rev.edges[i].w;
      if (dis[u] == dis[v] + w) {
        rst.add(u, v, w), rst.add(v, u, w);
        q.push(v);
      }
    }
  }

  auto ans = rst.solve();
  std::sort(ans.begin(), ans.end());

  std::cout << dis[n + 1] << "\n";
  std::cout << ans.size() << "\n";
  for (int i = 0; i < ans.size(); i++) {
    std::cout << ans[i] << " \n"[i == ans.size() - 1];
  }

  return 0;
}

Epilogue

我为什么只得了 80 呢?因为我是直接记录了每个点的前驱,而没有考虑到一个点可能有多个前驱,但是运气还是比较好,混到了 80。

posted @ 2023-04-29 19:54  ForgotDream  阅读(44)  评论(0)    收藏  举报