문제

와일드카드는 다양한 운영체제에서 파일 이름의 일부만으로 파일 이름을 지정하는 방법이다. 와일드카드 문자열은 일반적인 파일명과 같지만, * 나 ? 와 같은 특수 문자를 포함한다.

와일드카드 문자열을 앞에서 한 글자씩 파일명과 비교해서, 모든 글자가 일치했을 때 해당 와일드카드 문자열이 파일명과 매치된다고 하자. 단, 와일드카드 문자열에 포함된 ? 는 어떤 글자와 비교해도 일치한다고 가정하며, * 는 0 글자 이상의 어떤 문자열에도 일치한다고 본다.

예를 들어 와일드 카드 he?p 는 파일명 help 에도, heap 에도 매치되지만, helpp 에는 매치되지 않는다. 와일드 카드 *p* 는 파일명 help 에도, papa 에도 매치되지만, hello 에는 매치되지 않는다.

와일드카드 문자열과 함께 파일명의 집합이 주어질 때, 그 중 매치되는 파일명들을 찾아내는 프로그램을 작성하시오.

입력

입력의 첫 줄에는 테스트 케이스의 수 C (1 <= C <= 10) 가 주어진다. 각 테스트 케이스의 첫 줄에는 와일드카드 문자열 W 가 주어지며, 그 다음 줄에는 파일명의 수 N (1 <= N <= 50) 이 주어진다. 그 후 N 줄에 하나씩 각 파일명이 주어진다. 파일명은 공백 없이 알파벳 대소문자와 숫자만으로 이루어져 있으며, 와일드카드는 그 외에 * 와 ? 를 가질 수 있다. 모든 문자열의 길이는 1 이상 100 이하이다.

출력

각 테스트 케이스마다 주어진 와일드카드에 매치되는 파일들의 이름을 한 줄에 하나씩 아스키 코드 순서(숫자, 대문자, 소문자 순)대로 출력한다.

예제 입력

2 he?p 3 help heap helpp *p* 3 help papa hello

예제 출력

heap help help papa

출처 : https://www.algospot.com/judge/problem/read/WILDCARD

[접근방식]

  1. 완전탐색 가능? ⇒ 범위가 작아서 가능할듯(재귀 / for 문)
  2. 풀이기법 ⇒ 정렬로 풀 경우에 *가 여러자릿수가 된다는 게 문제가 됨 ⇒ 정규식으로는 가능할듯

[풀이코드(정규식)]

import re

#입력값
# wild = 'he?p'
# files = ['help', 'heap', 'helpp']

wild = '*p*'
files = ['help', 'papa', 'hello']

new_wild = ''
for char in wild:
    if char == '?':
        new_wild += '.'
    if char == '*':
        new_wild += '.*'
    else:
        new_wild += char
regExp = re.compile(new_wild)
ans = []
for file in files:
    match = re.match(regExp, file)
    if match:
        #자릿수를 맞춰주기 위해(?만 있을 땐 처음에 길이 확인하면 되지만, *때문에)
        if file == match.group():
            ans.append(file)
print(ans)

[책 풀이(DP)]

#입력값
# wild = 'he?p'
# files = ['help', 'heap', 'helpp']

wild = '*p*'
files = ['help', 'papa', 'hello']

# wild = '*bb*'
# files = ['babbbc']
cache = [[None]*101 for _ in range(101)]

def matchMemoized(w, s, W, S):
    ret = cache[w][s]
    if ret: return ret
    while s < len(S) and w < len(W) and (W[w] == '?' or W[w] == S[s]):
        w += 1
        s += 1
    if w == len(W):
        cache[w][s] = (s == len(S))
        return cache[w][s]
    if W[w] == '*':
        for skip in range(len(S)-s+1):
            if matchMemoized(w+1, s+skip, W, S):
                ret = True
                return ret
    ret = False
    return ret

ans = []
for file in files:
    if matchMemoized(0, 0, wild, file):
        ans.append(file)
print(ans)

[책 풀이에서 Cache 없앴을 때(완전탐색)]

#입력값
# wild = 'he?p'
# files = ['help', 'heap', 'helpp']

wild = '*p*'
files = ['help', 'papa', 'hello']

# wild = '*bb*'
# files = ['babbbc']

