东华大学新生赛2023题解

好像没什么好写的……按照习惯还是写一下吧。

Problem A. 向夜晚奔去

小学数学题

Problem B. 偶像

本场最难的题目其实是(

如果字符串小于 \(10^5\),那么字符串前面直接丢一个 \(a\) 就可以了。

如果等于,那么丢 \(a\) 会导致构造不合法……这个时候就变成了构造哈希冲突。

构造哈希冲突其实是比较复杂的问题,貌似除了暴力搜索以外没有什么直观做法,考虑一个正常的位数,算一下会发现大概 \(8~12\) 位的字符串就足够满足碰撞的需求了,这个时候 \(998244353\) 中间每一个哈希值都被犁了不知道多少次了。

直接暴力是会死的非常惨的,但是可以折半搜索,然后就活下来了(

然后就结束了(

这里提供一个随机化思路:

考虑先构造部分哈希值丢到某个容器里,然后随机化一个新字符串 \(str\),然后在容器里查找是否可以拼接成需要的目标哈希值。假定容器元素数量为 \(S\),那么最后我们随机化新字符串的范围可以被压制到 \(S\) 之中。考虑生日悖论,会发现容器范围处在一个很合适的范围内的时候,我们能在很少的步骤内找到需要的 \(str\)。这个 \(S\) 理论上取 \(\sqrt{998244353}\) 是最优秀的,但是事实上可以更小一点。正确性比较神秘,但是速度非常快(比标程快了大概六十到一百倍左右)

#include <cassert>
#include <iostream>
#include <map>
#include <random>

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, B, n;
std::string str;

int hash(const std::string& str) {
  int ans = 0;
  for (auto ch : str) {
    ans = inc(mul(ans, B), ch - 'a');
  }
  return ans;
}

std::string rand(int n) {
  std::string str;
  while (n--) str += rand('a', 'z');
  return str;
}

void solve() {
  cin >> n >> B;
  cin >> str;
  if (n < 1e5) {
    return cout << 'a' << str << endl, void();
  }
  int h = hash(str), limit = 30000, Bt = pow(B, 6);
  std::map<int, std::string> mp;
  while (mp.size() < limit) {
    auto str = rand(6);
    int h = mul(hash(str), pow(B, 6));
    if (!mp.count(h)) mp[h] = str;
  }
  // cerr << "BEGIN SIR" << endl;
  while (1) {
    auto str = rand(6);
    int tmp = dec(h, hash(str));
    if (mp.count(tmp)) {
      std::string ans = mp[tmp] + str;
      cout << mp[tmp] << str << endl;
      break;
    }
  }
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem C. 群青

没有什么写的必要。

Problem D. 再多一点

这道题有 CF 原题,原题就是 CF706C

答案比较显然,就是简单的 \(DP\),状态设计 dp[i][0/1] 为是否翻转情况下的最小代价,转移也比较显然就不赘了。

唯一的问题就是它好像没讲清楚,并不是严格单增的字典序,因为有重复元素。

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

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

const char endl = '\n';
const int kMaxN = 2e5 + 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 t;
int n;
int dp[kMaxN][2];
int c[kMaxN];
std::string str[kMaxN], rev[kMaxN];

void solve() {
  cin >> n;
  for (int i = 0; i <= n + 1; i++) {
    dp[i][0] = dp[i][1] = 1e18;
  }
  for (int i = 1; i <= n; i++) cin >> c[i];
  for (int i = 1; i <= n; i++) {
    cin >> str[i], rev[i] = str[i];
    std::reverse(rev[i].begin(), rev[i].end());
  }
  dp[1][0] = 0, dp[1][1] = c[1];
  for (int i = 2; i <= n; i++) {
    if (str[i] >= str[i - 1]) upmin(dp[i][0], dp[i - 1][0]);
    if (rev[i] >= str[i - 1]) upmin(dp[i][1], dp[i - 1][0] + c[i]);
    if (str[i] >= rev[i - 1]) upmin(dp[i][0], dp[i - 1][1]);
    if (rev[i] >= rev[i - 1]) upmin(dp[i][1], dp[i - 1][1] + c[i]);
  }
  int ans = std::min(dp[n][0], dp[n][1]);
  if (ans >= 1e17) ans = -1;
  cout << ans << endl;
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem E. 倘若生命可描绘

简单题,简单改一下就是问一个二次函数在正半轴是否有根的问题。

当然要考虑 \(ax^2 + bx^2 + c = 0\)\(a=0\) 的情况,这个时候会退化成常函数,要特判一下。

Problem F/G. 勇者

字符串倍长以后直接跑,看是否存在一个长度为 \(n\) 的子串是回文。判断的方法可以用 Manacher 也可以直接哈希,我比较懒就哈希了(

#include <iostream>
#include <random>

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;
std::string str;
int hash[kMaxN];
int p[kMaxN];
int inv[kMaxN];
int rev[kMaxN];

int get(int l, int r) {
  return dec(hash[r], mul(hash[l - 1], p[r - l + 1]));
}

int get_rev(int l, int r) {
  return dec(rev[l], mul(rev[r + 1], p[r - l + 1]));
}

void solve() {
  cin >> str;
  int n = str.size();
  str = '#' + str + str;
  for (int i = 1, ch; i <= n * 2; i++) {
    hash[i] = inc(mul(hash[i - 1], p[1]), str[i]);
  }
  rev[n * 2 + 1] = 0;
  for (int i = n * 2, ch; i >= 1; i--) {
    rev[i] = inc(mul(rev[i + 1], p[1]), str[i]);
  }
  for (int l = 1, r = n; r <= 2 * n; l++, r++) {
    int mid = (l + r) >> 1;
    if ((n & 1) && get(l, mid) == get_rev(mid, r)) {
      return cout << "YES" << endl, void();
    } else if ((n % 2 == 0) && get(l, mid) == get_rev(mid + 1, r)) {
      return cout << "YES" << endl, void();
    }
  }
  cout << "NO" << endl;
}

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

Problem H. 温柔的彗星

嗯莫名其妙的构造题。找到一个 \(01\)\(S\),使得 \((S)^{\infty}\) 中本质不同的回文子串尽可能少。

嗯就,随便找找就可以找到这个构造:\(101001\),当然 \(010110\) 或者类似的都可以。这种情况下本质不同的回文子串至多为 \(6\)

感觉题面上很多花里胡哨的东西很无意义。

#include <set>
#include <iostream>
#include <random>

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;
std::string str;
int hash[kMaxN];
int p[kMaxN];
int inv[kMaxN];
int rev[kMaxN];

int get(int l, int r) {
  return dec(hash[r], mul(hash[l - 1], p[r - l + 1]));
}

int get_rev(int l, int r) {
  return dec(rev[l], mul(rev[r + 1], p[r - l + 1]));
}

int calc(const std::string& str) {
  int n = str.size();
  for (int i = 1, ch; i <= n; i++) {
    hash[i] = inc(mul(hash[i - 1], p[1]), str[i - 1]);
  }
  rev[n + 1] = 0;
  for (int i = n, ch; i >= 1; i--) {
    rev[i] = inc(mul(rev[i + 1], p[1]), str[i - 1]);
  }
  std::set<int> s;
  for (int l = 1; l <= n; l++) {
    for (int r = l; r <= n; r++) {
      int mid = (l + r) >> 1;
      if ((r - l + 1) & 1) {
        if (get(l, mid) == get_rev(mid, r)) s.insert(get(l, mid));
      } else {
        if (get(l, mid) == get_rev(mid + 1, r)) s.insert(get(l, mid));
      }
    }
  }
  return s.size();
}

std::string ans = "ababba";

void solve() {
  int n, k;
  cin >> n >> k;
  for (int i = 0; i < n; i++) {
    cout << ans[i % 6];
  }
  cout << endl;
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  p[0] = 1, p[1] = 128;
  inv[0] = 1, inv[1] = pow(p[1], MOD - 2);
  for (int i = 2; i < kMaxN; i++) {
    p[i] = mul(p[i - 1], p[1]);
    inv[i] = mul(inv[i - 1], inv[1]);
  }
  // cerr << calc("101001") << endl;
  // cerr << calc("101001101001101001101001101001101001101001101001101001101001101001101001101001101001101001101001") <<
  // endl;
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem I. 安可

这个题其实也有 原题

我感觉这种质量的题目一般的模拟赛出不了,然后丢给 chatgpt 让它帮我找出处还真找到了

手玩会发现对面至多使用两次技能,因为单次技能一定可以拆掉所有的环。

问题就变成了找到子图里边权和最小的环。

这个问题好像没有优秀做法,只能暴力跑最短路然后找点对,就是 \(u\) 跑了一遍最短路后,找到一个 \(v\) 并且存在边 \(v\overset{w}{\to}u\),这样答案就是 \(dis[v] + w\)

#include <iostream>
#include <set>
#include <random>
#include <vector>

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 t;
int n, m;
std::vector<std::pair<int, int>> go[kMaxN];
std::vector<std::tuple<int, int, int>> e;

int dijkstra(int u) {
  int ans = 1e9;
  std::vector<int> dis(n + 1, 1e9);
  std::set<std::pair<int, int>> s;
  auto record = [&](int v, int d) {
    if (d >= dis[v]) return;
    s.erase({dis[v], d});
    dis[v] = d;
    s.insert({dis[v], v});
  };
  record(u, 0);
  while (!s.empty()) {
    auto [d, u] = *s.begin();
    s.erase(s.begin());
    for (auto [v, w] : go[u]) {
      record(v, w + d);
    }
  }
  for (auto [s1, s2, w] : e) {
    if (s2 == u) upmin(ans, w + dis[s1]);
  }
  return ans;
}

void solve() {
  cin >> n >> m;
  e.clear();
  for (int i = 1; i <= n; i++) {
    go[i].clear();
  }
  int minw = 1e9;
  for (int i = 1, u, v, w; i <= m; i++) {
    cin >> u >> v >> w;
    e.push_back({u, v, w});
    go[u].push_back({v, w});
    upmin(minw, w);
  }
  int ans = 1e9;
  for (int i = 1; i <= n; i++) {
    upmin(ans, dijkstra(i));
  }
  if (ans == 1e9) ans = minw;
  cout << ans << endl;
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem J. 春紫菀

容易发现添加一只小海嗣最多将两组能各自相互看到的海嗣们连接起来。

然后题目最后会构成若干个孤立的联通分量,统计联通分量的方法用并查集就可以了。

嗯,怎么连要连多少很显然了。

#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 t, n;
int fa[kMaxN];
int x[kMaxN], y[kMaxN];

int find(int x) {
  if (x == fa[x]) return x;
  return fa[x] = find(fa[x]);
}

void link(int x, int y) {
  x = find(x), y = find(y);
  fa[x] = y;
}

void solve() {
  cin >> n;
  for (int i = 1; i <= n; i++) fa[i] = i;
  for (int i = 1; i <= n; i++) {
    cin >> x[i] >> y[i];
    for (int j = 1; j < i; j++) {
      if (x[i] == x[j] || y[i] == y[j]) link(i, j);
    }
  }
  int ans = 0;
  for (int i = 1; i <= n; i++) ans += fa[i] == i;
  cout << (ans - 1) << endl;
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem K. 祝福

暴力枚举。

我一开始看着感觉 \(m\) 不会炸 int,结果 \(W\) 炸了(

没什么好写的。

Problem L. 怪物

这个行为有个名字叫二次前缀和(

Problem M. 大概

贪心的想博弈结果应该是中位数,其实也可以证明如果去掉了中位数那么最终答案一定会更劣。

posted @ 2025-11-26 19:52  sudoyc  阅读(41)  评论(0)    收藏  举报