競技プログラミング日記

主に AtCoder の記事です

第6回 ドワンゴからの挑戦状 B

問題へのリンク

解法

スライム\(i,j \ (i < j)\) を結合した新しいスライムは,スライム\(j\) とみなす.このとき,スライム\(i\) を選んで結合したと呼ぶ. 区間\(I_{i} := [x_{i}, x_{i+1})\) を通過する回数の期待値を各\(i\) に対して求めれば,期待値の線形性から答えが求まる.
区間\(I_{i}\) に対して,スライム\(j \ (j \leq i)\) が通過する回数の期待値を求める.

  • \(j = i\)のとき,期待値は 1.
  • \(j = i-1\)のとき,スライム\(i\) の後に スライム\(i-1\) が選ばれればよいので,期待値は \(\frac{1}{2}\).
  • \(j = i-2\)のとき,スライム\(i\) とスライム\(i-1\) の後に スライム\(i-2\) が選ばれればよいので,期待値は \(\frac{1}{3}\).
  • \(\vdots\)
  • \(j = 0\)のとき, 期待値は \(\frac{1}{i+1}\).

よって,\(I_{i}\) を通過する回数の期待値は,期待値の線形性から $$ s_{i} := \sum_{k \in [1,i+1] \frac{1}{k}}. $$

実装

\(s_{i}\) を累積和として求めながら問題の答えを求めれば\(O(N)\).

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  ll n;
  cin >> n;
  vll a(n); rep(i,n) { cin >> a[i]; }

  mint ans;
  mint s;
  rep(i,n-1){
    s += mint(1) / mint(i+1);
    ans += s * mint(a[i+1] - a[i]);
  }

  rep1(i,n-1) ans *= i;
  cout << ans.val() << endl;


  return 0;
}

AtCoder Regular Contest 033C問題

ARC033C

問題概要

整数の集合 \(S\) に対して,以下のクエリを処理する.

  • \(S\) に整数 \(x\) を追加する.
  • \(S\) に整数 \(x\) を削除する.
  • \(S\) のうち \(x\) 番目に小さい数を答える.

\(S\) に追加する元 \(x\) は \(1\) 以上 \(2\cdot 10^{5} =: D\) 以下.

解法0: 平方分割

\(D\) を 2次元に分割する.\(D \leq quo \cdot mod\) となるように \(quo, mod\) をとる. \(S\) に元 \(x\) が入っているかを判定する配列 \(cnt\) を用意する. \(cnt[x] := cnt[quo][mod] = 1\) のとき入っていて,\(0\) のときに入ってないとする.

走査するとき,\([1,D]\) の 1次元でなく,\([0,quo] \times [0,mod)\) の 2次元になる.


大まかに言えば,以下のようになる.

追加クエリ

\(x = q * mod + r\) となる \(q \in [0,quo], r \in [0,mod)\) に対して \(cnt[q][r] = 1\) にすればよく,\(O(1)\).

削除クエリ

追加クエリと同様に, \(cnt[q][r] = 0\) にすればよく,\(O(1)\).

答えるクエリ