def matchMemoized(w, s, W, S):
    while s < len(S) and w < len(W) and (W[w] == '?' or W[w] == S[s]):
        w += 1
        s += 1
    if w == len(W):
        return (s == len(S))
    if W[w] == '*':
        for skip in range(len(S)-s+1):
            if matchMemoized(w+1, s+skip, W, S):
                return True
    return False

ans = []
for file in files:
    if matchMemoized(0, 0, wild, file):
        ans.append(file)
print(ans)

참고 : https://junho-one.tistory.com/13

[느낀점/의문점]

  1. ‘책 풀이’에서 아래의 부분에서 여러개가 불러져서 가고 또 거기서 *로 끝나면 또 부를거니까 memoization을 적용시킨다고 생각했음
if W[w] == '*':
    for skip in range(len(S)-s+1):
        if matchMemoized(w+1, s+skip, W, S):
            ret = True
            return ret
  1. 그런데, 어차피 w+1로 *을 뛰어넘어서 줄테고 매칭이 안 되면 False를 반환하게 되고 앞서서 for구문에서 skip 적용해서 돌리기 때문에 캐싱을 사용하고 있다기 보다는 완전탐색에 가깝다고 사료됨
  2. 왜 메모이제이션을 여기서 썼는지 잘 모르겠음. 입력값이 아주 크거나 복잡한 값이 들어올 경우에 메모이제이션을 쓰게되나?
  3. 문자열 parsing 문제에서는 정규식 만큼 강력한 도구가 없는 것 같음

문제

땅따먹기를 하다 질린 재하와 영훈이는 땅따먹기의 변종인 새로운 게임을 하기로 했습니다. 이 게임은 그림과 같이 n*n 크기의 격자에 각 1부터 9 사이의 정수를 쓴 상태로 시작합니다. 각 차례인 사람은 맨 왼쪽 윗 칸에서 시작해 외발로 뛰어서 오른쪽 아래 칸으로 내려가야 합니다. 이 때 각 칸에 적혀 있는 숫자만큼 오른쪽이나 아래 칸으로 움직일 수 있으며, 중간에 게임판 밖으로 벗어나면 안 됩니다.

균형을 잃어서 다른 발로 서거나 넘어져도 게임에서 집니다만, 재하와 영훈이는 젊고 활기차기 때문에 외발로 뛰어다니는 것은 아무것도 아닙니다. 다만 걱정되는 것은 주어진 게임판에 시작점에서 끝점으로 가는 방법이 존재하지 않을 수도 있다는 것입니다. 예를 들어 그림 (a)의 게임판에서는 사각형으로 표시된 칸들을 통해 끝에 도달할 수 있지만, 숫자가 하나 바뀐 그림 (b)에서는 그럴 수가 없습니다.

게임판이 주어질 때 왼쪽 위의 시작점에서 오른쪽 아래의 시작점에 도달할 수 있는 방법이 있는지 확인하는 프로그램을 작성하세요.

입력

입력의 첫 줄에는 테스트 케이스의 수 C(C <= 50)가 주어집니다. 각 테스트 케이스의 첫 줄에는 격자의 크기 n(2 <= n <= 100)이 주어지고, 그 후 n줄에 각 n개의 숫자로 왼쪽 위부터 각 칸에 쓰인 숫자들이 주어집니다. 오른쪽 아래 있는 끝 점 위치에는 0이 주어집니다.

출력

각 테스트 케이스마다 한 줄로, 시작점에서 끝 점으로 도달할 수 있을 경우 "YES"를, 아닐 경우 "NO"를 출력합니다. (따옴표 제외)

예제 입력

2 7 2 5 1 6 1 4 1 6 1 1 2 2 9 3 7 2 3 2 1 3 1 1 1 3 1 7 1 2 4 1 2 3 4 1 2 3 3 1 2 3 4 1 1 5 2 9 4 7 0 7 2 5 1 6 1 4 1 6 1 1 2 2 9 3 7 2 3 2 1 3 1 1 1 3 1 7 1 2 4 1 2 3 4 1 3 3 3 1 2 3 4 1 1 5 2 9 4 7 0

예제 출력

YES NO

출처 : https://algospot.com/judge/problem/read/JUMPGAME

