2025ccpc南昌邀请赛个人题解

vp25cc邀请南昌

后面会补图论的(不会QAQ)

G(模拟)

数目最多, 大于1的奇数, 那就是先分3块最多, n%3 == 1时候, 就得俩个三合在一起也就是7才行, n % 3 == 2那就是3 + 2 = 5即可

inline void solve(){
	int n;
	cin>>n;
	if(n<3||n==4)cout<<"-1"<<endl;
	else{
		int m=n%3;
		if(m==0||m==2)cout<<n/3<<endl;
		else cout<<n/3-1<<endl;
	}
}

L(模拟)

模拟每种状态就好, if判断一下就可以了

    int a,b;
    cin>>a>>b;
    if (a>=21&&a-b>=2||a==30) {
        cout<<"Alice\n";
    }
    else if (b>=21&&b-a>=2||b==30) {
        cout<<"Bob";
    }
    else {
        cout<<"Underway";
    }

A(二分答案)

二分答案, 去check是否可以满足即可, 每一个留1个就可以, 其他的都去换, 就好了

template<typename T, typename M>
T get_first_math(T lo, T hi, M math) {
    while (lo <= hi) {
        T mid = lo + (hi - lo) / 2;
        if (math(mid)) {
            hi = mid - 1;
        } else {
            lo = mid + 1;
        }
    }
    return lo;
}
inline void solve() {
    int n;
    i64 k;
    cin >> n >> k;
    vector<i64> a(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    sort(a.begin() + 1, a.end(), greater<i64>());
    vector<i64> pre(n + 1);
    for (int i = 1; i <= n; i++) {
        pre[i] = pre[i - 1] + a[i];
    }
    i64 sum = pre[n];
    auto check = [&](i64 m) -> bool {
        int d = lower_bound(pre.begin() + 1, pre.end(), m) - pre.begin();
        return m + (k - 1) * d >= k * n;
    };
    i64 ans = get_first_math<i64>(0, sum, check);
    cout << ans << endl;
}

K(思维)

让每次%4的值不为1即可, 那就是存每个数%4的数字就好, 然后记录{0, 1, 2, 3}都有多少个, 那么我们尽量去配对4就好了, 先用 3 1 消掉一部分,剩下的只会是 {1,2}{3,2}{2}
剩下的 1 必须两个一组,用一个 2 消掉;剩下的 3 也必须两个一组,用一个 2 消掉;最后剩下的 2 必须两个一组。

inline void solve() {
  int n;
  cin >> n;
  vector<vector<int>> id(4);
  for (int i = 1; i <= n; i++) {
    int x;
    cin >> x;
    id[x % 4].push_back(i);
  }
  vector<int> ans;
  auto get = [&](int r) {
    int x = id[r].back();
    id[r].pop_back();
    return x;
  };
  // 余数 0 放在最前面,前缀余数仍然是 0
  while (!id[0].empty()) {
    ans.push_back(get(0));
  }
  // 先尽量配 3 1
  while (!id[3].empty() && !id[1].empty()) {
    ans.push_back(get(3));
    ans.push_back(get(1));
  }
  // 如果剩下 1,就用 2 1 1 消掉
  while (id[1].size() >= 2 && !id[2].empty()) {
    ans.push_back(get(2));
    ans.push_back(get(1));
    ans.push_back(get(1));
  }
  // 如果剩下 3,就用 3 3 2 消掉
  while (id[3].size() >= 2 && !id[2].empty()) {
    ans.push_back(get(3));
    ans.push_back(get(3));
    ans.push_back(get(2));
  }
  // 最后剩下的 2,只能 2 2 配
  while (id[2].size() >= 2) {
    ans.push_back(get(2));
    ans.push_back(get(2));
  }
  if (!id[1].empty() || !id[2].empty() || !id[3].empty()) {
    cout << -1 << endl;
    return;
  }
  for (int i = 0; i < n; i++) {
    cout << ans[i] << " \n"[i == n - 1];
  }
}

H(思维, 后缀最小值)

把序列变成a_i - i, 此时的答案就是sum_{1 ...n} + su{a_i - i}, 然后对序列a_i - i进行操作, 然后从后往前走,找到第一个比他小的数字, 这些区间可以进行减少操作(他和比他小的数字的差 * 数字的个数)(其实就是维护后缀最小值就好了)

inline void solve() {
  int n;
  cin >> n;
  vector<i64> a(n + 1), c(n + 2), mn(n + 2);
  i64 sum = 0;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    c[i] = a[i] - i;
    if (c[i] < 0) {
      cout << -1 << endl;
      return;
    }
    sum += c[i];
  }
  mn[n + 1] = (i64)1e18;
  for (int i = n; i >= 1; i--) {
    mn[i] = min(mn[i + 1], c[i]);
  }
  i64 cur = 0;   
  i64 pool = 0;  
  i64 save = 0;  
  for (int i = 1; i <= n; i++) {
    save += cur;
    pool += c[i] - cur;
    if (i < n) {
      i64 can = mn[i + 1] - cur;
      i64 d = min(pool, can);
      pool -= d;
      cur += d;
    }
  }
  cout << n + sum - save << endl;
}

