Codeforces Round 977 (Div. 2, based on COMPFEST 16 - Final Round)

Luckyblock發表於2024-10-08

目錄
  • 寫在前面
  • A 簽到
  • B 貪心,列舉
  • C1 貪心
  • C2 貪心,列舉
  • D DP
  • E1/E2 Kruscal 重構樹,樹上揹包
  • 寫在最後

寫在前面

補題地址:https://codeforces.com/contest/2021

上大分失敗呃呃呃呃

我不要上班嗚嗚

A 簽到

考慮僅有三個數 \(a, b, c(a < b < c)\) 時最優操作,手玩下發現最優操作順序一定是按照升序進行操作。

進一步歸納可知對於全域性按照升序操作一定是最優的。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 110;
//=============================================================
int n, 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;
    for (int i = 1; i <= n; ++ i) std::cin >> a[i];
    std::sort(a + 1, a + n + 1);
    
    int ans = a[1];
    for (int i = 2; i <= n; ++ i) ans = (ans + a[i]) / 2;
    std::cout << ans << "\n";
  }
  return 0;
}

B 貪心,列舉

顯然應該先把原數列的 \(\operatorname{mex}\) 求出來,在此過程中對 \(\operatorname{mex}\) 有用的數顯然之後不會被操作。然後僅需不斷考慮使用剩下的數如何使 \(\operatorname{mex}\) 增加 1 即可。

發現每次操作一定選擇最小的可行的數,且發現每次操作僅能加 \(x\),即每個數僅能被修改成在 \(\bmod x\) 剩餘系下相同,且大於該數的數。於是考慮將所有數 \(a_i\) 插入下標為 \(a_i\bmod x\) 的 set 中,每次僅需檢查 \(\bmod x\) \(\operatorname{mex} \bmod x\) 的 set 中最小值是否不大於 \(\operatorname{mex}\) 即可。

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

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, x, mex, 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 >> x;
    for (int i = 1; i <= n; ++ i) std::cin >> a[i];
    std::sort(a + 1, a + n + 1);

    mex = 0;
    std::map<int, std::multiset<int> > cnt;
    for (int i = 1; i <= n; ++ i) {
      if (a[i] != mex) cnt[a[i] % x].insert(a[i]);
      if (a[i] == mex) ++ mex;
    } 
    while (!cnt[mex % x].empty() && *cnt[mex % x].begin() <= mex) {
      cnt[mex % x].erase(cnt[mex % x].begin());
      ++ mex;
    }
    std::cout << mex << "\n";
  }
  return 0;
}

C1 貪心

發現這個“上臺過的人隨便插隊”是非常靈活的,則若某個人能夠成功第一次上臺,則之後的上臺一定可以滿足。

於是僅需考慮 \(b\) 中每個權值第一次出現的位置,則可對 \(b\) 去重後得到一個排列,顯然當且僅當該排列是 \(a\) 的一個字首時合法。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, m, q, a[kN], b[kN];
bool vis[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 >> m >> q;
    for (int i = 0; i < n; ++ i) std::cin >> a[i], vis[a[i]] = 0;
    for (int i = 0; i < m; ++ i) std::cin >> b[i];
    
    int now = 0;
    bool flag = 1;
    for (int i = 0; i < m; ++ i) {
      if (vis[b[i]]) continue;
      if (a[now] == b[i]) vis[a[now]] = 1, ++ now;
      else flag = 0;
    }
    std::cout << (flag ? "YA" : "TIDAK") << "\n";
  }
  return 0;
}

C2 貪心,列舉

發現 \(a\) 是排列,於是考慮按下標對 \(a\) 進行重對映到排列 \(1\sim n\) 上,同理對 \(b\) 進行重對映一下。則根據 C1 的結論,僅需檢查 \(b\) 中出現的所有權值是不是恰好為 \(1, 2, \cdots\),且權值 \(1, 2, \cdots\) 的第一次出現位置是升序的。

