8 月 9 日测试题解

发布时间 2023-08-09 18:58:03作者: ForgotDream

集体被大样例薄纱了。

T1 P1292

有两个容量分别为 \(a\) 与 \(b\) 的酒杯与一个容量无限的酒桶,有以下几种操作:

  1. 用酒桶将 \(a\) 倒满;
  2. 将 \(b\) 中的酒全部倒入酒桶;
  3. 将 \(b\) 中的酒倒入 \(a\),直到 \(a\) 被装满或 \(b\) 被倒空。

问 \(a\) 中可能倒出的最少的酒是多少以及分别至少需要操作 \(1\) 与操作 \(2\) 几次。
\(a, b \le 10^9\) 且保证 \(a \le b\)。

被大样例骗了,以为是结论题,结果喜提 \(\text{30pts}\)。

容易发现当 \(a \perp b\) 时,最小的酒量显然为 \(1\)。那么推广到 \(a \not \perp b\) 上,由于我们知道 \(\frac{a}{\gcd(a, b)} \perp \frac{b}{\gcd(a, b)}\),且这是满足关系的最小的一对数,那么就有最小酒量为 \(\gcd(a, b)\)。

再考虑倒酒的次数,这等价于求线性方程 \(ax + by = \gcd(a, b)\) 的最小的一组非负解。首先使用 exgcd 求出满足关系的一组解 \(x', y'\),又该方程的通解形式为 \(\cases{x = x' + k\frac{b}{\gcd(a, b)} \\ y = y' - k\frac{a}{\gcd(a, b)}}\),那么就可以不断拓展求得最小的解,时间复杂度 \(\mathcal{O}(\log n)\)。

#include <bits/stdc++.h>
using i64 = long long;
int a, b;
int exgcd(int a, int b, int &x, int &y) {
  if (!b) return x = 1, y = 0, a;
  int d = exgcd(b, a % b, y, x);
  y -= a/ b * x;
  return d;
}
signed main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
  std::cin >> a >> b;
  int x, y, g;
  std::cout << (g = exgcd(a, b, x, y)) << "\n";
  x *= -1, a *= -1;
  while (x < 0 || y < 0) {
    x += b / g * (x < 0);
    y -= a / g * (x >= 0);
  }
  std::cout << x << " " << y << "\n";
  return 0;
}

T2

一张点带权的图,定义删去一个点的代价为与其相连的所有点的点权和,求删去所有点的最小代价。
\(n \le 10^5\)。

不懂为什么要出这种仅靠猜答案就可以拿到满分的题。

观察样例可以发现最优方案显然为按点权从大到小删去,时间复杂度为 \(\mathcal{O}(n \log n)\)。

什么?你问我怎么得到的?猜的呗,OI 不需要证明!

如果你还不满足于上边的复杂度的话,容易发现对于任意一条边 \((u, v)\),其贡献为 \(\min(a_u, a_v)\),\(a\) 为点权。那么时间复杂度 \(\mathcal{O}(m)\)。

如果不会写代码的话建议退役或者发邮件给 Jeff Dean 让他帮你写。

T3

一张 \(n \times m\) 有障碍的网格图,现在给定起点 \(s = (sx, sy)\) 与 \(p\) 个点,要求经过这 \(p\) 个点的最短路以及 \(\max \sum_{(x, y) \in P} f(x, y)\),其中 \(f(x, y)\) 定义为网格图中以点 \((x, y)\) 为中心的最大无障碍正方形的边长的一半,\(P\) 为路径上的点集。
\(n, m \le 300\),\(p \le 15\)。

先考虑最短路。由于是不带权的网格图,其边权均为 \(1\),那么从一个点到另一个点的最短路容易用 BFS 在 \(\mathcal{O}(nm)\) 时间内求出。而经过全部 \(p\) 个点的最短路也是经典的状压 DP 问题 TSP 的常见变种之一,容易在 \(\mathcal{O}(p^2 2^p)\) 的时间内求解。

解决了最短路的问题之后再来看如何最大化 \(f\)。对于每一个点来说,\(f\) 都是独立的,因此我们可以先将其预处理出来。我的做法是考虑做四次递推,每次都求出对于每一个点以其作为正方形的四个端点之一的最大正方形边长,那么 \(f\) 便容易求出。这部分的时间复杂度为 \(\mathcal{O}(nm)\)。当然也有其他更为简便的做法,不过大概这种应该是比较好想的吧。

