문제
14888번: 연산자 끼워넣기
첫째 줄에 수의 개수 N(2 ≤ N ≤ 11)가 주어진다. 둘째 줄에는 A1, A2, ..., AN이 주어진다. (1 ≤ Ai ≤ 100) 셋째 줄에는 합이 N-1인 4개의 정수가 주어지는데, 차례대로 덧셈(+)의 개수, 뺄셈(-)의 개수, 곱
www.acmicpc.net
정답(순열)
from itertools import permutations
n = int(input())
nums = list(map(int,input().split()))
data = list(map(int,input().split()))
exam = ["p","m","c","d"]
opers = []
for d in range(len(data)):
for _ in range(data[d]):
opers.append(exam[d])
# 어차피 연산자는 4개이므로 다음과 같이 더 간단하게 작성할 수 있다
# opers = 'p' * data[0] + 'm' * data[1] + 'c' * data[2] + 'd' * data[3]
cal_min = 1e9
cal_max = -1e9
def calculator(operlist):
res = nums[0]
for i in range(1,len(nums)):
if operlist[i-1] == "p":
res += nums[i]
elif operlist[i-1] == "m":
res -= nums[i]
elif operlist[i-1] == "c":
res *= nums[i]
elif operlist[i-1] == "d" :
res = int(res/nums[i])
# res //= nums[i]
return res
for per in list(permutations(opers,len(opers))):
res = calculator(per)
cal_min = min(cal_min,res)
cal_max = max(cal_max,res)
print(cal_max)
print(cal_min)
정답(dfs)
n = int(input())
data = list(map(int,input().split()))
add,sub,mul,div = map(int,input().split())
min_v = 1e9
max_v = -1e9
def dfs(i,now):
global min_v, max_v,add,sub,mul,div
if i == n:
min_v = min(min_v,now)
max_v = max(max_v,now)
else:
if add>0:
add-=1
dfs(i+1,now+data[i])
add+=1
if sub>0:
sub-=1
dfs(i+1,now-data[i])
sub+=1
if mul>0:
mul-=1
dfs(i+1,now*data[i])
mul+=1
if div>0:
div-=1
dfs(i+1,int(now/data[i]))
div+=1
dfs(1,data[0])
print(min_v)
print(max_v)
해설
처음 풀 때는 당연히 순열을 떠올려 풀었다.
주어진 연산자들로 만들 수 있는 순열 경우의 수 마다 연산을 수행한 후 최소/최대값을 갱신하는 방식으로 풀리긴 했다.
하지만 주어진 연산자의 개수에 따라 경우의 수가 급격히 늘어날 수 있고, 따라서 시간,공간 복잡도가 늘어나게 되므로 바람직한 풀이방식은 아니었다.
dfs 풀이방식은 재귀호출을 이용해 주어진 연산자를 모두 사용했을 때 연산의 결과값을 갱신하는 방식으로 구현한다.
정답 코드를 보고 내가 궁금했던 점은 두 가지였다.
1. 저 코드가 어떻게 모든 경우의 수를 한번씩 다 해 볼 수 있다는 건지 로직에 대한 의문
2. 순열과 dfs방식 둘 다 결국은 전체 경우의 수를 한번씩 모두 계산해보는건데 어떻게 dfs가 더 효율적인지에 대한 의문
공부를 하면서 내 나름대로 의문에 대한 해답을 찾을 수 있었다.
예를 들어, 수열 [1,2,3,4] 와 연산자 +,-,x 가 주어지고 연산자 끼워넣기를 통해 최소/최대값을 구하는 문제가 있을 때
순열을 사용한 풀이
- 주어진 연산자를 배치하는 모든 경우의 수를 구해 각각의 경우의 수 마다 연산하여 결과값을 갱신한다.
다시말해, 1 + 2 - 3 x 4 를 계산해 나온 결과값인 0을 처음 저장하고, 그 다음 1 + 2 x 3 - 4 를 계산해 그 결과값인 5를 이전 결과값과 비교하여 저장하는 과정을 총 경우의 수 6번만큼 반복하게 된다.
문제풀이 코드에서는 이 경우의 수를 구하는 과정을 permutations를 사용해 구현했다.
dfs를 사용한 풀이
- 각 연산자의 사용횟수를 체크하고, 재귀호출을 통해서 다음 연산자와의 계산을 하여 모든 연산자를 사용했을 때 결과값을 갱신한다.
처음 함수를 호출할 때 1과 숫자배열의 첫 번째 값을 넘겨준다.
그리고 각 연산자의 남은 개수가 0보다 많으면 해당 연산자의 개수를 -1하고, 재귀호출을 하면서 연산을 수행한 값을 넘김
이 때 i의 역할은 재귀호출의 단계를 표시하는 용도 = 연산을 수행한 횟수를 체크하는 용도 라고 생각하니까 이해하기 쉬웠다.
예를 들어서 1 + 2 - 3 x 4 연산을 처음에 수행했다면 재귀호출은
1. i = 1인 단계에서 호출된 dfs(2, 3) : +연산자 사용
2. i = 2인 단계에서 호출된 dfs(3, 0) : -연산자 사용
3. i = 3인 단계에서 호출된 dfs(4, 0) : x 연산자 사용
4. i = 4인 단계에서는 i == n 이 성립하기 때문에 최종 결과값 0을 min_v로 갱신하고 함수를 빠져나간다.
5. 그러면 i=3이었던 단계의 함수호출이 끝난 후 다시 해당 연산자 x 의 개수를 증가시키고, 다음 div연산자(/)가 있는지 확인하지만 존재하지 않기 때문에 이 단계의 함수도 종료된다.
6. 그 다음 i=2 인 단계로 돌아와서 함수호출이 끝난 후 연산자 - 의 개수를 증가시키고, 다음 mul 연산자(x)가 있는지 확인하고, dfs(3,9)를 재귀호출한다. : x연산자 사용
7. 다시 i = 3인 단계가 되고, sub연산자(-)가 있으므로 dfs(4,5)를 재귀호출 : - 연산자 사용
8. i==4가 되었기 때문에 결과값 5를 갱신하고 함수를 빠져나옴
9. 위 과정을 반복하면 결국 모든 연산자를 조합한 경우의 수를 한번씩 계산할 수 있다.
전체 과정에서 i는 연산자가 몇 번 사용되었는지 알 수 있기 때문에 연산의 종료시점을 정할 수 있다.
( n은 숫자의 개수이고, 연산자의 개수는 항상 (n-1)이기 때문에 처음 1을 가지고 들어간 dfs함수에서 모든 연산자를 다 사용했을 때 i의 값은 n과 일치하게 될 것이므로)
결과적으로, 이러한 로직을 통해서 dfs로도 모든 경우의 수를 계산할 수 있음을 알 수 있었고,
두 번째 의문이었던 왜 dfs가 더 효율적인지도 나름 그럴듯한 답을 생각해봤다.
저장된 이전의 값을 다시 활용하는 점에서 다이나믹 프로그래밍이 떠오르기도 했는데
재귀호출을 하고, 다시 돌아와서 다른 연산을 하는 과정을 통해 연산의 총 횟수자체를 줄이는 효과가 나기 때문에 더 효율적이지 않을까 하는 나만의 답을 얻게 되었다..
전체적으로 어렵게 느껴왔던 dfs와 재귀호출에 대해 조금 더 이해하게 되었고
백트래킹의 개념도 처음 알게되어서 아주 의미있는 문제였다!
참조
이것이 취업을 위한 코딩테스트다(with. python)
'코딩테스트 > 👩💻 이코테' 카테고리의 다른 글
[이코테/bfs/삼성 기출] 인구 이동(정답, 풀이, 풀리지 않는 의문..) (0) | 2023.10.17 |
---|---|
[이코테/dfs/백준] 감시 피하기(정답,풀이) (0) | 2023.10.15 |
[이코테/구현/카카오 기출] 외벽 점검 (정답, 풀이) (1) | 2023.10.13 |
[이코테/구현] 치킨배달 (1) | 2023.10.13 |
[이코테/그리디] 볼링공 고르기 (정답, 풀이) (1) | 2023.09.24 |