答えたいのは \(\sum_{y \leq x} cnt[y]\). 愚直に 1次元を走査すると \(O(D)\,つまり \(O(quo \cdot mod)\) 程度掛かってしまう. そこで,2次元に分けて走査するのが本解法.

各 \(q \in [0,quo]\) に対して,\(sum[q] := \sum_{r \in [0,mod)} cnt[q][r]\) を前計算しておけば,\(O(quo + mod)\) で求まる.


相加相乗平均の不等式から,\(mod\) と \(quo\) が同じくらいになるときに一番小さくなる. よって,\(mod\) と \(quo\) は \(D^{1/2}\) 位に取ると一番速い.
答えるクエリで前計算が必要になったので,追加クエリと削除クエリも修正する.

追加クエリ'

\(x = q * mod + r\) となる \(q \in [0,quo], r \in [0,mod)\) に対して \(cnt[q][r] = 1\) にする. さらに,

\(sum[q][r]++\)

をする. 合わせて \(O(1)\).

削除クエリ'

追加クエリと同様に,\(cnt[q][r] = 0\) と

\(\sum[q][r]--\)

を合わせて \(O(1)\).

答えるクエリ

各 \(q \in [0,quo]\) に対して,\(sum[q] := \sum_{r \in [0,mod)} cnt[q][r]\) を前計算しておけば,\(O(quo + mod)\) で \(\sum_{y \leq x} sum[y]\) が求まる.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

// devide square
class Kth{
  using CNT = int; // count
  using VAL = int; // value
  vector<vector<CNT>> cnt; // cnt[q][r] is the count of q*mod + r. cnt is the (quo)x(mod)-matrix.
  vector<CNT> sum_cnt; // sum[q] is the sum of cnt[q]. (quo -> CNT)
  int quo, mod;
public:
 
  Kth(){}
  Kth(int _d2){
    mod = sqrt(_d2);
    quo = _d2 / mod;
    cnt.resize(quo + 1);
    for(int i = 0; i < cnt.size(); i++){
      cnt[i].resize(mod);
    }
    sum_cnt.resize(quo + 1);

    assert(quo+1 >= _d2/mod);
  }

  void add(VAL x){
    assert(x >= 0);
    VAL q = x / mod;
    VAL r = x % mod;
    cnt[q][r]++;
    sum_cnt[q]++;
  }

  void remove(VAL x){
    assert(x >= 0);
    VAL q = x / mod;
    VAL r = x % mod;
    assert(cnt[q][r] > 0);
    assert(sum_cnt[q] > 0);

    cnt[q][r]--;
    sum_cnt[q]--;
  }

  // return the k-th element
  VAL query(int k){
    CNT t = 0;
    for(int q = 0; q < quo+1; q++){
      if(t + sum_cnt[q] < k) {
        t += sum_cnt[q];
        continue;
      }
     
      for(int r = 0; r < mod; r++){
        t += cnt[q][r];
        if(t >= k) return q*mod + r;
      }
    }

    return -1; // fail
  }

};

int main() {
  Kth k_th((ll)2e5 + 5);
  ll q; cin >> q;
  rep(qi,q){
    ll t,x ; cin >> t >> x;
    if(t == 1){
      k_th.add(x);
    }else{
      ll v = k_th.query(x);
      cout << v << endl;
      k_th.remove(v);
    }
  }

  return 0;
}
#endif

AtCoder Beginner Contest 312F問題

\(\def \set #1#2{\{ #1 \ \vert \ #2 \}}\) \(\def \order #1{\lvert {#1} \rvert}\)

 

問題概要

\(N\) 頂点の tree \(G\) が与えられる. \begin{align} U &:= \set{(i,j,k) \in V(G)}{ i,j,k \text{は相異なる} }, \\ L &:= \set{(i,j,k) \in U}{i,j,k \text{は一直線状に並ぶ}} \end{align} とおく. ここで,\(i,j,k\) が一直線上に並ぶとは, 次のいずれかが成り立つことである.

  • \(k \in V(mini\text{-}path_{i,j})\),
  • \(i \in V(mini\text{-}path_{j,k})\),
  • \(j \in V(mini\text{-}path_{k,i})\).
解法

答えは \[ \begin{matrix} ans &=& \order{U} - \order{L} \\ &=& {}_{N} C_{3} - \sum_{i,j \in V(G) \\ i \neq j} (dis_{i,j} - 1)\\ &=& {}_{N} C_{3} - \sum_{e \in E(G)}\order{ \set{ (i,j) \in V(G)^{2} }{ i \neq j \text{かつ} e \in E(mini\text{-}path_{i,j})} } + {}_{N}C_{2} \end{matrix} \] となる.

頂点の組 \((i,j)\) を動かす代わりに, 辺 \(e = (i,j)\) を動かすことで,全探索する対象を減らしている.

Root を固定して DFS で求まる. \(i,j\) のうち,root から見て深い方を \(i\) とすると, \begin{align} &\order{ \set{ (i,j) \in V(G)^{2} }{ i \neq j \text{かつ} e \in E(mini\text{-}path_{i,j})} } \\ =& \order{V(G_{i})} \cdot( \order{ V(G) } - \order{V(G_{i})}). \end{align} ここで,\(V(G_{i})\) は \(i\) を root とする \(G\) における subtree.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  ll n;
  cin >> n;
  vvll to(n);
  rep(i,n-1){
    ll a,b; cin >> a >> b;
    --a, --b;
    to[a].push_back(b);
    to[b].push_back(a);
  }

  vll sz(n);
  ll ans = nCr(n,3LL) + nCr(n,2LL);
  auto dfs = [&](auto dfs, ll cu, ll pa = -1) -> void {
    sz[cu] = 1;
    for(auto ne: to[cu]) if(ne != pa){
      dfs(dfs, ne, cu);
      sz[cu] += sz[ne];
    }

    if(pa != -1){
      ans -= sz[cu] * (n - sz[cu]);
    }
  };
  dfs(dfs, 0);
 
  cout << ans << endl;

  return 0;
}

AtCoder Beginner Contest 314F問題

\(\def \set #1#2{\{ #1 \ \vert \ #2 \}}\)

ABC314F

問題概要

有向 tree \(T\) を構成する.

  • \(N := \{0, 1, \cdots, N-1\}\)
  • \(|V(T)| = 2N-1\)
  • The set \(L := \set{\{i\}}{ i \in N}\) is the set of leafs of T.
  • For any vertex \(x\) of \(V\) is a subset of \(L\).
  • There is a unique \(r \in V(T)\) such that \(r = N\). We denote that \(r\) is a root of \(T\).
  • For any distinct vertices \(x\) and \(y\) with same parents such that \(x \cap y\) is the empty set.
解法

プレイヤー \(i \in N\) が,どの vertex \(in V(T)\) に属すのかを \(gp : N \rightarrow V(T)\) で表す.

\(V(T)\) に対して,代表元を決めながら合併する. \(p, q \in N\) の試合の後に新しくできる vertex を \(v \in V(T)\) とおく.
試合の後にするべき操作は,

  • 頂点の更新:
    • \(gp(p)\) と \(gp(q)\) のマージ.
    • 代表元の更新: \(gp(leader(v)) = v\)
  • 辺の更新:
    • \(v \rightarrow gp(p)\), \(v \rightarrow gp(q)\) を作る.

するべき操作が決まれば,用意すべきものが決まる.
用意するものは,

  • 各 \(w in V(T)\) に対する代表元
  • 各 \(i \in N\) に対して,今 \(i\) が属している \(V(T)\) の元であって, 一番新しく作成されたもの.

これらは,Uinon-Find で実現できる.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  ll n;
  cin >> n;

  UnionFind<ll> uf(n);
  vll gp(n); rep(i,n) gp[i] = i;
  vll sz(2*n-1,1);

  vvll to(2*n-1);
  rep(i,n-1){
    ll p,q; cin >> p >> q;
    --p, --q;
    p = uf[p];
    q = uf[q];

    // new vertex v
    ll v = i + n;
   
    // new edges
    // v -> gp[p],
    // v -> gp[q]
    to[v] = vll({gp[p], gp[q]});

    // vertex
    sz[v] = uf.size(p) + uf.size(q); // rem : not sz[uf.size(p)]
    uf.merge(p,q);
    gp[p] = v; // p = leader of v
  }

  ll root = 2*n-1 -1;
  vector<mint> dp(2*n-1);
  auto dfs = [&](auto dfs, ll cu) -> void {
    for(ll ne : to[cu]) {
      dp[ne] = dp[cu] + mint(sz[ne])/mint(sz[cu]);
      dfs(dfs, ne);
    }
  };
 
  dfs(dfs, root);
  rep(i,n) cout << dp[i].val() << " ";

  return 0;
}

AtCoder Beginner Contest 321F問題

 

ABC321F

問題概要

Muliset に対する部分和問題に,追加と削除クエリを与えたもの. 俗に戻すDPと呼ばれる.

解法

(0) まず,multiset が固定されている場合を考える. これは普通の部分和DP. とくに,配列を 1次元にした inplace DP で考えると, 今回の問題に応用するときに楽.
(1) 次に,multiset に1つ元 \(x\) を追加または削除することを考える. 簡単な追加の方を考える. (0)の考えを用いると,

\(dfor \ i \in [x,k]\)
   \(dp_{i} += dp_{i-x}\)

という処理をしている.
削除の方も同様に,

\(for \ i \in [x,k]\)    \(dp_{i} -= dp_{i-x}\)

と出来る.for の順番が反転することと,\(+\) と \(-\) も反転している.

注意

多項式を使った数え上げを考えると,操作が可換であることが用意に分かる. \(t\) に関する多項式とする. \(x\) を mutiset に追加することは,多項式に \(1+t^{x}\) を掛けることに対応して, \(x\) を mutiset から削除することは,多項式に \(1+t^{x}\) で割ることに対応する. 積と割り算は共に可換.
また,Multiset から元を選んで部分和を作るとき,一つの元に対して 1回しか使えないというルールなので \(dfor \ i \in [x,k]\)    \(dp_{i} += dp_{i-x}\) としている. もし,1つの元を何度でも使えるというルールなら, \(for\) になる.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  ll q,k; cin >> q >> k;

  vector<mint> dp(k+1);
  dp[0] = 1;
  rep(qi,q){
    char c; ll x; cin >> c >> x;
    if(c == '+'){
      for(int i = k; i >= x; i--){
        dp[i] += dp[i-x];
      }
    }else{
      for(int i = x; i <= k; i++){
        dp[i] -= dp[i-x];
      }
    }
   
    cout << dp[k].val() << endl;
  }
 
  return 0;
}