[접근방식]

  1. 완전탐색? ⇒ 재귀로 가능하나, 단순 완전탐색은 시간복잡도 때문에 불가능
  2. 메모이제이션? ⇒ 2차원배열 cache 사용 but 실제로 사용해보니, dfs탐색할 때 visited처리랑 똑같은듯
  3. 풀이방식 dfs로 탐색해주면서 cache(visited)라는 2차원 배열에 업데이트하고 만일 cache에 이미 있으면 넘기고 없을때만 cache에 업데이트하는 방식으로 진행 이 때, 도달할 수 없다면 같은 셀을 계속 돌게되고 그 때마다 cache 값이 존재할 것이므로 dfs가 추가적으로 불리지 않고 끝나게 될 것 마지막에 cache[n-1][n-1]이 여전히 초기값(0)이면, 도달할 수 없는 것이고 0보다 크면 도달가능

[풀이코드]

#입력값
n = 7
# board = [
#     [2, 5, 1, 6, 1, 4, 1],
#     [6, 1, 1, 2, 2, 9, 3],
#     [7, 2, 3, 2, 1, 3, 1],
#     [1, 1, 3, 1, 7, 1, 2],
#     [4, 1, 2, 3, 4, 1, 2],
#     [3, 3, 1, 2, 3, 4, 1],
#     [1, 5, 2, 9, 4, 7, 0]]

board = [
    [2, 5, 1, 6, 1, 4, 1],
    [6, 1, 1, 2, 2, 9, 3],
    [7, 2, 3, 2, 1, 3, 1],
    [1, 1, 3, 1, 7, 1, 2],
    [4, 1, 2, 3, 4, 1, 3],
    [3, 3, 1, 2, 3, 4, 1],
    [1, 5, 2, 9, 4, 7, 0]]

dir = [(1,0), (0, 1)]
cache = [[0]*n for _ in range(n)]

def dfs(r, c):
    global board, dir, cache, n
    if r == n-1 and c == n-1:
        return
    steps = board[r][c]
    for i in range(2):
        nxt_r = r + steps*dir[i][0]
        nxt_c = c + steps*dir[i][1]
        if nxt_r < 0 or nxt_r >= n or nxt_c < 0 or nxt_c >= n:
            continue
        if cache[nxt_r][nxt_c] > 0:
            continue
        else:
            cache[nxt_r][nxt_c] = steps + cache[r][c]
            dfs(nxt_r, nxt_c)

dfs(0, 0)
if cache[n-1][n-1] > 0:
    print('Yes')
else:
    print('No')

여기서는 도착여부만 판단하므로 cache(visited)의 초기값을 False로 두고 True/False로 표기해도 될 것으로 판단됨

[책 풀이]

#입력값
n = 7
board = [
    [2, 5, 1, 6, 1, 4, 1],
    [6, 1, 1, 2, 2, 9, 3],
    [7, 2, 3, 2, 1, 3, 1],
    [1, 1, 3, 1, 7, 1, 2],
    [4, 1, 2, 3, 4, 1, 2],
    [3, 3, 1, 2, 3, 4, 1],
    [1, 5, 2, 9, 4, 7, 0]]

# board = [
#     [2, 5, 1, 6, 1, 4, 1],
#     [6, 1, 1, 2, 2, 9, 3],
#     [7, 2, 3, 2, 1, 3, 1],
#     [1, 1, 3, 1, 7, 1, 2],
#     [4, 1, 2, 3, 4, 1, 3],
#     [3, 3, 1, 2, 3, 4, 1],
#     [1, 5, 2, 9, 4, 7, 0]]

cache = [[-1]*n for _ in range(n)]

def jump(r, c):
    global n, board, cache
    if r >= n or c >= n: return 0
    if r == n-1 and c == n-1: return 1
    if cache[r][c] != -1: return cache[r][c]
    steps = board[r][c]
    ret = jump(r+steps, c) or jump(r, c+steps)
    cache[r][c] = ret
    return ret

if jump(0, 0) == 1:
    print('Yes')
else:
    print('No')

책에서도 그래프로 모델링시, 아주 간단한 문제가 된다고 하니 이 문제의 경우에는 dfs 그래프 문제로 해결하면 될듯

문제

