Processing math: 44%

全国統一プログラミング王決定戦本戦 A~E問題メモ

A - Abundant Resources

問題

  • N 要素の数列 A1,A2,...,AN
  • 1N のそれぞれについて(K とする)以下を求めよ
    • 「連続する K 個の要素の和」の最大値

解法

まず A について累積和をとる。これを Acc とする。

連続する K 要素の和は、Acc から、AccK 要素ずらした配列を引くと求まる。

  A      10  20  30  40  50
Acc   0  10  30  60 100 150

K=3   0  10  30  60 100 150
    -             0  10  30
    =            60  90 120  → 最大値は120

1
2
3
4
5
6
7
8
import numpy as np
 
n = int(input())
aaa = np.fromiter(map(int, input().split()), dtype=np.int64)
aaa = np.insert(aaa, 0, 0)
aaa = np.cumsum(aaa)
buf = [(aaa - np.roll(aaa, k))[k:].max() for k in range(1, n + 1)]
print('\n'.join(map(str, buf)))

B - Big Integers

問題

  • 長さ N の整数列 A、長さ M の整数列 B、整数 K が与えられる
  • X,Y を以下のように定義する
    • X=Ni=1Ai×KNi=A1×KN1+A2×KN2+...+AN×K0
    • Y=Mi=1Bi×KMi=B1×KM1+B2×KM2+...+BM×K0
  • XY のどちらが小さいか求めよ
  • 0Ai,BiK1
  • 1A1,B1

解法

ややこしい書き方をしているが、K 進法同士の比較。

桁数が小さい方、同じなら上位桁から見ていって、最初に小さい数字が現れた方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def solve(n, m, k, aaa, bbb):
    if n < m:
        return 'X'
    if n > m:
        return 'Y'
    if aaa < bbb:
        return 'X'
    if aaa > bbb:
        return 'Y'
    return 'Same'
 
 
n, m, k = map(int, input().split())
aaa = list(map(int, input().split()))
bbb = list(map(int, input().split()))
print(solve(n, m, k, aaa, bbb))

C - Come Together

問題

  • H×W の各マス目にコマが1つずつ置かれている
  • K 個のコマを取り除く((R1,C1),(R2,C2),...,(RK,CK) で与えられる)
  • 取り除いた後、以下の操作を繰り返して、盤面に残る全てのコマを1箇所のマスに集める
    • 1つのコマを、上下左右に隣接するいずれかのマスに移動させる
  • 操作回数の最小値を求めよ

解法

どこのマスに集めるかを決める。

縦に動かす動作・横に動かす動作は互いに干渉しないので、縦横を独立に考えられる。なので以下 X 座標を考える。

仮に集める座標を x とする。 これを x+1 に変更すると、x より右にある全てのコマの必要回数が1回ずつ減り、x 以左にある全てのコマの必要回数が1回ずつ増える。

なので、最適な xa は、全てのコマの X 座標の中央値となる。

または、下に凸の形をしているので、三分探索でも求められる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import bisect
import sys
import numpy as np
 
 
def search(aaa, size):
    l, r = 0, size
    coef_base = np.arange(size, dtype=np.int64)
    while l < r:
        m1 = (l + r) // 2
        m2 = m1 + 1
        cost1 = (abs(coef_base - m1) * aaa).sum()
        cost2 = (abs(coef_base - m2) * aaa).sum()
        if cost1 < cost2:
            r = m1
        elif cost1 > cost2:
            l = m2
        else:
            return cost1
    return (abs(coef_base - l) * aaa).sum()
 
 
h, w, k = map(int, input().split())
rrr = np.full(h, w, dtype=np.int64)
ccc = np.full(w, h, dtype=np.int64)
for line in sys.stdin:
    r, c = map(int, line.split())
    rrr[r - 1] -= 1
    ccc[c - 1] -= 1
 
print(search(rrr, h) + search(ccc, w))

D - Deforestation

問題

  • 竹が N 本あり、初期の高さはいずれも0。時刻1につき1m伸びる
  • M 回、竹を切る
    • i 回目に切る時は、時刻 Ti に、[Li,Ri] の範囲の竹を切る
    • 切った竹の長さの合計値が得点として加算される
  • 切られた竹は高さ0になるが、再び伸び続ける
  • 最終的に得られる得点を求めよ

解法

途中で何回切ったかに関わらず、ある竹1本から得られるスコアは、その竹を「最後に切った時刻」に等しくなる。

なので、RangeUpdateQuery を処理できるセグメント木を作成し、時刻で更新していき、最後に1つ1つの最終的な数を合計すればよい。

セグ木はそれなりに配列参照が多く、Pythonでは間に合わないこともあるためPyPyを検討することになるが、PyPyは関数の再帰呼び出しがあまり得意では無いため、なるべく再帰を使わない書き方で実装した方がよいっぽい。

