第十一届中国大学生程序设计竞赛 女生专场(CCPC 2025 Women's Division)题解

我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?

很水的一场,我认为其实金线甚至可能会被冲到八九题。这场除了 \(L\) 细节比较多以外没有任何不可做题。

除了 \(I\)\(L\) 以外所有题都可以在半小时内写出来。\(F\) 题想到思路了也容易写。

Problem A. 环状线

题目保证了 \(n\) 为奇数所以答案唯一。写不出退役的那种。

#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, s, t;

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> s >> t;
  if (s < t) {
    if (t - s < n - t + s) {
      cout << 1 << endl;
    } else {
      cout << 2 << endl;
    }
  } else {
    if (s - t < n - s + t) {
      cout << 2 << endl;
    } else {
      cout << 1 << endl;
    }
  }
  return 0;
}

Problem B. 爬山

简单的分层图最短路。

#include <iostream>
#include <set>
#include <queue>
#include <random>
#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, h;
int dis[104][(signed)(1e4) + 10];
int a[kMaxN];
std::vector<std::pair<int, int>> go[kMaxN];

void dijkstra() {
  for (int i = 0; i <= h; i++) {
    for (int j = 0; j <= n; j++) {
      dis[i][j] = 1e18;
    }
  }
  // dis h v
  std::set<std::tuple<int, int, int>> s;
  auto record = [&](int h, int v, int d) {
    if (h > ::h) return;
    if (d >= dis[h][v]) return;
    s.erase({dis[h][v], h, v});
    dis[h][v] = d;
    s.insert({d, h, v});
  };
  record(0, 1, 0);
  while (!s.empty()) {
    auto [d, h, u] = *s.begin();
    // cerr << "GET " << d << ' ' << h << ' ' << u << endl;
    s.erase(s.begin());
    for (auto [v, w] : go[u]) {
      if (a[v] < a[u]) {
        record(0, v, w + d);
      } else {
        record(h + a[v] - a[u], v, w + d);
      }
    }
  }
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> m >> h;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
  }
  for (int i = 1; i <= m; i++) {
    int u, v, t;
    cin >> u >> v >> t;
    go[u].push_back({v, t});
    go[v].push_back({u, t});
  }
  dijkstra();
  for (int i = 2; i <= n; i++) {
    int min = 1e18;
    for (int j = 0; j <= h; j++) upmin(min, dis[j][i]);
    if (min == 1e18) min = -1;
    cout << min << ' ';
  }
  cout << endl;
  return 0;
}

Problem C. 短视频

题目描述非常的奇怪和诡异,但是读懂了不难。

#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;

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, T;
int t[kMaxN], k[kMaxN];

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> T;
  for (int i = 1; i <= n; i++) {
    cin >> t[i] >> k[i];
  }
  int sum = 0;
  for (int i = 1; i <= n; i++) {
    if (sum + t[i] <= T) {
      sum += t[i];
    } else {
      // 接下来考虑放几秒会导致没意思
      if (sum + t[i] - T <= k[i]) {
        sum += t[i];
      } else {
        int x = k[i] - sum + T;
        if (x <= 0) x = 0;
        x++;
        sum += x;
      }
    }
  }
  cout << sum << endl;
  return 0;
}

Problem D. 网络改造

现场居然只有四个队写出来了是我没绷住的。

发现题目的范围刚好允许我们去考虑状态压缩 DP。状态设计就比较显然了:\(dp_{s}\) 表示状态 \(s\) 为这些点被添加进来的时候的代价最小值。

考虑转移,若添加一个新的点 \(u\) 当作新的零出度点。DAG 和存在拓扑序是互为充要条件的,所以这个转移过程比较类似模拟拓扑排序,那么 \(u\) 有两个操作:

  • 炸点
  • 所有出边并且会和 \(s\) 中点相连的边,要么被反向要么被删除,二者等价可以视作被删了

这样才能保证新的点集一定是 DAG。

现在反思一下会发现整个转移非常类似去模拟一次拓扑排序,转移的过程就是挑选新的拓扑出队点。

这里不需要考虑炸点的后悔行为,也就是有些删边行为浪费了,比较容易证明就自己思考了。

不过判定有多少条边和点集有连是 \(O(n)\) 的,会导致 \(DP\) 的复杂度劣到 \(O(n^2 2^n)\)。这个时候跑一个二进制的前缀和优化一下可以做到 \(O(n 2^n)\)

#include <iostream>
#include <random>
#include <tuple>
#include <vector>

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