너비가 같은 N개의 나무 판자를 붙여 세운 울타리가 있습니다. 시간이 지남에 따라 판자들이 부러지거나 망가져 높이가 다 달라진 관계로 울타리를 통째로 교체하기로 했습니다. 이 때 버리는 울타리의 일부를 직사각형으로 잘라내 재활용하고 싶습니다. 그림 (b)는 (a)의 울타리에서 잘라낼 수 있는 많은 직사각형 중 가장 넓은 직사각형을 보여줍니다. 울타리를 구성하는 각 판자의 높이가 주어질 때, 잘라낼 수 있는 직사각형의 최대 크기를 계산하는 프로그램을 작성하세요. 단 (c)처럼 직사각형을 비스듬히 잘라낼 수는 없습니다.

판자의 너비는 모두 1이라고 가정합니다.

입력

첫 줄에 테스트 케이스의 개수 C (C≤50)가 주어집니다. 각 테스트 케이스의 첫 줄에는 판자의 수 N (1≤N≤20000)이 주어집니다. 그 다음 줄에는 N개의 정수로 왼쪽부터 각 판자의 높이가 순서대로 주어집니다. 높이는 모두 10,000 이하의 음이 아닌 정수입니다.

출력

각 테스트 케이스당 정수 하나를 한 줄에 출력합니다. 이 정수는 주어진 울타리에서 잘라낼 수 있는 최대 직사각형의 크기를 나타내야 합니다.

예제 입력

3 7 7 1 5 9 6 7 3 7 1 4 4 4 4 1 1 4 1 8 2 2

예제 출력

20 16 8

출처 : https://www.algospot.com/judge/problem/read/FENCE

분할정복 풀이

[접근방식]

  1. 가장 단순하게 푸는 방법? ⇒ 울타리 하나씩 돌아가며 기준으로 잡고 전체 울타리 탐색(완전탐색)
  2. 완전탐색 가능? ⇒ O(N^2)라 불가능
  3. 풀이기법 ⇒ 분할정복
  4. 분할정복에 쓰인 아이디어⇒ 가장 큰 사각형은 3가지 경우가 있음 1) 왼쪽에서 가장 큰 것, 2) 오른쪽에서 가장 큰 것, 3) 겹치는 것 중 가장 큰 것
  5. 3)번의 경우에서 중앙의 2개를 포함하게 되어있음
  6. 1)과 2)의 경우 재귀로 넘겨주고 3)의 기저조건은 울타리가 하나일 때로 설정

[어려웠던 점]

  1. 분할정복 자체를 생각해내기가 어려웠음
  2. 나는 1), 2), 3)을 각각의 함수로 생각했고 1)과 2)를 재귀로 넘겨준다는 것을 생각하지 못했음
  3. 3)의 경우에 중앙의 2개를 무조건 가지게 된다는 조건에 대해 잘 생각하지 못했던 것 같음

[분할정복 풀이코드(책 코드 참조)]

#입력값
n = 7
h = [1, 4, 4, 4, 4, 1, 1]
def dfs(left, right):
    global h
    if left == right: return h[left]
    mid = (left+right)//2
    lo = mid
    hi = mid+1
    ret = max(dfs(left, lo), dfs(hi, right))
    height = min(h[lo], h[hi])
    ret = max(ret, height*2)
    while left < lo or hi < right:
        if hi<right and (h[lo-1]<h[hi+1] or lo == left):
            hi += 1
            height = min(height, h[hi])
        else:
            lo -= 1
            height = min(height, h[lo])
        ret = max(ret, height*(hi-lo+1))
    return ret

print(dfs(0, n-1))

스위핑 풀이

[스위핑이란?]

스위핑 알고리즘이란, 이미 연산이 진행된 이전 공간에 대해서는 연산을 하지않는다는 개념

[접근방식]

  1. 사각형을 구하기 위해 필요한 정보는 해당 사각형의 왼쪽 끝과 오른쪽 끝 그리고 높이임
  2. 높이가 오른쪽 끝점의 높이라고 했을 때, 왼쪽으로 연속된 판자들 중 이 높이를 포함하는 가장 낮은 지점이 왼쪽 끝점이 될 것
  3. 위의 사항을 이용하기 위해 index(폭 구할 때 이용)를 stack에 저장하고 stack에서 빼내면서 폭을 구하고 해당 index로 높이 정보에 접근도 가능
  4. index를 꺼내는 시점의 경우, 현재 울타리 높이가 stack의 마지막 울타리 높이보다 낮을 때 현 울타리 보다 낮은 울타리 전까지 하나씩 빼면서 넓이를 업데이트 해주면 됨
  5. 단, 스텍에서 뺄 때, 스택이 비어있는지 확인이 필요함
  6. 추가적으로 마지막 울타리의 높이가 굉장히 높을 경우가 있으므로 울타리 높이 배열에 0을 추가해주면 이 경우 처리 가능
  7. 해당 문제를 푸는 알고리즘 요약 (O(N) 풀이 가능)