然后题目转化成了一个经典模型:有两种代价的最短(长)路。即在保证一种代价之和最优的情况下,尽量优化另一种代价。这是容易解决的,我们在 BFS 与 DP 的过程中多做一次判断即可。又由于这是一张无权的网格图,那么 BFS 过程不需要重新将某个点入队,即每一个点只需入队一次,因此复杂度仍然有保证。

总体时间复杂度为 \(\mathcal{O}(pnm + p^2 2^p)\)。

#include <bits/stdc++.h>
using i64 = long long;
using pii = std::pair<int, int>;
constexpr int N = 310, P = 16;
int n, m, t, a[N][N], sx, sy, p;
pii ed[P];
int enc[P];
int f[4][N][N], g[N * N];
std::vector<pii> adj[N * N];
int dis[P][N * N], val[P][N * N];
int dp1[1 << P][P], dp2[1 << P][P];
bool vis[N * N];
int encode(int i, int j) { return (i - 1) * m + j; }
pii decode(int i) { return std::make_pair(i / m + 1, i % m); }
void bfs(int s, int *dis, int *val, int siz = N * N) {
  std::queue<int> q;
  std::memset(dis, 0x3f, sizeof(int) * siz);
  std::memset(vis, 0, sizeof(vis));
  dis[s] = 0, val[s] = 0, q.push(s);
  vis[s] = true;
  while (!q.empty()) {
    int u = q.front();
    q.pop();
    for (auto [v, w] : adj[u]) {
      if (!vis[v]) {
        dis[v] = dis[u] + 1;
        val[v] = val[u] + g[v];
        q.push(v), vis[v] = true;
      } else if (dis[v] == dis[u] + 1 && val[v] < val[u] + g[v]) {
        val[v] = val[u] + g[v];
        q.push(v);
      }
    }
  }
}
signed main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
  std::cin >> n >> m >> t;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) std::cin >> a[i][j];
  }
  std::cin >> sx >> sy >> p;
  sx++, sy++;
  for (int i = 1; i <= p; i++) {
    auto &[l, r] = ed[i];
    std::cin >> l >> r, l++, r++;
  }
  int lx[4] = {1, 1, n, n}, ly[4] = {1, m, 1, m};
  int rx[4] = {n, n, 1, 1}, ry[4] = {m, 1, m, 1};
  int dx[4] = {1, 1, -1, -1}, dy[4] = {1, -1, 1, -1};
  for (int k = 0; k < 4; k++) {
    for (int i = lx[k]; i != rx[k] + dx[k]; i += dx[k]) {
      for (int j = ly[k]; j != ry[k] + dy[k]; j += dy[k]) {
        if (a[i][j]) continue;
        f[k][i][j] = std::min({f[k][i - dx[k]][j], f[k][i][j - dy[k]],
                               f[k][i - dx[k]][j - dy[k]]}) +
                     1;
      }
    }
  }
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i][j]) continue;
      int cur = encode(i, j);
      g[cur] = std::min({f[0][i][j], f[1][i][j], f[2][i][j], f[3][i][j]}) - 1;
      g[cur] = std::min(t, g[cur]);
    }
  }
  dx[0] = 1, dx[1] = -1, dx[2] = 0, dx[3] = 0;
  dy[0] = 0, dy[1] = 0, dy[2] = 1, dy[3] = -1;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i][j]) continue;
      for (int k = 0; k < 4; k++) {
        int cx = i + dx[k], cy = j + dy[k];
        if (a[cx][cy] || cx < 1 || cx > n || cy < 1 || cy > m) continue;
        adj[encode(i, j)].emplace_back(encode(cx, cy), 1);
      }
    }
  }
  ed[0] = std::make_pair(sx, sy);
  for (int i = 0; i <= p; i++) {
    enc[i] = encode(ed[i].first, ed[i].second);
    bfs(enc[i], dis[i], val[i]);
  }
  std::memset(dp1, 0x3f, sizeof(dp1));
  for (int i = 1; i <= p; i++) {
    dp1[1 << (i - 1)][i - 1] = dis[0][enc[i]];
    dp2[1 << (i - 1)][i - 1] = val[0][enc[i]];
  }
  for (int i = 0; i < 1 << p; i++) {
    for (int j = 0; j < p; j++) {
      if (!(i & (1 << j))) continue;
      for (int k = 0; k < p; k ++) {
        if (i & (1 << k)) continue;
        auto pre = dp1[i][j] + dis[j + 1][enc[k + 1]];
        if (dp1[i | (1 << k)][k] > pre) {
          dp1[i | (1 << k)][k] = pre;
          dp2[i | (1 << k)][k] = dp2[i][j] + val[j + 1][enc[k + 1]];
        } else if (dp1[i | (1 << k)][k] == pre &&
                   dp2[i | (1 << k)][k] < dp2[i][j] + val[j + 1][enc[k + 1]]) {
          dp2[i | (1 << k)][k] = dp2[i][j] + val[j + 1][enc[k + 1]];
        }
      }
    }
  }
  int ans1 = 0x3f3f3f3f, ans2 = 0;
  for (int i = 1; i <= p; i++) {
    if (dp1[(1 << p) - 1][i - 1] < ans1) {
      ans1 = dp1[(1 << p) - 1][i - 1];
      ans2 = dp2[(1 << p) - 1][i - 1];
    } else if (dp1[(1 << p) - 1][i - 1] == ans1) {
      ans2 = std::max(ans2, dp2[(1 << p) - 1][i - 1]);
    }
  }
  std::cout << ans1 << " " << ans2 + g[enc[0]] << "\n";
  return 0;
}

