1
2
3
4
5
6
7
8
import sys, random
sys.path.append("/home/swyoo/algorithm/")
from utils.verbose import logging_time
from sys import stdin
from copy import deepcopy
from collections import defaultdict
from pprint import pprint
import numpy as np
15684. 사다리 조작
주어진 사다리의 격자 shape 는 $(n, h)$ 이며, input으로 $m$ 개의 위치에 사다리가 주어진다.
다음과 같이 사다리를 나타낸다.
1
2
j-th (j+1)-th ... j-th (j+1)-th ..
|_______| | ==> 1 0
우리의 목표는 사다리 수를 최소화 하여 j
번째 열이사다리를 타고 내려와 최종적으로 j
번째에 떨어지도록 조작할 것이다.
단, 조건은 다음과 같다.
- 만약, 정답이
3
보다 큰 값이면-1
을 출력한다. 또, 불가능한 경우에도-1
을 출력한다.
이러한 조건으로 유추해 봤을때, 경우의 수를 enumerate해면서 목표를 구하는데 필요없는 부분은 pruning하는 문제 일 것이다.
Parse Data
사다리 representation 행렬 a
는 다음과 같다.
입력은 번째 수 이므로 -1
을 하여 index 처리한다.
1
2
3
4
5
6
7
8
9
10
plot = lambda a: print(np.array(a))
stdin = open('data/ladder.txt')
input = stdin.readline
n, m, h = list(map(int, input().split()))
a = [[0] * n for _ in range(h)]
ladders = [list(map(lambda x: int(x) - 1, input().split())) for _ in range(m)]
for i, j in ladders:
a[i][j] = 1
plot(a)
1
2
3
4
5
6
7
[[1 0 0 0 0]
[0 0 1 0 0]
[0 1 0 0 0]
[0 0 0 0 0]
[1 0 0 1 0]
[0 0 0 0 0]]
Idea
- 사다리 놓을 수 있는 부분이 어디인지 파악하고, 사다리를 놓는 경우를 enumerate한다.
- DFS를 통해 enumerate (pruning, that is, backtracking will be used.)
- 사다리를 놓은뒤 각 경우마다 각 열마다 사다리를 내려와 j번째열이 j번째에 도달하는가 체크한다.
- 사다리를 내려오는 subroutine 구현 필요.
optimal substructure, overlapping subproblems property가 만족되지 않아 dynamic programming은 불가.
Step 1. 사다리 내려오기
각 열마다 사다리를 내려오는 subroutine을 구현하고, 체크함수 ladder
만든다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def go(y):
""" given y, global: h, n"""
x = 0
while 0 <= x < h:
if a[x][y] and 0 <= y + 1 < n and not a[x][y + 1]:
y += 1
elif not a[x][y] and 0 <= y - 1 < n and a[x][y - 1]:
y -= 1
x += 1
return y
plot(a)
print("after down:")
for i in range(n):
print(go(i), end=' ')
1
2
3
4
5
6
7
8
[[1 0 0 0 0]
[0 0 1 0 0]
[0 1 0 0 0]
[0 0 0 0 0]
[1 0 0 1 0]
[0 0 0 0 0]]
after down:
2 1 4 0 3
1
2
3
4
5
6
def ladder():
for i in range(n):
if i != go(i):
return False
return True
ladder()
1
False
Step 2. 사다리 놓기
2-2. points
사다리 놓을 수 있는 부분이 어디인지 파악한다.
points
로 모아놓고 순차적으로 경우의 수를 파악하겠다.
다음 그림과 같이 사다리를 놓을 수 있는 부분은 파란색 동그라미(사다리 representation a
에서는 1
이 될 수 있는 부분)로 표시하였다.
i,j
에 사다리를 놓을 수 있으려면 인접한 부분이 0 이어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
points = []
for i in range(h):
for j in range(n - 1):
if a[i][j]: continue
flag = False
for jj in [k for k in [j - 1, j + 1] if 0 <= k < n]:
if a[i][jj]: flag = True
if flag: continue
points.append((i, j))
print(len(points))
print(points)
1
2
3
12
[(0, 2), (0, 3), (1, 0), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3), (5, 0), (5, 1), (5, 2), (5, 3)]
2-3. enumerate
그런데 만약 points
에서 idx
번째에 사다리를 놓았을 경우, idx + 1
번째에는 사다리를 놓지 못할 수 있다
왜냐하면 두 가로선이 연속하거나 서로 접하면 안 된다는 조건이 있기 때문이다.
이 경우를 다음과 같이 구현할 것이다.
1
2
3
4
5
x, y = points[idx]
if idx + 1 < len(points) and (x == points[idx + 1][0]) and (points[idx + 1][1] - y == 1):
dfs(idx + 2, cnt + 1)
else:
dfs(idx + 1, cnt + 1)
따라서, 사다리를 놓는 경우를 dfs를 통해 eumerate 할 경우 다음과 같이 구현한다.
여기서 cnt
는 놓은 사다리 수 이다.
주의해야할 점은 (사다리를 놓는 경우, 놓지 않는 경우) 각각 1
번씩 꼭 call
해야한다.
(처음에 이걸 놓쳐서 많은 시간을 소비했다. Appendix에서 자세히 다루겠다.)
1
print(points)
1
2
[(0, 2), (0, 3), (1, 0), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3), (5, 0), (5, 1), (5, 2), (5, 3)]
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
k = 0
def dfs(idx, cnt, selected):
global k
# print(selected) # cnt <= 4
if cnt == 4: # limit recursion at most |points| choose (4 - 1) cases after this line.
# print(selected) # distinct cases of cnt == 4
return
if idx == len(points): # this line is indispensible.
k += 1 # distinct cases of cnt <= 3
return
x, y = points[idx]
# case 1 - let a ladder at points[idx]
a[x][y] = 1
if idx + 1 < len(points) and (x == points[idx + 1][0]) and (points[idx + 1][1] - y == 1):
dfs(idx + 2, cnt + 1, selected + [idx]) # because points[idx + 1] is adjacent with points[idx], skip points[idx + 1]
else:
dfs(idx + 1, cnt + 1, selected + [idx])
a[x][y] = 0
# case 2 - skip letting the ladder at points[idx]
dfs(idx + 1, cnt, selected)
dfs(idx=0, cnt=0, selected=[])
print(k)
1
2
226
구현
따라서, 다음과 같이 구현한다.
백준 에 채점할 때는 pruning을 recursion중간에 하지 않으면 시간초과 가 뜬다.
따라서, 다음과 같이 pruning during recursion하는 코드들을 써야 통과할 수 있다.
1
2
3
4
5
6
7
8
9
10
if ans <= cnt: return # pruning during recursion
if ladder(): # pruning during recursion
ans = min(ans, cnt)
return
if cnt == 3: return # pruning by limitting the selection of 3 points.
if idx == len(points):
return
또한, Pypy3 로 해야 통과가 된다. (더 효율적인 알고리즘을 만들 필요가 있다. )
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
@logging_time
def solution(a, show=False):
h, n, INF = len(a), len(a[0]), 1e20
def go(y):
""" given y, global: h, n"""
x = 0
while 0 <= x < h:
if a[x][y] and 0 <= y + 1 < n and not a[x][y + 1]:
y += 1
elif not a[x][y] and 0 <= y - 1 < n and a[x][y - 1]:
y -= 1
x += 1
return y
def ladder():
for i in range(n):
if i != go(i):
return False
return True
points = []
for i in range(h):
for j in range(n - 1):
if a[i][j]: continue
flag = False
for jj in [k for k in [j - 1, j + 1] if 0 <= k < n]:
if a[i][jj]: flag = True
if flag: continue
points.append((i, j))
if show: print(points)
if not points: return 0 if ladder() else -1
ans = INF
snapshot = None
def dfs(idx, cnt):
nonlocal ans, snapshot
if ans <= cnt: return # pruning during recursion
if ladder(): # pruning during recursion
if show and cnt < ans: snapshot = deepcopy(a)
ans = min(ans, cnt)
return
if cnt == 3: return # pruning by limitting the selection of 3 points.
if idx == len(points):
return
x, y = points[idx]
a[x][y] = 1
if idx + 1 < len(points):
i, j = points[idx + 1]
if x == i and j - y == 1:
dfs(idx + 2, cnt + 1)
else:
dfs(idx + 1, cnt + 1)
else:
dfs(idx + 1, cnt + 1)
a[x][y] = 0
dfs(idx + 1, cnt)
dfs(idx=0, cnt=0)
if show: plot(snapshot)
return ans if ans != INF else -1
print(solution(a, show=True, verbose=True))
1
2
3
4
5
6
7
8
9
10
[(0, 2), (0, 3), (1, 0), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3), (5, 0), (5, 1), (5, 2), (5, 3)]
[[1 0 1 0 0]
[0 0 1 0 0]
[0 1 0 1 0]
[0 1 0 0 0]
[1 0 0 1 0]
[0 0 0 0 0]]
WorkingTime[solution]: 1.15156 ms
3
Test Cases
제대로 구현했나 check하기 위해 test data를 generate하는 함수 구현.
1
2
3
4
5
6
7
8
9
10
11
12
## Generate Test data
def generate(n, h):
a = [[0] * n for _ in range(h)]
for i in range(h):
if random.randint(0, 10) % 2:
for j in random.choices(range(1, n - 1, 3), k=random.randint(0, n//2)):
a[i][j] = 1
else:
for j in random.choices(range(0, n - 1, 3), k=random.randint(0, n//2)):
a[i][j] = 1
return a
generate(n, h)
1
2
3
4
5
6
[[0, 0, 0, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0]]
Discuss
rebas’s blog 의 코드는 다음과 같다. 내 코드보다 약간 더 빠른 것같다.
시간 복잡도는 동일하지만, 이유는 다음과 같다.
- 사다리를 타고 내려오는
ladder()
함수가 조금 더 빠르다. - 사다리를 놓을 수 있는 지점들
points
를 찾아놓고 enumerate하지 않고
곧바로 enumerate하기때문에 overhead가 준다.
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
@logging_time
def another(a, show=False):
h, n, INF = len(a), len(a[0]), 1e20
def ladder():
for i in range(n):
k = i
for j in range(h): # (j, k) will be a cursor.
if a[j][k]:
k += 1
elif k > 0 and a[j][k-1]:
k -= 1
if i != k:
return False
return True
ans = 4
snapshot = None
def solve(cnt, x, y):
nonlocal ans, snapshot
if ans <= cnt:
return
if ladder():
if show and cnt < ans: snapshot = deepcopy(a)
ans = min(ans, cnt)
return
if cnt == 3:
return
for i in range(x, h):
k = y if i == x else 0
for j in range(k, n-1):
if a[i][j]: # skip
j += 1
else:
a[i][j] = 1
solve(cnt+1, i, j+2)
a[i][j] = 0
solve(0, 0, 0)
if show: plot(snapshot)
return ans if ans < 4 else -1
another(a, show=True, verbose=True)
1
2
3
4
5
6
7
8
[[1 0 1 0 0]
[0 0 1 0 0]
[0 1 0 1 0]
[0 1 0 0 0]
[1 0 0 1 0]
[0 0 0 0 0]]
WorkingTime[another]: 1.24741 ms
1
3
Mycode vs Another
1
2
3
4
5
6
7
8
9
10
11
12
a = [[0] * n for _ in range(h)]
for i in range(h):
if random.randint(0, 10) % 2:
for j in random.choices(range(1, n - 1, 3), k=random.randint(0, n//2)):
a[i][j] = 1
else:
for j in random.choices(range(0, n - 1, 3), k=random.randint(0, n//2)):
a[i][j] = 1
plot(a)
ans1 = solution(a, show=True, verbose=True)
ans2 = another(a, show=True, verbose=True)
ans1, ans2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[[0 0 0 1 0]
[0 0 0 0 0]
[1 0 0 0 0]
[0 1 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]]
[(0, 0), (0, 1), (1, 0), (1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3), (4, 0), (4, 1), (4, 2), (4, 3), (5, 0), (5, 1), (5, 2), (5, 3)]
[[1 0 0 1 0]
[0 0 0 1 0]
[1 0 0 0 0]
[0 1 0 0 0]
[0 1 0 0 0]
[0 0 0 0 0]]
WorkingTime[solution]: 2.28500 ms
[[1 0 0 1 0]
[0 0 0 1 0]
[1 0 0 0 0]
[0 1 0 0 0]
[0 1 0 0 0]
[0 0 0 0 0]]
WorkingTime[another]: 0.94986 ms
1
(3, 3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
""" sanity checks. """
for _ in range(3):
n, h = 5, 6
a = [[0] * n for _ in range(h)]
for i in range(h):
if random.randint(0, 10) % 2:
for j in random.choices(range(1, n - 1, 3), k=random.randint(0, n//2)):
a[i][j] = 1
else:
for j in random.choices(range(0, n - 1, 3), k=random.randint(0, n//2)):
a[i][j] = 1
# plot(a)
ans1 = solution(a, show=False, verbose=True)
ans2 = another(a, verbose=True)
assert ans1 == ans2, "{} vs {}| {}".format(ans1, ans2, a)
print(ans1, ans2)
1
2
3
4
5
6
7
8
9
10
WorkingTime[solution]: 0.59271 ms
WorkingTime[another]: 0.81420 ms
2 2
WorkingTime[solution]: 0.05865 ms
WorkingTime[another]: 0.03052 ms
1 1
WorkingTime[solution]: 0.42868 ms
WorkingTime[another]: 0.70071 ms
1 1
Submitted Code
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 sys import stdin
stdin = open('data/ladder.txt')
input = stdin.readline
n, m, h = list(map(int, input().split()))
a = [[0] * n for _ in range(h)]
ladders = [list(map(lambda x: int(x) - 1, input().split())) for _ in range(m)]
for i, j in ladders:
a[i][j] = 1
def solution(a):
h, n, INF = len(a), len(a[0]), 1e20
def go(y):
x = 0
while 0 <= x < h:
if a[x][y] and 0 <= y + 1 < n and not a[x][y + 1]:
y += 1
elif not a[x][y] and 0 <= y - 1 < n and a[x][y - 1]:
y -= 1
x += 1
return y
def ladder():
for i in range(n):
if i != go(i):
return False
return True
points = []
for i in range(h):
for j in range(n - 1):
if a[i][j]: continue
flag = False
for jj in [k for k in [j - 1, j + 1] if 0 <= k < n]:
if a[i][jj]: flag = True
if flag: continue
points.append((i, j))
if not points: return 0 if ladder() else -1
ans = INF
def dfs(idx, cnt):
nonlocal ans
if ans <= cnt:
return
if ladder():
ans = min(ans, cnt)
return
if cnt == 3:
return
if idx == len(points): return
x, y = points[idx]
a[x][y] = 1
if idx + 1 < len(points) and (x == points[idx + 1][0]) and (points[idx + 1][1] - y == 1):
dfs(idx + 2, cnt + 1)
else:
dfs(idx + 1, cnt + 1)
a[x][y] = 0
dfs(idx + 1, cnt)
dfs(idx=0, cnt=0)
return ans if ans != INF else -1
print(solution(a))
1
2
3
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
from sys import stdin
from itertools import combinations
stdin = open('data/ladder.txt')
input = stdin.readline
n, m, h = list(map(int, input().split()))
a = [[0] * n for _ in range(h)]
ladders = [list(map(lambda x: int(x) - 1, input().split())) for _ in range(m)]
for i, j in ladders:
a[i][j] = 1
def solution(a):
h, n, INF = len(a), len(a[0]), 1e20
def go(y):
x = 0
while 0 <= x < h:
if a[x][y] and 0 <= y + 1 < n and not a[x][y + 1]:
y += 1
elif not a[x][y] and 0 <= y - 1 < n and a[x][y - 1]:
y -= 1
x += 1
return y
def ladder():
for i in range(n):
if i != go(i):
return False
return True
points = []
for i in range(h):
for j in range(n - 1):
if a[i][j]: continue
flag = False
for jj in [k for k in [j - 1, j + 1] if 0 <= k < n]:
if a[i][jj]: flag = True
if flag: continue
points.append((i, j))
if not points: return 0 if ladder() else -1
ans = INF
for i in range(4):
for selected in list(combinations(range(len(points)), i)):
for k in selected: a[points[k][0]][points[k][1]] = 1
if ladder():
ans = min(ans, i)
for k in selected: a[points[k][0]][points[k][1]] = 0
return ans if ans != INF else -1
print(solution(a))
1
2
3
Appendix. consideration
All combination cases can be found as follows.
${3 \choose 0} + {3 \choose 1} + {3 \choose 2} + {3 \choose 3} = 8$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
indices = list(range(3))
c1 = c2 = c3 = k = 0
def dfs(idx, selected):
global c1, c2, c3, k
# print(selected) # recursion 중에 똑같은 selected 여러번 불릴 수 있다.
if len(selected) == 1: c1 += 1 # 이 시점에 return하면 distinct한 5C1을 구할 수 있다.
if len(selected) == 2: c2 += 1 # 이 시점에 return하면 distinct한 5C2을 구할 수 있다.
if len(selected) == 3: c3 += 1 # 이 시점에 return하면 distinct한 5C3을 구할 수 있다.
if idx == len(indices):
k += 1
print(selected, end=' ') # distintive한 cases들을 출력가능.
return
dfs(idx + 1, selected + [idx])
dfs(idx + 1, selected)
dfs(idx=0, selected=[])
c1, c2, c3, k
1
[0, 1, 2] [0, 1] [0, 2] [0] [1, 2] [1] [2] []
1
(6, 4, 1, 8)
Partial combinations
Partial combinations can be found by limiting as follows.
1
2
if len(selected) == 4:
return
${4 \choose 0} + {4 \choose 1} + {4 \choose 2} + {4 \choose 3} = 15$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
indices = list(range(4))
k = 0
def dfs(idx, selected):
global k
# print(selected) # recursion 중에 똑같은 selected 여러번 불릴 수 있다.
if len(selected) == 4:
return
if idx == len(indices):
k += 1
print(selected, end=' ')
return
dfs(idx + 1, selected + [idx])
dfs(idx + 1, selected)
dfs(idx=0, selected=[])
k
1
[0, 1, 2] [0, 1, 3] [0, 1] [0, 2, 3] [0, 2] [0, 3] [0] [1, 2, 3] [1, 2] [1, 3] [1] [2, 3] [2] [3] []
1
15
Use Libary
1
2
3
4
5
from itertools import permutations, combinations
n = 3
for i in range(n + 1):
for x in list(combinations(range(n), i)):
print(x, end=' ')
1
() (0,) (1,) (2,) (0, 1) (0, 2) (1, 2) (0, 1, 2)
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
@logging_time
def solution2(a, show=False):
h, n, INF = len(a), len(a[0]), 1e20
def ladder():
for i in range(n):
k = i
for j in range(h):
if a[j][k]:
k += 1
elif k > 0 and a[j][k-1]:
k -= 1
if i != k:
return False
return True
points = []
for i in range(h):
for j in range(n - 1):
if a[i][j]: continue
flag = False
for jj in [k for k in [j - 1, j + 1] if 0 <= k < n]:
if a[i][jj]: flag = True
if flag: continue
points.append((i, j))
if show: print(points)
if not points: return 0 if ladder() else -1
ans = INF
snapshot = None
for i in range(4):
for selected in list(combinations(range(len(points)), i)):
for k in selected: a[points[k][0]][points[k][1]] = 1
if ladder():
if show and i < ans: snapshot = deepcopy(a)
ans = min(ans, i)
for k in selected: a[points[k][0]][points[k][1]] = 0
if show: plot(a)
return ans if ans != INF else -1
1
2
3
4
5
6
7
8
9
10
11
n, h = 10, 8
a = [[0] * n for _ in range(h)]
for i in range(h):
if random.randint(0, 10) % 2:
for j in random.choices(range(1, n - 1, 3), k=random.randint(0, n//2)):
a[i][j] = 1
else:
for j in random.choices(range(0, n - 1, 3), k=random.randint(0, n//2)):
a[i][j] = 1
plot(a)
print(solution2(a, show=True, verbose=True))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[[0 0 0 0 0 0 0 0 0 0]
[0 1 0 0 1 0 0 1 0 0]
[0 0 0 0 1 0 0 1 0 0]
[1 0 0 1 0 0 1 0 0 0]
[0 1 0 0 1 0 0 1 0 0]
[1 0 0 0 0 0 0 0 0 0]
[0 0 0 0 1 0 0 1 0 0]
[0 0 0 1 0 0 1 0 0 0]]
[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (2, 0), (2, 1), (2, 2), (3, 8), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (6, 0), (6, 1), (6, 2), (7, 0), (7, 1), (7, 8)]
[[0 0 0 0 0 0 0 0 0 0]
[0 1 0 0 1 0 0 1 0 0]
[0 0 0 0 1 0 0 1 0 0]
[1 0 0 1 0 0 1 0 0 0]
[0 1 0 0 1 0 0 1 0 0]
[1 0 0 0 0 0 0 0 0 0]
[0 0 0 0 1 0 0 1 0 0]
[0 0 0 1 0 0 1 0 0 0]]
WorkingTime[solution2]: 8.08096 ms
2
Leave a comment