const char endl = '\n';
const int kMaxN = 1e3 + 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 c[kMaxN];
int dp[(1 << 22) + 1];
int w[22][(1 << 22) + 1];
int a[23][23];
std::vector<std::pair<int, int>> in[kMaxN];

int 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 >> c[i], in[i].reserve(n - 1);
  }
  for (int i = 1; i <= m; i++) {
    int u, v, a, b;
    cin >> u >> v >> a >> b;
    in[v].push_back({u, std::min(a, b)});
    ::a[u][v] = std::min(a, b);
    w[u - 1][(1 << (v - 1))] = std::min(a, b);
  }
  for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
      for (int s = 0; s < (1 << j); s++) {
        w[i][s | (1 << j)] = w[i][s] + w[i][1 << j];
      }
    }
  }
  int mx = (1 << n);
  for (int s = 0; s < mx; s++) dp[s] = 1e9;
  dp[0] = 0;
  for (int s = 0; s < mx; s++) {
    // cerr << s << ' ' << dp[s] << endl;
    for (int i = 0; i < n; i++) {
      if (s & (1 << i)) continue;
      int to = s | (1 << i);
      int s1 = 0;
      // for (auto& [u, w] : in[i + 1]) {
      //   if (!((1 << (u - 1)) & s)) s1 += w;
      // }
      upmin(dp[to], w[i][s] + dp[s]);
      upmin(dp[to], c[i + 1] + dp[s]);
    }
  }
  cout << dp[mx - 1] << endl;
  return 0;
}

Problem E. 购物计划

一眼题。没有意思。

由于满减行为是可以不断翻倍的,所以可以通过带根号的算法处理出每个整数价格时的最多满减,记作 \(d_i\)

通过上述操作可以处理出:一段整数区间内的最小价格,也就是 \(w_i = i - d_i\) 的区间最小值,这里用个 ST 表之类的写一下就好了。

每次询问相当于在一个实数区间内挑选最小值,事实上我们可以拆成两个询问区间,其中一个双边界为整数,另外一个区间包括小数边界和整数边界。

有小数边界的区间由于小于一可以直接找到满减,整数边界的区级可以用 ST 表查出来最小价格,二者做个比较就算出来了。

很简单的题目,但是却成为了这场的金牌题,很莫名其妙。

#include <iostream>
#include <algorithm>
#include <random>
#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 a[kMaxN], b[kMaxN];
int w[kMaxN], p[kMaxN], q[kMaxN];

int dec[kMaxN];
int cost[21][kMaxN];
int BIN[kMaxN];

int mincost(int l, int r) {
  int t = BIN[r - l + 1];
  return std::min(cost[t][l], cost[t][r - (1 << t) + 1]);
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> m;
  for (int i = 1, a, b; i <= m; i++) {
    cin >> a >> b;
    dec[a] = std::max(dec[a], b);
  }
  BIN[2] = 1;
  for (int i = 3; i < kMaxN; i++) {
    BIN[i] = BIN[i >> 1] + 1;
  }
  for (int i = 1; i < kMaxN; i++) {
    for (int j = 1; i * j < kMaxN; j++) {
      upmax(dec[i * j], dec[i] * j);
    }
  }
  for (int i = 1; i < kMaxN; i++) {
    upmax(dec[i], dec[i - 1]);
  }
  for (int i = 1; i < kMaxN; i++) {
    cost[0][i] = i - dec[i];
  }
  for (int t = 1; t < 21; t++) {
    for (int l = 1; l + (1 << t) - 1 < kMaxN; l++) {
      cost[t][l] = std::min(cost[t - 1][l], cost[t - 1][l + (1 << (t - 1))]);
    }
  }
  for (int i = 1, w, p, q; i <= n; i++) {
    cin >> w >> p >> q;
    p *= w;
    int dec1 = dec[p / q], s = p / q, t = w;
    if (p % q) s++;
    int min = mincost(s, t);
    p = p - dec[p / q] * q;
    if (min * q <= p) {
      cout << min << ' ' << 1 << endl;
    } else {
      auto gcd = std::__gcd(p, q);
      p /= gcd, q /= gcd;
      cout << p << ' ' << q << endl;
    }
  }
  return 0;
}

Problem F. 丝之歌

一开始把这个题强化了,以为小怪死了不会复活甚至以为这个题不可做……

题目生怕你想不到矩阵优化转移都给你把转移矩阵甩脸上了……要是还想不到矩阵乘法,出门左传高职赛。

