Processing math: 11%

Educational DP Contest N,O,P,Q,Rメモ

N - Slimes

問題

  • N 匹のスライムが横一列に並ぶ
  • 最初の大きさは左から a1,a2,...,aN
  • 隣り合う2匹を合成することを繰り返して、1つにまとめる
  • 合成する時、2匹のその時点の大きさの和のコストがかかる
  • 最小コストを求めよ
  • 1N400

解法

区間DP。ループ回数は多め。

データ

DP[l][r]=初期状態で左から [l,r) の区間のスライムを1匹に合成する最小コスト

初期条件

全て0

遷移

例えば[2,7)の区間のスライムを1匹に合成するには、その直前で、[2,x)[x,7) の2匹に分かれていないといけない。(x36の任意の数)

逆に、[2,x)[x,7) がどう合成されたものであろうが、それぞれのコストの最小値さえわかっていれば、[2,7) の合成コストは求められる。

このxを全て試し、最小を取るとよい。

DP[l][r]=min

大きさの合計は、最初に累積和accを取っておけば、acc[r-1]-acc[l-1] で求められる。

1
2
3
4
5
6
7
8
9
10
from itertools import accumulate
 
n = int(input())
acc = [0] + list(accumulate(map(int, input().split())))
dp = [[0] * (n + 1) for _ in range(n)]
for w in range(2, n + 1):
    for l in range(n - w + 1):
        r = l + w
        dp[l][r] = min(dp[l][m] + dp[m][r] for m in range(l + 1, r)) + acc[r] - acc[l]
print(dp[0][n])

O - Matching

問題

  • N 人の男と N 人の女
  • 相性は N \times N 行列で示される
    • A_{i,j}=1 ならi 番目の男と j 番目の女が好相性、0なら相性なし
  • 好相性同士で N 組のペアを作る時、組み方は何通りあるか
  • 1 \le N \le 21

解法

bitDP。集合をビットフラグで表し、0から積み上げて 2^N 通りの集合の値を計算する。

データ

DP[i][s]=i 番目までの男たちが、集合 s で示される女たちとペアになるパターン数

初期条件

DP[0][0]=1

遷移

i-1 番目の男まで計算済みとする。

i 番目の男と j 番目の女が好相性の時、

DP[i-1] の中で、まだ 女_j が含まれていない集合 s に対して、DP[i][s|女_j] += DP[i-1][s] と遷移する。

たとえば j=5 とすると、下から5番目だけ立ったビットフラグb=10000を用意して、

  • 集合 s=11000 に対しては、既に5番目のbitが立っている(女_5 が既に他の男とペアになっている)ので何もしない
  • 集合 s=00101 に対しては、ペアが1組作れるので、DP[i][10101] += DP[i-1][00101] と加算する

…という処理を、DP[i-1] に含まれる各 s について行っていく。

計算量

i から i+1 への遷移で必要なのは、ビットフラグ sN 本のビットのうち、立っているのがちょうど i 本のもののみ。 (男が i 人なので、女も同数のもののみ考慮すればよい) そのようなビットは、\displaystyle {}_NC_{i} 通りある。

「立っているのがちょうどk本のビットフラグのみのイテレート」はやりにくく、結局は 0~2^N を回して条件に合う物のみ処理するのが単純だし早い。 ただ、この問題の場合「立っているのがちょうどi本のビットフラグ」は、i-1 から i への遷移の際に新しく作られた s に他ならない。 これを記録しておくことで、DP[i+1] の計算時に過不足のないイテレートが可能となる。 代わりに記録するコストがかかるので、言語によっては却って遅くなるかも知れないが……。

ともかく、\displaystyle {}_NC_{i} 通りのそれぞれで、男_i と相性のよい女(最大 N人)の遷移を行うので、全体での計算量は

