CODE FESTIVAL 2018 qual A

A - 配点

問題

  • 問題を3問作り、配点を決めようとしている
  • 以下の条件を満たす配点にできるか、判定せよ
    • 1問目は $A$ 点または $A+1$ 点
    • 2問目は $B$ 点または $B+1$ 点
    • 3問目は $C$ 点または $C+1$ 点
    • 3問の合計は $S$ 点

解法

合計は $(A+B+C)~(A+B+C+3)$ の間の任意の値を取れる。この中に $S$ が入っていればよい。

a = int(input())
b = int(input())
c = int(input())
s = int(input())
print('Yes' if a + b + c <= s <= a + b + c + 3 else 'No')

B - みかん

問題

  • $N$ 個のみかんがあり、$1~N$ の番号が付いている
  • それぞれの房の数は、ちょうど $A$ または $B$ である
    • $A \lt B$
  • $M$ 個の情報がある
  • $i$ 個目の情報では「$l_i~r_i$ のみかんの房は $A$ 個である」ことがわかる
  • 全てのみかんの房の数の合計として、考えられる最大値を求めよ
  • $1 \le N,M \le 100$

解法

未確定の房は $B$ であった方が明らかに合計が大きくなるので、最大値を求める上では $B$ と考えてよい。

制約が小さいので、シミュレーションする。

$N$ 要素の配列を用意して、$B$ で初期化する。

それぞれの情報で $l_i~r_i$ のみかんを $A$ にする。

n, m, a, b = map(int, input().split())
mikan = [b] * n
for _ in range(m):
    l, r = map(int, input().split())
    for m in range(l - 1, r):
        mikan[m] = a
print(sum(mikan))

C - 半分

問題

  • 数列 $A=\{a_1,a_2,\ldots,a_N\}$
  • 以下の操作をちょうど $K$ 回行う
    • $A$ の1要素を選び、2で割る。余りは切り捨てる
  • $K$ 回の操作後の $A$ の状態としてあり得るものの数を、$\mod{10^9+7}$ で答えよ
  • $1 \le N \le 50$
  • $0 \le a_i \le 10^{18}$
  • $0 \le K \le 10^9$

解法

  • 0は2で割っても0のまま
  • 他の数は2で割ると、数字が変化し、0までの必要回数が1回減る
  • $a_i$ を2で割って0にするために必要な回数を $cnt_i$ とすると、
    • $a_i=0$ の時は0
    • それ以外は $\log_2{a_i}+1$(小数点以下切り捨て)

これらを踏まえ、以下のように問題は言い換えられる。

  • 石の山が $N$ 個あり、それぞれ $cnt_1,cnt_2,\ldots,cnt_N$ 個の石が積まれている
  • 以下の操作をちょうど $K$ 回行う
    • 山を1つ選び、石が残っていたら1個取る。残ってなかったら何もしない
  • $K$ 回の操作後、各石山に石が何個残っているかの状態数を求める

実際にやってみると、以下のことが分かる。

  • 0個の石山が存在すると、それを選べば状態を変化させること無く何回でも操作を行える
  • 0個の石山が存在しないと、どれかから1個取って状態を変化させざるを得ない

まず思いつくDP

解法としてDPと当たりを付け、$dp_{i,k}$ を「$1~i$ 番目までの石山だけに注目して、$k$ 回操作した後の状態数」としたとする。

だがこのままでは、なかなか遷移が難しい。実際に簡単な例で試してみる。

石山の初期状態 : {1, 2, 3}

i=1
iまでの石山の初期状態: {1}
0回操作で       {1} の1通り
1回(以上)操作で {0} の1通り

i=2
iまでの石山の初期状態: {1, 2}
0回操作で {1, 2} の1通り
1回操作で {0, 2}, {1, 1} の2通り
2回操作で {0, 2}, {0, 1}, {1, 0} の3通り
3回操作で {0, 2}, {0, 1}, {1, 0}, {0, 0} の4通り
4回以上の操作ではずっと4通り

i=3
iまでの石山の初期状態: {1, 2, 3}
操作回数    0   1   2   3   4   5   6以上
パターン数  1   3   6  10  14  17  18

どういう計算で遷移させればよいか? 法則性が見えづらい。

ややこしくしている原因は0個の石山の扱いにあって、

  • 遷移
    • $dp_{i,k}$ の中で0個の石山が含まれている状態数は、そのまま $dp_{i,k+1}$ に引き継ぐことができる
    • $dp_{i,k}$ の中で0個の石山が含まれていない状態数は、引き継ぐことができない
  • 空間・計算量
    • dpの計算量は $O(N \times (kの上限))$
    • 全ての石山が0になっても操作は続けられるので $k$ の上限は $K$ となるが、$K \le 10^9$ なのでとても間に合わない
    • ただ、これは回避する方法がある(後述)

改良版DP

もう一つDPに区別を設け、$dp_{i,k,j}$ とし、$j$ を0か1の値を取る「0個の石山が含まれるかフラグ」とすれば遷移はやりやすそう。