\(dp_i\) 为还有 \(i\) 个念珠没有被兑换的时候的概率。每次转移的时候只需要考虑是否是一命通关,这个判定是否一命通关通过一大坨矩阵乘法优化转移即可。

每次转移的时候可以累计念珠串的期望,注意最后一次转移不需要算到念珠串期望去了。

但是你不优化矩阵乘法,复杂度是 \(O(ntc^3 \log(1e9))\) 这个级别的,非常的巨大恐怖吓人。

所以我们要抛弃矩阵快速幂,提前存下来每个转移矩阵的 \(2^k\) 次幂。同时注意到转移的时候有一个运算矩阵只是一个向量,可以将矩阵乘法复杂度压倒 \(c^2\)

这样和矩阵相关的复杂最后可以处理成 \(O(ntc^2 \log(1e9))\),复杂度就对了。

同时题目还有两个小的常数优化:

  • 参与运算的全是下三角矩阵,下三角矩阵乘下三角一定还是下三角,所以矩乘的时候 \(i \leq j \leq k\),优化出 \(\frac{1}{6}\) 的常数
  • 矩阵运算减少取模运算,优化方式看代码
#include <cassert>
#include <iostream>
#include <random>

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

const char endl = '\n';
const int kMaxN = 1e3 + 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(long long x, long long y) {
  x %= MOD, y %= MOD;
  return 1ll * x * y % MOD;
}

using ull = unsigned long long;

struct matrix {
  int a[11][11];

  int* operator[](int id) { return a[id]; }

  matrix operator*(matrix& b) {
    matrix c;
    for (int i = 0; i < 11; i++) {
      for (int j = 0; j <= i; j++) {
        ull tmp = 0;
        for (int k = j; k <= i; k++) {
          tmp += (ull)a[i][k] * b[k][j];
        }
        c[i][j] = tmp % MOD;
      }
      for (int j = i + 1; j < 11; j++) c[i][j] = 0;
    }
    return c;
  }
} mon[kMaxN], f[kMaxN][30];

int n, m, a, b, c;
int w[kMaxN];

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;
}

matrix pow(matrix x, int p) {
  matrix ans;
  for (int i = 0; i < 11; i++) {
    for (int j = 0; j < 11; j++) ans[i][j] = i == j;
  }
  while (p) {
    if (p & 1) ans = ans * x;
    x = x * x, p >>= 1;
  }
  return ans;
}

// i 个念珠的概率
int dp[2][kMaxN];

int cur = 0;
int ans = 0;
std::vector<int> list;

void calc(int id) {
  cur ^= 1;
  int t, lst = cur ^ 1;
  cin >> t;
  matrix begin;
  for (int i = 0; i <= c; i++) begin[0][i] = i == c;
  long long all = 0;
  for (int i = 1; i <= t; i++) {
    int type, cnt;
    cin >> type >> cnt;
    for (int t = 0; t < 30; t++) {
      if (((cnt >> t) & 1) ^ 1) continue;
      auto tmp = f[type][t];
      // auto tmp = pow(mon[type], cnt);
      // begin = begin * tmp;
      // 手动展开转移
      for (int j = 0; j <= c; j++) {
        ull s = 0;
        for (int k = 0; k <= c; k++) {
          s += 1ll * begin[0][k] * tmp[k][j];
          // begin[0][j] =
        }
        begin[0][j] = s % MOD;
      }
    }
    all += 1ll * cnt * w[type];
  }
  int win = 0;
  for (int i = 1; i <= c; i++) {
    win = inc(win, begin[0][i]);
    if (begin[0][i] < 0 || win < 0) {
      exit(-1);
    }
  }
  // assert(inc(win, begin[0][0]) == 1);
  // for (int i = 0; i < a; i++) {
  //   dp[cur][i] = 0;
  // }
  // cerr << "END CALC " << endl;
  if (id != n) {
    int p, nxt, val;
    std::vector<int> tmp;
    for (auto i : list) {
      p = mul(win, dp[lst][i]), nxt = (i + all) % a;
      if (dp[cur][nxt] == 0 && p != 0) tmp.push_back(nxt);
      dp[cur][nxt] = inc(dp[cur][nxt], p);
      ans = inc(ans, mul(p, (i + all) / a));

      p = mul(dec(1, win), dp[lst][i]), nxt = all % a;
      if (dp[cur][nxt] == 0 && p != 0) tmp.push_back(nxt);
      dp[cur][nxt] = inc(dp[cur][nxt], p);
      ans = inc(ans, mul(p, all / a));

      dp[lst][i] = 0;
    }
    list.swap(tmp);
    // cerr << list.size() << endl;
  } else {
    ans = inc(mul(ans, b), all % MOD);
    for (int i = 0; i < a; i++) {
      ans = inc(ans, mul(win, mul(dp[lst][i], i)));
    }
  }
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> m >> a >> b >> c;
  for (int i = 1; i <= m; i++) {
    cin >> w[i];
  }
  for (int i = 1; i <= m; i++) {
    mon[i][0][0] = 1;
    for (int x = 1; x <= c; x++) {
      int sum = 0;
      for (int y = 0; y <= x; y++) {
        cin >> mon[i][x][y];
        sum = inc(sum, mon[i][x][y]);
      }
      sum = pow(sum, MOD - 2);
      int sum2 = 0;
      for (int y = 0; y <= x; y++) {
        mon[i][x][y] = mul(mon[i][x][y], sum);
        sum2 = inc(sum2, mon[i][x][y]);
      }
    }
    f[i][0] = mon[i];
    for (int j = 1; j < 30; j++) f[i][j] = f[i][j - 1] * f[i][j - 1];
  }
  dp[cur][0] = 1, list.push_back(0);
  for (int i = 1; i <= n; i++) {
    calc(i);
  }
  cout << ans << endl;
  cerr << list.size() << endl;
  return 0;
}

