Codeforces Round 975 (Div. 2)

Luckyblock發表於2024-09-29

目錄
  • 寫在前面
  • A 簽到
  • B 排序,模擬
  • C 數學,貪心
  • D 模擬,思維
  • E 樹,貪心,暴力 or 長鏈剖分
  • F 貪心,列舉
  • 寫在最後

寫在前面

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

唉唉不敢打 div1 只敢開小號打 div2 太菜了。

A 簽到

顯然最優方案只有兩種——取所有奇數位置或所有偶數位置。

//
/*
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 cnt[2] = {0}, sum[2] = {0};
    for (int i = 1; i <= n; ++ i) {
      int x; std::cin >> x;
      ++ cnt[i % 2];
      sum[i % 2] = std::max(sum[i % 2], x);
    }
    std::cout << std::max(cnt[0] + sum[0], cnt[1] + sum[1]) << "\n";
  }
  return 0;
}

B 排序,模擬

發現對於每個點,以及兩點間的區間,僅需考慮兩邊分別有多少點即可就算出它們被多少線段覆蓋。

則覆蓋線段數不同的區間(單點也算)僅有 \(2n\) 級別,列舉這些區間並計算線段數量,然後使用 map 維護即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, q, x[kN];
LL cnt[kN];
std::map <LL, int> ans;
//=============================================================
LL query(LL k_) {
  return ans[k_];
}
//=============================================================
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 >> x[i];
    ans.clear();
    for (int i = 1; i <= n; ++ i) {
      cnt[i] = 1ll * i * (n - i + 1) - 1;
      ++ ans[cnt[i]];
    }
    for (int i = 1; i < n; ++ i) {
      int l = x[i] + 1, r = x[i + 1] - 1;
      LL sum = 1ll * i * (n - i);
      ans[sum] += r - l + 1;
    }

    while (q --) {
      LL k; std::cin >> k;
      std::cout << query(k) << " ";
    }
    std::cout << "\n";
  }
  return 0;
}
/*
1
6 15
1 2 3 5 6 7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
*/

C 數學,貪心

我去怎麼還有一個結論在三個題裡出現的。

發現 \(\sum n\le 10^5\),考慮列舉每組的物品數量上限 \(c\),並檢查將已有物品按照該限制至少分多少組 \(M\),則可求出補齊 \(M\) 組所需的最少的額外數量,然後再不斷加整組即可。

那麼至少分多少組呢?考慮經典結論:要求將 \(n\) 種顏色的物品按每組上限 \(c\) 個分組,保證每組內所有物品顏色不同,則有結論最少分組數為:

\[M = \max\left( \max {b_i}, \left\lceil \dfrac{\sum b_i}{c} \right\rceil\right) \]

則補齊 \(M\) 組所需的最少的額外數量為:

\[c\times M - \sum a_i \]

則僅需檢查可額外加入的數量 \(k\) 的合法性,並求得還可以加多少整組即可,總時間複雜度 \(O(n)\) 級別。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n;
LL k, a[kN];
//=============================================================
//=============================================================
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 >> k;
    LL maxa = 0, suma = 0;
    for (int i = 1; i <= n; ++ i) {
      std::cin >> a[i]; 
      maxa = std::max(maxa, a[i]);
      suma += a[i];
    }
    LL ans = 1;
    for (LL i = 2; i <= n; ++ i) {
      LL mins = std::max(maxa, 1ll * (suma + i - 1) / i);
      LL mincnt = i * mins;
      if (mincnt > suma + k) continue;
      ans = i;
    }
    std::cout << ans << "\n";
  }
  return 0;
}

D 模擬,思維

對上電波就比較好做的題。

先考慮對於每個起點 \(i\),依次檢查到時間 \(j\) 時是否合法。此時僅需要考慮 \(a_i \le j\) 的位置的限制,設這些位置中最靠左/右的位置分別為 \(L_i, R_i\),則 \(i\) 合法當且僅當對於所有時間 \(j\),有 \(|i - L_j| \le j\)\(|i - R_j|\le j\),即保證從 \(i\) 出發能到達所有 \(a_i \le j\) 的位置。

更進一步地,發現上述限制即限制了 \(i\) 與所有 \(L_i, R_i\) 距離的上界。則發現對於每個時刻 \(j\),合法的 \(i\) 一定構成一段連續的區間,且這個區間一定是單調縮小的,於是每次時間增加時,更新 \(L_j, R_j\) 後大力檢查上述區間端點作為起點是否合法即可。

總時間複雜度 \(O(n)\) 級別。