AtCoder Beginner Contest 324F問題

ABC324F

解法

平均の最大化なので,binary search が典型. \(I \subset E\) に対して, $$ f_{I} := \frac{\sum_{i \in I}b_{i}}{\sum_{i \in I}c_{i}} $$ とおく. \(x \in \mathbb{R}\) を固定したとき,\(f_{I} \geq x\) となる \(I\) が存在するか, という判定問題を解けばよい.\(i \in I\) に対して \(w_{i} := b_{i} - c_{i}x\) とおく.
与えられたグラフが DAG なので,このグラフ上で DP が可能. \(i \in V\) に対して,

\(dp_{i} := \) \(0 \rightarrow i\) のパスとなる \(I \subset E\) の取り方のうち, \(\sum_{i \in I} w_{i}\) の最大値

とおく.これは DPで求まる.

注意

Double 型の binary search なので,while を使うよりは, 回数を決めて rep の方が安全.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  ll n,m;
  cin >> n >> m;
  vector<vector<Edge>> to(n);
  rep(i,m){
    ll s,t,b,c; cin >> s >> t >> b >> c;
    --s, --t;
    to[s].emplace_back((Edge){t,b,c});
  }

  double ok = 0, ng = INFL;
  rep(_,150){
    double mid = (ok + ng)/2.0;

    vector<double> dp(n, -INFL);
    dp[0] = 0;
    rep(i,n){
      for(auto ed: to[i]){
        chmax(dp[ed.to], dp[i] + ed.b - ed.c*mid);
      }
    }

    if(dp[n-1] >= 0) ok = mid;
    else ng = mid;
  }
  printf("%.10lf", ok);

  return 0;
}