Problem G. 最大公约数

写不出退役。

#include <iostream>
#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 n, k;

bool no[kMaxN];

void sieve() {
  for (int i = 2; i < kMaxN; i++) {
    if (!no[i])
      for (int j = 2; j * i < kMaxN; j++) {
        no[i * j] = true;
      }
  }
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> k;
  sieve();
  std::vector<int> ans;
  for (int i = 1; i <= n && ans.size() < k; i++) {
    if (!no[i]) ans.push_back(i);
  }
  if (ans.size() != k) return cout << "NO" << endl, 0;
  cout << "YES" << endl;
  for (auto& i : ans) {
    cout << i << ' ';
  }
  cout << endl;
  return 0;
}

Problem H. 缺陷解码器

可以看官方题解,写的很好,是一个很好的期望 DP 入门题。

我这里只讲一点碎碎念。一个字符串注意到我们只会关系它本质不同的字符个数,新加进来的到底是多少我们并不怎么关心,基于这个思路去优化状态设计和转移。

#include <algorithm>
#include <iostream>
#include <random>
#include <vector>

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

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

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 n, m;

bool check(const std::vector<int>& str) {
  if (str.size() < 2) return true;
  bool check1 = true, check2 = true;
  int mid = str.size() >> 1;
  for (int i = 0; i < mid; i++) {
    check1 &= str[i] == str[str.size() - 1 - i];
    check2 &= str[i] == str[mid + i + (str.size() & 1)];
  }
  return !(check1 | check2);
}

int inv;
bool flag = false;
std::pair<int, int> dfs(std::vector<int>& str, int max) {
  if (!check(str)) return {1, 0};
  if (str.size() == n) {
    flag = true;
    return {0, 0};
  }
  int a = 0, b = 0;
  str.push_back(0);
  for (int i = 1; i <= max; i++) {
    str.back() = i;
    auto [na, nb] = dfs(str, max);
    a = inc(a, na), b = inc(b, nb);
  }
  if (max + 1 <= m) {
    str.back() = max + 1;
    auto [na, nb] = dfs(str, max + 1);
    a = inc(a, mul(na, m - max)), b = inc(b, mul(nb, m - max));
  }
  a = mul(a, inv), b = inc(1, mul(b, inv));
  str.pop_back();
  return {a, b};
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  // 容易发现字符集超过 n 的时候无意义
  cin >> n >> m;
  inv = pow(m, MOD - 2);
  std::vector<int> tmp;
  auto [a, b] = dfs(tmp, 0);
  if (!flag) {
    return cout << -1 << endl, 0;
  }
  auto ans = mul(b, pow(dec(1, a), MOD - 2));
  cout << ans << endl;
  return 0;
}

Problem I. 调色滤镜

题目中的滤镜就是置换操作,置换这个行为是不一定存在逆的而且不满足交换律,单位元是 0 1 2 3 4 5 ...

包含关系一定会构成一个树,这个树可以通过扫描线构造出来。扫描线的时候由于保证两个线段只会包含而不是相交,可以使用的数据结构非常多,简单一点直接写个 set 都是可以的。但是我却写了一个线段树去构建关系树

