NOIP 模拟赛 3 比赛总结

分数:\(100 + 60 + 0 + 10 = 170\)

T4 最终结果没取模,挂了 60 分!永康喵喵又翻车了!

T1

一道不太水的水题。

这道题要求满足 \(1 \le i < j < k < l \le n\)\(a_i \oplus a_j \oplus a_k \oplus a_l = 0\) 的四元组数量。很明显要用到异或的一个性质,即 \(\forall \ n \in \mathrm{\mathbf{Z}}, n \oplus n = 0\)。所以,我们可以先暴力计算出所有二元组的异或值,将它们存入桶(以下记为 \(b\))中,此时如果忽略特殊情况,答案就是 \(\sum\limits_{i=1}^{\max(a_i \oplus a_j)}\dfrac{(b_i-1)b_i}{2}\)。但是注意到可能会出现类似 \((x, x, y, z)\) 的错误四元组(由 \((x, y)\)\((x, z)\) 两个二元组结合而成),这是 \(a_y = a_z\) 造成的,所以需要去重。对于每两个重复的数,会产生 \(n-2\) 个错误四元组。设 \(a\) 中每个数 \(i\) 出现的的次数为 \(c_i\),则总共需要去除 \(\left(\sum\limits_{i=1}^{max(a_i)}\dfrac{(c_i-1)c_i}{2}\right)(n-2)\) 个错误二元组。此外考虑四元组 \((i, j, k, l)\) 可能由三对二元组组合而成:\((i, j)\)\((k, l)\)\((i, k)\)\((j, l)\),以及 \((i, l)\)\((j, k)\),因此最终答案需要除以 \(3\)

#include <bits/stdc++.h>
#define int long long
const int N = 5010;
int n, a[N];
int xorBuc[(int)1.5e6], numBuc[(int)1e6+10];
int ans;

int calc(int x) {
  if (x == 0 || x == 1) return 0;
  x--;
  return (1 + x) * x / 2;
}

signed main() {
  freopen("xor.in", "r", stdin);
  freopen("xor.out", "w", stdout);
  std::ios::sync_with_stdio(false); std::cin.tie(0);
  std::cin >> n;
  int numMx = 0;
  for (int i = 1; i <= n; i++) {
    std::cin >> a[i];
    numBuc[a[i]]++;
    numMx = std::max(numMx, a[i]);
  }
  int xorMx = 0;
  for (int i = 1; i <= n; i++) {
    for (int j = i + 1; j <= n; j++) {
      xorBuc[a[i] ^ a[j]]++;
      xorMx = std::max(xorMx, a[i] ^ a[j]);
    }
  }
  for (int i = 0; i <= xorMx; i++) {
    ans += calc(xorBuc[i]);
  }
  for (int i = 0; i <= numMx; i++) {
    ans -= calc(numBuc[i]) * (n - 2);
  }
  std::cout << ans / 3 << '\n';
  return 0;
}

T2

据机房里的大佬说这是一道非常简单的分层图题,可惜我并没有学过。

分层图,可以理解为把图复制成好几份,然后垂直叠放。在这些图中有一些通道,可以上下穿梭。仔细阅读题目,我们就会发现,汽车的速度看似很复杂,实际上只可能是 \(2\) 的倍数。考虑到在每条边上经过的时间需要向上取整,且边长最多为 \(10^6 \approx 2^{20}\),因此太快的速度是没有意义的,最多只需要进行 \(20\) 次加速,因此我们只需要建 \(21\) 层图。首先我们按照正常建图的流程,水平地把图建好,并把从下往上数第 \(i\) 层图的边权除以 \(2^i\),向上取整。然后开始搭建垂直穿梭的“电梯”:在每个维修站处,向上搭一条边权为 \(c_i\) 的边;除第一层外,把每一条坑洼路段的终点改接到第一层(可以在最开始建图时完成,这么叙述是为了便于理解)。然后按照正常方法跑 Dijkstra 即可。


使用画图 3D 绘制的简陋分层图图示。红色结点代表维修站,绿色边代表加速通道,紫色边代表坑洼路段。

