task4233のめも

書きたいことをつらつらと

ABC115参加記録と解説

結果

f:id:task4233:20181209123422p:plain 3WA全完で648位でした。
推定緑パフォらしいですが, 全完できて嬉しかったので良いです。

beta.atcoder.jp

A - Christmas Eve Eve Eve

beta.atcoder.jp

概要

12月の日付Dが与えられる。
D=25なら'Christmas',
D=24なら'Christmas Eve',
D=23なら'Christmas Eve Eve',
D=22なら'Christmas Eve Eve Eve'を出力せよ。

制約

  • 22 \leq D \leq 25

解法

if文で問題文の通りに書きます。

#include <iostream>
using namespace std;

int main() {
  int D;
  cin >> D;
  
  if (D == 25) cout << "Christmas";
  else if (D == 24) cout << "Christmas Eve";
  else if (D == 23) cout << "Christmas Eve Eve";
  else cout << "Christmas Eve Eve Eve";
  cout << endl;
  return 0;
}

B - Christmas Eve Eve

beta.atcoder.jp

i個目の品物がp_i円の品物がN個ある。
N個の品物のうち最も高い品物を半額にできる時, 品物の合計金額はいくらか?

制約

  • 2 \leq N \leq 10
  • 100 \leq p_i \leq 10000
  • p_iは偶数

解法

最も高い品物のみを半額にするので, 最大値のみを1/2すれば良いですね。そのため, ソートしてから最後の要素(最大値)のみを1/2して全ての要素を足せば良いです。

#include <iostream>
#include <algorithm>
using namespace std;

int main() {
  int N;
  cin >> N;
  vector< int > p(N);
  for (int i=0; i<N; ++i) cin >> p[i];
  
  sort(p.begin(), p.end());
  
  int ans = 0;
  for (int i=0; i<N; ++i) {
    if (i < N - 1) ans += p[i];
    else ans += p[i] / 2;
  }
  
  cout << ans << endl;
  return 0;
}

C - Christmas Eve

beta.atcoder.jp

i本目の木の高さがh_iメートルの木がN本植えられています。
その中からK本の木を選んで装飾する時, 装飾した木のうち最も高い木の高さと最も低い木の差を最小化してください。

制約

  • 2 \leq K < N \leq 10^5
  • 1 \leq h_i \leq 10^9

解法

K本を選ぶときに木の高さの幅を最小化するためには, ソートしてから連続したK本を見てあげるのが良いでしょう。

なぜなら, 連続したK本のうち最初の木が最小, 最後の木が最大となるので, (最大の木) - (最小の木)を全て試せば木の高さの幅を最小化できるからです。

#include <iostream>
#include <algorithm>
using namespace std;

costexpr int INF 1 << 30;

int main() {
  int N, K;
  cin >> N >> K;
  vector< int > h(N);
  for (int i=0; i<N; ++i) cin >> h[i];
  
  sort(h.begin(), h.end());
  
  int ans = INF;
  for (int i=0; i<N-K+1; ++i) {
    ans = min(ans, h[i+k-1] - h[i]);
  }
  
  cout << ans << endl;
  return 0;
}

D-Christmas

beta.atcoder.jp

多次元バーガーはパティ(以下Pと略す)とバン(以下Bと略す)で出来ています。
レベルLの多次元バーガーは次のように定義される。

  • レベル0バーガーは, P1枚である。
  • レベルLバーガー(L\geq1)とは,
    • B1枚,
    • レベルL-1バーガー,
    • P1枚,
    • レベルL-1バーガー,
    • B1枚,
      をこの順に下から積み上げたものである。

レベルNバーガーを作って下からX枚取った時, その中にPは何枚あるか?

解法

f:id:task4233:20181209155156p:plain

レベルLバーガー(以下Lバーガーと略する)の定義を見ると, 再帰的にL-1バーガーを呼び出しています。それなので,

f(L) := Lバーガーの枚数