[(참고) 스텍 생성 울타리를 저장하는 배열에 0 추가 0부터 n까지(n+1 번) 다음을 순회]

  1. 스텍이 비지 않고 현재 스택에 가장 나중에 저장된 위치의 울타리 높이가 현 울타리 높이 이상일때까지 다음을 반복 a. 스텍에 가장 나중에 저장된 위치 가져옴 b. 너비는 스텍이 비었을 땐 현재위치 i를, 비지 않았다면 i-다음으로 나중에 저장된 위치-1 c. 넓이를 구하고 최대값 갱신
  2. 현재 idx를 스텍에 넣어줌

[어려웠던 점]

  1. 스텍까지는 떠올렸었는데, 울타리의 idx를 스텍에 넣는다는 것을 떠올리지 못해서 풀이로 연결시키기 어려웠던 것 같다.
  2. 스텍을 넣고 빼고하는 지점의 조건에 대해 정당성을 입증하기가 어려웠던 것 같다. 정작 그림을 그려가며 생각해보니, 모든 조건에서 정상작동하였다.
  3. 울타리의 idx가 0부터 시작한다는 점에서 너비의 조건이 i 혹은 i-다음으로 나중에 저장된 위치-1가 된다는 것을 잘 연결시키지 못했다.

[느낀 점]

  1. 분할정복 보다 스위핑이 시간복잡도도 낮은데다, 스위핑은 재귀를 부르지 않기 때문에 이해만 잘 한다면 스위핑이 훨씬 간단하고도 강력한 알고리즘이 될 것이다.
  2. 앞으로 스위핑과 스텍을 이용하는 방법에 대해 항상 유념하고 비슷한 문제를 만나면 잘 활용해봐야겠다.

[스위핑 풀이코드]

#입력값
n = 7
h = [1, 4, 4, 4, 4, 1, 1]

#마지막 울타리 처리를 위해서 0높이의 울타리 추가
h.append(0)
#울타리 idx 담을 stack
stack = []
#업데이트할 결과값
ret = 0

for i in range(n+1):
    #현재 울타리가 stack의 마지막 울타리보다 낮으면
    #현 울타리보다 작은 울타리 나오기 전까지 뺴주면서 넓이 업데이트
    while stack and h[stack[-1]] >= h[i]:
        #높이를 stack 마지막걸로 업데이트해가며 진행
        height = h[stack[-1]]
        stack.pop()
        #stack이 비었다면(idx가 0부터 시작)
        if not stack:
            width = i
        #stack이 남아있으면(idx가 0부터 시작)
        else:
            width = i-stack[-1]-1
        #넓이 업데이트
        ret = max(ret, height*width)
    #stack에 넣어주기
    stack.append(i)

print(ret)

참고출처 https://gurumee92.tistory.com/90 https://doctcoder.tistory.com/27

 

문제

그림과 같이 4 x 4 개의 격자 형태로 배치된 16개의 시계가 있다. 이 시계들은 모두 12시, 3시, 6시, 혹은 9시를 가리키고 있다. 이 시계들이 모두 12시를 가리키도록 바꾸고 싶다.

시계의 시간을 조작하는 유일한 방법은 모두 10개 있는 스위치들을 조작하는 것으로, 각 스위치들은 모두 적게는 3개에서 많게는 5개의 시계에 연결되어 있다. 한 스위치를 누를 때마다, 해당 스위치와 연결된 시계들의 시간은 3시간씩 앞으로 움직인다. 스위치들과 그들이 연결된 시계들의 목록은 다음과 같다.

제목 없음

시계들은 맨 윗줄부터, 왼쪽에서 오른쪽으로 순서대로 번호가 매겨졌다고 가정하자. 시계들이 현재 가리키는 시간들이 주어졌을 때, 모든 시계를 12시로 돌리기 위해 최소한 눌러야 할 스위치의 수를 계산하는 프로그램을 작성하시오.

입력

첫 줄에 테스트 케이스의 개수 C (<= 30) 가 주어진다.각 테스트 케이스는 한 줄에 6개의 정수로 주어지며, 각 정수는 0번부터 15번까지 각 시계가 가리키고 있는 시간을 12, 3, 6, 9 중 하나로 표현한다.