我们在对关系树进行遍历的时候同步维护一个线段树,线段树的节点值就是置换,线段树叶子节点要初始化成单位元,并且规定 pushup 操作(也就是区间并)是以右儿子为置换规则去置换左儿子。

设定节点 \(i\) 为矩形 \(i\) 对应的置换关系,这样就可以处理好置换添加顺序的问题,同时在关系树上转移的时候维护一下这个线段树就好。

当某个节点 \(i\) 出栈的时候强行将 \(i\) 初始化为单位元。

比较有意思的一个题。

如果题目保证置换操作存在逆,也就是每种元素只出现一次的话,其实是可以允许矩形是相交关系的。但是这样写会变成比较板的扫描线,可以当个 idea 去恶心别人。

#include <algorithm>
#include <cassert>
#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);
}

class SegmentTree {
  struct node {
    int l, r, val, laz;
    node* son[2];
  }* root;

  void init(node* p, int s) { p->val = p->laz = s; }

  void pushdown(node* p) {
    if (~p->laz) init(p->son[0], p->laz), init(p->son[1], p->laz), p->laz = -1;
  }

  void build(node* p, int l, int r) {
    p->l = l, p->r = r, p->laz = -1, p->val = 0;
    if (l == r) return p->val = 0, void();
    int mid = (l + r) >> 1;
    build(p->son[0] = new node, l, mid), build(p->son[1] = new node, mid + 1, r);
  }

  void init(node* p, int l, int r, int s) {
    if (l <= p->l && p->r <= r) return init(p, s);
    pushdown(p);
    if (p->son[0]->r >= l) init(p->son[0], l, r, s);
    if (p->son[1]->l <= r) init(p->son[1], l, r, s);
  }

  int ask(node* p, int x) {
    if (p->l == p->r) return p->val;
    pushdown(p);
    if (p->son[0]->r >= x) return ask(p->son[0], x);
    return ask(p->son[1], x);
  }

public:
  int ask(int x) { return ask(root, x); }
  void build(int n) { build(root = new node, 1, n); }
  void init(int l, int r, int s) { init(root, l, r, s); }
} tree;

class SegmentTree2 {
  struct node {
    int l, r, f[10];
    node* son[2];
  }* root;

  void pushup(node* p) {
    for (int c = 0; c < 10; c++) {
      p->f[c] = p->son[1]->f[p->son[0]->f[c]];
    }
  }

  void build(node* p, int l, int r) {
    p->l = l, p->r = r;
    for (int i = 0; i < 10; i++) p->f[i] = i;
    if (l == r) return void();
    int mid = (l + r) >> 1;
    build(p->son[0] = new node, l, mid), build(p->son[1] = new node, mid + 1, r);
    pushup(p);
  }

  void change(node* p, int* f) {
    for (int i = 0; i < 10; i++) p->f[i] = f[i];
  }

  void change(node* p, int l, int r, int* f) {
    if (l <= p->l && p->r <= r) return change(p, f);
    if (p->son[0]->r >= l) change(p->son[0], l, r, f);
    if (p->son[1]->l <= r) change(p->son[1], l, r, f);
    pushup(p);
  }

public:
  int* ask() { return root->f; }
  void build(int n) { build(root = new node, 1, n); }
  void change(int l, int r, int* f) { change(root, l, r, f); }
} tree2;

int n, q;
int xl[kMaxN], xr[kMaxN], yl[kMaxN], yr[kMaxN];
int f[kMaxN][10];
int x[kMaxN], y[kMaxN], c[kMaxN];
int ans[kMaxN];

// 从属于哪个矩形
std::vector<int> bel[kMaxN];
// 矩形之间的父子关系
int fa[kMaxN];

int tot = 0;
int l[kMaxN], r[kMaxN];

std::vector<int> uno;

int map(int y) {
  return std::lower_bound(uno.begin(), uno.end(), y) - uno.begin() + 1;
}

std::vector<int> go[kMaxN];
void dfs(int u) {
  if (u) tree2.change(u, u, f[u]);
  auto sf = tree2.ask();
  for (auto id : bel[u]) {
    ans[id] = sf[c[id]];
  }
  for (auto v : go[u]) dfs(v);
  if (u) tree2.change(u, u, f[0]);
}