\displaystyle \sum_{i=0..N-1} N \times {}_NC_{i} となる。これは、N=21 で約 4.4 \times 10^7 となる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
n = int(input())
dp = [0] * (1 << n)
dp[0] = 1
curr = {0}
MOD = 10 ** 9 + 7
for i in range(n):
    compatibilities = [1 << i for i, v in enumerate(input().split()) if v == '1']
    nxt = set()
    for k in curr:
        p = dp[k] % MOD
        for c in compatibilities:
            if k & c:
                continue
            dp[k | c] += p
            nxt.add(k | c)
    curr = nxt
print(dp[(1 << n) - 1] % MOD)

P - Independent Set

問題

  • N 頂点の木を、白か黒に塗り分ける
  • 辺で直接結ばれた2頂点を同時に黒で塗ってはいけない
  • 塗り方は何通りか、\mod{10^9+7} で求めよ

解法

木DP。適当な頂点を根として、部分木の解を求めていく。

どの頂点から先に計算すれば良いかわかりづらいので、再帰関数で書くのがやりやすい。

一応、DFSで計算順序を求め、その順にDP配列を埋めていくやり方でボトムアップでも可能。PyPyは再帰が遅いので、PyPyの速度が必要な場合はそのようなやり方をする。

データ

DP[i][w]=頂点1と根とした時、頂点 i 以下の部分木で、頂点 i 自身の色がw=白/黒だった場合のパターン数

DP[1] を再帰で求める

初期条件(末端条件?)

葉のパターン数: (白,黒) = (1,1)

遷移

頂点 i を黒にするには、子供が全部白でなくてはならない。逆に頂点 i が白なら、子供は白でも黒でもよい。

\begin{eqnarray} DP[i][白] &=& \prod_{c \in iの子供} (DP[c][白]+DP[c][黒]) \\ DP[i][黒] &=& \prod_{c \in iの子供} DP[c][白] \end{eqnarray}

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
import sys
 
sys.setrecursionlimit(100005)
MOD = 1000000007
 
 
def dfs(v, a):
    w, b = 1, 1
    for u in links[v]:
        if u == a:
            continue
        cw, cb = dfs(u, v)
        w *= cw + cb
        b *= cw
        w %= MOD
        b %= MOD
    return w, b
 
 
n = int(input())
links = [set() for _ in range(n)]
for line in sys.stdin.readlines():
    x, y = map(int, line.split())
    x -= 1
    y -= 1
    links[x].add(y)
    links[y].add(x)
 
print(sum(dfs(0, None)) % MOD)

Q - Flowers

問題

  • N 本の花が横1列に並ぶ
  • 花の高さは左から h_1,h_2,...,h_N で、これは \{1,2,...,N\} を並び替えたものである
    • つまり、1~N の高さの花が1本ずつある
  • 花の価値は、左から a_1,a_2,...,a_N
  • 何本かの花を摘み取り、左から高さが単調増加になるようにしたい
  • 残る花の価値を最大化せよ

解法

セグメント木上でのDP

データ

DP[i]=左から 1~i 本目までの花だけを考慮し、i 番目の花は必ず残す場合の、残る花の価値の最大値

セグメント木上で実装し、更新・取得をそれぞれ \log{N} で行えるようにする

  • 更新(i, x): DP[i] を xにする
  • 取得(i): DP[1]~DP[i] の最大値を取得する

初期条件

全て0

遷移

DP[i-1] まで確定して、i 番目の花を残すときを考える。

この時、「左から 1~i-1 番目に位置し、かつ h_i より低い高さである花のうち、DP[j] が最大のもの」に、a_i を加えたものが DP[i] となる。

                    ↓いまここ
     1   2   3   4   5   6
 h   1   5   3   6   4   2
 a  10  20  30  40  50  60
DP  10  30  40  70

i=5 より左にあり、h_5=4 より低い花の中で、DP が最大のものは、DP[3]=40。よって、DP[5]=40+50=90 となる。(花1,3,5を残す時にこれが実現できる) 4より高い花が左にあると置けなくなってしまうので、自分より低い花の中から最大値を探すことになる。

