トヨタシステムズプログラミングコンテスト2024(AtCoder Beginner Contest 377) E,F,G問題メモ
E - Permute K times 2
問題文
(1,2,…,N) の並べ替え P=(P1,P2,…,PN) が与えられます。
次の操作を K 回行います。
i=1,2,…,N に対して同時に Pi を PPi で更新する
すべての操作を終えたあとの P を出力してください。
制約
解法
問題設定としてよくありそうだけど、実は初めて?なのか、忘れてるのか、解法がすぐにはわからなかった。
i 0 1 2 3 4
P 4 2 0 1 3
まず 0→4→3→1→2→0 のように、i→Pi→... と辿っていってループをその順に並べる。
操作の過程で、これらのindexの位置に、これら以外の数字が来ることはないので、ループ毎に考える。
1回目の操作では、i=0 にある 4 は、矢印を辿って 3 に変化する。他の値も、1回矢印を辿った値に変化する。
k 回操作後の P を Qk とすると、
i 0 1 2 3 4
Q0 4 2 0 1 3
Q1 3 0 4 2 1
Q2 2 3 1 4 0
Q3 1 4 3 0 2
Q4 4 2 0 1 3
矢印を辿っていけばいいのかな? と i=0 の位置の変化に着目しても、
4→3→2→1→4→... であり、ループの順 0→4→3→1→2 とは合わない。う~む。
1回1回詳しく見ると、 Q1[0]=3 になった時、Q1[3]=2 であり、これが次の Q2[0] となる。
Q1 までに i=0 から 0→4→3 と変化しているのと同時に、i=3 からは 3→1→2 と変化しており、
Q1 から Q2 へは、この2回分の変化を一気に辿っていると言える。
よって、Q2 は、元のループから見ると、Q1 から2つ分進めた値となることがわかる。
同様に Q2[0]=2 から Q3[0]=1 へは、i=0 が 0→4→3→(1)→2 と辿った後に続き、
i=2 が辿った 2→0→4→(3)→1 という4回分のループを一気に辿るといえる。
最初の i=0 からループを開始するとみると、あわせて8回分ループを辿った先となる。
このように、2倍ずつループを進める数が増えていく。
■Qk の時に i=0 に来る値
0→4→3→1→2→0→4→3→1→2→0→4→3→1→2→0→4→...
k 0 1 2 3 4
各位置から 2K だけループを進めた値が、最終的な QK になる。
Python3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
def solve(n, k, ppp):
ppp = [p - 1 for p in ppp]
ans = [-1] * n
for i in range(n):
if ans[i] != -1:
continue
loop = [i]
j = ppp[i]
while j != i:
loop.append(j)
j = ppp[j]
m = len(loop)
kk = pow(2, k, m)
for t in range(m):
ans[loop[t]] = loop[(t + kk) % m]
ans = [a + 1 for a in ans]
return ans
n, k = map(int, input().split())
ppp = list(map(int, input().split()))
ans = solve(n, k, ppp)
print(*ans)
|
F - Avoid Queen Attack
問題文
縦 N マス、横 N マスの N2 マスからなる盤面に、M 個のクイーンが置かれています。
クイーンは、縦・横・斜め45度の好きな方向に好きなだけ移動できます。
クイーンの利きにないマスがいくつあるか求めてください。
制約
1≤N≤109
1≤M≤103
1≤ak≤N,1≤bk≤N (1≤k≤M)
(ak,bk)≠(al,bl) (1≤k<l≤M)
入力はすべて整数
解法
同じクイーンが同じ列や同じナナメにあることもあるのがややこしい。
1つのクイーンの利きを4本の縦・横・ナナメの直線に分解し、「直線が引かれないマス」を数えると考える。
異なるクイーンから同じ列に直線ができた場合、重複は除いて1本と捉える。
答えは以下になる。大まかに全体から直線が引かれるマスを引いた後、引きすぎた分を戻す。
重複を除くことで、あるマスに引かれうる直線の本数は最大4本となり、引きすぎた分を考えやすくなる。
全マス N x N
- 各直線について個別に、引かれるマスの合計 ...①
+ 2本の直線が交差するマス数 x 1 ┬②
+ 3本の直線が交差するマス数 x 2 ┤
+ 4本の直線が交差するマス数 x 3 ┘
--------------------------------------------
①は O(M) で計算できる。
②も、直線同士の交点となるマスの数は O(M2) に限られるので、
各交点につき交差する直線ペア数を数えていけば O(M2) で求められる。
Python3
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 |
from collections import defaultdict
def solve(n, m, queens):
rows = set()
cols = set()
diag1 = set()
diag2 = set()
for i in range(m):
x, y = queens[i]
rows.add(x)
cols.add(y)
diag1.add(x + y)
diag2.add(x - y)
controlled = (len(rows) + len(cols)) * n
for rc in diag1:
if rc <= n + 1:
controlled += rc - 1
else:
controlled += n * 2 - rc + 1
for rc in diag2:
controlled += n - abs(rc)
duplicated = defaultdict(int)
for r in rows:
for c in cols:
duplicated[r, c] += 1
for rc in diag1:
c = rc - r
if 1 <= c <= n:
duplicated[r, c] += 1
for rc in diag2:
c = r - rc
if 1 <= c <= n:
duplicated[r, c] += 1
for c in cols:
for rc in diag1:
r = rc - c
if 1 <= r <= n:
duplicated[r, c] += 1
for rc in diag2:
r = rc + c
if 1 <= r <= n:
duplicated[r, c] += 1
for rc1 in diag1:
for rc2 in diag2:
if rc1 % 2 != rc2 % 2:
continue
r = (rc1 + rc2) // 2
c = rc1 - r
if 1 <= r <= n and 1 <= c <= n:
duplicated[r, c] += 1
ans = n * n - controlled
for cnt in duplicated.values():
if cnt == 1:
ans += 1
elif cnt == 3:
ans += 2
else:
ans += 3
return ans
n, m = map(int, input().split())
queens = list(tuple(map(int, input().split())) for _ in range(m))
ans = solve(n, m, queens)
print(ans)
|
G - Edit to Match
問題文
N 個の文字列 S1,S2,…,SN が与えられます。各文字列は英小文字からなります。
k=1,2,…,N に対し以下の問題を解いてください。
T=Sk として、 T に対して以下の 2 種類の操作を好きな順番で好きな回数繰り返すことを考える。
T を空文字列、 S1,S2,…,Sk−1 のいずれかと一致させるために払うコストの総和の最小値を求めよ。
制約
解法
Trie木を知っていれば、F問題より解法はわかりやすい。
最適な操作は、操作1で末尾をいい感じのところ(あるいは空文字列)まで削って、
操作2でprefixが同じ他の文字列にする、という流れになる。
複数の文字列のprefixを扱うのは、Trie木の出番。
各ノード n に、「Cn:= ここから末尾への文字の追加だけを行うことで、いずれかの Si と一致させることができる最小コスト」を持たせる。
はじめはルートノード r だけがあり、
k=1,2,…,N の順に、「Sk の答えを求めた後、Trie木に Sk の情報を追加」をしていく。
長さ L の文字列 Sk の、i 文字目までを示すノード ni がTrie木に存在した場合、操作1で i 文字になるまで削るような操作のコストは
これらの合計となる。i=0,1,...,L について調べ、その中の最小値が Sk に対する答えとなる。
Python3
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 |
def solve(n, sss):
class Node:
def __init__(self):
self.length = 1 << 60
self.children = {}
def __str__(self):
s = [f'({self.length})']
for c, node in self.children.items():
node = str(node).split('\n')
node[0] = f' |-{c}- {node[0]}'
for i in range(1, len(node)):
node[i] = f' | {node[i]}'
s.extend(node)
return '\n'.join(s)
root = Node()
buf = []
for s in sss:
t = ans = len(s)
node = root
for i, c in enumerate(s):
if c not in node.children:
node.children[c] = Node()
node = node.children[c]
ans = min(ans, t - i - 1 + node.length)
node.length = min(node.length, t - i - 1)
buf.append(ans)
return buf
n = int(input())
sss = [input() for _ in range(n)]
ans = solve(n, sss)
print('\n'.join(map(str, ans)))
|