3X3 크기의 한 패턴이 정해져 있고 크기가 증가할수록 같은 패턴이 반복되는 형태인데 처음엔 직관적으로 간단하게 생각해서 9 부분으로 나눠 각 Position에 대해 번호를 파라미터로 넘기고 해당 Position에 따라 별, 공백, 별 줄 바꿈 출력을 생각했으나 뜻대로 되지 않았다.
3X3 패턴이 출력되는 만큼 줄 바꿈도 같이 출력이 되었고 일정한 패턴의 확장이 아니라 그냥 패턴의 연속된 출력이었기 때문이다.
그래서 접근 방식을 다시 생각해봤다.
일단 3X3 패턴을 하나의 원소로 가정하고 3배씩 증가할 때마다 행과 열을 복사하는 방식이다.
패턴 복사 방법
위 사진처럼 직전의 패턴에 대해 증가할때마다 행을 3배로 복사하고 1,3번째 행은 열을 3배로, 2번째 행은 패턴의 길이만큼 공백을 넣어주면 된다.
파이썬은 리스트나 문자열의 복사가 상당히 쉬운 편이기 때문에 다행인 것 같다.
직전의 패턴에서 한 줄씩 문자열로 치환을 하면 나머진 구현에 어려움이 있을 것 같진 않다.
N=int(input())
# 하나의 별표에서 시작한다 가정
res=["*"]
deffunc(n):global res
if n>3:
func(n//3)
# 행 X3
res=res*3# 열 X3for i inrange(len(res)//3):
res[i]=res[i]*3# 열 + 공백 + 열for i inrange(len(res)//3,len(res)*2//3):
res[i]=res[i]+" "*len(res[i])+res[i]
# 열 X3for i inrange(len(res)*2//3,len(res)):
res[i]=res[i]*3
func(N)
for i inrange(len(res)):
print(res[i])
재귀를 타면서 문자열 리스트를 공유해야 하기 때문에 결과 리스트를 전역변수로 공유하는 global을 사용했다.
필자는 이게 직관적이기에 사용했는데 각자 파라미터로 직접 데이터를 보내는 방식과 비교하여 본인이 편한 방식대로 구현하면 된다.
문제에서 입력 조건이 3 이상이기 때문에 조건문에 3 초과인 경우 재귀를 타도록 구현하였는데 제출하고 생각해 보니 애초에 res 리스트는 빈 리스트로 초기화하고 n == 1인 경우에 res 리스트를 별표 문자열 하나로 초기화 하는 방식이 다소 깔끔하지 않을까 생각이 든다.
결과 화면
문제를 맞긴 했지만 처음에 다소 접근 방식에서 헤맸던 문제이다.
함수를 실행시켰을 때와 재귀를 타고 다시 실행했을 때의 실행결과를 고려하다 보니 스스로 발목을 잡는 것처럼 복잡하고 헷갈려서 재귀 문제는 최대한 같은 부분을 단순화시켜서함수 그 자체가 실행하는 과정에 집중하는 연습과 경험이 필요한 것 같다.
DP 문제는 구현 자체가 어렵진 않지만 문제 해결법에 접근하는 과정이 다소 어려운 것 같다.
다른 알고리즘 같은 경우는 연습을 통해 익숙해진다면 DP는 아무리 연습해도 문제에 따라 쉽게 떠오르지 않는 알고리즘 같다.
문제 풀이
해당 문제의 소재는 다소 익숙한 원하는 금액을 지불하는 동전의 최소 개수다.
동전의 개수는 n개이고 가치의 합은 k이며 각각 1 <=n <=100, 1 <=k <=10000의 제한 조건을 갖는다.
같은 동전이 여러 번 주어질 수 있지만 가치의 합인 k를 dp 리스트로 잡고 각 인덱스마다 n번 반복하면 되므로 시간 복잡도는 O(n*k), 즉 최대 100만 번이라 딱히 고려하진 않아도 되는 조건이었다.
물론 필자는 효율성을 높이기 위해 Set로 형 변환했다가 다시 List로 형 변환했었다.
이렇게 Set로 형 변환하면 중복된 원소를 제거할 수 있다.
다만 이 문제에서 중요하게 고려해야 할 점은 가치의 합인 k로 만드는 것이 불가능할 수도 있단 점이다.
여태 경험했었던 동전 DP 문제는 애초에 불가능에 대한 조건은 없었기에 최소 개수만 구하는데 집중했었고 이번 문제도 처음에 똑같이 시도했다가 틀렸단 메시지를 받았다.
그 과정에서 문제를 다시 정확히 읽고 스스로 테스트 케이스를 만들어 오류를 잡아내는 연습도 필요한 것 같다.
각설하고 전체 코드를 통해 설명하도록 하겠다.
n,k=map(int,input().split())
coin=[]
for i inrange(n):
coin.append(int(input()))
coin=list(set(coin))
dp=[0for i inrange(k+1)]
for i inrange(1,len(dp)):
for j inrange(len(coin)):
if i-coin[j]==0:
dp[i]=dp[i-coin[j]]+1elif i-coin[j]>0and dp[i-coin[j]]!=0:
if dp[i]==0:
dp[i]=dp[i-coin[j]]+1elif dp[i-coin[j]]+1<dp[i]:
dp[i]=dp[i-coin[j]]+1if dp[k]==0:
print(-1)
else:
print(dp[k])
1번째 줄은 n과 k를 입력받고 2번째 줄의 coin은 동전의 가치를 입력받을 리스트다.
6번째 줄이 중복을 제거하는 코드이며 7번째 줄의 dp는 k인 가치의 합을 인덱스로 갖는 dp 리스트다.
어떤 것을 dp 리스트로 잡을지는 문제를 많이 풀다 보면 감 잡게 될 것이다.
이제 8번째 줄부터 이중 for문을 통해 전체 dp 리스트를 돌며 각 인덱스마다 coin 리스트를 반복해 해당 인덱스가 해당하는 값에서 동전을 쓸 수 있는지 확인하고 쓸 수 있다면 최소 개수인지 확인한다.
10번째 줄의 if 문은 동전의 가치를 현재의 가치에서 뺏을 때 0원이라면 동전을 한 개 사용할 수 있단 뜻이므로 현재 가치의 동전 개수를 업데이트했다.
12번째 줄의 if 문에 주목해야 하는데 현재 가치에서 동전의 가치를 뺀 값이 0보다 크고 그 뺀 값의 동전 개수가 0개보다 많을 때만 개수를 업데이트해줬다.
왜냐하면 만약 현재의 가치에서 동전의 가치를 뺀 가치에 대한 동전의 개수가 0개 라면 해당 가치로 동전을 조합할 수 없기 때문이다.
예를 들어 k가 10이고 n이 1이며 동전의 가치가 3일 때를 고려해 보자.
dp[10]인 상황에서 동전의 가치를 뺀 dp[7]을 생각해보면 dp[7]은 0이다.
3으로는 절대로 7을 만들 수 없기 때문이다.
따라서 7원으론 동전을 조합할 수 없기 때문에 10도 불가능하다는 얘기가 된다.
이 부분만 고려한다면 골드 문제를 도전하는 사람들에게 나머진 딱히 어려운 부분이 없을 것이라 생각한다.
결과 화면
이 문제를 통해 지문을 잘 읽고 테스트 케이스를 만들어보는 과정을 직접 해보는 것이 중요하다는 것을 체감했다.
다만 보편적인 그래프 탐색은 2차원 배열에서 인덱스 접근을 통해 4방 혹은 8방 탐색을 하며 가장 가까운 Target Node를 찾지만 이번 문제는 1번부터 100번까지 각 노드를 번호로 구분할 수 있는 점, 1부터 6까지 적혀있는 주사위를 던져서 옮긴다는 점에 착안하여 1차원 배열에서 6가지 방법을 고려하면 된다는 것을 깨달아야 한다.
이점 외에는 딱히 조심해야 할 부분이 없으므로 바로 코드를 통해 설명하도록 하겠다.
from collections import deque
edge_info=[0for i inrange(101)]
n,m=map(int,input().split())
visited=[0for i inrange(100)]
queue=deque()
for i inrange(n+m):
temp=list(map(int,input().split()))
edge_info[temp[0]]=temp[1]
#curr,cnt
queue.append([1,0])
whilelen(queue)>0:
curr,cnt=queue.popleft()
if curr==100:
print(cnt)
breakelif curr>=94:
print(cnt+1)
breakelse:
for i inrange(1,7):
if edge_info[curr+i]==0and visited[curr+i]==0:
visited[curr+i]=1
queue.append([curr+i,cnt+1])
else:
if visited[curr+i]==0:
visited[curr+i]=1
visited[edge_info[curr+i]]=1
queue.append([edge_info[curr+i],cnt+1])
1번째 줄은 deque를 사용하기 위해 모듈을 Import 하였다.
물론 이번 문제는 최대 칸이 100개 밖에 없기 때문에 애초에 사이즈가 작아서 시간 복잡도를 줄이기 위해 굳이 deque를 사용할 필요는 없었지만 막상 필요한 상황에서 참고 문서 없이 Import 하려면 까먹기 때문에 잊지 않기 위해서라도 deque를 사용하였다.
2번째 줄부터 5번째 줄까지는 간선 정보와 방문 체크, 큐 등을 사용하기 위한 초기화 작업이다.
6번째 줄은 반복문을 통해 간선 정보를 저장하였고 11번째 줄은 1번 칸부터 시작해서 BFS를 진행하기 위해 deque에 초기화했다.
12번째 줄부터는 BFS를 구현한 코드이다.
문제에 주어진대로만 구현하면 되기 때문에 딱히 설명할 부분은 없지만 필자는 조금이라도 시간 복잡도 상으로 효율적인 코드를 구현하기 위해 100일 때는 현재 카운트 출력, 94 이상일 때는 주사위를 한 번만 더 돌리면 100이 나오므로 카운트+1 출력, 그 외의 경우에만 다시 deque에 넣도록 구현하였다.
이렇게 구현하면 매번 deque에 넣을 때도 100을 초과하는지 체크하지 않아도 되기 때문이다.
결과 화면
문제는 10X10의 2차원 그래프인 것처럼 나왔지만 1차원 배열로 변환하여 생각하는 게 필요한 문제였다.
먼저 전체적인 구성은 각 기능에 해당하는 10개의 함수를 선언하고 메인 함수에서는 입력방식대로 입력을 받은 후 필요한 경우 해당하는 함수를 호출해서 연산 결과를 사용하는 방식으로 구현하였다.
1번째 줄의 NUM_X는 인자로 받은 숫자를 스택의 가장 위에 저장(Push)하는 함수다.
3번째 줄의 POP은 스택에 아무것도 없다면 ERROR를 반환하고, 있다면 가장 위에 있는 숫자를 제거하는 함수다.
11번째 줄의 INV는 스택에 아무것도 없다면 ERROR를 반환하고, 있다면 첫번째 수(가장 위에 있는 수)의 부호를 바꿔서 저장하는 함수다. (이제 보니 굳이 pop 하지 않고 array [len(array)-1]=-1*array [len(array)-1] 이렇게 직접 접근해서 한 줄로 처리 가능할 것 같다.)
19번째 줄의 DUP는 스택에 아무것도 없다면 ERROR를 반환하고, 있다면 가장 위에 있는 숫자를 한번 더 추가하는 함수다.
28번째 줄의 SWP는 스택에 2개 이상의 데이터가 없다면 ERROR를 반환하고, 있다면 첫 번째 숫자와 두 번째 숫자의 위치를 바꾸는 함수다.
38번째 줄의 ADD는 스택에 2개 이상의 데이터가 없다면 ERROR를 반환하고, 있다면 첫 번째 숫자와 두 번째 숫자를 더한다.
이때 문제 조건에서 연산 결과의 절댓값이 10^9를 넘어간다면 프로그램 에러이므로 꼭 이 부분을 처리해줘야 한다.
49번째 줄의 SUB는 스택에 2개 이상의 데이터가 없다면 ERROR를 반환하고, 있다면 두 번째 숫자에서 첫 번째 숫자를 빼면서 이때도 마찬가지로 절댓값이 10^9를 넘어가는지 체크해야한다.
60번째 줄의 MUL은 스택에 2개 이상의 데이터가 없다면 ERROR를 반환하고, 있다면 첫 번째 숫자와 두 번째 숫자를 곱하고 값을 체크한다.
71번째 줄의 DIV는 스택에 2개 이상의 데이터가 없다면 ERROR를 반환하고, 있다면 두 번째 숫자를 첫 번째 숫자로 나눈 몫을 저장하는데 이때 0으로 나눠지는지, 부호는 어떻게 되는지 정확히 판단해서 구현해야 한다.
89번째 줄의 MOD는 스택에 2개 이상의 데이터가 없다면 ERROR를 반환하고, 있다면 두 번째 숫자를 첫 번째 숫자로 나눈 나머지를 반환하는데 이때도 마찬가지로 부호와 0으로 나눠질 경우를 예외 처리해야 한다.
106번째 줄부터는 QUIT를 입력받기 전까지 반복하면서 명령어에 해당하는 함수를 실행시키면 된다.
이때 END를 입력받기 전까지 일련의 명령어를 입력받고 수행해야 하므로 명령어를 리스트에 저장해서 수행하는 것을 잊으면 안 된다.
결과 화면
다소 구현해야 하는 기능이 많고 예외처리를 해야 되는 부분도 많아서 각각의 구현 난이도는 쉽지만 어렵게 느껴질 수 있는 문제였다.
테트리스와 비슷하지만 뿌요뿌요는 같은 색의 슬라임들이 4개 이상 연결되어있으면 터지고 빈 공간을 채운다는 점이 조금 다르다.
따라서 만약 테트리스였다면 행을 기준으로 탐색을 했겠지만 뿌요뿌요는 전체 탐색을 하면서 빈 공간이 아닐 때는 4개 이상 연결되었는지 BFS를 통해 체크하며 4개 이상인 모든 부분들을 빈 공간으로 치환하고 다시 바닥으로 정렬하는 과정을 반복해야 한다.
이때 터질 수 있는 뿌요가 여러 그룹이 있다면 동시에 터지고 한 번의 연쇄가 추가된다고 하니 연쇄를 추가하는 부분을 정렬하는 과정에 위치시키면 적절할 것이다.
사실 12X6 크기의 행렬이라 상당히 크기가 작아 시간이나 공간에 대한 효율성을 따질 필요성까진 없지만 구현하는 과정에서 다소 헷갈리거나 복잡한 부분이 있었기에 문제를 세분화하여 주석을 통해 구현해야 할 기능들을 구분해놓고 구현하는 것이 도움 될 것 같다.
코드가 짧은 편은 아니라서 한눈에 들어오긴 힘들겠지만 한 줄씩 따라가다 보면 이해할 수 있을 것이다.
graph=[]
dir=[[-1,0],[1,0],[0,-1],[0,1]]
queue=[]
for i inrange(12):
graph.append(list(input()))
res=0whileTrue:
check=True#터졌는지 체크하는 변수
visited=[[0for i inrange(6)]for j inrange(12)]
changed_col=[]
del_queue=[]
for i inrange(12):
for j inrange(6):
#뿌요가 존재하는 칸인 경우if graph[i][j]!="."and visited[i][j]==0:
curr_char=graph[i][j] #현재 뿌요 저장
temp_cnt=0
queue.append([i,j])
visited[i][j]=1#현재뿌요와 연결되어있는 뿌요 탐색whilelen(queue)>0:
curr=queue.pop(0)
temp_cnt+=1for k inrange(len(dir)):
dy=dir[k][0]+curr[0]
dx=dir[k][1]+curr[1]
if0<=dy<12and0<=dx<6and graph[dy][dx]==curr_char and visited[dy][dx]==0:
queue.append([dy,dx])
visited[dy][dx]=1#연결된 뿌요가 4개 이상일 경우if temp_cnt>=4:
check=False
changed_col.append(j)
del_queue.append([i,j])
whilelen(del_queue)>0:
temp=del_queue.pop(0)
graph[i][j]="."for k inrange(len(dir)):
dy=dir[k][0]+temp[0]
dx=dir[k][1]+temp[1]
if0<=dy<12and0<=dx<6and graph[dy][dx]==curr_char:
del_queue.append([dy,dx])
graph[dy][dx]="."if dx notin changed_col:
changed_col.append(dx)
if check:
breakelse:
res+=1#빈칸없게 위에 있는 뿌요들을 정렬for i inrange(len(changed_col)):
for j inrange(10,-1,-1):
if graph[j][changed_col[i]]!=".":
curr=j
while curr<=10and graph[curr+1][changed_col[i]]==".":
graph[curr+1][changed_col[i]],graph[curr][changed_col[i]]=graph[curr][changed_col[i]],graph[curr+1][changed_col[i]]
curr+=1print(res)
6번째 줄까지는 그래프를 입력받고 방향 리스트, 사용할 큐 초기화, 결과 변수를 선언했다.
8번째 줄의 while문은 더 이상 터진 뿌요들이 없을 때까지 터뜨리고 빈칸을 정렬하는 과정을 반복하는 부분이다.
check변수를 통해 터진 뿌요가 없을 경우 반복문을 탈출하고 매번 방문했는지 체크하는 visited 리스트를 초기화하도록 구현하였다.
13번째 줄부터는 전체 리스트를 반복하면서 뿌요가 존재할 경우 현재 뿌요를 저장하고 근접한 동일 뿌요들의 개수를 세서 4개 이상이라면 해당하는 뿌요들을 빈칸으로 초기화하였다.
이때 뿌요는 5종류가 있지만 변수에 현재 위치한 뿌요를 저장함으로써 종류에 대한 고려를 하지 않도록 했고 47번째 줄의 changed_col은 삭제된 뿌요가 존재하는 열을 추가하여 이후에 뿌요들을 정렬할 때 불필요한 탐색을 줄이는 방식으로 구현하였다.
53번째 줄에서 res+=1을 통해 카운트를 증가시키는데 문제에서 터질 수 있는 뿌요가 여러 그룹이라면 동시에 터지며 한 번의 연쇄가 추가되므로 다 터지고 난 뒤에 추가하도록 증가시키는 위치를 주의해야 한다.
터진 뿌요가 없다면 50번째 줄에서 반복문을 탈출하겠지만 터진 뿌요가 존재한다면 빈칸보다 위에 뿌요가 존재하지 않도록 정렬시킨 후 다시 반복문으로 돌아가면 된다.
그래프의 행을 기준으로 리스트 시작은 위, 끝은 바닥을 뜻하므로 끝부터 시작까지 역으로 오면서 뿌요를 최대한 리스트의 끝까지 정렬하는 것에 주의해야 한다.
해당 반복문이 끝나면 결과 변수인 res를 출력하면 끝이다.
결과 화면
그래프의 크기도 작고 특별한 알고리즘도 없이 흔한 그래프 이론으로 푸는 구현 문제였지만 구현 과정이 다소 길어지다 보니 충분히 헷갈릴 수도 있는 문제였다.
따라서 구현해야 하는 부분과 과정을 간략하게 주석으로 써놓고 차근차근 작성해 나가는 것이 실수를 방지하기 위해 중요한 것 같다.