セグ木上の各要素にsentinel以外の値が入っていれば、その区間の下は全て同じ値、ということにしている。(変数名が実態と合ってないのでよくない)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import sys
 
 
class RangeUpdateTree:
    def __init__(self, n, initial=0, sentinel=-1):
        n2 = 1 << n.bit_length()
        self.n = n2
        self.offset = n2
        self.data = [initial] * (n2 << 1)
        self.sentinel = sentinel
 
    def update(self, a, b, x):
        stack = [(1, 0, self.n)]
        while stack:
            i, l, r = stack.pop()
            if a <= l and r <= b:
                self.data[i] = x
                continue
            y = self.data[i]
            j = i << 1
            if y != self.sentinel:
                self.data[i] = self.sentinel
                self.data[j] = self.data[j + 1] = y
            m = (l + r) // 2
            if a < m:
                stack.append((j, l, m))
            if m < b:
                stack.append((j + 1, m, r))
 
    def aggregate(self):
        stack = [1]
        ans = 0
        while stack:
            i = stack.pop()
            if self.data[i] != self.sentinel:
                ans += self.data[i] * (self.n >> i.bit_length() - 1)
                continue
            i <<= 1
            stack.append(i)
            stack.append(i + 1)
        return ans
 
    def debug_print(self):
        i = 1
        while i <= self.offset:
            print(self.data[i:2 * i])
            i <<= 1
 
 
n, m = map(int, input().split())
rut = RangeUpdateTree(n)
for line in sys.stdin:
    t, l, r = map(int, line.split())
    rut.update(l - 1, r, t)
print(rut.aggregate())

E - Erasure

問題

  • 箱が N 個、横一列に並んでいる
  • 整数 K が与えられる
  • 連続する箱の選び方のうち、長さ K+1 以上の区間は、(NK)(NK+1)/2 通りある
  • つまり、それぞれの区間を選ぶか選ばないかで「区間の選び方の組」は 2(NK)(NK+1)/2 通りある
  • このうち、全ての箱がいずれかの区間に入っているような「区間の選び方の組」の個数を \mod{10^9+7} で求めよ
  • 1 \le N \le 5000

解法

以下の説明では、以下の記号を用いる。

  1   2  ...     i
|------------------|  1~iを覆う単一の区間
|==================|  1~iを覆える区間の組(単一とは限らないが、いずれかでは覆われている)
|..................|  特に覆う覆わないに関係なく、1~iの範囲

ひとまず、こんなDPを試してみる。

DP[j]=1~j 個目の箱まで覆うが、j+1 個目の箱はまだ覆われていないような区間の組の選び方

これで DP[j] から DP[j+1] に遷移を考える。j+1 を覆わねばならず、j+2 は覆われてはいけないので、j+1 が右端である区間をどれか1つは使わなくてはならない。

    1  2  3 .. j-K ..  j   j+1
  |======================|            DP[j]
  |---------------------------|    ┐
     |------------------------|    │ j-K+1 個の内
        |---------------------|    │ 少なくともどれか1つあればいい
           ...                     │ DP[j+1] = DP[j] * (2^(j-K+1) - 1) ???
                |-------------|    ┘

という感じで遷移したいが、以下のようなケースで困る。

        |---------------------|    ←例えばこれを追加するときは
  |========|  |------|               追加区間で覆われていない区間さえ覆われていれば、
                                     途中で途切れていてもよい
                                     この途中で途切れた選び方の数は、DPに入っていない

改めてDPをこう定義する。

DP[i][j]=右端が i 個目までで終わる区間だけを使って、1~j 個目の箱は覆われ、j+1 個目は覆われていないような区間の組の選び方

DP[N][N] が答えとなる。実際は、i+1 を求める際には i の情報があれば良いので、DP[j] を逐次的に更新していくことで1次元配列で実装できる。

違いは、途切れた区間でもパターン数を数え上げられる点。

   1      j                  i
  |========|  |------| |--|        表現するパターン数 
           ^                  ^
      旧DP[j]             新DP[i][j]

1~j まで覆われ、j+1 が覆われていなければ、「j+2~i はどうなっていてもよい」という部分を織り込んだ値となる。

DP[i][i]の遷移

DP[i-1][1~i-1] まで求められていて、DP[i][1~i] を求める。まずは、DP[i][i] を求める。

i の箱は覆わなければならないが、i+1 の箱は覆われてはいけないため、「i を右端に持つ区間」を1つは使う必要がある。

1~iを覆う単一の区間を使う場合
 1  2  3  ...    i
|-----------------|
      |----|          ←残りは自由