struct node {
  // type 0: insert
  // type 1: query
  // type 2: erase
  int x, type, id;
  bool operator<(const node& b) const {
    if (x != b.x) return x < b.x;
    if (type != b.type) return type < b.type;
    return id < b.id;
  }
};

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> q;
  // 第一个节点表示x坐标,第二个用来标记是查询点还是矩阵
  std::set<node> s;
  // std::set<int> inset;
  for (int i = 1; i <= n; i++) {
    cin >> xl[i] >> xr[i] >> yl[i] >> yr[i];
    for (int j = 0; j <= 9; j++) cin >> f[i][j];
    uno.push_back(yl[i]), uno.push_back(yr[i]);
    s.insert({xl[i], 0, i});
    s.insert({xr[i], 2, i});
  }
  for (int i = 1; i <= q; i++) {
    cin >> x[i] >> y[i] >> c[i];
    uno.push_back(y[i]);
    s.insert({x[i], 1, i});
  }
  std::sort(uno.begin(), uno.end()), uno.erase(std::unique(uno.begin(), uno.end()), uno.end());
  tree.build(uno.size());
  for (auto [x, type, id] : s) {
    if (type == 1) {
      bel[tree.ask(map(y[id]))].push_back(id);
      // bel[id] = tree.ask(map(y[id]));
    } else if (type == 2) {
      tree.init(map(yl[id]), map(yr[id]), fa[id]);
    } else if (type == 0) {
      fa[id] = tree.ask(map(yl[id]));
      assert(fa[id] == tree.ask(map(yr[id])));
      tree.init(map(yl[id]), map(yr[id]), id);
    }
  }
  for (int i = 1; i <= n; i++) {
    go[fa[i]].push_back(i);
  }
  for (int i = 0; i < 10; i++) f[0][i] = i;
  tree2.build(n);
  dfs(0);
  for (int i = 1; i <= q; i++) {
    cout << ans[i] << endl;
  }
  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 main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  std::string str;
  while (cin >> str) {
    if (str.back() == 'n') str += "g";
    cout << str << ' ';
  }
  return 0;
}

Problem K. 左儿子右兄弟

手玩会发现,某个节点的儿子一定会构成一条往右下方走的链,而且统计子树大小的时候靠后的节点会被计算多次。

基于这个思路会有显然的贪心想法,也就是子树越大的尽量在这条链的前面,这样就解决了计数问题。

至于方案数的问题……都想到了怎么计数真的想不出这个?

#include <iostream>
#include <algorithm>
#include <random>
#include <vector>

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 mul(int x, int y) {
  return 1ll * x * y % MOD;
}

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

int n;

std::vector<int> go[kMaxN];

long long ans = 0;
int siz[kMaxN];
int cnt = 1;
int fact[kMaxN];

void dfs(int u) {
  siz[u] = 1;
  for (auto v : go[u]) {
    dfs(v);
    siz[u] += siz[v];
  }
  int sum = siz[u] - 1;
  std::sort(go[u].begin(), go[u].end(), [](int i, int j) { return siz[i] > siz[j]; });
  for (int i = 0; i < go[u].size(); i++) {
    ans += sum;
    sum -= siz[go[u][i]];
  }
  for (int i = 0, j; i < go[u].size(); i = j) {
    for (j = i; j < go[u].size() && siz[go[u][i]] == siz[go[u][j]]; j++);
    cnt = mul(cnt, fact[j - i]);
  }
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n;
  fact[0] = 1;
  for (int i = 1; i < kMaxN; i++) {
    fact[i] = mul(fact[i - 1], i);
  }
  for (int i = 2, f; i <= n; i++) {
    cin >> f, go[f].push_back(i);
  }
  dfs(1);
  cout << ans + n << endl;
  cout << cnt << endl;
  return 0;
}

Problem L. 挑战多边形

这套题里为数不多有点难度的题目。但是抽象的范围让这道题变的比较搞笑。

将所有可能的有向边进行排序,然后钦定某个点一定会出现在凸包里面。

由于被删除的点数量不超过 \(k\),则前 \(k + 1\) 中某个节点一定会在答案凸包里。

记录 DP 状态 \(f_{i, j}\) 为,从 \(p\) 开始,以极座标排序后,边连到 \(i\) 点并且凸包边数为 \(j\) 的最大权。转移的时候只需要考虑第二维的最大 \(k\) 个即可,这个性质的正确性也是基于被删点不超过 \(k\) 这个性质。

不想写了以后有时间再补。

posted @ 2025-11-09 22:46  sudoyc  阅读(306)  评论(0)    收藏  举报