則此時僅需比較 \(b\) 中所有位置 \(i\),其對應的權值 \(b_{i}\) 第一次出現位置,是否小於 \(b_{i}+1\) 第一次出現位置即可。若上述位置數量恰好為 \(n-1\),則說明數列 \(b\) 合法。

考慮使用 set 維護每種權值的出現數量,則檢查位置合法僅需檢查兩個 set 中最小值即可。發現每次修改僅會影響兩個 set,至多 4 個位置的合法性,即可 \(O(\log n)\) 級別維護合法性。

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

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, m, q, a[kN], b[kN], map[kN];
std::set<int> s[kN];
int cnt;
//=============================================================
bool check(int p_) {
  if (p_ <= 0 || p_ >= n) return 0;
  return (*s[a[p_]].begin()) <= (*s[a[p_ + 1]].begin());
}
bool query() {
  return cnt == n - 1;
}
void modify(int p_, int t_) {
  cnt -= check(map[b[p_]]) + check(map[b[p_]] - 1);
  s[b[p_]].erase(p_);
  cnt += check(map[b[p_]]) + check(map[b[p_]] - 1);

  cnt -= check(map[t_]) + check(map[t_] - 1);
  s[t_].insert(p_), b[p_] = t_;
  cnt += check(map[t_]) + check(map[t_] - 1);
}
void init() {
  cnt = 0;
  for (int i = 1; i < n; ++ i) cnt += check(i);
}
//=============================================================
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 >> m >> q;
    for (int i = 1; i <= n; ++ i) std::cin >> a[i], map[a[i]] = i;
    for (int i = 1; i <= n; ++ i) s[i].clear(), s[i].insert(m + 1);
    for (int i = 1; i <= m; ++ i) std::cin >> b[i], s[b[i]].insert(i);
    init();
    std::cout << (query() ? "YA" : "TIDAK") << "\n";
    
    while (q --) {
      int p, t; std::cin >> p >> t;
      modify(p, t);
      std::cout << (query() ? "YA" : "TIDAK") << "\n";
    }
  }
  return 0;
}

D DP

我去這題在哪見過?這不今年牛客6的I?不過那題是欽定選擇的區間中的某個數,而這題是欽定選擇的區間的某個端點。

參考:https://blog.csdn.net/Code92007/article/details/142734368

E1/E2 Kruscal 重構樹,樹上揹包

我去這題在哪見過?這不去年牛客6的A

兩點間的貢獻是路徑上的最長邊這個性質太典了,考慮 Kruscal 重構樹,原圖節點變為重構樹中的葉節點,兩點間的最長邊貢獻變為兩點 lca 的點權值的貢獻。

考慮樹上揹包,列舉邊時將兩個子樹合併,兩子樹代表的點集之間的最長邊,即為列舉到的邊的邊權。則每次轉移時僅需考慮兩個子樹中分別已經選擇了多少點作為伺服器即可,且當且僅當其中某個子樹沒有伺服器選擇時,需要考慮當前邊權的貢獻。

於是可以在 Kruscal 重構樹上得到一個很顯然的樹形 DP,設 \(f_{u, i}\) 表示在 \(u\) 的子樹中一共有 \(i\) 個點為伺服器時,這棵樹代表的點集對答案的貢獻,初始化 \(f_{u, i} = +\infin, f_{u, 1} = 0\)。在構建 Kruscal 重構樹列舉到邊 \((u, v, w)\) 合併 \(u, v\) 所在點集 \(r_u, r_v\) 得到點集 \(r\) 的同時,進行轉移,考慮從 \(r_u, r_v\) 中分別選擇了多少個伺服器,有:

\[\begin{cases} f_{r, i + j} \leftarrow f_{r_u, i} + f_{r_v, j}(1\le i\le \operatorname{size}_{r_u}, 1\le j\le \operatorname{size}_{r_v})\\ f_{r, i}\leftarrow f_{r_u, i} + w\times \operatorname{cnt}_{r_v} (1\le i\le \operatorname{size}_{r_u})\\ f_{r, j}\leftarrow f_{r_v, j} + w\times \operatorname{cnt}_{r_u} (1\le j\le \operatorname{size}_{r_v})\\ \end{cases}\]