출력

각 테스트 케이스당 한 줄을 출력한다. 시계들을 모두 12시로 돌려놓기 위해 눌러야 할 스위치의 최소 수를 출력한다. 만약 이것이 불가능할 경우 -1 을 출력한다.

예제 입력

2 12 6 6 6 6 6 12 12 12 12 12 12 12 12 12 12 12 9 3 12 6 6 9 3 12 9 12 9 12 12 6 6

예제 출력

2 9

출처 : https://www.algospot.com/judge/problem/read/CLOCKSYNC

[접근]

  1. 완전탐색 가능? ⇒ 최대 4**10이므로 가능
  2. 문제의 특징? 스위치를 4번 누르면 시계가 원복됨 ⇒ 스위치는 0~3번까지 누를 수 있음
  3. 풀이기법 : dfs
  4. 각 스위치마다 돌아가면서 0~3번 누르고 dfs에 넘겨줌
  5. 이 때, 스위치 num += 1 을 해주는 식으로 넘길 것이므로 스위치 num이 최대 num+1이 되었을 때 clock이 모두 12인지 검사하고 answer 업데이트
  6. 스위치 누르고 시간을 더하거나 뺄 때, 시계는 12시가 기점인 것을 항상 유념!

[느낀 점]

이 문제는 각 스위치마다 0~3번까지 누를 수 있다는 특징을 잡아내야 풀 수 있는 문제였음

각 문제마다 짧게라도 시뮬레이션 해보면서 특징(제한할 수 있는 조건 등)을 잡아내는 것이 중요!

import sys
sys.setrecursionlimit(10**6)

def is_finished(clocks):
    for clock in clocks:
        if clock != 12:
            return False
    return True

def set_clock(switch, clocks, switch_clocks, time):
    for clock in switch_clocks[switch]:
        clocks[clock] += time
        if clocks[clock] < 0 : clocks[clock] += 12
        if clocks[clock] > 12: clocks[clock] -= 12
        if clocks[clock] == 0: clocks[clock] = 12

answer = int(1e9)

def dfs(switch, clocks, switch_clocks, cnt):
    global answer
    if switch == 10:
        if is_finished(clocks): answer = min(answer, cnt)
        return
    #안 누르고 그냥 넘기기
    dfs(switch+1, clocks, switch_clocks, cnt)
		#1~3번 누른 결과 dfs로 넘기기
    for i in range(1, 4):
        set_clock(switch, clocks, switch_clocks, 3*i)
        dfs(switch+1, clocks, switch_clocks, cnt+i)
        set_clock(switch, clocks, switch_clocks, (-3)*i)
    return

dfs(0, clocks, switch_clocks, 0)
print(answer)

[(참조) 문제 풀 때 입력값]

clocks = [12, 9, 3, 12, 6, 6, 9, 3, 12, 9, 12, 9, 12, 12, 6, 6]
# clocks = [12, 6, 6, 6, 6, 6, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12]
switch_clocks = [[0, 1, 2],
        [3, 7, 9, 11],
        [4, 10, 14, 15],
        [0, 4, 5, 6, 7],
        [6, 7, 8, 10, 12],
        [0, 2, 14, 15],
        [3, 14, 15],
        [4, 5, 7, 14, 15],
        [1, 2, 3, 4, 5],
        [3, 4, 5, 9, 13]]

문제

H*W 크기의 게임판이 있습니다. 게임판은 검은 칸과 흰 칸으로 구성된 격자 모양을 하고 있는데 이 중 모든 흰 칸을 3칸짜리 L자 모양의 블록으로 덮고 싶습니다. 이 때 블록들은 자유롭게 회전해서 놓을 수 있지만, 서로 겹치거나, 검은 칸을 덮거나, 게임판 밖으로 나가서는 안 됩니다. 위 그림은 한 게임판과 이를 덮는 방법을 보여줍니다.

게임판이 주어질 때 이를 덮는 방법의 수를 계산하는 프로그램을 작성하세요.

입력

력의 첫 줄에는 테스트 케이스의 수 C (C <= 30) 가 주어집니다. 각 테스트 케이스의 첫 줄에는 2개의 정수 H, W (1 <= H,W <= 20) 가 주어집니다. 다음 H 줄에 각 W 글자로 게임판의 모양이 주어집니다. # 은 검은 칸, . 는 흰 칸을 나타냅니다. 입력에 주어지는 게임판에 있는 흰 칸의 수는 50 을 넘지 않습니다.

