目次

CODE FESTIVAL 2018 qual A

CODE FESTIVAL 2018 qual A

A - 配点

A - 配点

問題

解法

合計は $(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 - みかん

B - みかん

問題

解法

未確定の房は $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 - 半分

C - 半分

問題

解法

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

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

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

まず思いつく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

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

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

どういうことかというと、$k$ 回の操作後に0個の石山が含まれている場合は、$k+1,k+2,...$ 回の操作後の全ての場合においてその状態が実現可能。なので $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,...,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,...,cnt_{i+1}$)に引き継がれる。

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

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

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

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

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

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

よって、状態数(仮)から全ての石山が0でない $cnt_1 \times cnt_2 \times ... \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 - 通勤

D - 通勤

問題

解法

$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,...,GS_{i-1}$ の取り壊され方のパターン数」とする。

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

これを先頭から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)