The 2025 ICPC Asia Chengdu Regional Contest (The 4rd Universal Cup. Stage 4: Grand Prix of Chengdu) 题解

神秘场,或者说神仙打架场

榜单强的恐怖,金牌线好像是八道题(至少 codeforces 上好像是这么写的)

其实大部分题都是可做题,但是有两个题对灵感需求很高

题目 E, H, F 比其他题目难度上明显断档了,导致这场十题队非常多(

质量很好,貌似有一个好玩的思想贯彻了很多题:正确的贪心可以减少很多分类讨论。包括在 C、D、H、和 M 都有这个思想的体现

F 那个构造题确实不好写,以后有时间再做吧。(都欠了多少题了)

Problem A. A Lot of Paintings

每一个数 \(a_i\) 都会对应一个原始数字 \(p \leq 1\),这个原始数字有一个可取的下界和 可能 不可取的上界。

  • \(a_i \neq 100\) 的时候 \(p\) 的上界就是 \(p + 0.05\)(不可取)
  • \(a_i \neq 0\) 的时候 \(p\) 的下界就是 \(p - 0.05\)(可取)

明白了这两点后就可以得到原始数字求和的可能范围,只要 \(1\) 在这个范围之内那就是合法的,然后就可以拿去构造答案了。由于上界是可能可取可能不可取的,这里要简单判一下(说白了就是 \(a_i\) 求和是不是 \(100\)

构造答案就相当于当你对 \(a_i\) 进行修改之后四舍五入还是 \(a_i\),但是求和要等于 \(100\)

现在分类讨论,记 \(s=\sum a_i\)

  • \(s = 100\):真的需要考虑吗?
  • \(s < 100\):将差的那一点点均匀涂抹到所有的 \(a_i\) 上面,具体说就是 \(a_i\) 加上 \(\frac{100 - s}{n}\),考虑精度问题可以将所有数字乘上 \(n\)。容易知道这种情况下一定不会触发进位,因为我们做判定的时候就已经保证这一点。
  • \(s > 100\):直接将所有 \(a_i\)\(10\),这样每个 \(a_i\) 最多可以减去 \(5\)。然后尽可能减减到 \(\sum a_i=1000\)
#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 t, n;
int b[kMaxN];

void solve() {
  int sum = 0, cnt1 = 0, cnt2 = 0;
  cin >> n;
  for (int i = 1; i <= n; i++) cin >> b[i], sum += b[i], cnt1 += !!b[i], cnt2 += b[i] != 100;
  if (2 * sum - cnt1 > 200) return cout << "No" << endl, void();
  if (cnt2) {
    if (200 >= cnt2 + 2 * sum) return cout << "No" << endl, void();
  } else if (200 > 2 * sum)
    return cout << "No" << endl, void();
  cout << "Yes" << endl;
  if (sum == 100) {
    for (int i = 1; i <= n; i++) cout << b[i] << ' ';
  } else if (sum > 100) {
    sum *= 10;
    sum -= 1000;
    for (int i = 1; i <= n; i++) {
      b[i] = b[i] * 10;
    }
    for (int i = 1; i <= n; i++) {
      if (b[i]) {
        if (sum <= 5)
          b[i] -= sum, sum = 0;
        else
          sum -= 5, b[i] -= 5;
      }
      cout << b[i] << ' ';
    }
  } else {
    for (int i = 1; i <= n; i++) cout << n * b[i] + (100 - sum) << ' ';
  }
  cout << endl;
}

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

Problem B. Blood Memories

嗯写这个题的时候忘记广义矩阵乘法了……

比较简单的题吧,由于 \(n\) 很小所有可以暴力枚举操作状态,然后写出来转移矩阵,套一个矩阵快速幂就结束了。很水的题。

比较需要注意的是广义矩阵乘法下的矩阵单位元不好找,做快速幂的时候别去找单位元初始化了。

#include <iostream>
#include <cstring>
#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 T;
int n, m, k, R;
int popcnt[kMaxN];

struct matrix {
  int a[64][64];
  int* operator[](int k) { return a[k]; }
  matrix operator*(matrix& b) {
    matrix tmp;
    for (int i = 0; i < 64; i++) {
      for (int j = 0; j < 64; j++) {
        tmp[i][j] = 0;
        for (int k = 0; k < 64; k++) {
          upmax(tmp[i][j], a[i][k] + b[k][j]);
        }
      }
    }
    return tmp;
  }
};

int a[kMaxN], c[kMaxN];

int cost(int state) {
  int sum = 0;
  for (int i = 1; i <= n; i++) {
    if ((state >> (i - 1)) & 1) sum += c[i];
  }
  return sum;
}

int dam(int state) {
  int sum = 0;
  for (int i = 1; i <= n; i++) {
    if ((state >> (i - 1)) & 1) sum += a[i];
  }
  return sum;
}

void solve() {
  // 每一轮的消耗仅取决于上一轮的消耗
  // 考虑维护转移:头状态s,尾状态t,长度 2^k(至多30
  // 2^12 * 30,空间复杂度完全够
  cin >> n >> m >> k >> R;
  matrix tmp, ans;
  memset(tmp.a, 0, sizeof(tmp.a));
  memset(ans.a, 0, sizeof(ans.a));
  for (int i = 1; i <= n; i++) {
    cin >> a[i] >> c[i];
  }
  int all = 1 << n;
  for (int s = 0; s < all; s++) {
    for (int t = 0; t < all; t++) {
      tmp[s][t] = -1e9;
      int c1 = cost(s), c2 = cost(t);
      if (c2 + popcnt[s & t] * k <= m && c1 <= m) {
        tmp[s][t] = dam(t);
        // cerr << s << ' ' << s << ' ' << c1 << ' ' << c2 << endl;
        // cerr << c2 + popcnt[s & t] * k << endl;
      }
    }
  }
  for (int i = 0; i < all; i++) {
    ans[0][i] = (cost(i) <= m ? dam(i) : -1e9);
  }
  R--;
  for (int i = 0; i < 32; i++) {
    if ((R >> i) & 1) ans = ans * tmp;
    tmp = tmp * tmp;
  }
  int max = 0;
  for (int i = 0; i < all; i++) upmax(max, ans[0][i]);
  cout << max << endl;
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  popcnt[0] = 0;
  for (int i = 0; i <= 64; i++) {
    popcnt[i << 1] = popcnt[i];
    popcnt[i << 1 | 1] = popcnt[i] + 1;
  }
  cin >> T;
  while (T--) solve();
  return 0;
}

Problem C. Crossing River

神秘题

我好像之前在哪里见过这种题,可能是 USACO?比较 trick 的题,有点吃灵感说真的

答案合法性显然具有单调性,考虑二分答案然后判定

正着做判定其实不好做,因为很难得知需要是否需要等待某个人或者等待多久。考虑做最短路状态数也达到了 \(O(n^2)\) 这个级别。

然后就考虑反着做判定……反着判定就很简单了而且有很明显的贪心特征:每个人渡河的时间是固定的,要做的就是尽可能减少浪费的时间……

感觉就是要找一个正确抽象将题目变成具有贪心特征的题目……

#include <iostream>
#include <algorithm>
#include <random>
#include <tuple>
#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 t;
int n, m, k;
int a[kMaxN], b[kMaxN], id1[kMaxN], id2[kMaxN];

std::vector<std::tuple<int, int, int>> tmp;

bool check(int t, int beg) {
  tmp.clear();
  int now = beg, i = n, j = m;
  while (i + j) {
    // cerr << t << ' ' << now << ' ' << i << ' ' << j << endl;
    if (now == 0) {
      if (j) {
        if (t - k < b[id2[j]]) return false;
        tmp.push_back({t - k, 1, id2[j]});
        now ^= 1, j--, t -= k;
      } else {
        if (t < k) return false;
        t -= k, now ^= 1;
      }
    } else {
      if (i) {
        if (t - k < a[id1[i]]) return false;
        tmp.push_back({t - k, 0, id1[i]});
        now ^= 1, i--, t -= k;
      } else {
        if (t < k) return false;
        t -= k, now ^= 1;
      }
    }
  }
  return true;
}

void solve() {
  cin >> n >> m >> k;
  for (int i = 1; i <= n; i++) cin >> a[i], id1[i] = i;
  for (int i = 1; i <= m; i++) cin >> b[i], id2[i] = i;
  std::sort(id1 + 1, id1 + 1 + n, [](int i, int j) { return a[i] < a[j]; });
  std::sort(id2 + 1, id2 + 1 + m, [](int i, int j) { return b[i] < b[j]; });
  int l = 0, r = 1e17;
  int anst = -1;
  std::vector<std::tuple<int, int, int>> ans;
  while (l <= r) {
    int mid = (l + r) >> 1;
    if (check(mid, 0) || check(mid, 1)) {
      r = mid - 1, ans = tmp, anst = mid;
    } else {
      l = mid + 1;
    }
  }
  cout << anst << endl;
  std::reverse(ans.begin(), ans.end());
  for (auto [t, p, id] : ans) {
    cout << t << ' ' << p << ' ' << id << endl;
  }
}

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

Problem D. Deductive Snooker Scoring

我一开始打算写分类讨论的,这个题就简单分讨其实也不难。

观察到状态数非常非常少,我们直接从起点状态做一个 \(bfs\) 然后记录一下转移就……结束了……

#include <cassert>
#include <queue>
#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 a, b, n, p;
std::string str;

struct type {
  int n, a, b, p;
} lst[22][201][201][2];
bool vis[22][201][201][2];
std::string link[22][201][201][2];

void bfs() {
  std::queue<type> q;
  auto record = [&](int n, int a, int b, int p, int ln, int la, int lb, int lp, const std::string& s) {
    if (vis[n][a][b][p]) return;
    vis[n][a][b][p] = true;
    lst[n][a][b][p] = {ln, la, lb, lp};
    q.push({n, a, b, p});
    link[n][a][b][p] = s;
  };
  record(21, 0, 0, 0, -1, -1, -1, -1, "");
  while (!q.empty()) {
    auto [n, a, b, p] = q.front();
    q.pop();
    if (n == 0) continue;
    record(n, a, b, p ^ 1, n, a, b, p, "/");
    // 还没有到清场阶段
    if (n > 6) {
      for (char s = '2'; s <= '7'; s++) {
        if (p)
          record(n - 1, a, b + s - '0' + 1, p, n, a, b, p, (std::string) "1" + s);
        else
          record(n - 1, a + s - '0' + 1, b, p, n, a, b, p, (std::string) "1" + s);
      }
      if (p)
        record(n - 1, a, b + 1, p ^ 1, n, a, b, p, (std::string) "1" + "/");
      else
        record(n - 1, a + 1, b, p ^ 1, n, a, b, p, (std::string) "1" + "/");
    } else {
      // 清场阶段
      int s = (6 - n) + 2;
      std::string str = "0";
      str.back() += s;
      if (p)
        record(n - 1, a, b + s, p, n, a, b, p, str);
      else
        record(n - 1, a + s, b, p, n, a, b, p, str);
    }
  }
}

std::string ans;

void make_ans(int n, int a, int b, int p) {
  if (a == -1) return;
  auto [ln, la, lb, lp] = lst[n][a][b][p];
  make_ans(ln, la, lb, lp), ans += link[n][a][b][p];
}

void solve() {
  cin >> a >> b >> n >> p;
  cerr << vis[n][a][b][p] << endl;
  if (!vis[n][a][b][p]) return cout << "NA" << endl, void();
  ans.clear(), make_ans(n, a, b, p);
  cout << ans << endl;
}

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

Problem E. Escaping from Trap

逆天题

纯数学题

考虑将多边形边长强行锁死成 \(1\)。由于是正多边形,所以可以强行规定一下点的坐标。

考虑点 \(A, B, C\),假设查询了 \(S_{\Delta PAB}\)\(S_{\Delta PAC}\),那么根据这两个面积的比值可以确定 \(P\) 的轨迹在一个二次曲线上,由于做比值所以消除了多边形边长 \(d\) 的影响。

根据简单的中学知识可以知道 \(P\) 的轨迹会退化成两条直线,当然平方去掉符号影响后直接去算也是可以得知 \(P\) 的轨迹喽……

假定查询了 \(S_{\Delta PAD}\),那么就可以唯一确定一条经过 \(A\) 的直线为 \(P\) 的轨迹。也就是说通过三个三角形的面积就可以确定一条 \(P\) 的轨迹。这个时候再去找几个就可以确定另外一条轨迹直线了,求一下交点就可以了。

思路是这样的,但是代码细节好像有点多……我没有写挂了好多次……不想写了 QAQ。

Problem F. Following Arrows

很难的构造题。以后有时间再写吧。

Problem G. GCD of Subsets

签到题,写不出退役吧

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

void solve() {
  cin >> n >> k >> m;
  long long p = n / k;
  long long only = 1, two = (p - 1) / 2, extra = n - only - two * 2;
  only += std::min(m, extra), m -= std::min(m, extra);
  if (m >= two * 2) {
    only += two * 2, m -= two * 2, two = 0;
  } else {
    long long max = m / 2;
    two -= max, m -= max * 2, only += max * 2;
  }
  cout << only + two << endl;
}

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

Problem H. Heuristic Knapsack

神仙题,把我的智力按在地板上疯狂摩擦。

通过强大的贪心技巧减少了一万个分类讨论的典型

而且涉及到了非常可怕的 trick,建议看官方题解,写的很好(好吧事实上就是我也觉得这个题过于神秘不可做了)

#include <iostream>
#include <algorithm>
#include <random>
#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 t;
int n, W;
int w[kMaxN], r[kMaxN];
std::vector<int> A, B, sA;
int w2[kMaxN], r2[kMaxN];
std::vector<int> id1, id2;

bool check(int k) {
  for (int i = 1; i <= n; i++) w2[i] = w[i], r2[i] = r[i] ? r[i] : 1;
  int res = W;
  for (int i = 0; i < k; i++) {
    res -= w[A[i]];
    if (r[A[i]] == 0) r2[A[i]] = 1e9;
  }
  if (res < 0) return false;
  // 为 b 中的重量重新标定
  int j = 0;
  if (k < A.size())
    while (j < B.size() && res) {
      int limit = 1e9;
      if (W == 1e9) limit--;
      upmin(limit, res);
      if (A[k] < B[j]) {
        upmin(limit, w[A[k]] - 1);
      } else {
        upmin(limit, w[A[k]]);
      }
      if (limit == 0) {
        w2[B[j++]] = 1e9;
      } else {
        w2[B[j++]] = limit, res -= limit;
      }
    }
  // cerr << "END2" << endl;
  // if (k == A.size() && W == 1e9 && j < B.size() && res) {
  //   w2[B[j++]] = 1;
  // }

  while (j < B.size()) w2[B[j++]] = 1e9;
  std::vector<bool> vis1(n + 1, false), vis2(n + 1, false);
  std::sort(id1.begin(), id1.end(), [](int i, int j) { return (w2[i] == w2[j] ? i < j : w2[i] < w2[j]); });
  std::sort(id2.begin(), id2.end(), [](int i, int j) { return (r2[i] == r2[j] ? i < j : r2[i] > r2[j]); });
  res = W;
  for (auto& i : id1) {
    if (w2[i] <= res) res -= w2[i], vis1[i] = true;
  }
  res = W;
  for (auto& i : id2) {
    if (w2[i] <= res) res -= w2[i], vis2[i] = true;
  }
  for (int i = 0; i <= n; i++) {
    if (vis1[i] != vis2[i]) return false;
  }
  return true;
}

void solve() {
  cin >> n >> W;
  id1.resize(n), A.clear(), B.clear();
  for (int i = 0; i < n; i++) id1[i] = i + 1;
  id2 = id1;
  for (int i = 1; i <= n; i++) cin >> w[i], (w[i] ? A : B).push_back(i);
  for (int i = 1; i <= n; i++) cin >> r[i];
  std::sort(A.begin(), A.end(), [](int i, int j) {
    if (w[i] == w[j]) return i < j;
    return w[i] < w[j];
  });
  std::sort(B.begin(), B.end(), [](int i, int j) {
    if (r[i] == r[j]) return i < j;
    return r[i] > r[j];
  });
  // 若一个方案是合法的,那么最后二者选取一定是 A 的一个前缀并且同时也是 B 的一个前缀
  // 枚举 A 的一个前缀
  for (int i = 0; i <= A.size(); i++) {
    if (check(i)) {
      cout << "Yes" << endl;
      for (int i = 1; i <= n; i++) cout << w2[i] << ' ';
      cout << endl;
      for (int i = 1; i <= n; i++) cout << r2[i] << ' ';
      cout << endl;
      return;
    }
  }
  cout << "No" << endl;
}

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

Problem I. Inside Polygon

简单的计算几何。好写。

对于外凸包的任意一个点,可以 \(O(n)\) 初始化出内凸包的外切线并且 \(O(1)\) 维护。

容易证明,三角形的边一定要在外切线的两边。假设考虑外凸包的节点 \(i\),那么可以获得一个逆时针连续区间和一个顺时针连续区间,记作 \(R_i\)\(L_i\),在这个区间里的点都可以和 \(i\) 构成一个三角形边。

\(R_i\)\(L_i\) 的初始化整体上的复杂度是线性的

题目就转变成了:存在多少点对 \(i, j, k\),使得 \(j \in R_i, k \in R_j, i \in R_k\)。当然等价于\(j \in R_i, k \in R_j, k \in L_i\)

到这里其实比较简单了,遍历 \(i\),但后可以动态维护有多少个点对 \(j, k\) 使得 \(j \in R_i, k \in R_j\),然后求一个区间和来筛选 \(k \in L_i\) 的点数量。

这个就是一个区间和区间加的事情,由于性质比较好其实可以做到线性。我比较懒套了一个线段树(

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

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

const char endl = '\n';
const int kMaxN = 4e6 + 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);
}

struct node {
  int x, y;
  node operator+(const node& b) const { return {x + b.x, y + b.y}; }
  int operator*(const node& b) const { return x * b.y - y * b.x; }
  node operator-(const node& b) const { return {x - b.x, y - b.y}; }
  friend node operator*(int i, const node& a) { return {i * a.x, i * a.y}; }
};

int t;

int n, m;
std::vector<node> p1, p2;

int R[kMaxN], L[kMaxN];

int lst(int x, int n) {
  return (x == 0 ? n - 1 : x - 1);
}

int nxt(int x, int n) {
  return (x == n - 1 ? 0 : x + 1);
}

bool leq(const node& a, const node& b) {
  return a * b >= 0;
}

int a[kMaxN];
int laz[kMaxN];

void add(int p, int v, int l, int r) {
  a[p] += (r - l + 1) * v;
  laz[p] += v;
}

void pushdown(int p, int l, int r) {
  if (laz[p]) {
    add(p << 1, laz[p], l, (l + r) >> 1);
    add(p << 1 | 1, laz[p], ((l + r) >> 1) + 1, r);
    laz[p] = 0;
  }
}

void pushup(int p) {
  a[p] = a[p << 1] + a[p << 1 | 1];
}

void add(int p, int l, int r, int L, int R, int v) {
  if (L > R) return add(p, l, r, 1, R, v), add(p, l, r, L, n, v);
  if (L <= l && r <= R) return add(p, v, l, r);
  pushdown(p, l, r);
  int mid = (l + r) >> 1;
  if (L <= mid) add(p << 1, l, mid, L, R, v);
  if (mid + 1 <= R) add(p << 1 | 1, mid + 1, r, L, R, v);
  pushup(p);
}

int ask(int p, int l, int r, int L, int R) {
  // cerr << "YOU ASK " << p << ' ' << l << ' ' << r << ' ' << L << ' ' << R << endl;
  if (L > R) return ask(p, l, r, 1, R) + ask(p, l, r, L, n);
  if (L <= l && r <= R) return a[p];
  pushdown(p, l, r);
  int sum = 0, mid = (l + r) >> 1;
  if (L <= mid) sum += ask(p << 1, l, mid, L, R);
  if (mid + 1 <= R) sum += ask(p << 1 | 1, mid + 1, r, L, R);
  return sum;
}

void solve() {
  cin >> n;
  p1.resize(n);
  for (auto& [x, y] : p1) cin >> x >> y;
  cin >> m;
  p2.resize(m);
  for (auto& [x, y] : p2) cin >> x >> y;
  for (int i = 0; i < (n << 2); i++) a[i] = laz[i] = 0;
  int l = 0, r = 0;
  for (int i = 0; i < m; i++) {
    auto t = p2[i] - p1[0];
    if (leq(p2[l] - p1[0], t)) l = i;
    if (leq(t, p2[r] - p1[0])) r = i;
  }
  L[0] = R[0] = 0;
  while (leq(p2[l] - p1[0], p1[lst(L[0], n)] - p1[0])) L[0] = lst(L[0], n);
  for (int i = 0; i < n; i++) {
    if (i) L[i] = L[i - 1], R[i] = R[i - 1];
    while (leq(p2[nxt(r, m)] - p1[i], p2[r] - p1[i])) r = nxt(r, m);
    while (leq(p2[l] - p1[i], p2[nxt(l, m)] - p1[i])) l = nxt(l, m);
    while (!leq(p2[l] - p1[i], p1[L[i]] - p1[i])) L[i] = nxt(L[i], n);
    while (leq(p1[nxt(R[i], n)] - p1[i], p2[r] - p1[i])) R[i] = nxt(R[i], n);
  }
  int now = 0, ans = 0;
  for (int i = 0; i < n; i++) {
    while (lst(now, n) != R[i]) {
      add(1, 1, n, now + 1, R[now] + 1, 1);
      now = nxt(now, n);
    }
    add(1, 1, n, i + 1, R[i] + 1, -1);
    ans += ask(1, 1, n, L[i] + 1, i + 1);
  }
  ans /= 3;
  cout << ans << endl;
}

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

Problem J. Judging Papers

写不出退役

#include <iostream>
#include <algorithm>
#include <vector>
#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;
int n, m, k, b;

void solve() {
  cin >> n >> m >> k >> b;
  int ans = 0;
  std::vector<int> list;
  for (int i = 1; i <= n; i++) {
    int sum = 0, cnt = 0, change = 0;
    for (int j = 1, v; j <= m; j++) {
      cin >> v;
      sum += v;
      change += v + (v <= 0 ? 1 : -1);
    }
    if (sum >= k)
      ans++;
    else if (change >= k && b)
      b--, ans++;
  }
  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 K-Coverage

这个题有线性做法woc

写了一坨 \(O(n\log n)\) 的做法

将操作拆成两部分来考虑:

  • 删除起点为 \(i\) 的一条线段
  • 在起点 \(j\) 加入一条线段,下文将这个行为称作决策 \(j\)

下文用 \(cnt[k]\) 来表示某段区间中某个数的数量,显然在不做任何操作的情况下答案就是 \(cnt[k]\)

删除 \(i\) 的线段,对答案的贡献为:\(-cnt[k] + cnt[k + 1]\)

\(j\) 加入一条线段,对答案的贡献为:\(-cnt[k] + cnt[k - 1]\)

但是,如果 \(i\)\(j\) 有重叠的话,重叠区域的贡献计算是要诡异一点的,要同时去掉删除和加入的两个影响,也就是说,重叠区域会有一坨额外的影响:\(2cnt[k] - cnt[k + 1] - cnt[k - 1]\)

我们在做滑动窗口去枚举 \(i\) 的时候其实可以动态维护某个点究竟与多少个决策 \(j\) 有重叠、然后把这个偏移量加到决策 \(j\) 上,会影响的决策 \(j\) 显然是连续的,相当于用一个线段树了来动态维护就可以了。

事实上反思目前的操作,你在枚举 \(i\) 的时候,会影响两段 \(j\),一段是重叠区域越来越少的,另一段是重叠区域越来越多的,但是两段之内的 \(j\) 决策的相对优秀性是不会改变的,感觉套个单调队列可以优化到线性了。

#include <iostream>
#include <random>

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

const char endl = '\n';
const int kMaxN = 2e6 + 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, L, k;
int l[kMaxN];
int a[kMaxN];
int cntK[kMaxN];
int cntKd[kMaxN];
int cntKp[kMaxN];

int N;
int laz[kMaxN << 2];
int max[kMaxN << 2];
#define mid ((l + r) >> 1)

void add(int p, int l, int r, int v) {
  max[p] += v, laz[p] += v;
}

void pushdown(int p, int l, int r) {
  if (laz[p]) {
    add(p << 1, l, mid, laz[p]);
    add(p << 1 | 1, mid + 1, r, laz[p]);
    laz[p] = 0;
  }
}

void pushup(int p) {
  max[p] = std::max(max[p << 1], max[p << 1 | 1]);
}

void build(int p, int l, int r) {
  laz[p] = max[p] = 0;
  if (l == r) return max[p] = cntKd[l + L - 1] - cntKd[l - 1] - (cntK[l + L - 1] - cntK[l - 1]), void();
  build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
  pushup(p);
}

int ask(int p, int l, int r, int L, int R) {
  if (L <= l && r <= R) return max[p];
  pushdown(p, l, r);
  if (R <= mid) return ask(p << 1, l, mid, L, R);
  if (mid + 1 <= L) return ask(p << 1 | 1, mid + 1, r, L, R);
  return std::max(ask(p << 1, l, mid, L, R), ask(p << 1 | 1, mid + 1, r, L, R));
}

void add(int p, int l, int r, int L, int R, int v) {
  if (L <= l && r <= R) return add(p, l, r, v);
  pushdown(p, l, r);
  if (L <= mid) add(p << 1, l, mid, L, R, v);
  if (mid + 1 <= R) add(p << 1 | 1, mid + 1, r, L, R, v);
  pushup(p);
}

void build() {
  build(1, 1, N);
}

void add(int l, int r, int v) {
  add(1, 1, N, l, r, v);
}

int ask() {
  return max[1];
}

void work(int i, int v) {
  if (i == 0) return;
  int val = -(a[i] == k - 1) - (a[i] == k + 1) + (a[i] == k) * 2;
  val *= v;
  add(std::max(1, i - L + 1), i, val);
}

void solve() {
  cin >> n >> L >> k;
  N = n << 2;
  for (int i = 1; i <= (N << 1); i++) l[i] = a[i] = cntK[i] = cntKp[i] = cntKd[i] = 0;
  for (int i = 1, t; i <= n; i++) {
    cin >> t, t++;
    l[t]++;
    a[t]++, a[t + L]--;
  }
  for (int i = 1; i <= N + 2 * n; i++) {
    a[i] += a[i - 1];
    cntK[i] = cntK[i - 1] + (a[i] == k);
    cntKp[i] = cntKp[i - 1] + (a[i] == k + 1);
    cntKd[i] = cntKd[i - 1] + (a[i] == k - 1);
  }
  build();
  int tl = 0, tr = 0, ans = cntK[N];
  for (int i = 1; i <= N; i++) {
    if (l[i] == 0) continue;
    while (tl < i) work(tl++, -1);
    while (tr < (i + L - 1)) work(++tr, 1);
    upmax(ans, cntK[N] + ask() + cntKp[i + L - 1] - cntKp[i - 1] - (cntK[i + L - 1] - cntK[i - 1]));
  }
  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 L. Label Matching

树上启发式合并的板子题。想到了启发式合并就随便写了。

感觉没什么好写的其实。

吐槽一下数据很水(或者说这种题没法造的很强)

#include <iostream>
#include <map>
#include <algorithm>
#include <vector>
#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;
int n;
std::vector<int> go[kMaxN];
std::vector<int> go2[kMaxN];
int a[kMaxN], b[kMaxN];
bool ans[kMaxN];
int siz[kMaxN];

void dfs(int u, int fa = 0) {
  siz[u] = 1;
  for (auto v : go[u]) {
    if (v == fa) continue;
    go2[u].push_back(v);
    dfs(v, u);
    siz[u] += siz[v];
  }
  std::sort(go2[u].begin(), go2[u].end(), [](int i, int j) { return siz[i] > siz[j]; });
}

int moreA = 0, moreB = 0;
int cntA[kMaxN], cntB[kMaxN];

void add(int u) {
  if (a[u]) {
    if (cntA[a[u]] >= cntB[a[u]]) moreA++;
    if (cntA[a[u]] < cntB[a[u]]) moreB--;
  }
  cntA[a[u]]++;
  if (b[u]) {
    if (cntA[b[u]] > cntB[b[u]])
      moreA--;
    else if (cntA[b[u]] <= cntB[b[u]])
      moreB++;
  }
  cntB[b[u]]++;
}

void fill(int u) {
  for (auto v : go2[u]) {
    fill(v);
  }
  add(u);
}

void clear(int u) {
  for (auto v : go2[u]) {
    clear(v);
  }
  cntA[a[u]] = cntB[b[u]] = 0;
}

void work(int u) {
  for (int i = 1; i < go2[u].size(); i++) {
    work(go2[u][i]);
    clear(go2[u][i]), moreA = moreB = 0;
  }
  if (go2[u].size()) work(go2[u][0]);
  for (int i = 1; i < go2[u].size(); i++) fill(go2[u][i]);
  add(u);
  ans[u] = cntA[0] >= moreB && cntB[0] >= moreA;
}

void solve() {
  cin >> n;
  for (int i = 1; i <= n; i++) go[i].clear(), go2[i].clear();
  for (int i = 1; i <= n; i++) cin >> a[i];
  for (int i = 1; i <= n; i++) cin >> b[i];
  for (int i = 1, u, v; i < n; i++) {
    cin >> u >> v;
    go[u].push_back(v), go[v].push_back(u);
  }
  dfs(1);
  work(1);
  clear(1), moreA = moreB = 0;
  for (int i = 1; i <= n; i++) cout << ans[i];
  cout << endl;
}

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

Problem M. Meeting for Meals

要跑两遍最短路,一次获取 从某点开始到 1 的最短路,另外一次获取 到达某点路径最短的人和路径

最大化陪伴时间等价于最小化第一次碰面时间

试图最小化碰面时间,那么最好的方式是两个人不能等待而是双向奔赴,这也是记录最早达到某点的人的必要性

任何一个人显然都至少有一个点,使得他是最早到达这个点的。记录最早到达的人为 \(f_u\)

对于任何一个人 \(i\) 来说,它显然要找到一个点 \(f_u = i\),且存在一个邻居 \(f_v \neq f_u\),这样 \(i\) 就可以和 \(f_v\)\((u, v, w)\) 这条边上碰面。

判定在这条边上碰面之后是否可以在截止时间到达 \(1\) 是比较简单的,碰面的时间是 \(\frac{dis_u + dis_v + w}{2}\)。找到最小的合法碰面时间就可以了。

#include <algorithm>
#include <set>
#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 t;
int n, m, k;
std::vector<int> st;
int from[kMaxN];
int dis[kMaxN], dis2[kMaxN];
std::vector<std::pair<int, int>> go[kMaxN];

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

double ans[kMaxN];
int cnt[kMaxN];

void solve() {
  cin >> n >> m >> k;
  for (int i = 1; i <= n; i++) go[i].clear(), cnt[i] = 0, ans[i] = 0;
  st.resize(k);
  for (auto& v : st) cin >> v, cnt[v]++;
  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(dis2, 0);
  dijkstra(dis, 1);
  // dis 用来记录最早到达某个节点的是谁
  long long tmeet = 0;
  // for (int i = 1; i <= n; i++) {
  //   cerr << dis2[i] << ' ';
  // }
  // cerr << endl;
  for (auto v : st) upmax(tmeet, dis2[v]);
  for (int u = 1; u <= n; u++) {
    int i = from[u];
    for (auto [v, w] : go[u]) {
      if (from[u] == from[v]) continue;
      if (dis[u] + dis2[v] + w <= tmeet || dis[v] + dis2[u] + w <= tmeet) {
        double tmp = (dis[u] + dis[v] + w) / 2.0;
        upmax(ans[from[u]], tmeet - tmp);
        upmax(ans[from[v]], tmeet - tmp);
      }
    }
  }
  for (auto v : st) {
    if (cnt[v] > 1) {
      printf("%.1lf ", (double)tmeet);
    } else {
      printf("%.1lf ", (double)ans[v]);
    }
  }
  printf("\n");
  // cerr << endl;
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> t;
  while (t--) solve();
  return 0;
}
posted @ 2025-11-20 23:06  sudoyc  阅读(18)  评论(0)    收藏  举报