C(双指针+线段树)

可以转换为一个经典问题就是区间覆盖, 然后每个位置覆盖的数目不能超过k个, 就是用线段树维护就好了, 懒标记就是正常+ 1就好, 然后Node就是维护区间最大值就好,每次给这个右点放进去,看这个区间是里面的最大值,是否大于k, 如果大于k那么左指针就往后移动就好, 直到小于等于k就好, 每次记录一个max就好, 时间复杂度是O(nlogn)可以通过

struct Node {
  int v = 0;
  Node operator+(const Node& rhs) { return {max(v, rhs.v)}; }
  template <typename Tag>
  void apply(const Tag& tag) {
    v += tag;
  }
};

inline void solve() {
  int n, k;
  cin >> n >> k;
  vector<pair<int, int>> a(n);
  for (auto& [l, r] : a) {
    cin >> l >> r;
  }
  seg_tree<Node, true, i64> seg(n + 1);
  int ans = 0;
  int l = 0;
  for (int r = 0; r < n; r++) {
    auto [ql, qr] = a[r];
    seg.update(ql, qr, {1});
    while (seg.top().v > k) {
      auto [pl, pr] = a[l];
      seg.update(pl, pr, {-1});
      l++;
    }
    ans = max(ans, r - l + 1);
  }
  cout << ans << endl;
  return;
}

D(最小生成树)

