Codeforces Round 979 (Div. 2)

Luckyblock發表於2024-10-26

目錄
  • 寫在前面
  • A 簽到
  • B 構造
  • C 博弈
  • D 模擬
  • E 組合數學
  • 寫在最後

寫在前面

比賽地址:https://codeforces.com/contest/2030

賽時 E 看錯題了變成神題了而且居然還口胡了一個自然根號的做法還寫出來瞭然而樣例沒過最後才發現讀錯題了媽的。

掉分!

A 簽到

\(b, c\) 即字首最小值和最大值,顯然最優的構造是把最大值和最小值放在前兩個位置,答案即:

\[(n-1)\left(\max a_i - \min a_i\right) \]

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    int n; std::cin >> n;
    int mina = 1100, maxa = -1;
    for (int i = 1; i <= n; ++ i) {
      int a; std::cin >> a;
      mina = std::min(mina, a);
      maxa = std::max(maxa, a);
    }
    std::cout << (1ll * (n - 1) * (maxa - mina)) << "\n";
  }
  
  return 0;
}

B 構造

因為是 01 串且選的是子序列,所以並不需要關注選的位置,僅需關注選的字元種類即可。

設構造的串 \(t\) 裡有 \(k\) 個 0,則 \(f(t) = 2^k - 1, g(t) = 2^n - 1 - f(t)\),則 \(|f(t) - g(t)| = |2^{k +1} - 2^n|\)

僅需取 \(k=n-1\) 即可令 \(|f(t)-g(t)| = 0\),於是隨便構造一個有 \(n-1\) 個 0,1 個 1 的串即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    int n; std::cin >> n;
    for (int i = 1; i <= n - 1; ++ i) std::cout << 0;
    std::cout << 1;
    std::cout << "\n";
  }
  return 0;
}

C 博弈

顯然若第一個或最後一個字元為 1,則 Alice 必勝。

注意到題面中特別指出 and 運算優先順序更高,則發現若有兩個 1 相鄰,則 Alice 一定可以在將兩者之一的兩側均插入一個 or 從而必勝,否則若所有 1 均不相鄰,則 Bob 可以透過優先順序更高的 and 將所有 1 均變為 0;

於是僅需判斷第一個字元、最後一個字元是否為 1,或是否有 1 相鄰即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    int n; std::cin >> n;
    std::string s; std::cin >> s;
    
    int flag = 0;
    for (int i = 0; i < n; ++ i) {
      if (i == 0 && s[i] == '1') flag = 1;
      if (i == n - 1 && s[i] == '1') flag = 1;
      if (0 < i && s[i] == '1' && s[i - 1] == '1') flag = 1;
    }
    std::cout << (flag ? "YES\n" : "NO\n");
  }
  return 0;
}

D 模擬

發現將操作分為 \(L, R\) 是無必要的,先把所有操作均轉化為 \((i-1, i)\) 的形式。

發現如果在某個位置 \(i\) 前面有大於 \(i\) 的數,說明操作 \((i-1, i)\) 是必要的。因為每次詢問排列均為初始狀態,則於是必要操作的種類是不變的,僅需考慮維護當前已經有了多少必要操作即可。

可以透過桶簡單實現。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, q, p[kN], premax[kN], sufmin[kN];
int need[kN], have[kN];
std::string s;
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    std::cin >> n >> q;
    for (int i = 1; i <= n; ++ i) std::cin >> p[i], need[i] = have[i] = 0;
    std::cin >> s; s = "$" + s;

    for (int i = 1; i <= n; ++ i) premax[i] = std::max(premax[i - 1], p[i]);
    sufmin[n + 1] = n + 1;
    for (int i = n; i; -- i) sufmin[i] = std::min(sufmin[i + 1], p[i]);
    
    int cnt = 0, needcnt = 0;
    for (int i = 1; i <= n; ++ i) {
      if (s[i] == 'L') ++ have[i - 1];
      if (s[i] == 'R') ++ have[i];
    }
    for (int i = 1; i < n; ++ i) {
      if (premax[i] > i || sufmin[i + 1] <= i) need[i] = 1;
      needcnt += need[i];
      cnt += (need[i] && have[i]);
    }

    while (q --) {
      int x; std::cin >> x;
      cnt -= (need[x - 1] && have[x - 1]);
      cnt -=  (need[x] && have[x]);

      if (s[x] == 'L') -- have[x - 1], ++ have[x], s[x] = 'R';
      else -- have[x], ++ have[x - 1], s[x] = 'L';

      cnt += (need[x - 1] && have[x - 1]);
      cnt += (need[x] && have[x]);

      std::cout << (cnt == needcnt ? "YES" : "NO") << "\n";
    }
  }
  return 0;
}