#include <bits/stdc++.h>
#define int long long
typedef long long ll;
const int N = 5e6+10;
struct Edge {
  int to, len;
};
std::vector<Edge> g[N];
int n, m, p, s, t, dis[N]; bool vis[N];
int getCeil(int a, int b) {
  if (a % b == 0) return a / b;
  else return a / b + 1;
}
struct Node {
  int u, dis;
  bool operator > (const Node &a) const {
    return dis > a.dis;
  }
};
std::priority_queue<Node, std::vector<Node>, std::greater<Node> > q;

signed main() {
  std::ios::sync_with_stdio(false); std::cin.tie(0);
  std::cin >> n >> m >> s >> t;
  for (int i = 1; i <= m; i++) {
    int u, v, w; char ch;
    std::cin >> u >> v >> w >> ch;
    g[u].push_back({v, w});
    if (ch == 'G') {
      for (int level = 1; level <= 20; level++) {
        g[n * level + u].push_back({n * level + v, getCeil(w, 1 << level)});
      }
    } else {
      for (int level = 1; level <= 20; level++) {
        g[n * level + u].push_back({v, getCeil(w, 1 << level)});
      }
    }
  }
  std::cin >> p;
  for (int i = 1; i <= p; i++) {
    int x, c;
    std::cin >> x >> c;
    for (int level = 0; level <= 19; level++) {
      g[n*level+x].push_back({n*(level+1)+x, c});
    }
  }
  for (int i = 1; i <= n * 21; i++) {
    dis[i] = 1e18;
  }
  dis[s] = 0;
  q.push({s, 0});
  while (!q.empty()) {
    auto u = q.top().u;
    q.pop();
    if (vis[u]) continue;
    vis[u] = true;
    for (auto &v : g[u]) {
      if (dis[u] + v.len < dis[v.to]) {
        dis[v.to] = dis[u] + v.len;
        q.push({v.to, dis[v.to]});
      }
    }
  }
  int ans = 1e18;
  for (int level = 0; level <= 20; level++) {
    ans = std::min(ans, dis[n * level + t]);
  }
  if (ans == 1e18) {
    std::cout << "-1\n";
  } else {
    std::cout << ans << '\n';
  }
  return 0;
}

如何判断何时应使用分层图?当在移动时,一些数据发生了变化(如速度、代价、油量)等,且变化的可能性不是很多(如本题的速度最多只有 \(21\) 种)时,很适合用分层图。

T3

这道题可以想到一个比较朴素的 DP 做法:设 \(f(i, j, k)\) 表示当前在 \((i, j)\),已经走了 \(k\) 步的方案数。很容易可以转移。

然而,这道题的 \(e\) 最大可达 \(10^9\),如果真的要转移这么多次的话,恐怕等我们 AFO 了,大样例也算不出来。然而,每一次的转移过程又是相同而枯燥的,就像是计算幂一样,把一个数一步一步地乘以本身。而幂是有快速幂算法的,那这道题能不能也利用类似的思想呢?

当然可以!不过需要亿点点前置知识。

1. 矩阵

矩阵这个名字听起来很高级,其实就是一个二维数组。我们通常用大写字母来表示矩阵,在其右下角标注长宽信息,如 \(A_{3 \times 4}\) 就是一个有三行四列的矩阵。

在表示矩阵的元素时,通常用一个中括号包裹。如

\[A_{3 \times 4} = \left[ \begin{matrix} 1 & 1 & 4 & 5\\ 1 & 4 & 1 & 9\\ 1 & 9 & 8 & 1\\ \end{matrix} \right] \]

2. 矩阵乘法

矩阵和数一样,是可以进行加法和乘法运算的。这里我们只讨论乘法。

两个矩阵若要相乘,则必须满足第一个矩阵的列数等于第二个矩阵的行数。乘积矩阵第 \(i\) 行第 \(j\) 列的元素等于左矩阵第 \(i\) 行与右矩阵的第 \(j\) 列对应元素之和,即 \((AB)_{ij} = \sum\limits_{k=1}^{n} a_{ik} b_{ij}\)

3. 矩阵快速幂

矩阵的幂的定义和整数的幂一模一样,都是重复乘以自身。两者快速幂的写法也非常类似(具体可以看稍后的代码)。

说了这么多,这道题和矩阵乘法究竟有什么关系呢?如果我们用一个包含布尔值的矩阵 \(B_{i, j}\) 来表示是否可以从状态 \(i\) 一步转移到状态 \(j\),初始状态为矩阵 \(A\),那么 \((AB^n)_{i, j}\) 就可以表示从状态 \(i\) 经过 \(n\) 步到达状态 \(j\) 的路径数。

