AtCoder Beginner Contest 296 F,Ex問題メモ
F - Simultaneous Swap
問題
長さ $N$ の数列 $A=(A_1,A_2,\ldots,A_N)$, $B=(B_1,B_2,\ldots,B_N)$ が与えられる
次の操作を好きなだけ ($0$ 回でも良い) 繰り返す事ができる
$1$ 以上 $N$ 以下の、どの $2$ つも互いに相異なる $3$ つの整数 $i,j,k$ を選ぶ
$A$ の $i$ 番目の要素と $j$ 番目の要素を交換し、$B$ の $i$ 番目の要素と $k$ 番目の要素を交換する
うまく操作を繰り返すことによって、$A$ と $B$ を一致させる事が可能か判定せよ
$3 \leq N \leq 2\times 10^5$
$1\leq A_i,B_i\leq N$
解法
まず、こういうのは2つの数列に同時に操作するよりは、
片方を固定してもう片方を合わせにいく方が楽なので、そういう言い換えができないか考える。
するとこれは、片方(たとえば $A$)の任意の3つをシフトさせる操作に言い換えられる。
(実際に問題文の通りに操作をして、$B$ の方を元の並びに直したとき、$A$ の変化を確認するとわかる)
そうとわかれば実際に操作をして $A$ を $B$ に合わせにいくのは $O(N)$ でできるが、
という懸念が生じる。
swap回数の偶奇
この証明に有用なのが、swap回数の偶奇に着目する方法。
要素が全て区別できる数列と、それに任意の2要素のswapを繰り返し行った数列を比べると、
どういう順番でやっても、swapを偶数回やったのか奇数回やったのかは特定できる。
3つの数をシフトするのは、swapを2回行うことに相当する。
つまり、$A$ から $B$ にするのに奇数回のswapが必要なら、この問題の操作によって一致させるのは不可能ということになる。
$A$ の要素が全て異なる場合
実際に端から $A$ を $B$ に合わせにいくと、最後から3要素目までは必ず一致させることができる。
その時、最後の2要素は、自然に揃っているか、入れ替わっているかのどちらかである。
揃っているならその事実ができるという証明になり、
入れ替わっている場合は、$A$ から $B$ にするのに奇数回のswapが必要と言うことになるので不可能となる。
$A$ の要素に同じものがある場合
この場合は必ず一致させられる。
実際に以下のようなケースを手で解いてみてもよいが、
A 1 1 2 3
B 1 1 3 2
最後が入れ替わっていても、いったん揃った 1 を崩すことで、最後の2要素を入れ替えて $B$ に合わせられる。
これは、最後の2と3のswapで奇数回となったswapの総数を、1同士でもswapすることで偶数回に戻しているとも言える。
Python3
def solve(n, aaa, bbb):
if sorted(aaa) != sorted(bbb):
return False
if len(set(aaa)) < len(aaa):
return True
pos = {a: i for i, a in enumerate(aaa)}
for i in range(n - 2):
if aaa[i] == bbb[i]:
continue
k = pos[bbb[i]]
if i + 1 == k:
j = i + 2
else:
j = i + 1
a = aaa[i]
b = aaa[j]
c = aaa[k]
aaa[i] = c
aaa[j] = a
aaa[k] = b
pos[c] = i
pos[a] = j
pos[b] = k
if aaa[-1] == bbb[-1]:
return True
else:
return False
n = int(input())
aaa = list(map(int, input().split()))
bbb = list(map(int, input().split()))
ans = solve(n, aaa, bbb)
print('Yes' if ans else 'No')
Ex - Unite
問題
$N$ 行 $M$ 列のマス目があり、各マスは黒または白で塗られている
白く塗られたいくつかのマス ($0$ 個でもよい) を新しく黒く塗ることによって、黒く塗られたマス全体が連結になるようにしたい
目標を達成するために新しく塗る必要のあるマスの個数としてあり得る最小値を求めよ
$1 \leq N \leq 100$
$1\leq M \leq 7$
解法
連結DPと呼ばれることがある問題。
横幅 $M$ が小さいので、行の黒マス配置を $2^M$ のbitフラグで持ったDPをしそうなことは想像つく。
1つ前の行 $2^7$ 通り→今の行 $2^7$ 通りの遷移を全探索するのは $2^{14} = 16384$ なので、
これを $100$ 行繰り返すと $1.64 \times 10^6$ となり、計算量的に余裕だぜ!となる。。。
が、よく考えるとそれでは足りない。
ある離れた2マスが、より前の行で繋がっているのか、繋がってないのでこれ以降で繋げないといけないのか、区別できない。
i ① ② ③
0 #### #.## #.##
prev> 1 #..# #..# #..#
now > 2 #..# #..# #..#
3 #..# #..# ####
①は $i=0$ で既に繋がっているので、以下の行でも繋げる必要は無い
②は繋がっていないが、後の行で繋げるよりも $i=0$ で繋げてしまった方がよい
③は $i=2$ では繋がっていなくても $i=3$ で繋がるので、繋げる必要は無い
特に、②と③を区別できない。($i=0$ で繋げたのならnowまでの最小数は1個、繋げてないのならこれ以降で繋げる必要がありつつ最小数は0個)
なので、「どのマスとどのマスが繋がっているの?」という連結状態も管理してやる必要がある。
連結状態の表現方法
連結状態も含めてDPの「状態」とすれば、今度こそ、全体がひとつながりになれるかどうかを区別できる。
連結状態をどう表現するかにはある程度の個人差がありそうだが、例えば以下の実装がある。
例えば過去の類題 Typical DP Contest S - マス目 の解説ブログの1つでは、
bitフラグでなく $k$ 進数で状態を表し、別々の連結成分に属する黒マスには別々の数字を割り当てる、としている。
今回は最大 $7$ マスなので別々の連結成分の最大は4つで、白マスと合わせて $5$ 進数で状態を表せる。
連結性の判定
「前の行の、連結性を含んだ状態」と「今の行の、連結性は未計算の黒マス配置」があれば、今の行の連結性は計算できる。
連結性を含めても、とある黒マス配置に対して実際に可能な連結状態は限られているので、思ったより状態数は増えない。
(実際にやってみると、320ちょっとくらい。$2^7=128$ なので、3倍にもなってない)
連結性チェックの部分は、Union-Findを使っている例もあるし、自分はDFSをおこなった。
計算量は、連結性含みの状態数を $C$ とすると、
1行の遷移で、$C \times 2^M$ の総当たり、その中で $O(M)$ とか $O(M^2)$ とかの連結性チェック
$N$ 行の遷移全部で、$O(N C M 2^M)$
最大値を代入して $3 \times 10^7$ 程度になるので、間に合う。
連結性チェックに $O(M^2)$ かけた場合はちょっと厳しくなるが、
そもそも遷移の種類数 $C \times 2^M$ が限られているので、メモ化すれば十分間に合う。
失敗しそうな入力とか
いずれも答えは0
6 7
#######
#.....#
#.###.#
#.#.#.#
#.#...#
#.#####
5 7
#######
#.....#
#.###.#
#.#.#.#
#.#.###
Python3
from collections import defaultdict
def pop_count_8(n):
c = (n & 0x55) + ((n >> 1) & 0x55)
c = (c & 0x33) + ((c >> 2) & 0x33)
c = (c & 0x0f) + ((c >> 4) & 0x0f)
return c
transit_memo = {}
def transit(prev_row, prev_mat, curr_row):
"""
前の行の黒マス・接続状況と、今の行の黒マスから、それが可能な遷移か、可能なら今の行の接続状況がどうなるかを返す。
状態は限られているため、メモ化することで高速になる
"""
if (prev_row, prev_mat, curr_row) in transit_memo:
return transit_memo[prev_row, prev_mat, curr_row]
curr_mat = 0
checked = 0
for s in range(m):
sb = 1 << s
if curr_row & sb == 0:
continue
if checked & sb:
continue
grp = sb
checked |= sb
q = [sb] # 現在の行[0,m) 前の行[m,2m)
while q:
ub = q.pop()
if ub < p2m:
vb = ub << 1
if vb != p2m and (curr_row & vb) and (checked & vb == 0):
q.append(vb)
grp |= vb
checked |= vb
vb = ub >> 1
if vb != 0 and (curr_row & vb) and (checked & vb == 0):
q.append(vb)
grp |= vb
checked |= vb
vb = ub << m
if (prev_row & (vb >> m)) and (checked & vb == 0):
q.append(vb)
checked |= vb
else:
vb = ub >> m
if (curr_row & vb) and (checked & vb == 0):
q.append(vb)
grp |= vb
checked |= vb
u = bit2idx[ub >> m]
for v in range(m):
vb = 1 << (v + m)
if u != v and (prev_mat & (1 << (u * m + v))) and (checked & vb == 0):
q.append(vb)
checked |= vb
for u in range(s, m):
ub = 1 << u
if grp & ub == 0:
continue
for v in range(u + 1, m):
vb = 1 << v
if grp & vb:
curr_mat |= 1 << (u * m + v)
curr_mat |= 1 << (v * m + u)
# 前の行から繋がらないマスがある
if ~(checked >> m) & prev_row:
res = -1
else:
res = curr_mat
transit_memo[prev_row, prev_mat, curr_row] = res
return res
n, m = map(int, input().split())
field = []
for _ in range(n):
s = input()
field.append(int(s.replace('.', '0').replace('#', '1'), 2))
# 最初行と最終行に必ず黒マスがあるようにする
i = 0
while field[i] == 0:
i += 1
field = field[i:]
while field[-1] == 0:
field.pop()
bit2idx = {1 << i: i for i in range(m)}
INF = 1 << 60
dp_factory = lambda: INF
dp = {0: 0}
# dp key: (前のrowに配置した黒マスのbitset << m*m) | (前の行の接続関係を示すm*mのmatrixを1つのbitsetにしたもの)
# dp value: keyの状態にするのに必要な最小追加黒マス数
p2m = 1 << m
p2m2 = 1 << (m * 2)
mm = m * m
mask_mm = (1 << mm) - 1
mask_m = (1 << m) - 1
for orig_row in field:
ndp = defaultdict(dp_factory)
# 現在行の白マス集合(黒マスを置ける集合)
cleared_bits = mask_m & ~orig_row
for key, count in dp.items():
prev_row = key >> mm
prev_mat = key & mask_mm
set_bits = cleared_bits
prev_set_bits = -1
while prev_set_bits != 0:
curr_row = orig_row | set_bits
prev_set_bits = set_bits
set_bits = (set_bits - 1) & cleared_bits
curr_mat = transit(prev_row, prev_mat, curr_row)
if curr_mat == -1:
continue
new_count = count + pop_count_8(orig_row ^ curr_row)
new_key = (curr_row << mm) | curr_mat
ndp[new_key] = min(ndp[new_key], new_count)
dp = ndp
ans = INF
for key, count in dp.items():
row = key >> mm
mat = key & mask_mm
ib = row & -row
i = bit2idx[ib]
# 最終行に配置された黒マスが全て連結状態にあるかチェック
if (row ^ ib) == ((mat >> (i * m)) & mask_m):
ans = min(ans, count)
print(ans)