この場合、他の区間は完全に使うも使わないも自由。他の区間は (i-K-1) \times (i-K+2)/2 個あるので、選び方は 2^{(i-K-1) \times (i-K+2)/2} 通りある。

1~iを覆う区間を使わない場合

場合分けのため、i を右端に持つ区間の内、最も長いものを x~i とする。

すると、遷移元が DP[i-1][x-1]~DP[i-1][i-1] のいずれかであれば、1~i が全て覆われた状態を保てる。 また、自身より短いi を右端に持つ区間 x+1~i, x+2~i, ... は加えても加えなくても良い。

よって、\displaystyle \sum^{i-1}_{z=x-1}(DP[i-1][z]) \times 2^{i-x-K} 通りが加えられる。

 1  2  3 .. x-1  x       ...    i
               |-----------------|
遷移元
|==============|                   DP[i-1][x-1]
|==================|               DP[i-1][x]
...
|=============================|    DP[i-1][i-1]
新規任意追加
                   |-------------|
                      |----------| i-x-K 個
                          |------|

x2~i-K まで動くので、\displaystyle \sum^{i-K}_{x=2}\sum^{i-1}_{z=x-1}(DP[i-1][z]) \times 2^{i-x-K} となる。うわあ、式で書くとややこしい。

でもこれ、DP[i-1]の後ろからの累積和と、2の累乗を事前計算しておけば、それらのドット積となり、そこそこ簡単にはなる。

K=2
            1    2    3    4    5    6    7
DP[i-1]     0    0    1    5   50  904
ACC       960  960  960  959  954  904
2^x         8    4    2    1   __   __  (末尾K個は不使用)
----------------------------             
ACC*2^x  7680 3840 1920  959  --合計->  14399

これに区間 1~i を使う場合の2^{14}=16384 を加えて、30783DP[i][i] となる。

DP[i][j]の更新

DP[i][j] は、DP[j][j](旧DP[j])に、j+2~i-1 の間の箱だけで取れる区間のパターン数をかけた値となる。

   1      j    j+2         i-1  i
  |========|  |...............|
           ^    ここだけで何パターンできるか?
      旧DP[j]

なので区間の長さ i-jK+1 に満たないと1(何も置かない)

置ける場合は、DP[i-1][j] の値に「2^{i-1 が右端となる区間の数}」をかけていくと更新できる。

K=2
          1    2    3    4    5    6     7       8
DP[7]     0    0    2    5   50  904 30783
         16    8    4    2   ※   ※   ※           ※: 置けない
         -----------------------------------------
DP[8]     0    0    8   10   50  904 30783 2032629

これで DP[i] が全て求まったので、N まで繰り返せば答えとなる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np
 
n, k = map(int, input().split())
MOD = 10 ** 9 + 7
 
pow2 = [0] * (n - k + 1)
pow2[0] = 1
for i in range(1, n - k + 1):
    pow2[i] = pow2[i - 1] * 2 % MOD
pow2 = np.array(pow2, dtype=np.int64)
 
dp = np.array([0] * k + [1, 5], dtype=np.int64)
p = 5
 
for i in range(k + 2, n):
    if i > (k + 1) * 2:
        tmp = dp[k:i - k - 1] * pow2[:i - 2 * k - 1][::-1] % MOD
        dp[k:i - k - 1] = tmp
    dp_acc = np.cumsum(dp[::-1]) % MOD
    dp = np.append(dp, (pow(2, p, MOD) + ((dp_acc[k:] * pow2[:i - k]) % MOD).sum()) % MOD)
    p += i - k + 2
 
print(dp[n - 1])

解説pdfにあった、DP[i][j]=i より左の区間のみを使って、1~j が覆われて j+1 がまだ覆われていないような区間の組の選び方 とする解法(後で考えたら i の条件が右端→左端になっただけ?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
n, k = map(int, input().split())
MOD = 10 ** 9 + 7
 
pow2 = [0] * (n - k + 1)
pow2[0] = 1
for i in range(1, n - k + 1):
    pow2[i] = pow2[i - 1] * 2 % MOD
 
dp = [0] * n
dp_acc = [1] * n
 
for i in range(n - k):
    ndp = [0] * n
    for j in range(i + k, n):
        ndp[j] = (dp[j] * pow2[j - i - k + 1] +
                  dp_acc[j - 1] * pow2[j - i - k]) % MOD
    dp = ndp
 
    if i > k:
        r = dp_acc[i - 1]
        for j in range(i - 1, n):
            dp_acc[j] -= r
 
    if i == 0:
        dp_acc[k - 1] = 0
 
    for j in range(i + k, n):
        dp_acc[j] = (dp_acc[j - 1] + dp[j]) % MOD
 
print(dp[-1])

programming_algorithm/contest_history/atcoder/2019/0217_nikkei2019-final.txt · 最終更新: 2019/02/20 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0