其中 \(\operatorname{size}_{r_u}\) 表示點集大小,\(\operatorname{cnt}_{r_u}\) 表示點集中限定的需要連線的點的數量。

設最終得到的點集為 \(\operatorname{root}\),答案即:

\[\forall 1\le i\le n, f_{\operatorname{root}, i} \]

這樣實現起來比較符合重構樹的思路也比較直觀,但是時間複雜度上限是 \(O(n^3)\) 級別,跑不過去。

發現新建節點 \(rt\) 表示合併後的點集是沒有必要的,可以在維護並查集時直接按秩合併到原節點上進行轉移。於是修改定義 \(f_{u, i}\) 表示以 \(u\) 為根的點集中一共有 \(i\) 個點為伺服器時,這棵樹代表的點集對答案的貢獻,其餘部分均不變。

並查集合並時使用按秩合併時,總時間度複雜度變為 \(O(n^2)\) 級別。

1A 了爽。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5010;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, m, p, yes[kN];
struct Edge {
  int u, v, w;
} e[kN];
std::vector<int> edge[kN << 1];
int fa[kN], sz[kN], cnt[kN];
LL f[kN][kN], temp[kN];
//=============================================================
bool cmp(const Edge& fir_, const Edge& sec_) {
  return fir_.w < sec_.w;
}
void init() {
  for (int i = 1; i <= n; ++ i) yes[i] = 0, fa[i] = i, sz[i] = 1, cnt[i] = 0;
  for (int i = 1; i <= p; ++ i) {
    int x; std::cin >> x;
    yes[x] = cnt[x] = 1;
  }
  for (int i = 1; i <= m; ++ i) {
    int u, v, w; std::cin >> u >> v >> w;
    e[i] = (Edge) {u, v, w};
  }
  std::sort(e + 1, e + m + 1, cmp);
}
int find(int x_) {
  return x_ == fa[x_] ? x_ : (fa[x_] = find(fa[x_]));
}
void merge(int x_, int y_, int w_) {
  int fx = find(x_), fy = find(y_);
  int fxy = (sz[fx] > sz[fy]) ? fx : fy;

  for (int i = 0; i <= n; ++ i) temp[i] = kInf;
  for (int i = 1; i <= sz[fx]; ++ i) {
    for (int j = 1; j <= sz[fy]; ++ j) {
      temp[i + j] = std::min(temp[i + j], f[fx][i] + f[fy][j]);
    }
  }
  for (int i = 1; i <= sz[fx]; ++ i) {
    temp[i] = std::min(temp[i], f[fx][i] + 1ll * w_ * cnt[fy]);
  }
  for (int j = 1; j <= sz[fy]; ++ j) {
    temp[j] = std::min(temp[j], f[fy][j] + 1ll * w_ * cnt[fx]);
  }

  fa[fx] = fa[fy] = fxy;
  sz[fxy] = sz[fx] + sz[fy];
  cnt[fxy] = cnt[fx] + cnt[fy];
  for (int i = 0; i <= n; ++ i) f[fxy][i] = temp[i];
}
//=============================================================
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 >> m >> p;
    init();
    for (int i = 1; i <= n; ++ i) {
      for (int j = 0; j <= n; ++ j) f[i][j] = kInf;
      f[i][1] = 0;
    }
    for (int i = 1; i <= m; ++ i) {
      auto [u, v, w] = e[i];
      if (find(u) == find(v)) continue;
      merge(u, v, w);
    }
    for (int i = 1; i <= n; ++ i) std::cout << f[find(1)][i] << " ";
    std::cout << "\n";
  }
  return 0;
}
/*
1
3 3 2
3 1
1 2 1
2 3 3
1 3 2
*/

寫在最後

參考:

  • https://blog.csdn.net/Code92007/article/details/142734368

學到了什麼:

  • C2:排列可以隨便對映;
  • D:限定選擇的區間,轉化為限定選擇的區間中的某個位置;
  • E:兩點間的貢獻是路徑上的最長邊這個性質太典了,考慮 Kruscal 重構樹。

相關文章