とすると, このf(L)は以下のように表せます。

 \displaystyle f(L) = \left\{
\begin{array}{ll}
1 & (L = 0) \\
1 + f(L-1) + 1 + f(L-1) + 1 & (L \geq1)
\end{array}
\right.



そして, Pの数は図の赤い部分と, f(L-1)の再帰に含まれるPの数のみです。それなので,

getP(L) := Lバーガーに含まれるPの枚数

とすると, このgetP(L)は以下のように表せます。

 \displaystyle getP(L) = \left\{
\begin{array}{ll}
1 & (L = 0) \\
getP(L-1) + 1 + getP(L-1) & (L \geq1)
\end{array}
\right.



また, 図と式を見ると分かりますが, Lバーガー(L\geq1)は以下の手順で作れます。

  1. Bを1つ置く
  2. f(L-1)を置く
  3. Pを1つ置く
  4. f(L-1)を置く
  5. Bを1つ置く

この上で, 下からX枚取ったときを考えてみます。作成手順を見ると上下対称なのが分かるので, 上からX枚の中にPが何枚あるかを同様の手順で考えてみます。

手順 上からの枚数 含まれるPの数
Bを1つ置く 1 0
f(L-1)を置く 1 + f(L-1) getP(L-1)
Pを1つ置く 1 + f(L-1) + 1 getP(L-1) + 1
f(L-1)を置く 1 + f(L-1) + 1 + f(L-1) getP(L-1) + 1 + getP(L-1)
Bを1つ置く 1 + f(L-1) + 1 + f(L-1) + 1 getP(L-1) + 1 + getP(L-1)

f:id:task4233:20181209123624p:plain

すると, 上の図のように場合分けされます。X = 0の時は当然1つも選べないので省略しています。この図に従って再帰関数を実装し, main関数から呼び出すことで解を求められます。

#include <iostream>
using namespace std;
// オーバーフローの可能性があるため, 64bitのint_fast64_tを使用します。
using int64 = int_fast64_t;

// Lバーガーの枚数を格納します。
int64 memo[55];
// Lバーガーに含まれるPの枚数を格納します。
int64 p_memo[55];

int64 f(int64 L) {
  // すでにメモされている場合はそちらの値を利用します。
  if (memo[L] > 0) return memo[L];
  // 0バーガーの枚数はPのみで1枚です。
  if (L == 0) return memo[0] = 1;
  // LバーガーはB + f(L-1) + P + f(L-1) + Bなので, 
  // まとめると全体の枚数は2 * f(L-1) + 3枚になります。
  return memo[L] = 2 * f(L-1) + 3;
}

int64 getP(int64 L) {
  if (p_memo[L] > 0) return p_memo[L];
  if (L == 0) return p_memo[0] = 1;
  // LバーガーはB + f(L-1) + P + f(L-1) + Bなので, 
  // まとめるとPの枚数は2 * f(L-1) + 1枚になります。
  return p_memo[L] = 2 * getP(L-1) + 1;
}

int64 solve(int64 L, int64 X) {
  if (X == 0) {
    // 残りの枚数が0枚になったら0を返します。
    return 0;
  } else if (L == 0) {
    // 階層が0になってしまったらgetP(0)=1を返します。
    return getP(0);
  } else if (X <= 1 + f(L-1)) {
    return solve(L-1, X-1);
  } else if (X <= 2 + f(L-1)) {
    return getP(L-1) + 1;
  } else if (X <= 2 + 2*f(L-1)) {
    return getP(L-1) + 1 + solve(L-1, X-f(L-1)-2);
  } else {
    return getP(L-1) + 1 + solve(L-1, X-f(L-1)-2);
  }
}

int main() {
  int64 N, X;
  cin >> N >> X;
  f(N);
  getP(N);
  
  int64 ans = solve(N, X);
  cout << ans << endl;
}

感想

今回のABCは, 最近のABCとは異なる難易度だったと考えています。A-Cは最近のABCの傾向からすると拍子抜けな簡単さだったと思います。そして, D問題は難しいと言う人がいるかもしれませんが, それは「考える」難しさではなく, 「実装する」難しさだと思います。なぜなら, 今回のD問題は実際に書き出してみれば再帰関数の目星はつき, 朧げでも実装方法は思いついていたはずだからです。そのため, 落ち着いて, 時間をかければACすることは不可能ではなかったでしょう。

逆に, 厄介なのは, Small Multipleのような「考える」難しさを要求してくる問題だと考えています。あの問題を見てdijkstra法が出てきますか?私は出てきませんでした。そのような, 問題と解法を繋げるのが難しい問題が私にとっては難しいようです。

長々と語ってしまいましたが, 久々に粘ってACをもぎ取るムーブが出来て非常に楽しいコンテストになりました。このようなコンテストが続くと良いです。