출력

한 줄에 하나씩 흰 칸을 모두 덮는 방법의 수를 출력합니다.

예제 입력

3 3 7 #.....# #.....# ##...## 3 7 #.....# #.....# ##..### 8 10 ########## #........# #........# #........# #........# #........# #........# ##########

예제 출력

0 2 1514

출처 : https://www.algospot.com/judge/problem/read/BOARDCOVER

[접근]

  1. 문제 단순화/순서 강제 ⇒ 왼쪽 상단부터 검사한다는 순서강제
  2. 왼쪽상단부터 비어있는 칸을 찾아내서 검사
  3. 편의를 위해 데이터 전처리(리스트화, 숫자화)
  4. 블록방향의 경우, 상대적 위치를 정해두고 타겟 위치에 따라 검사진행
  5. dfs 사용

[주의사항]

  1. 파이썬의 경우, 다중 for문에서 한꺼번에 break하는 기능이 없음⇒ flag사용해서 상위 for문 break해주는 기법 사용
  2. 파이썬은 기본 recursion limit이 10**3이므로, 재귀 사용 시에 limit 풀어주기

[기타 참고사항]

  1. 처음에 is_fulfilled함수를 따로 빼서 검사를 해주었는데, 책처럼 빈칸을 찾는 완전탐색에서 찾지 못하면 fulfilled로 간주하는 것이 더 효율적
  2. 파이썬 다중for문에서 break를 걸 때는 항상 주의하자!
import sys
sys.setrecursionlimit(10**6)

L_type = [[(1, 0), (1, 1)], [(0, 1), (1, 1)], [(1, 0), (1, -1)], [(1, 0), (0, 1)]]

new_board = []
for i in range(H):
    temp = board[i].replace('#', '1')
    temp = temp.replace('.', '0')
    new_board.append(list(map(int, temp)))

def dfs(new_board, L_type, H, W):
    ret = 0
    startH = -1
    startW = -1
    #다중 for문 break를 위한 flag처리
    flag = False
    for h in range(H):
        for w in range(W):
            if new_board[h][w] == 0:
                #빈칸찾으면 변수 지정해주고 멈추기
                startH = h
                startW = w
                flag = True
                break
        if flag: break
    #변수가 지정되지 않았다는 것은 모두 채워졌다는 의미이므로 return
    if startH == -1 and startW == -1: return 1
    for i in range(4):
        h1 = startH + L_type[i][0][0]
        w1 = startW + L_type[i][0][1]
        h2 = startH + L_type[i][1][0]
        w2 = startW + L_type[i][1][1]
        if 0<= h1 < H and 0<= h2 < H and 0<= w1 < W and 0<= w2 < W and new_board[h1][w1] == 0 and new_board[h2][w2] == 0:
            new_board[h1][w1] = 1
            new_board[h2][w2] = 1
            new_board[h][w] = 1
            ret += dfs(new_board, L_type, H, W)
            new_board[h1][w1] = 0
            new_board[h2][w2] = 0
            new_board[h][w] = 0
    return ret

print(dfs(new_board, L_type, H, W))

[재귀 관련 참고사항]

모든 재귀함수는 ‘더 이상 쪼개지지 않는’ 최소한의 작업에 도달했을 때 답을 곧장 반환하는 조건문 포함필요(기저 사례 base case)

기저사례를 선택할 때는 존재하는 모든 입력이 항상 기저 사례의 답을 이용해서 계산될 수 있도록 신경써야 함

또한, 입력이 잘못되거나 범위에서 벗어난 경우도 기저사례로 택해서 맨 처음 처리하면 함수 호출 시점에 이런 오류를 검사할 필요가 없음

⇒ 재귀함수는 보통 한군데 이상에서 호출되기 때문에 이 습관은 반복적 코드 제거에 큰 도움이 됨

 

문제

안드로메다 유치원 익스프레스반에서는 다음 주에 율동공원으로 소풍을 갑니다. 원석 선생님은 소풍 때 학생들을 두 명씩 짝을 지어 행동하게 하려고 합니다. 그런데 서로 친구가 아닌 학생들끼리 짝을 지어 주면 서로 싸우거나 같이 돌아다니지 않기 때문에, 항상 서로 친구인 학생들끼리만 짝을 지어 줘야 합니다.