E 組合數學

結論縫合題。首先要推出來兩個結論:

給定 \(n\) 個整數,記其中權值 \(i\) 的數量為 \(\operatorname{cnt}_i\),則 \(\operatorname{mex}\le i\) 的子集數量為:\(s_i = 2^{n} - 2^{\operatorname{cnt}_i}\),簡單容斥可知 \(\operatorname{mex}= i\) 的子集數量為 \(s_i - s_{i - 1}\)

給定 \(n\) 個整數,記其中權值 \(i\) 的數量為 \(\operatorname{cnt}_i\),將它們分為若干個集合 \(S_1\sim S_k\),最大化 \(\sum_j \operatorname{mex}(S_j)\),顯然貪心地分配,每次將當前所有權值都拿出一個來建立一個新集合即可,最大價值為:\(\operatorname{cnt}_0 + \min(\operatorname{cnt}_0, \operatorname{cnt}_1) + \min(\operatorname{cnt}_0, \operatorname{cnt}_1, \operatorname{cnt}_2) + \cdots\)

因為是選子序列,於是僅需考慮選擇的各種權值的數量即可。根據上述結論,考慮升序列舉所有權值 \(i\),考慮該種權值產生了 \(j\) 的貢獻(即分出來的子集中恰有 \(j\)\(\operatorname{mex}> i\))的子序列數量有多少。考慮套用結論 1,先求 \(s_j\) 表示該種權值產生了 \(\ge j\) 的貢獻的方案數 \(s_j\),則對於權值 \(0\sim i\),都需要選至少 \(j\) 個,對於其他權值任選,可以簡單透過組合意義+字首和得到,則總貢獻即為:

\[\sum_{j=1}^{\min(\operatorname{cnt}_0, \cdots, \operatorname{cnt}_i)} j\times (s_{j + 1} - s_{j}) \]

實現詳見程式碼,總時間複雜度 \(O(n)\) 級別。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
const LL p = 998244353;
//=============================================================
int n, a[kN];
LL pw2[kN], fac[kN], invfac[kN], cnt[kN], sum[kN], pre[kN];
//=============================================================
LL C(LL x_, LL y_) {
  if (y_ == 0) return 1;
  if (y_ > x_) return 0;
  return fac[x_] * invfac[y_] % p * invfac[x_ - y_] % p;
}
LL qpow(LL x_, LL y_) {
  LL ret = 1;
  while (y_) {
    if (y_ & 1) ret = ret * x_ % p;
    x_ = x_ * x_ % p, y_ >>= 1ll;
  }
  return ret;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  pw2[0] = fac[0] = invfac[0] = 1;
  for (int i = 1; i < kN; ++ i) {
    pw2[i] = 2ll * pw2[i - 1] % p;
    fac[i] = fac[i - 1] * i % p;
  }
  invfac[kN - 1] = qpow(fac[kN - 1], p - 2);
  for (int i = kN - 2; i; -- i) invfac[i] = invfac[i + 1] * (i + 1) % p;

  int T; std::cin >> T;
  while (T --) {
    std::cin >> n;
    for (int i = 0; i <= n; ++ i) cnt[i] = 0;
    for (int i = 1; i <= n; ++ i) std::cin >> a[i], ++ cnt[a[i]];

    LL ans = 0, mincnt = kN, sumcnt = 0;
    for (int i = 1; i <= n; ++ i) pre[i] = 1;

    for (int i = 0; i < n; ++ i) {
      if (cnt[i] == 0) break;
      for (int j = 0; j <= cnt[i] + 1; ++ j) sum[j] = 0;
      
      LL sumc = 0;
      mincnt = std::min(mincnt, cnt[i]), sumcnt += cnt[i];
      for (int j = cnt[i]; j; -- j) {
        sumc = (sumc + C(cnt[i], j)) % p;
        pre[j] = pre[j] * sumc % p;
      }
      for (int j = 1; j <= mincnt; ++ j) {
        sum[j] = 1ll * pre[j] * pw2[n - sumcnt] % p;
      }
      for (int j = 1; j <= mincnt; ++ j) {
        ans += 1ll * j * (sum[j] - sum[j + 1] + p) % p;
        ans %= p;
      }
    }
    std::cout << ans << "\n";
  }
  return 0;
}

寫在最後

學到了什麼:

  • E:考慮單個元素對集合的貢獻之和。

相關文章