T4

给定一张 \(n\) 个点 \(m\) 条边的带权无向图,\(q\) 次询问,每次询问某个点到点 \(n\) 的最短路。
这里的路径长度定义稍有不同,设已经过的边集为 \(P\),\(g = \gcd_{i \in P} w_i\),当前边原始边权为 \(pre\),那么这条边的实际边权为 \(\frac{pre}{\gcd(pre, g)}\)。
\(n \le 1000\),\(n - 1 \le m \le 2000\),\(q \le 10^5\),最大边权 \(\max(w) \le 500\)。

赛时最后想了个很正确的分层图做法,是对的但是没有时间写了。

发现值域很小,那么我们考虑暴力分层图。将每个点拆成 \(500\) 个点,其中点 \((i, j)\) 表示走到 \(i\) 时边权 \(\gcd\) 为 \(j\) 的情况。容易发现 \((i, j)\) 只能向 \(\gcd\) 为 \(j\) 的因子的点转移,又由于题目要求的是到点 \(n\) 的最短路,那么可以考虑将图转置,即为求从点 \(n\) 出发的最短路,且 \((i, j)\) 只能向 \(j\) 的倍数转移。询问则直接暴力的枚举 \(\gcd\) 即可。

使用 Dijkstra 跑最短路,那么总体时间复杂度为 \(\mathcal{O}(n\max(w)\log n + q\max(w))\)。

#include <bits/stdc++.h>
using i64 = long long;
using pii = std::pair<int, int>;
using tiii = std::tuple<int, int, int>;
constexpr int N = 1050, M = 505;
int n, m, q;
std::vector<tiii> adj[N][M];
int dis[N][M];
bool vis[N][M];
void dijkstra(int s) {
  std::priority_queue<tiii, std::vector<tiii>, std::greater<tiii>> pq;
  std::memset(dis, 0x3f, sizeof(dis));
  std::memset(vis, 0, sizeof(vis));
  for (int i = 0; i <= 500; i++) pq.emplace(0, s, i), dis[s][i] = 0;
  while (!pq.empty()) {
    auto [_, u, i] = pq.top();
    pq.pop();
    if (vis[u][i]) continue;
    vis[u][i] = true;
    for (auto [v, k, w] : adj[u][i]) {
      if (dis[v][k] > dis[u][i] + w) {
        dis[v][k] = dis[u][i] + w, pq.emplace(dis[v][k], v, k);
      }
    }
  }
}
signed main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
  std::cin >> n >> m;
  for (int i = 1, u, v, w; i <= m; i++) {
    std::cin >> u >> v >> w;
    for (int j = 0; j <= 500; j++) {
      int g = std::__gcd(j, w);
      adj[v][g].emplace_back(u, j, w / g);
      adj[u][g].emplace_back(v, j, w / g);
    }
  }
  std::cin >> q;
  dijkstra(n);
  for (int i = 1, u; i <= q; i++) {
    std::cin >> u;
    int ans = 0x3f3f3f3f;
    for (int j = 1; j <= 500; j++) ans = std::min(ans, dis[u][j]);
    std::cout << ans << "\n";
  }
  return 0;
}