在代码实现中,我们使用 struct 封装了一个矩阵,使得矩阵的运算更加方便快捷。JZ8 非常喜欢矩阵,因为他可封装性很好。

#include <bits/stdc++.h>
#define int long long
const int N = 110, MOD = 19260817;
int m;

struct Matrix {
  int n, m, a[N][N];
  inline Matrix(int nn = 0, int mm = 0) {
    n = nn, m = mm;
    for (int i = 1; i <= nn; i++) {
      for (int j = 1; j <= mm; j++) {
        a[i][j] = 0;
      }
    }
  }
  inline void reset(int nn = 0, int mm = 0) {
    *this = Matrix(nn, mm); // 从 AeeE5x 大神那儿学来的神秘科技
  }
  inline Matrix operator*(const Matrix &w) const {
    Matrix ans(n, w.m);
    if (m != w.n)
      return ans;
    for (int i = 1; i <= n; i++) {
      for (int j = 1; j <= w.m; j++) {
        for (int k = 1; k <= m; k++) {
          ans.a[i][j] = (ans.a[i][j] + a[i][k] * w.a[k][j] % MOD) % MOD;
        }
      }
    }
    return ans;
  }
  inline void operator*=(const Matrix &w) {
    *this = *this * w; // 第一个星号是取内容,第二个星号是乘号,别被弄晕了
  }
} matrixA, matrixB; // matrixB[i][j] == 1 表示可以从状态 i 一步移动到状态 j

signed main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(0);
  std::cin >> m;
  while (m--) {
    int op, a, b, c, d, e;
    std::cin >> op >> a >> b >> c >> d >> e;
    matrixA.reset(1, a * b);
    matrixB.reset(a * b, a * b);
    if (op == 0) {
      for (int x = 1; x <= a * b; x++) { // 遍历第一个矩阵
        int xRow = (x - 1) / b + 1;      // 向上取整除法
        int xCol = (x % b == 0 ? b : x % b);
        for (int y = x + 1; y <= a * b; y++) {
          int yRow = (y - 1) / b + 1;
          int yCol = (y % b == 0 ? b : y % b);
          if (xRow == yRow ||
              xCol ==
                  yCol /*车走直的移动方式,另外两种棋子同理,都是用判断条件表示移动方式*/)
            matrixB.a[x][y] = matrixB.a[y][x] = 1;
        }
      }
    } else if (op == 1) {
      for (int x = 1; x <= a * b; x++) {
        int xRow = (x - 1) / b + 1;
        int xCol = (x % b == 0 ? b : x % b);
        for (int y = x + 1; y <= a * b; y++) {
          int yRow = (y - 1) / b + 1;
          int yCol = (y % b == 0 ? b : y % b);
          if ((abs(xRow - yRow) == 2 && abs(xCol - yCol) == 1) ||
              (abs(xRow - yRow) == 1 && abs(xCol - yCol) == 2)) {
            matrixB.a[x][y] = matrixB.a[y][x] = 1;
          }
        }
      }
    } else {
      for (int x = 1; x <= a * b; x++) {
        int xRow = (x - 1) / b + 1;
        int xCol = (x % b == 0 ? b : x % b);
        for (int y = x + 1; y <= a * b; y++) {
          int yRow = (y - 1) / b + 1;
          int yCol = (y % b == 0 ? b : y % b);
          if (abs(xRow - yRow) == 2 && abs(xCol - yCol) == 2) {
            matrixB.a[x][y] = matrixB.a[y][x] = 1;
          }
        }
      }
    }
    matrixA.a[1][(c - 1) * b + d] = 1;
    while (e) {
      if (e & 1)
        matrixA *= matrixB;
      matrixB *= matrixB;
      e /= 2;
    } // 矩阵快速幂
    int ans = 0;
    for (int i = 1; i <= a * b; i++) {
      ans = (ans + matrixA.a[1][i]) % MOD;
    }
    std::cout << ans << '\n';
  }
  return 0;
}

T4

这道题有点过于超出我的能力范围了,先不补了。不过有一个教训:需要取模的题要在过程和结果中都取模,切忌忘了任何一个,不然你就会像我一样挂掉宝贵的 60 pts 并掉 1 点 rating。

posted @ 2025-11-07 17:59  JZ8  阅读(7)  评论(0)    收藏  举报