각 학생들의 쌍에 대해 이들이 서로 친구인지 여부가 주어질 때, 학생들을 짝지어줄 수 있는 방법의 수를 계산하는 프로그램을 작성하세요. 짝이 되는 학생들이 일부만 다르더라도 다른 방법이라고 봅니다. 예를 들어 다음 두 가지 방법은 서로 다른 방법입니다.

  • (태연,제시카) (써니,티파니) (효연,유리)
  • (태연,제시카) (써니,유리) (효연,티파니)

입력

입력의 첫 줄에는 테스트 케이스의 수 C (C <= 50) 가 주어집니다. 각 테스트 케이스의 첫 줄에는 학생의 수 n (2 <= n <= 10) 과 친구 쌍의 수 m (0 <= m <= n*(n-1)/2) 이 주어집니다. 그 다음 줄에 m 개의 정수 쌍으로 서로 친구인 두 학생의 번호가 주어집니다. 번호는 모두 0 부터 n-1 사이의 정수이고, 같은 쌍은 입력에 두 번 주어지지 않습니다. 학생들의 수는 짝수입니다.

출력

각 테스트 케이스마다 한 줄에 모든 학생을 친구끼리만 짝지어줄 수 있는 방법의 수를 출력합니다.

예제 입력

3 2 1 0 1 4 6 0 1 1 2 2 3 3 0 0 2 1 3 6 10 0 1 0 2 1 2 1 3 1 4 2 3 2 4 3 4 3 5 4 5

예제 출력

1 3 4

출처 : https://www.algospot.com/judge/problem/read/PICNIC

[접근법]

  1. 완전탐색으로 풀 수 있을까? ⇒ 현 문제 상의 데이터크기라면 가능
  2. 조합으로 다 뽑아서 검사할까? ⇒ 너무 무식해서 하고싶지 않음
  3. 문제의 단순화/순서강제 ⇒ 순서대로 뽑는 걸로 강제하면 선택지가 줄어듬
  4. 짝들은 그래프로 데이터 전처리하고 dfs로 접근
  5. 모두 짝을 이뤘는지는 visited(set)의 length로 체크

[주의사항]

visited를 넘겨줄 때, deepcopy로 새 visited만들어서(new_visited) 넘겨줘야함

그렇지 않으면 그 전 방문기록이 쌓여서 결과값에 오류 발생

[기타 refactoring]

deepcopy로 visited를 다 복사하지 말고 책의 풀이처럼, visited처리 후 dfs 넘긴다음에 unvisited하는 것이 더 효율적일 수도 있을듯

[내가 푼 코드]

from collections import defaultdict
from copy import deepcopy

#데이터 전처리
graph = defaultdict(list)
for pair in friends:
    student1, student2 = pair
    graph[student1].append(student2)
    graph[student2].append(student1)

answer = 0
visited = set()

#dfs
def dfs(visited, graph, n):
    global answer
    if len(visited) == n:
        answer += 1
        return
    for i in range(n):
        if i not in visited:
            visited.add(i)
            for friend in graph[i]:
                if friend not in visited:
                    new_visited = deepcopy(visited)
                    new_visited.add(friend)
                    dfs(new_visited, graph, n)
            break
    return

dfs(visited, graph, n)
print(answer)

[책에 나온 스타일대로 풀어본 코드]

visited를 True/False를 가진 배열(taken)로 처리함

해당 값들을 넘겨주고 visited 정보를 다시 초기화하기 위한 작업들을 눈여겨 볼 것(다시 False 처리해줌)

def are_friends(i, j, graph):
    if j in graph[i]: return True
    return False

def is_finished(taken):
    for i in range(n):
        if taken[i] != True:
            return False
    return True

taken = [False]*n

def count_pairs(graph, taken, n):
    if is_finished(taken): return 1
    ret = 0
    target = -1
    for i in range(n):
        if taken[i] == False:
            target = i
            break
    for j in range(n):
        if taken[j] == False and are_friends(target, j, graph):
            taken[target] = True
            taken[j] = True
            ret += count_pairs(graph, taken, n)
            taken[target] = False
            taken[j] = False
    return ret

print(count_pairs(graph, taken, n))

 

+ Recent posts