また区別することで「各操作で、0個の石山は選ばない(常にどこかから石を取る)」とルールを設けてもよくなり、これにより空間の問題も解消する。

どういうことかというと、$k$ 回の操作後に0個の石山が含まれている場合は、$k+1,k+2,\ldots$ 回の操作後の全ての場合においてその状態が実現可能。なので $K=k+1$ の時の答えを求めたければ、$K=1~k$ の、0個の石山を含む状態数から取ってこれるので、dp(k+1)自身が持つ必要が無い。

これにより考慮すべき $k$ の上限が $cnt_i$ の総和までとなり、各 $cnt_i$ は $log_2{10^{18}}=60$ 程度までなので、$N=50$ でも3000程度となり、十分小さくなる。

石山の初期状態 : {1, 2, 3}
便宜上、dp(i,k,j) の j=0をdp0, j=1をdp1と表現する

i=1
  k  __0__1___
dp0 :  1  0    # 0個の石山を含まない場合
dp1 :  0  1    # 0個の石山を含む場合
0回の操作後は {1} の、0個の石山を含まない場合が1通り
1回の操作後は {0} の、0個の石山を含む場合が1通り
2回以上は操作を行えない

i=2
  k  __0__1__2__3__
dp0 :  1  1
dp1 :     1  2  1
1回の操作後は {0, 2}, {1, 1}  それぞれ0個の石山を含む場合と含まない場合が1通りずつ
2回の操作後は {0, 1}, {1, 0}  0個の石山を含む場合が2通り
                              ※{0, 2} も元のルールでは2回の操作後に実現可能だが、
                                0個の石山に対して操作しないとならず、
                                1回の操作後で既にカウントされているのでここでは考慮しない
3回の操作後は {0, 0}          0個の石山を含む場合が1通り

i=3
  k  __0__1__2__3__4__5__6__
dp0 :  1  2  2  1
dp1 :     1  3  5  5  3  1
各操作後の状態は省略

K=3の時の状態数を求める時は、
dp0からはそのまま取ってきて1通り。
dp1からは、3以下の状態数を合計して 1+3+5=9通り。
計10通り

遷移

いずれの遷移も、$dp_i$ の状態のみから $dp_{i+1}$ を決定できる。

dp0 からの遷移

$dp0_{i,k}$ からは、$dp0_{i+1}$ と $dp1_{i+1}$ の両方に遷移する。

まず、$dp0_{i+1,k+q}$(ただし $q=0,1,\ldots,cnt_{i+1}-1$)に状態数は引き継がれる。

これは、$i+1$ 個目の石山を、0個減らした場合、1個減らした場合、……、$cnt_{i+1}-1$ 個減らした場合を意味している。石山は0にしてはいけないのでその直前まで。

また、$dp1_{i+1,k+cnt_{i+1}}$ に引き継がれる。

これは、$i+1$ 個目の石山に $cnt_{i+1}$ 回操作を加えて0にした場合を意味している。

dp1 からの遷移

$dp1_{i,k}$ からは、$dp1_{i+1}$ に遷移する。

$dp1_{i+1,k+q}$(ただし $q=0,1,\ldots,cnt_{i+1}$)に引き継がれる。

これは、$i+1$ 個目の石山を、0個減らした場合、1個減らした場合、……を意味している。こちらは0まで減らしてもよい。

余談: 改良前のDPでKが大きい時の回避方法

$K$ が $cnt_i$ の総和($cnt_1+cnt_2+\ldots+cnt_N$)以上の場合、つまり、全ての石山を0にするのに十分な場合、簡単に計算できる。$cnt_i$ の総和以上の $K$ では、既に取り得る全ての状態を数え上げているので、それ以上 $K$ が増えても答えが増えることはない。

$i$ 個目の石山のみに着目すると、$0,1,2,\ldots,cnt_i$ の $cnt_i+1$ 通りの状態を取れる。

全ての状態数(仮)は、$(cnt_1+1)\times(cnt_2+1)\times \ldots \times(cnt_N+1)$ となる。

ただしこの中には、$K$ 回の操作では不可能なものもある。それは全ての石山が0でない状態。$K$ 回操作すると、少なくとも1つの石山は必ず0にせざるを得ない。逆に少なくとも1つの石山が0であれば、$K$ 回の操作後でも可能となる。

よって、状態数(仮)から全ての石山が0でない $cnt_1 \times cnt_2 \times \ldots \times cnt_N$ 通りを除けば、答えが求まる。

この枝刈りをしておけば、保持すべき $K$ の上限は $cnt_i$ の総和までとなり、空間の問題については解決する。(まぁ結局遷移が難しいんですけど)

def dividable(a):
    if a == 0:
        return 0
    return len(bin(a)) - 2