E(公式推导 + 线性转换)

  1. 对于一个同色线段, 左点为\(col_{i, j}\) 右点为\(col_{i, j + 1}\)表示i颜色的第j个点, 那么对于他来说一个颜色的贡献为

    \(sum[y]_{in} * sum[y]_{out}\)

    \(sum[y]_{out} = sum[y] - sum[y]_{in}\)

    \(sum[y]_{in} * (sum[y] - sum[y]_{in})\)

    其中\(sum[y]\)是固定的值, 然后\(sum[y]_{in}\)可以用前缀和快速求得

    也就是\(sum[y]_{in} = pre[y][r] - pre[y][l - 1]\)

  2. 那么对于颜色x所有的点都可以俩俩配对形成线段, 当你枚举到当前点j那么之前所有同色点i对答案的贡献就是

    \(res = \sum_{i}^{i\in x} a[i] * a[j] * sum[y]_{in} * (sum[y] - sum[y]_{out})\)

    为了方便公式推导我们令

    \(pj = pre[y][j]\)

    \(pi = pre[y][i]\)

    \(sy = sum[y]\)

    原公式变为

    \(res = \sum_{i}^{i\in x} a[i] * a[j] * (pj - pi) * (sy - pj + pi)\)

    \(res = a[j] * \sum_{i}^{i\in x} a[i] * (sy * pj - pj^2 + 2 * pj * pi - sy * pi - pi^2 )\)

    \(res = a[j] * \sum_{i}^{i\in x} a[i] * (sy * pj - pj^2 -pi^2 + pi *(2pj - sy))\)

    \(res = a[j]* (sy*pj - pj^2) * \sum_{i}^{i\in x}a[i] - \sum_{i}^{i\in x}a[i]*pi^2 +(2pj - sy)* \sum_{i}^{i\in x}a[i] * pi\)

    那就是维护 \(\sum_{i}^{i\in x}a[i]\) \(\sum_{i}^{i\in x}a[i]*pi^2\) \(\sum_{i}^{i\in x}a[i]*pi\)这三个就好了

    考虑用开一个颜色维护区间就好了\(sum[x][0]\)去维护第一个, 然后其他维护第二个......

    伪代码

    for j = 1...n:
        x = c[j]
    
        // 先统计以 j 作为右端点的贡献
        for y = 1...k:
            if y == x:
                continue
            pj = pre[y]
            syy = sy[y]
            cur =
                (pj * syy - pj * pj) * sum0[x]
              + (2 * pj - syy) * sum1[x][y]
              - sum2[x][y]
    
            ans += a[j] * cur
    
        // 再把当前点加入前缀
        pre[x] += a[j]
    
        // 当前点 j 以后可以作为左端点 i
        sum0[x] += a[j]
        for y = 1...k:
            pi = pre[y]
            sum1[x][y] += a[j] * pi
            sum2[x][y] += a[j] * pi * pi
    
    
  3. 最后一步 ans / 2

    why?

    如果有两条相交异色线段:

    线段 A:颜色 x
    线段 B:颜色 y

    当我们枚举线段 A 时,会把线段 B 算进去一次。

    当我们枚举线段 B 时,也会把线段 A 算进去一次。

    所以每一对相交异色线段会被统计两次。

    因此最后答案要除以 2 : ans = ans * inv2 % mod;

ok 那么来写代码

const int MOD = 998244353;
const int INV2 = 499122177;
inline int addmod(int a, int b) {
  a += b;
  if (a >= MOD) a -= MOD;
  return a;
}
inline int submod(int a, int b) {
  a -= b;
  if (a < 0) a += MOD;
  return a;
}
inline int mulmod(i64 a, i64 b) { return a * b % MOD; }

inline void solve() {
  int n, k;
  cin >> n >> k;
  vector<int> c(n);
  vector<int> a(n);
  for (auto& x : c) cin >> x;
  for (auto& x : a) cin >> x;
  int ans = 0;
  vector<int> pre(k + 10), sy(k + 10), sum0(k + 10);
  vector<vector<int>> sum1(k + 10, vector<int>(k + 10, 0));
  auto sum2 = sum1;
  for (int i = 0; i < n; i++) {
    sy[c[i]] = addmod(sy[c[i]], a[i]);
  }
  for (int i = 0; i < n; i++) {
    int x = c[i];
    for (int y = 1; y <= k; y++) {
      if (y == x) continue;
      int pj = pre[y];
      int syy = sy[y];
      int A = mulmod(pj, submod(syy, pj));
      int B = submod(addmod(pj, pj), syy);
      int cur = 0;
      cur = addmod(cur, mulmod(A, sum0[x]));
      cur = addmod(cur, mulmod(B, sum1[x][y]));
      cur = submod(cur, sum2[x][y]);
      ans = addmod(ans, mulmod(a[i], cur));
    }
    pre[x] = addmod(pre[x], a[i]);
    sum0[x] = addmod(sum0[x], a[i]);
    for (int y = 1; y <= k; y++) {
      int pi = pre[y];
      sum1[x][y] = addmod(sum1[x][y], mulmod(a[i], pi));
      sum2[x][y] = addmod(sum2[x][y], mulmod(mulmod(a[i], pi), pi));
    }
  }
  ans = mulmod(ans, INV2);
  cout << ans << endl;
}

B(竞赛图?)

posted @ 2026-05-07 11:10  khh_fighting  阅读(99)  评论(0)    收藏  举报