AtCoder Beginner Contest 325F問題

ABC325F

解法

まず,素直に bool 型 dp を考える.

\( dp_{i,j,k} := \) 区間 \(i\) まで, センサー 1 を \(j\) 個, センサー 2 を \(k\) 個 で可能か
\(\in Bool\)

とする. 次に,bool 型 dp の定義域のいくつかを値域に移すことを考える. このとき,\(i,j\) は対象な条件なのでどちらでも良い.

値域に移すものは,何か良い性質を持っていないと,高速化は難しい. 定義域は全探索するので,綺麗な性質である必要が無い.

今回は,\(i,j\) を固定した前提で \(k\) は単調性があるので,値域に持っていくと高速化できる.

\(dp_{i,j} := \) 区間 \(i\) まで, センサー 1 を \(j\) 個 のときの \(k\) の min の合計
\(\in \mathbb{Z}\)

とする. 区間\(i\) について,\(j\) が決まれば,\(k\) の min が求まる. また,\(i\) に関しては,単調性などの綺麗な性質が見当たらないので, 値域に持って行っても高速化は難しい.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  ll n;
  cin >> n;
  vll d(n); rep(i,n) { cin >> d[i]; }
  vll l(2), c(2), k(2);
  rep(i,2) cin >> l[i] >> c[i] >> k[i];

  vll dp(k[0]+1, INFL);
  dp[0] = 0;
  rep(i,n){
    vll old(k[0]+1, INFL);
    swap(old,dp);

    rep(j,k[0]+1){
      // rep(add,k[0]+1)if(j+add <= k[0]){
      rep(add, k[0]+1-j){
        ll x = ceil(d[i] - add*l[0], l[1]);
        chmax(x, 0LL);
        chmin(dp[j+add], old[j] + x);
      }
    }
  }

  ll ans = INFL;
  rep(j,k[0]+1){
    if(dp[j] <= k[1]){
      chmin(ans, j*c[0] + dp[j]*c[1]);
    }
  }
  if(ans == INFL) ans = -1;
  cout << ans << endl;

  return 0;
}