位置と高さを両方考えるのはややこしいので、事前にh_iでソートしておき、低い方から処理していくと、高さは無視できる。 花 i を処理時点での DP の値は必ずh_i より低い花のみを考慮した値となっている。

                    ↓いまここ
     1   2   3   4   5   6
 h   1   5   3   6   4   2
 a  10  20  30  40  50  60
DP  10      40          70

セグメント木に対する更新は点更新、取得は必ず[1, k) の形となるので、簡略化した形で書ける

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
def solve(n, hhh, aaa):
    n2 = 1 << n.bit_length()
    offset = n2 - 1
    data = [0] * ((n2 << 1) - 1)
 
    def update(k, x):
        i = k + offset
        while i >= 0:
            if data[i] < x:
                data[i] = x
            else:
                break
            i = (i - 1) // 2
 
    def get_max(k):
        i = k + offset
        ret = data[i]
        while i:
            # 自身が右枝の場合のみ、左枝と比較
            if i % 2 == 0:
                ret = max(ret, data[i - 1])
            i = (i - 1) // 2
        return ret
 
    srt = sorted((h, i) for i, h in enumerate(hhh))
    for h, i in srt:
        update(i, get_max(i) + aaa[i])
 
    return data[0]
 
 
n = int(input())
hhh = list(map(int, input().split()))
aaa = list(map(int, input().split()))
print(solve(n, hhh, aaa))

R - Walk

問題

  • N 頂点の単純有向グラフ
  • 辺の有無は行列の形で与えられる
    • 1 \le i,j \le N について、a_{i,j}=1 なら i→j の辺が存在し、0なら存在しない
  • 長さ K の有効パスは何通りあるか求めよ
    • 同じ辺を複数回通ってもよい
  • 1 \le N \le 50
  • 1 \le K \le 10^{18}

解法

行列累乗

データ

現在考慮中のパス長が k として、

DP[i][j]= 頂点iからj へ、パス長がちょうど k の行き方の通り数

初期条件

全て0

遷移

有向グラフの経路パターン数は、辺の有無を0-1行列にし、累乗することで算出できる。

①-->②<->③
 <------->

        1 2 3
     1| 0 1 1
A^1  2| 0 0 1
     3| 1 1 0

     1| 1 1 1
A^2  2| 1 1 0
     3| 0 1 2

     1| 1 2 2
A^3  2| 0 1 2
     3| 2 2 1

A^3_{1,2} を例に取ると、

  • A^1 より、
    • 1→1 への長さ1の経路が0通り(A)
    • 1→2 への長さ1の経路が1通り(B)
    • 1→3 への長さ1の経路が1通り(C)
  • A^2 より、
    • 1→2 への長さ2の経路が1通り(A)
    • 2→2 への長さ2の経路が1通り(B)
    • 3→2 への長さ2の経路が1通り(C)

(A)(B)(C) それぞれ同士を掛け合わせて合計すればよいが、この計算が行列の積算と一致する。

A^3_{1,2} = A^1_{1,1}A^2_{1,2}+A^1_{1,2}A^2_{2,2}+A^1_{1,3}A^2_{3,2} = 2

ただし、行列の積算は1回に N^3 かかり、とても10^{18}回は計算できない。 繰り返し二乗法を用いると、計算回数を \log{10^{18}} に減らせる。

行列演算ならNumPyを使いたくなるが、オーバーフローするのかWAになる。以外と競プロでNumPy使う機会って無い。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def dot(a, b):
    ret = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            ret[i][j] = sum(a[i][k] * b[k][j] for k in range(n)) % MOD
    return ret
 
 
n, k = map(int, input().split())
dp = [list(map(int, input().split())) for _ in range(n)]
ans = [[0] * n for _ in range(n)]
for i in range(n):
    ans[i][i] = 1
MOD = 1000000007
while k:
    k, i = divmod(k, 2)
    if i:
        ans = dot(ans, dp)
    dp = dot(dp, dp)
print(sum(sum(a) % MOD for a in ans) % MOD)

programming_algorithm/contest_history/atcoder/2019/0106_educational_dp_2.txt · 最終更新: 2019/01/17 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0