def solve(n, k, aaa):
    ddd = list(map(dividable, aaa))
    s = sum(ddd)
    if k >= s:
        ans = 1
        exc = 1
        for d in ddd:
            ans *= (d + 1)
            ans %= MOD
            exc *= d
            exc %= MOD
        return (ans - exc) % MOD

    dp0 = [1]
    dp1 = [0]
    for d in ddd:
        ndp0 = [0] * (len(dp0) + d)
        ndp1 = [0] * (len(dp0) + d)
        for i, (p0, p1) in enumerate(zip(dp0, dp1)):
            for q in range(d):
                ndp0[i + q] += p0
                ndp1[i + q] += p1
            ndp1[i + d] += p0 + p1
        dp0 = ndp0
        dp1 = ndp1
        print(dp0)
        print(dp1)
    return (dp0[k] + sum(dp1[:k + 1])) % MOD


MOD = 1000000007

n, k = map(int, input().split())
aaa = list(map(int, input().split()))
print(solve(n, k, aaa))

D - 通勤

問題

  • 1次元上の座標 0 から $D$ まで車で移動する
  • 車のガソリンタンクは出発時、満タンで $F$ リットル入っている
  • 座標1移動するのに1リットル消費する
  • $N$ 個のガソリンスタンドが座標 $0 \lt x_1 \lt x_2 \lt \ldots \lt x_N \lt D$ にある
  • ガソリンスタンドの前を通過する時、以下のルールでガソリンを補充する
    • ガソリンの残りが $T$ リットル以上あればスルー
    • それ未満ならそこで満タンまで補充
  • いくつかのガソリンスタンドを取り壊したい
  • $D$ までたどり着けるような取り壊し方は、何通りあるか、$\mod{10^9+7}$ で答えよ

解法

$i$ 番目のガソリンスタンドを $GS_i$ と表す。

$GS_i$ で補充後、次に補充できる可能性のあるGSは、座標 $x_i+(F-T) \lt x_k \le x_i+F$ にある $GS_k$ となる。

FULL                   NEXT  NEXT  NEXT
 ↓
 x1 --- x2 x3 x4 ------ x5 -- x6 -- x7 ----- x8
  <--- F-T --->
  <----------------- F ----------------->

DPを考える。$dp_{i}=$「$GS_i$ で補充することになるような、$GS_1,GS_2,\ldots,GS_{i-1}$ の取り壊され方のパターン数」とする。

上の例で、$GS_1$ で補充し、次に補充するGSを考える。

  • $GS_2,GS_3,GS_4$ は残ってても壊されてても通過するので関係ない
  • $dp_1$ から $dp_5$ へは、通過した3箇所分の取り壊され方が自由のため $2^3$ 倍して遷移する
  • $dp_1$ から $dp_6,dp_7$ へも同様
    • $GS_1$ の次に $GS_6$ で補充するためには $GS_5$ は取り壊されていないといけないので、そこでパターン数が増える余地は無い

これを先頭からDPで配っていけばよい。座標0の出発地点を $GS_0$ と見なし、$dp_0=1$ としてそこから始める。

答えの数え上げは、最後に給油することになりえる(つまり、$x_i+F \ge D$)各 $GS_i$ につき、残ってても通過するGSの数 $t$ を求め、$dp_i \times 2^t$ を加算する。

最後に給油
↓
xi --- x x x -------- x x x ----- D
 <----- F-T ----->
 <-------------------- F -----------...->
 
 `---------------'|`--------------'
残ってても通過する|xiを最後の給油地点にするためには
      GSは3個     |必ず取り壊されてないといけない

ANS += dp[i] * 2^3

実装は、区間への足し込みを行うため、BITなどを使って累積和で管理する。が、もっと速い提出があるのでよりよい方法はありそう(たぶんbisectで探してる部分は尺取り法でいける)。BITではMODを取ることに注意。

import bisect


class Bit:
    def __init__(self, n):
        self.size = n
        self.tree = [0] * (n + 1)

    def sum(self, i):
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i
        return s % MOD

    def add(self, i, x):
        while i <= self.size:
            self.tree[i] += x
            self.tree[i] %= MOD
            i += i & -i


MOD = 1000000007
d, f, t, n = map(int, input().split())
xxx = [0] + list(map(int, input().split()))
r = f - t
dp = Bit(n + 2)
dp.add(1, 1)
dp.add(2, -1)
reachable = d - f
ans = 0
for i, x in enumerate(xxx):
    i += 1
    s = dp.sum(i)
    if s == 0:
        continue
    ti = bisect.bisect(xxx, x + r, lo=i)
    fi = bisect.bisect(xxx, x + f, lo=ti)
    inc = s * pow(2, ti - i, MOD) % MOD
    if ti < fi:
        dp.add(ti + 1, inc)
        dp.add(fi + 1, -inc)
    if x >= reachable:
        ans += inc
        ans %= MOD
print(ans)

programming_algorithm/contest_history/atcoder/2018/0922_codefes2018_quala.txt · 最終更新: 2018/09/25 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0