−目次
AtCoder Beginner Contest 175 D,F問題メモ
C, D問題は、漏れなく考察できてるかの確信を見誤りやすい。
D - Moving Piece
問題
- 1~N の番号が付いたマス上でゲームを行う
- マスには C1,C2,...,CN の数字が書かれている
- 各マスからの移動先を示す 1~N の順列 P1,P2,...,PN が与えられる
- ゲームは、まず好きなマスにコマを置き、以下の操作を繰り返す
- 今いる位置を i として、Pi に駒を動かす
- 動かした先に書かれている数字(CPi)をスコアに加算する
- 操作を 1 回以上 K 回以下行ったときのスコアの最大値を求めよ
- 2≤N≤5000
- 1≤K≤109
- Pi≠i
- −109≤Ci≤109
解法
「順列 P があり、i→Pi への移動を繰り返す」という操作は、いくつかの要素からなるサイクルをぐるぐる回ることになる。
i 1 2 3 4 5 P 2 4 5 1 3 C 3 4 -10 -8 8 1→2→4→1→... 3→5→3→...
O(N2) 解法
各マスからスタートしてシミュレートする。
K がでかいので最後までやるとTLEだが、最大でも N 回たどるとスタートに戻ってくるので、サイクルの長さ l がわかったら周回数 d と余り m を計算する。
1 → 2 → 4 → 1 → ... 加算スコア 4 -8 3 4 ... 累計スコア 4 -4 -1 3 K=7 l=3 → 周回数=2 余り=1
- 1周の累積スコアが正
- d 周ギリギリまで回って「周回数×1周のスコア」を稼ぎ、残り m 回を改めてシミュレートする
- これは嘘で、後述のようなケースがある
- 1周の累積スコアが負や0
- 1周するまでの最大値で終了した方がよい
ただし、1周の累計スコアが正でも、最後の1周に関しては途中で終了するのが最良のケースがある。
K=12 ┌------サイクル--------┐ 加算スコア -1 100 1 100 -199 | -1 100 1 100 -199 | -1 100 1 100 -199 ... 累計スコア -1 99 100 200 1 | 0 100 101 201 2 | 1 101 102 202 3 ...
この場合、回数的には2周でき、1周のスコアも正なのだが、2周後の余り操作2回の中で到達できる累計スコアの最大値は 101 である。
しかし、前の周回の途中では 201 を達成できていて、真の最大値はそれである。
従って、1回以上の周回が可能な場合においては、周回数を1回減らし、最後のループを含めて辿るようにするとよい。
加算スコア -1 100 1 100 -199 | -1 100 1 100 -199 | -1 100 1 100 -199 ... 累計スコア -1 99 100 200 1 | 0 100 101 201 2 | 1 101 102 202 3 ... ここからでなく |--------> ここからシミュレート |------------------------------------>
O(NlogN) 解法
一度求めたサイクルの累計スコアの配列は、同じサイクルに属する各マスをスタートした場合で使い回せる。
i→Pi を辿っていってサイクルを発見したら、3周ほどの累計スコアを計算する。先頭に便宜的に0を追加する。
acc 0 -1 99 100 200 1 0 100 101 201 2 1 101 102 202 3
周回後の残り操作回数(1回以上のループが可能な場合は最後の1ループを含めて)を m とすると、i からスタートしたときの最大スコアは、以下で求められる。
acc 0 -1 99 100 200 1 0 100 101 201 2 1 101 102 202 3 ~~~~~~~ m個の中の最大値 ~~~~~~ max(acc[1:1+m]) - acc[0] + 1周のスコアが正の場合は(周回数x1周のスコア)
配列 A の区間和を累積和 Acc を使って高速に求めるテクニックとして、Al+Al+1+...+Ar=Acc[r]−Acc[l−1] とするのはよく出てくる。
今回の場合、開始地点 l を決めると、r は l~l+m−1 の中から好きに選べる。その中で Acc[r]−Acc[l−1] を最大化するには、Acc[l]~Acc[l+m−1] の区間最大値を求めればよい。
次、i から1回移動した Pi からスタートしたときの最大スコアは、1つずらして以下で求められる。
acc 0 -1 99 100 200 1 0 100 101 201 2 1 101 102 202 3 ~~~~~~~ m個の中の最大値 ~~~~~~ max(acc[2:2+m]) - acc[1] + 1周のスコアが正の場合は(周回数x1周のスコア)
平衡二分探索木またはそれに類似したデータ構造を使って区間最大値を管理しておけば、1回の更新に O(logN) で済むので、全体で O(NlogN) で求められる。
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 56 57 |
from collections import Counter from heapq import heapify, heappop, heappush from itertools import accumulate def solve(n, k, ppp, ccc): NINF = - ( 10 * * 18 ) ans = NINF checked = [ False ] * n for s in range (n): if checked[s] = = True : continue checked[s] = True scores = [ccc[ppp[s]]] v = ppp[s] while v ! = s: scores.append(ccc[ppp[v]]) checked[v] = True v = ppp[v] l = len (scores) d, m = divmod (k, l) loop = sum (scores) if d > 0 : d - = 1 m + = l scores + = scores * 2 scores.insert( 0 , 0 ) acc = list (accumulate(scores)) base = max ( 0 , loop * d) range_max = [ - a for a in acc[ 1 :m + 1 ]] available_max = Counter(range_max) heapify(range_max) for i in range (l): while available_max[range_max[ 0 ]] = = 0 : heappop(range_max) ans = max (ans, base - range_max[ 0 ] - acc[i]) old = - acc[i + 1 ] new = - acc[i + m + 1 ] available_max[old] - = 1 available_max[new] + = 1 heappush(range_max, new) return ans n, k = map ( int , input ().split()) ppp = list ( map ( int , input ().split())) ppp = [p - 1 for p in ppp] ccc = list ( map ( int , input ().split())) print (solve(n, k, ppp, ccc)) |
E - Picking Goods
問題
解法
何個拾ったかを状態に加える素直なDP。PythonではNumbaでようやく間に合った。
R×C×4 のDP配列を作るのだが、R については1行前の情報以外は不要で下から計算できるので、配列を C×4 として逐次更新していけば多少の高速化になり、PyPyでもACできる、かも。
言語アップデートでNumbaが使える前は、
「ループ内で dp[i]
に繰り返しアクセスするときはループ外で dpi = dp[i]
と変数に入れておくと少し高速化される」とかやっていたが、
やっぱり可読性は低まるし、そういうことをせずNumbaで書けば間に合うようになったのは有難い。
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 |
import os import sys import numpy as np def solve(inp): r, c, k = inp[: 3 ] items = np.zeros((r + 1 , c + 1 ), dtype = np.int64) rrr = inp[ 3 :: 3 ] ccc = inp[ 4 :: 3 ] vvv = inp[ 5 :: 3 ] for r_, c_, v in zip (rrr, ccc, vvv): items[r_][c_] = v dp = np.zeros((r + 1 , c + 1 , 4 ), dtype = np.int64) for i in range ( 1 , r + 1 ): for j in range ( 1 , c + 1 ): up = dp[i - 1 ][j][ 3 ] for w in range ( 4 ): dp[i][j][w] = max (dp[i][j - 1 ][w], up) v = items[i][j] if v = = 0 : continue for w in range ( 2 , - 1 , - 1 ): dp[i][j][w + 1 ] = max (dp[i][j][w + 1 ], dp[i][j][w] + v) return dp[ - 1 ][ - 1 ][ 3 ] if sys.argv[ - 1 ] = = 'ONLINE_JUDGE' : from numba.pycc import CC cc = CC( 'my_module' ) cc.export( 'solve' , '(i8[:],)' )(solve) cc. compile () exit() if os.name = = 'posix' : # noinspection PyUnresolvedReferences from my_module import solve else : from numba import njit solve = njit( '(i8[:],)' , cache = True )(solve) print ( 'compiled' , file = sys.stderr) inp = np.fromstring(sys.stdin.read(), dtype = np.int64, sep = ' ' ) ans = solve(inp) print (ans) |
F - Making Palindrome
問題
- N 個の、英小文字からなる文字列 S1,S2,...,SN と、そのコスト C1,C2,...,CN が与えられる
- これらの文字列から1つ以上選び、連結させて回文を作りたい
- Si を利用するには Ci のコストがかかる
- 同じ文字列は何度でも使ってよいが、コストはそのたびにかかる
- 何でもいいので回文を1つ作るのにかかる最小コストを求めよ
- 不可能な場合は-1を出力せよ
- 1≤N≤50
- 1≤|Si|≤20
- 1≤Ci≤109
解法
最短経路探索に帰着。ただ、どのようなデータを用意しておけばいいかの整理がなかなか厄介。
まず、中心となり得る文字 or 2文字を洗い出す。そして、そこを中心とした場合に前・後ろにどんな文字列が余るかを経路探索のノードとし、ダイクストラなどで遷移していく。
具体的には、経路探索におけるノードは、以下の情報を持つ。
- 前に余ってるか後ろに余ってるか
- 余っている文字列
始点を求める
たとえば、以下の文字列があると、始点ノードは次のようになる。
abccbadab 中心 余り方向 余り文字列 aの手前 後 abccbadab a 後 bccbadab cc 後 dab d 前 abcc b 前 abccbada bの直後 前 abccbadab
これは、地道に各文字を中心として確認していっても、文字列1つあたり O(|Si|2) で全て洗い出せる。
ただし、最初から単独で回文となっている場合は、それ1つを選んだときのコスト(複数ある場合はその最小値)を暫定コストとする。
遷移
あるノードに訪れたとき、次のノードへは以下のように遷移する。
- N 個全ての文字列について、余りを解消する方に繋げられるかを確認し、可能なら次に余る文字列を求める
たとえば、「余り方向: 後、余り文字列: dab」から次のノードへの遷移を求める。
現在の暫定文字列はこんな感じになっている ◎☆◆...◆☆◎dab
余り方向が'後'の場合、それを解消すべく繋ぐのは'前'で、以下の文字列を繋げられる。
- ズバリ
bad
という文字列 - 末尾が
…bad
の文字列 d, ad
など、全体がbad
の接尾辞である文字列
繋げる文字列を Si として、
1.の場合、回文を作れる。これ以上続けて繋げる意味は無いので、暫定スコアを更新する。
2.の場合、次ノードは「余り方向: 前、余り文字列: Si の末尾から bad
を取り除いた文字列」となる。
3.の場合、次ノードは「余り方向: 後、余り文字列: dab
の先頭から Si の逆を取り除いた文字列」となる。
余り方向が前の場合も同様に整理する。
高速化
全文字列の長さの総和を T とすると、ノード数は、方向が2通り、文字列がどこより前/後かで |Si| 通り、全文字列合わせて 2T 個。
辺数は各ノードからそれぞれ N 個の文字列を確認するので、2TN 本。
Dijkstraの計算量は O((E+V)logV) なので、最大値を当てはめると約 5×106 となる。
実際には、各文字列がどれも長く、かつほぼ全ての接頭・接尾文字列がことごとく余り文字列となるケースはまず無い(と思う……)ので、この数分の一となると思われる。
これだけだと行けそうだが、上記の解法を文字列のまま行おうとすると、 遷移時に次の文字列を繋げられるかのチェック・繋げられる場合のノードの作成に、 文字列の切り出し・反転・照合という O(|Si|) 程度の計算量が毎回かかる。
これを含めると無理な可能性も高いので、高速化したい。
一方法として、「各文字列の j 文字目から k 文字目まで」を、反転した場合も含め、全てローリングハッシュ等で整数化しておく。
すると、次に繋げる文字列との照合は 「Si の p 文字目から q 文字目のハッシュ」と「Sj の r 文字目から s 文字目のハッシュ」が等しいか?という 配列参照と整数値比較で計算できるので、O(1) で行えるようになる。
事前計算も、時間・空間共に O(N|Smax|2) で、十分小さい。
ただし、複数の文字列長が混在するローリングハッシュで照合を行う場合、a=0,b=1,... とするとたとえば b, ab, aab, …
などが全て'1
'で同一となってしまう。
これを避けるには、「何もない文字」を0として、a=1,b=2,... と対応づけることで、文字列長の違いも区別できる。
またハッシュには衝突がつきものだが、今回は文字列長が短く 2720≃1028 程度なので、Pythonならmodで割らずそのまま持てば衝突の心配が無く安心できる。
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
from heapq import heapify, heappop, heappush def rolling(s): """ 文字列 [i, j] のハッシュ j < i の場合は逆順 """ n = len (s) ret = [[ 0 ] * n for _ in range (n)] for i in range (n): rh = 0 for j in range (i, n): b = ord (s[j]) - 96 rh = rh * 27 + b ret[i][j] = rh for j in range (n): rh = 0 for i in range (j, - 1 , - 1 ): b = ord (s[i]) - 96 rh = rh * 27 + b ret[j][i] = rh return ret def solve(n, sss, ccc, rhs): INF = 10 * * 18 ans = INF q = [] for i in range (n): s = sss[i] c = ccc[i] l = len (s) q.append((c, 0 , i, 0 )) q.append((c, 1 , i, l - 1 )) for j in range (l): k = 1 while j - k > = 0 and j + k < l: if s[j - k] ! = s[j + k]: break k + = 1 else : if j - k = = - 1 and j + k = = l: ans = min (ans, c) elif j - k = = - 1 : q.append((c, 0 , i, j + k)) else : q.append((c, 1 , i, j - k)) for j in range (l - 1 ): k = 0 while j - k > = 0 and j + k + 1 < l: if s[j - k] ! = s[j + k + 1 ]: break k + = 1 else : if j - k = = - 1 and j + k + 1 = = l: ans = min (ans, c) elif j - k = = - 1 : q.append((c, 0 , i, j + k + 1 )) else : q.append((c, 1 , i, j - k)) heapify(q) checked = [] for s in sss: checked.append([[ False , False ] for _ in range ( len (s))]) while q: cost, fb, i, j = heappop(q) if cost > = ans: break if checked[i][j][fb]: continue s = sss[i] ls = len (s) lm = ls - j if fb = = 0 else j + 1 checked[i][j][fb] = True rhr = rhs[i][j] if fb = = 0 : for ti in range (n): t = sss[ti] lt = len (t) if lm = = lt: if rhs[ti][ - 1 ][ 0 ] = = rhr[ - 1 ]: ans = min (ans, cost + ccc[ti]) elif lm > lt: if rhs[ti][ - 1 ][ 0 ] = = rhr[j + lt - 1 ]: heappush(q, (cost + ccc[ti], 0 , i, j + lt)) else : if rhs[ti][ - 1 ][lt - lm] = = rhr[ - 1 ]: heappush(q, (cost + ccc[ti], 1 , ti, lt - lm - 1 )) else : for ti in range (n): t = sss[ti] lt = len (t) if lm = = lt: if rhs[ti][ 0 ][ - 1 ] = = rhr[ 0 ]: ans = min (ans, cost + ccc[ti]) elif lm > lt: if rhs[ti][ 0 ][ - 1 ] = = rhr[j - lt + 1 ]: heappush(q, (cost + ccc[ti], 1 , i, j - lt)) else : if rhs[ti][ 0 ][lm - 1 ] = = rhr[ 0 ]: heappush(q, (cost + ccc[ti], 0 , ti, lm)) if ans = = INF: return - 1 else : return ans n = int ( input ()) sss = [] ccc = [] rhs = [] for si in range (n): s, c = input ().split() sss.append(s) ccc.append( int (c)) rhs.append(rolling(s)) print (solve(n, sss, ccc, rhs)) |