2025东华大学程序设计萌新挑战赛题解

质量远高于 23 年那场(逃

Problem A. 数字变换

答案就是数字对 9 取模,当然要判 0。

这个现象挺有趣的,可以同理推广到 \(p\) 进制。

对于任意一个 \(p\) 进制数字,我们都可以分解成:

\[\sum{c_i p^i} \]

由于 \(p^i \equiv 1 \pmod{p}\) 显然成立,所以我们得到:

\[\sum{c_i p^i} \equiv \sum{c_i} \pmod{p} \]

代码:

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  long long n;
  cin >> n;
  cout << ((n - 1) % 9 + 1) << endl;
  return 0;
}

Problem B. 月下梦城堡谜题

气笑了的一道题(逃

容易发现最后的运算顺序一定会退化成这两种情况:

\[((a\ ?\ b)\ ?\ c)\ ?\ d \]

\[(a\ ?\ b)\ ?\ (c\ ?\ d) \]

枚举中间的操作符就可以了。

Problem C. ACM自动机

bro 写这个题看错了,以为一行一个名字狂挂两发。

Problem D. 五彩斑斓的世界

这个题可以超进化一下,不要求是一个排列,只需要是一个置换也是可以计数的。

从置换的角度去看待排列,所有排列一定会构成若干个置换环。

对于一个长度为 \(n\) 的环的染色方案,由于它没有循环相同这个条件,很自然的就可以想到递推公式 \(A_n = m(m - 1)^{n - 1} - A_{n - 1}\),通项公式也好推就是:\(A_n = m(m - 1)^{n} + (-1)^{n}(m - 1)\)

这个时候乘上圆排列的计数 \((n - 1)!\) 就可以得到单个置换环的方案数了。

\(dp_n\) 为方案数。如果直接从 \(n\) 个数字里面随便选然后去算显然是会算重的,这里有两种处理方法:

  • 将置换环大小作为转移的阶段,以此规避算重的问题,但是这样转移方程会有点麻烦,而且复杂度是 \(O(n^2 \ln {n})\),并不是非常优秀。

  • 还是考虑直接在 \(dp_n\) 上转移,转移的时候强行将 \(n\) 纳入置换环的考虑中,然后从 \(n-1\) 个数中来选择去计算置换环,这样可以规避算重的问题。转移方程:

    \[dp_n = \sum_{k = 0}^{n - 1}{{{n - 1}\choose{k}}dp_{n - k - 1} A_{k + 1} k!} \]

现在考虑超进化一下这个题:\(p\) 不再是个排列了,而是一个单纯的序列,仅仅要求上界是 \(n\)。这样就不是构成若干置换环了,而是一个基环树森林

还是需要处理出单个环的计数问题,除此之外我们需要再做一次 DP 来处理出基环树的计数,转移套路其实和 \(dp_n\) 的转移相似

#include <iostream>
#include <random>
#define int long long

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;
// const int MOD = 998244353;
const int MOD = 20040403;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int inc(int x, int y) {
  return (x + y >= MOD ? x + y - MOD : x + y);
}

int dec(int x, int y) {
  return (x - y < 0 ? x - y + MOD : x - y);
}

int mul(int x, int y) {
  return 1ll * x * y % MOD;
}

int pow(int x, int p) {
  int ans = 1;
  while (p) {
    if (p & 1) ans = mul(ans, x);
    x = mul(x, x), p >>= 1;
  }
  return ans;
}

int t;
int fact[kMaxN];
int invf[kMaxN];
int calc[kMaxN];
int n, m;

int C(int n, int r) {
  return mul(fact[n], mul(invf[n - r], invf[r]));
}

int dp[kMaxN];

void solve() {
  cin >> n >> m;
  for (int i = 1; i <= n; i++) {
    calc[i] = inc(pow((m - 1) % MOD, i), mul((m - 1) % MOD, pow(dec(0, 1), i)));
    calc[i] = mul(calc[i], fact[i - 1]);
  }
  dp[0] = 1;
  for (int i = 1; i <= n; i++) {
    dp[i] = 0;
    for (int k = 0; k < i; k++) {
      dp[i] = inc(dp[i], mul(C(i - 1, k), mul(dp[i - k - 1], calc[k + 1])));
    }
  }
  cout << dp[n] << endl;
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  fact[0] = invf[0] = 1;
  for (int i = 1; i < kMaxN; i++) {
    fact[i] = mul(fact[i - 1], i);
  }
  invf[kMaxN - 1] = pow(fact[kMaxN - 1], MOD - 2);
  for (int i = kMaxN - 2; i >= 1; i--) {
    invf[i] = mul(invf[i + 1], i + 1);
  }
  // cerr << mul(invf[6], fact[6]) << endl;
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem E. 要开始了……吗?

简单的概率数学题。公式:

\[\sum_{i = 0}^{\min(a, k)} {\frac{{a\choose{i}}{b\choose{k-i}}}{{{a+b}\choose{k}}} \times \frac{a-i}{a+b-k}} \]

显然可以化简,但是我太懒了(

#include <iostream>
#include <random>
#define int long long

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;
const int MOD = 998244353;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int inc(int x, int y) {
  return (x + y >= MOD ? x + y - MOD : x + y);
}

int dec(int x, int y) {
  return (x - y < 0 ? x - y + MOD : x - y);
}

int mul(int x, int y) {
  return 1ll * x * y % MOD;
}

int pow(int x, int p) {
  int ans = 1;
  while (p) {
    if (p & 1) ans = mul(ans, x);
    x = mul(x, x), p >>= 1;
  }
  return ans;
}

int t;
int fact[kMaxN];
int invf[kMaxN];
int calc[kMaxN];
int n, m;

int C(int n, int r) {
  return mul(fact[n], mul(invf[n - r], invf[r]));
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  fact[0] = invf[0] = 1;
  for (int i = 1; i < kMaxN; i++) {
    fact[i] = mul(fact[i - 1], i);
  }
  invf[kMaxN - 1] = pow(fact[kMaxN - 1], MOD - 2);
  for (int i = kMaxN - 2; i >= 1; i--) {
    invf[i] = mul(invf[i + 1], i + 1);
  }
  int a, b, k;
  cin >> a >> b >> k;
  int ans = 0;
  for (int sa = 0; sa <= a; sa++) {
    int sb = k - sa;
    if (sb > b) continue;
    if (sb < 0) continue;
    ans = inc(ans, mul(mul(C(a, sa), C(b, sb)), mul(a - sa, pow(a - sa + b - sb, MOD - 2))));
  }
  ans = mul(ans, pow(C(a + b, k), MOD - 2));
  cout << ans << endl;
  return 0;
}

Problem F. 虚空输电

如果没有虚空输电这一步,这就是一个简单的多源最短路的问题。

每一个生产机器一开始就会被 \(d_i + i\oplus j\) 松弛一遍,显然我们只需要考虑需 \(i \oplus j\) 最小的进行松弛操作。

异或和最小是 trie 的经典应用,考虑贪心然后随便做做就出来了。

然后跑多源最短路……结束了。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <random>
#include <set>
#include <vector>
#define int long long

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int n, m;
int d[kMaxN];
std::vector<std::pair<int, int>> go[kMaxN];

int tot = 0;
signed nxt[kMaxN * 8][2];
bool vis[kMaxN];

void insert(int v) {
  int p = 0;
  for (int i = 20; i >= 0; i--) {
    auto& t = nxt[p][(v >> i) & 1];
    if (t == 0) t = ++tot;
    p = t;
  }
  vis[p] = true;
}

int query(int id) {
  int p = 0;
  int ans = 0;
  for (int i = 20; i >= 0; i--) {
    int ch = (id >> i) & 1;
    if (nxt[p][ch]) p = nxt[p][ch], ans <<= 1;
    else p = nxt[p][ch ^ 1], ans = ans << 1 | 1;
  }
  return ans;
}

int dis[kMaxN];

void dijkstra() {
  memset(dis, 0x3f, sizeof(dis));
  std::set<std::pair<int, int>> s;
  auto record = [&](int u, int d) {
    if (d >= dis[u]) return;
    s.erase({dis[u], u});
    dis[u] = d;
    s.insert({dis[u], u});
  };
  for (int i = 1; i <= n; i++) {
    if (d[i] == 0) record(i, 0);
    else record(i, query(i) + d[i]);
  }
  while (!s.empty()) {
    auto [d, u] = *s.begin();
    s.erase(s.begin());
    for (auto [v, w] : go[u]) {
      record(v, d + w);
    }
  }
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> m;
  for (int i = 1; i <= n; i++) {
    cin >> d[i];
    if (d[i] == 0) insert(i);
  }
  for (int i = 1, u, v, w; i <= m; i++) {
    cin >> u >> v >> w;
    go[u].push_back({v, w}), go[v].push_back({u, w});
  }
  dijkstra();
  for (int i = 1; i <= n; i++) {
    cout << dis[i] << endl;
  }
  return 0;
}

Problem G. 结界的巫女-easy

考察选手的读题能力。

Problem H. 结界的巫女-hard

神秘线性代数好题

异或是一个性质非常好的运算,它等价于加法对 \(2\) 取模。它良好的性质允许它和乘法运算一起构成向量空间(线性空间),这个空间好像有个名字叫布尔空间?于是我们可以套用很多线性代数的结论。

异或中,逆运算就是它自己,一个数的逆元也是它自己,这些性质都非常非常好。(所以我们可以失去大脑地写代码)

首先我们将原矩阵直接异或一下目标矩阵,这个操作的原因很显然自己思考。

所以一个最常见的套路就是构造一个 \(nm \times nm\) 的巨大线性方程组然后狠狠地高斯消元。当然这个方法是显然超时的。

假定我们已经知道了第一行的操作情况,记作 \(\mathbf{v} = \begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_m\end{bmatrix}\),那么第二行的操作情况一定由第一行的操作情况和第一行的数来决定。这启发我们可以将某一行的操作线性表示为上一行的操作情况,以此类推任意操作都可以被第一行的操作线性表出。

也就是说,记 \(s_{i, j}\)\((i, j)\) 是否操作,那么存在 \(\mathbf{c_{i, j}}\) 使得

\[s_{i, j} = \mathbf{c}^T\mathbf{v} = \begin{bmatrix}c_1 & c_2 & \cdots & c_m\end{bmatrix}\begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_m\end{bmatrix} \]

显然 \(\mathbf{c_1} = \begin{bmatrix}1 & 1 & 1 & \cdots\end{bmatrix}\)

容易发现:

\[s_{i, j} \oplus s_{i - 1, j} \oplus s_{i - 2, j} \oplus s_{i - 1, j - 1} \oplus s_{i - 1, j + 1} = a_{i - 1, j} \]

也就是说:

\[s_{i, j} = s_{i - 1, j} \oplus s_{i - 2, j} \oplus s_{i - 1, j - 1} \oplus s_{i - 1, j + 1} \oplus a_{i - 1, j} \]

合并同类项一下……

\[s_{i, j} = (\mathbf{c_{i - 1, j}} \oplus \mathbf{c_{i - 2, j}} \oplus \cdots)^T \mathbf{v } \oplus a_{i - 1, j} \]

这里会发现之前的思考是有问题的:因为你递推的过程中是受到 \(a_{i, j}\) 的影响的,\(s_{i, j}\) 不能简单的表示为 \(v_i\) 的线性组合。

官方题解的处理方式非常的讨巧:将 \(a_{i, j}\) 处理成之后最后一行有 \(1\) 的形式,这样在前面的递推过程中就完全不需要考虑 \(a_{i, j}\) 本身的影响。

我们处理的过程中可以单独去考虑这个多出来的数字,也可以直接将原本参与运算的向量扩展成为:

\[\mathbf{v'} = \begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_m \\ 1\end{bmatrix} \]

这样 \(s_{i, j}\) 就会表示为:

\[s_{i, j} = \begin{bmatrix}c_1 & c_2 & \cdots & c_m & c_{m + 1}\end{bmatrix}\begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_m \\ 1\end{bmatrix} \]

同时:

\[a_{i, j} = \begin{bmatrix}0 & 0 & \cdots & 0 & a_{i, j}\end{bmatrix}\begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_m \\ 1\end{bmatrix} \]

转移 \(\mathbf{c_{i, j}}\) 是有很多方法的,你可以直接 \(O(n)\) 的去转移,也可以用 bitset 优化转移。

这个时候我们递推得到了所有的 \(\mathbf{c_{i, j}}\),并且基于最后一行得到一个线性方程组:

\[\begin{bmatrix} (\mathbf{c_{n, 1}}\oplus \mathbf{c_{n, 2}} \oplus \mathbf{c_{n - 1, 1}})^T \\ (\mathbf{c_{n, 1}}\oplus \mathbf{c_{n, 2}} \oplus \mathbf{c_{n, 3}} \oplus \mathbf{c_{n - 1, 2}})^T \\ (\mathbf{c_{n, 2}}\oplus \mathbf{c_{n, 3}} \oplus \mathbf{c_{n, 4}} \oplus \mathbf{c_{n - 1, 3}})^T \\ \vdots \\ \end{bmatrix} \mathbf{v'} = \begin{bmatrix} a_{n, 1} \\ a_{n, 2} \\ a_{n , 3} \\ \vdots \end{bmatrix} \]

这个矩阵显然可以处理成关于 \(\mathbf{v}\) 的线性方程组,然后对着处理完的矩阵高斯消元就可以了。

  • 关于官方题解:

    题解中提到第一行的操作影响通过异或叠加,其实等价于我们之前对 \(s{i, j}\) 做递推的操作,这也是为什么题解要求一定要把前 \(n-1\) 行清零,不然影响将不只受到第一行的操作影响。

    官方题解枚举影响这个行为和我们转移 \(\mathbf{c_{i, j}}\) 这个行为是一致的

别看叭叭叭这么多,其实这个题代码非常短,就一个递推加高斯消元。

#include <bitset>
#include <cassert>
#include <iostream>
#include <random>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

using vec = std::bitset<1003>;

int dx[5] = {1, 0, -1, 0, 0};
int dy[5] = {0, 1, 0, -1, 0};

int n, m;
int a[kMaxN][kMaxN];
int cur, lst, llst;
vec vecs[3][kMaxN];
vec mat[kMaxN];

int opt[kMaxN][kMaxN];

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> m;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) cin >> a[i][j];
  }
  for (int i = 1; i <= n; i++) {
    for (int j = 1, w; j <= m; j++) cin >> w, a[i][j] ^= w;
  }
  cur = 0, lst = 2, llst = 1;
  for (int i = 1; i <= n; i++) {
    cur = (cur + 1) % 3, lst = (lst + 1) % 3, llst = (llst + 1) % 3;
    for (int j = 1; j <= m; j++) {
      if (i == 1) {
        vecs[cur][j] = 0;
        vecs[cur][j][j] = 1;
      } else {
        vecs[cur][j] = vecs[lst][j] ^ vecs[lst][j - 1] ^ vecs[llst][j] ^ vecs[lst][j + 1];
        vecs[cur][j][m + 1] = vecs[cur][j][m + 1] ^ a[i - 1][j];
      }
    }
  }
  for (int j = 1; j <= m; j++) {
    mat[j] = vecs[cur][j - 1] ^ vecs[cur][j] ^ vecs[cur][j + 1] ^ vecs[lst][j];
    mat[j][m + 1] = mat[j][m + 1] ^ a[n][j];
  }
  for (int i = 1; i <= m; i++) {
    if (mat[i][i] == 0) {
      for (int j = i; j <= m; j++) {
        if (mat[j][i] == 1) {
          std::swap(mat[i], mat[j]);
          break;
        }
      }
    }
    if (mat[i][i] == 0) {
      // cerr << "THIS IS A BAD INF" << endl;
      continue;
    }
    for (int j = 1; j <= m; j++) {
      if (i == j) continue;
      if (mat[j][i]) mat[j] ^= mat[i];
    }
  }
  for (int i = 1; i <= m; i++) {
    if (!mat[i][i] && mat[i][m + 1]) {
      cerr << "BAD" << endl;
    }
    if (mat[i][i]) {
      opt[1][i] = mat[i][m + 1];
    }
  }
  for (int i = 2; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      opt[i][j] = opt[i - 1][j] ^ opt[i - 2][j] ^ opt[i - 1][j - 1] ^ opt[i - 1][j + 1] ^ a[i - 1][j];
    }
  }
  std::vector<std::pair<int, int>> ans;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (opt[i][j]) {
        ans.push_back({i, j});
        for (int k = 0; k < 5; k++) {
          a[i + dx[k]][j + dy[k]] ^= 1;
        }
      }
      // cout << opt[i][j] << ' ';
    }
    // cout << endl;
  }
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      assert(a[i][j] == 0);
    }
  }
  cout << ans.size() << endl;
  for (auto [i, j] : ans) cout << i << ' ' << j << endl;
  return 0;
}

Problem I. 密室逃脱

考察选手的前缀和水平,直接贴代码吧,好像没什么好写的。

#include <iostream>
#include <random>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int n;
int a[101][101];

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
      cin >> a[i][j];
      a[i][j] += a[i - 1][j];
    }
  }
  int ans = 0;
  for (int up = 1; up <= n; up++) {
    for (int down = up; down <= n; down++) {
      for (int l = 1; l <= n; l++) {
        for (int r = l; r <= n; r++) {
          if (a[down][r] - a[up - 1][r] == a[n][r]) break;
          if (r - l > down - up) break;
          if (r - l == down - up) upmax(ans, (down - up + 1));
        }
      }
    }
  }
  cout << ans << endl;
  return 0;
}
posted @ 2025-11-30 19:32  sudoyc  阅读(5)  评论(0)    收藏  举报