目次
AtCoder Beginner Contest 175 D,F問題メモ
C, D問題は、漏れなく考察できてるかの確信を見誤りやすい。
D - Moving Piece
問題
- $1~N$ の番号が付いたマス上でゲームを行う
- マスには $C_1,C_2,...,C_N$ の数字が書かれている
- 各マスからの移動先を示す $1~N$ の順列 $P_1,P_2,...,P_N$ が与えられる
- ゲームは、まず好きなマスにコマを置き、以下の操作を繰り返す
- 今いる位置を $i$ として、$P_i$ に駒を動かす
- 動かした先に書かれている数字($C_{P_i}$)をスコアに加算する
- 操作を $1$ 回以上 $K$ 回以下行ったときのスコアの最大値を求めよ
- $2 \le N \le 5000$
- $1 \le K \le 10^9$
- $P_i \neq i$
- $-10^9 \le C_i \le 10^9$
解法
「順列 $P$ があり、$i→P_i$ への移動を繰り返す」という操作は、いくつかの要素からなるサイクルをぐるぐる回ることになる。
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(N^2)$ 解法
各マスからスタートしてシミュレートする。
$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(N \log{N})$ 解法
一度求めたサイクルの累計スコアの配列は、同じサイクルに属する各マスをスタートした場合で使い回せる。
$i→P_i$ を辿っていってサイクルを発見したら、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$ を使って高速に求めるテクニックとして、$A_l+A_{l+1}+...+A_{r}=Acc[r]-Acc[l-1]$ とするのはよく出てくる。
今回の場合、開始地点 $l$ を決めると、$r$ は $l~l+m-1$ の中から好きに選べる。その中で $Acc[r]-Acc[l-1]$ を最大化するには、$Acc[l]~Acc[l+m-1]$ の区間最大値を求めればよい。
次、$i$ から1回移動した $P_i$ からスタートしたときの最大スコアは、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(\log{N})$ で済むので、全体で $O(N \log{N})$ で求められる。
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 \times C \times 4$ のDP配列を作るのだが、$R$ については1行前の情報以外は不要で下から計算できるので、配列を $C \times 4$ として逐次更新していけば多少の高速化になり、PyPyでもACできる、かも。
言語アップデートでNumbaが使える前は、
「ループ内で dp[i] に繰り返しアクセスするときはループ外で dpi = dp[i] と変数に入れておくと少し高速化される」とかやっていたが、
やっぱり可読性は低まるし、そういうことをせずNumbaで書けば間に合うようになったのは有難い。
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$ 個の、英小文字からなる文字列 $S_1,S_2,...,S_N$ と、そのコスト $C_1,C_2,...,C_N$ が与えられる
- これらの文字列から1つ以上選び、連結させて回文を作りたい
- $S_i$ を利用するには $C_i$ のコストがかかる
- 同じ文字列は何度でも使ってよいが、コストはそのたびにかかる
- 何でもいいので回文を1つ作るのにかかる最小コストを求めよ
- 不可能な場合は-1を出力せよ
- $1 \le N \le 50$
- $1 \le |S_i| \le 20$
- $1 \le C_i \le 10^9$
解法
最短経路探索に帰着。ただ、どのようなデータを用意しておけばいいかの整理がなかなか厄介。
まず、中心となり得る文字 or 2文字を洗い出す。そして、そこを中心とした場合に前・後ろにどんな文字列が余るかを経路探索のノードとし、ダイクストラなどで遷移していく。
具体的には、経路探索におけるノードは、以下の情報を持つ。
- 前に余ってるか後ろに余ってるか
- 余っている文字列
始点を求める
たとえば、以下の文字列があると、始点ノードは次のようになる。
abccbadab 中心 余り方向 余り文字列 aの手前 後 abccbadab a 後 bccbadab cc 後 dab d 前 abcc b 前 abccbada bの直後 前 abccbadab
これは、地道に各文字を中心として確認していっても、文字列1つあたり $O(|S_i|^2)$ で全て洗い出せる。
ただし、最初から単独で回文となっている場合は、それ1つを選んだときのコスト(複数ある場合はその最小値)を暫定コストとする。
遷移
あるノードに訪れたとき、次のノードへは以下のように遷移する。
- $N$ 個全ての文字列について、余りを解消する方に繋げられるかを確認し、可能なら次に余る文字列を求める
たとえば、「余り方向: 後、余り文字列: dab」から次のノードへの遷移を求める。
現在の暫定文字列はこんな感じになっている ◎☆◆...◆☆◎dab
余り方向が'後'の場合、それを解消すべく繋ぐのは'前'で、以下の文字列を繋げられる。
- ズバリ
badという文字列 - 末尾が
…badの文字列 d, adなど、全体がbadの接尾辞である文字列
繋げる文字列を $S_i$ として、
1.の場合、回文を作れる。これ以上続けて繋げる意味は無いので、暫定スコアを更新する。
2.の場合、次ノードは「余り方向: 前、余り文字列: $S_i$ の末尾から bad を取り除いた文字列」となる。
3.の場合、次ノードは「余り方向: 後、余り文字列: dab の先頭から $S_i$ の逆を取り除いた文字列」となる。
余り方向が前の場合も同様に整理する。
高速化
全文字列の長さの総和を $T$ とすると、ノード数は、方向が2通り、文字列がどこより前/後かで $|S_i|$ 通り、全文字列合わせて $2T$ 個。
辺数は各ノードからそれぞれ $N$ 個の文字列を確認するので、$2TN$ 本。
Dijkstraの計算量は $O((E+V)\log{V})$ なので、最大値を当てはめると約 $5 \times 10^6$ となる。
実際には、各文字列がどれも長く、かつほぼ全ての接頭・接尾文字列がことごとく余り文字列となるケースはまず無い(と思う……)ので、この数分の一となると思われる。
これだけだと行けそうだが、上記の解法を文字列のまま行おうとすると、 遷移時に次の文字列を繋げられるかのチェック・繋げられる場合のノードの作成に、 文字列の切り出し・反転・照合という $O(|S_i|)$ 程度の計算量が毎回かかる。
これを含めると無理な可能性も高いので、高速化したい。
一方法として、「各文字列の $j$ 文字目から $k$ 文字目まで」を、反転した場合も含め、全てローリングハッシュ等で整数化しておく。
すると、次に繋げる文字列との照合は 「$S_i$ の $p$ 文字目から $q$ 文字目のハッシュ」と「$S_j$ の $r$ 文字目から $s$ 文字目のハッシュ」が等しいか?という 配列参照と整数値比較で計算できるので、$O(1)$ で行えるようになる。
事前計算も、時間・空間共に $O(N|S_{max}|^2)$ で、十分小さい。
ただし、複数の文字列長が混在するローリングハッシュで照合を行う場合、$a=0,b=1,...$ とするとたとえば b, ab, aab, … などが全て'1'で同一となってしまう。
これを避けるには、「何もない文字」を0として、$a=1,b=2,...$ と対応づけることで、文字列長の違いも区別できる。
またハッシュには衝突がつきものだが、今回は文字列長が短く $27^{20} \simeq 10^{28}$ 程度なので、Pythonならmodで割らずそのまま持てば衝突の心配が無く安心できる。
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))