呃呃我賽時寫的很醜很醜

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, a[kN], L[kN], R[kN];
std::vector<int> pos[kN];
int ansl, ansr;
//=============================================================
bool check1(int time1_, int time2_, int l_, int r_, int newl_, int newr_) {
  if (time1_ == 0) {
    return newr_ - newl_ + 1 <= time2_;
  }
  if (l_ <= newl_ && newr_ <= r_) return true;
  int d1 = std::max(0, l_ - newl_);
  int d2 = std::max(0, newr_ - r_);
  return time2_ - std::min(r_ - l_ + 1, time1_) >= d1 + d2;
}
void check2(int time_, int l_, int r_) {
  for (int i = ansl; i <= ansr; ++ i) {
    if (l_ <= i && i <= r_) break;
    if (i > r_) {
      if (i - l_ + 1 > time_) ansl = ansr + 1;
      break;
    }
    if (i < l_ && r_ - i + 1 <= time_) break;
    ++ ansl; 
  }
  for (int i = ansr; i >= ansl; -- i) {
    if (l_ <= i && i <= r_) break;
    if (i < l_) {
      if (r_ - i + 1 > time_) ansr = ansl - 1;
      break;
    }
    if (i > r_ && i - l_ + 1 <= time_) break;
    -- ansr;
  }
}
//=============================================================
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;
    for (int i = 1; i <= n; ++ i) pos[i].clear();
    for (int i = 1; i <= n; ++ i) std::cin >> a[i], pos[a[i]].push_back(i);
    
    int time = 0, l = n + 1, r = 0, yes = 1;
    for (int i = 1; i <= n; ++ i) {
      if (!pos[i].size()) continue;
      yes &= check1(time, i, l, r, pos[i].front(), pos[i].back());
      if (!yes) break;

      time = i;
      l = std::min(l, pos[i].front());
      r = std::max(r, pos[i].back());
      L[i] = l, R[i] = r;
    }
    if (!yes) {
      std::cout << 0 << "\n";
      continue;
    }
    
    ansl = 1, ansr = n;
    for (int i = 1; i <= n; ++ i) {
      if (pos[i].size()) check2(i, L[i], R[i]);
    }
    std::cout << std::max(0, ansr - ansl + 1) << "\n";
  }
  return 0;
}
/*
1
6
6 6 6 6 6 5
*/

E 樹,貪心,暴力 or 長鏈剖分

顯然若最終答案的葉節點均屬於某層,則原樹中該層的節點均可以保留。更進一步地可以發現,此時最終答案等價於選擇該層所有節點到根的鏈建立的虛樹。

一個顯然的做法是從根節點開始按層 bfs,每次透過當前層的節點構造下一層。則僅需考察當前層節點 \(u\) 是否有子節點:

  • 若有子節點,則子節點均屬於下一層,直接列舉並加入即可;
  • 否則,應當刪除從當前節點到某個祖先節點的一條鏈,直至祖先節點仍有子節點在虛樹中。

發現每個節點僅會被加入一次被刪除一次,則可以在上述刪除過程中直接暴力上跳。於是考慮在上述 bfs 過程中,維護每個節點有多少子節點還在虛樹中,在暴力上跳過程中若將父節點所有子節點刪完,則繼續上跳即可。

當然也可以不暴力上跳,而是透過類似長鏈剖分的預處理,來減去上述被刪掉的部分的貢獻,詳見其它題解。

總時間複雜度 \(O(n)\) 級別。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e5 + 10;
//=============================================================
int n, ans, maxdep, dep[kN], fa[kN];
std::vector<int> edge[kN], son[kN];
int cntson[kN];
//=============================================================
void dfs(int u_, int fa_) {
  fa[u_] = fa_;
  dep[u_] = dep[fa_] + 1;
  maxdep = std::max(maxdep, dep[u_]);
  for (auto v_: edge[u_]) {
    if (v_ == fa_) continue;
    son[u_].push_back(v_);
    dfs(v_, u_);
  }
}
void bfs() {
  int sum = 1, now = 0;
  std::vector<int> node[2];
  node[0].push_back(1);
  for (int i = 1; i < maxdep; ++ i) {
    node[now ^ 1].clear();
    for (auto u: node[now]) {
      if (son[u].empty()) {
        for (int p = u; p; p = fa[p]) {
          -- sum, -- cntson[fa[p]];
          if (cntson[fa[p]] > 0) break;
        }
      } else {
        for (auto v: son[u]) 
          node[now ^ 1].push_back(v), ++ sum;
        cntson[u] = son[u].size();
      }
    }
    ans = std::min(ans, n - sum);
    now ^= 1;
  }
}
//=============================================================
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;
    maxdep = 0;
    for (int i = 1; i <= n; ++ i) {
      edge[i].clear(), son[i].clear();
      dep[i] = cntson[i] = 0;
    }
    for (int i = 1; i < n; ++ i) {
      int u, v; std::cin >> u >> v;
      edge[u].push_back(v), edge[v].push_back(u);
    }
    dfs(1, 0);
    ans = n - 1;
    bfs();
    std::cout << ans << "\n";
  }
  return 0;
}

F 貪心,列舉

寫在最後

唉唉 AK div2 失敗太菜了。

學到了什麼:

  • C:經典結論;
  • D:考慮單個位置的合法性,然後根據性質考慮所有合法位置構成的集合的順序;
  • F:考慮調整法,證明選擇某個元素在答案裡一定不會更劣、

相關文章