ABC 231 E 以降
問題E
コンテスト中に考えてたこと
試行 その1.
- 壁を壊さずに行ける範囲をグループ分けする (幅優先探索)
- 各グループの隣接する壁から再帰的に壁を壊し、違うグループに至るまでの最小の破壊数を求める(幅優先探索)
- 各グループからグループへの移動に必要な破壊する壁の数が求まるのでこれをグラフ的に扱い、ダイクストラで解く
#!/usr/bin/env python3
from heapq import heappop,heappush
from typing import List,Tuple
INF = 10**10
# N: 都市の数
# Q: グラフ: [(next_city,cost)] * N
# s: スタートとする都市
# -> 各都市への距離
def dijkstra(N:int,Q:List[Tuple[int,int]],s:int)->List[int]:
dist = [INF]*N
que = [(0,s)] # スタート
dist[s] = 0
while que:
c,v = heappop(que)
if dist[v] < c:
continue
for cost,t in Q[v]:
if dist[v] + cost < dist[t]:
dist[t] = dist[v] + cost
heappush(que,(dist[t],t))
return dist
def main():
H,W = map(int,input().split())
DIRS = [(1,0),(-1,0),(0,1),(0,-1)]
M = []
for i in range(H):
M.append(list(input()))
WALL = -2
NOT_SET = -1
GM = [[WALL if M[i][j] == "#" else NOT_SET for j in range(W)] for i in range(H)]
GW = [set()]
group_id = 0
for i in range(H):
for j in range(W):
if GM[i][j] != NOT_SET:
continue
next = [(i,j)]
visited = set([(i,j)])
while next:
i,j = next.pop()
GM[i][j] = group_id
for dx,dy in DIRS:
ni = i + dx
nj = j + dy
if ni >= 0 and nj >= 0 and ni < H and nj < W:
if GM[ni][nj] != WALL and (ni,nj) not in visited:
next.append((ni,nj))
visited.add((ni,nj))
elif GM[ni][nj] == WALL:
GW[-1].add((ni,nj))
group_id += 1
GW.append(set())
if GM[0][0] == GM[H-1][W-1]:
print(0)
exit()
GC = group_id
INF = 10**10
GD = [[INF] * GC for i in range(GC)] # グループ間の距離
for i in range(GC):
GD[i][i] = 0
NW = [(0,2),(1,2),(-1,2),(0,-2),(1,-2),(-1,-2),(2,0),(2,-1),(2,1),(-2,0),(-2,-1),(-2,1)]
CH = [(0,1),(1,0),(0,-1),(-1,0),(1,-1),(-1,1),(-1,-1),(1,1)] + NW
for i in range(GC):
punch_count = 1
visited = set()
next = list(GW[i])
while next:
nn = []
while next:
wi,wj = next.pop() # 壊すとする壁の位置
for ci,cj in CH:
ni = wi + ci
nj = wj + cj
if ni >= 0 and nj >= 0 and ni < H and nj < W and GM[ni][nj] != WALL:
# 壁じゃないどこかにたどり着いた
if GD[i][GM[ni][nj]] > punch_count:
GD[i][GM[ni][nj]] = punch_count
for ci,cj in NW:
ni = wi + ci
nj = wj + cj
if ni >= 0 and nj >= 0 and ni < H and nj < W and GM[ni][nj] == WALL and (ni,nj) not in visited:
# 壁じゃないどこかにたどり着いた
visited.add((ni,nj))
nn.append((ni,nj))
next = nn
punch_count += 1
P = [[] for i in range(GC)]
for i in range(GC):
for j in range(GC):
if GD[i][j] != INF and j != i:
P[i].append((GD[i][j],j))
dist = dijkstra(GC,P,GM[0][0])
print(dist[GM[H-1][W-1]])
正直、よく小さいデータセットに限りACできるまでこの実装を持ってこれたと思うが、TLE
試行2. (この実装途中でコンテスト終了)
そもそも、グループ分けしなくても単純な幅優先探索でもいいのではないか?
- 壁じゃない場合は上下左右を次の手の候補として探索
- 壁である場合は、上下左右に加え、その壁を含む2x2の範囲を壊して行ける範囲を含める
#!/usr/bin/env python3
from heapq import heappop,heappush
def main():
H,W = map(int,input().split())
DIRS = [(1,0),(-1,0),(0,1),(0,-1)]
CH = [(0,1),(1,0),(0,-1),(-1,0),(1,-1),(-1,1),(-1,-1),(1,1)]
M = []
for i in range(H):
M.append(list(input()))
next = [(0,0)]
dp = [[INF] * W for i in range(H)]
dp[0][0] = 0
while next:
nn = []
while next:
i,j = next.pop()
for dx,dy in DIRS:
ni = i + dx
nj = j + dy
if ni >= 0 and nj >= 0 and ni < H and nj < W:
if M[ni][nj] == "#":
for cx,cy in CH:
nni = ni + cx
nnj = nj + cy
if nni >= 0 and nnj >= 0 and nni < H and nnj < W:
if dp[nni][nnj] > dp[i][j] + 1:
nn.append((nni,nnj))
dp[nni][nnj] = dp[i][j] + 1
else:
if dp[ni][nj] > dp[i][j]:
dp[ni][nj] = dp[i][j]
nn.append((ni,nj))
next = nn
print(dp[H-1][W-1])
試行1より実装は単純なのに早かった。しかし、3テストケース分 TLE。
試行3.
普通に壁の数をコストとして最小の壁を壊す経路を探索し、経路復元をしてその経路に必要な破壊する数を考えてみる。
実装を残していなかったが、最短コストを更新する際に、そのマスに行くのにどこのマスから遷移したかを覚えておく。
しかし、この手法はそもそも上手くいかない。
例えば、テストケース1では、こんな経路が出てくる。(Bが壊す壁)
壊す壁の数は2で最小ではあるが、この位置の2つの壁は同時に壊せないために、合計の壁の数では最小だが、壁を壊すためのパンチの数が最小ではない。
['.', '.', 'B', '.', '.']
['#', '.', '#', '.', '#']
['#', '#', '.', 'B', '#']
['#', '.', '#', '.', '#']
['.', '.', '#', '.', '.']
同様にテストケース3もこんな感じ
['.', '#', '#', '#', '#', '#', '#', '#']
['B', '#', '#', '#', '#', '#', '#', '#']
['B', '#', '#', '#', '#', '#', '#', '#']
['B', '#', '#', '#', '#', '#', '#', '#']
['B', '#', '#', '#', '#', '#', '#', '#']
['B', '#', '#', '#', '#', '#', '#', '#']
['B', '#', '#', '#', '#', '#', '#', '#']
['B', 'B', 'B', 'B', 'B', 'B', 'B', '.']
本当は曲がりくねったほうが、一度に壊せる、実際に通る壁の数が増えるのだがそうならない。
試行4.
試行2を改善する方法を考える。 そもそも、次の手が壁なのか、壁じゃないのかの状態を同じキューに入れているために可能性の低い手を優先して計算してしまっている。
01-BFS的な考え方で、次の遷移先が壁なら、探索候補のキューに左から追加、壁でないなら探索候補のキューに右から追加すれば、壁以外から先に探索できるのではないか?
def main():
H,W = map(int,input().split())
DIRS = [(1,0),(-1,0),(0,1),(0,-1)]
NW = [(0,2),(1,2),(-1,2),(0,-2),(1,-2),(-1,-2),(2,0),(2,-1),(2,1),(-2,0),(-2,-1),(-2,1)]
CH = [(0,1),(1,0),(0,-1),(-1,0),(1,-1),(-1,1),(-1,-1),(1,1)] + NW
M = []
for i in range(H):
M.append(list(input()))
D = [[INF]*W for i in range(H)]
P = [[None]*W for i in range(H)]
D[0][0] = 0
que = deque([(0,(0,0))]) # (punch_count,position)
while que:
cost,pos = que.pop()
i,j = pos
if D[i][j] < cost:
continue # 別の手段でもっと早くたどり着いてるので探索しない
if M[i][j] == "#":
for dx,dy in CH: # (i,j)は壁なので、これを壊すとしてたどり着けるすべての場所を次のキューに入れる
ni = i + dx
nj = j + dy
if ni >= 0 and nj >= 0 and ni < H and nj < W:
if D[ni][nj] > D[i][j] + 1:
D[ni][nj] = D[i][j] + 1
P[ni][nj] = (dx,dy)
if M[ni][nj] == "#": # (ni,nj)は壁なのであまり優先しない
que.appendleft((D[i][j],(ni,nj)))
else:
que.append((D[i][j],(ni,nj)))
else:
for dx,dy in DIRS: # (i,j)は壁でないので、単に上下左右を探索する
ni = i + dx
nj = j + dy
if ni >= 0 and nj >= 0 and ni < H and nj < W:
if D[ni][nj] > D[i][j]:
D[ni][nj] = D[i][j]
P[ni][nj] =(dx,dy)
if M[ni][nj] == "#":
que.appendleft((D[i][j],(ni,nj)))
else:
que.append((D[i][j],(ni,nj)))
これはAC。
なるほど、幅優先探索の候補のキューでは、どの順序で探索するかが大きく速度に影響する。
実際、だからダイクストラ等では優先度付きキューを用いているのだからそれは納得がいく。
ちなみに両方ともappendleftしてみるとやっぱり遅い。
ちなみに、ものすごく似ている類題を見つけた。
Eは解けるべき問題だったなーと思う。一応、Fまでは考えてみる。
問題F.
思ったこと
制約的に
いや、そもそも各
分からないので解説を読む。 なるほど、知らない単語だらけ。
なんにせよ、Suffix Arrayなるものを求める必要があるらしい。