당신은 전문털이범이다. 어느집에서든 돈을 훔쳐올 수 있지만 경보 시스템 때문에 바로 옆집은 훔칠 수 없고, 한 칸 이상 떨어진 집만 가능하다.
각 집에는 훔칠 수 있는 돈의 액수가 입력값으로 표기되어 있다. 훔칠 수 있는 가장 큰 금액을 출력하라.
class Solution:
def rob(self, nums: List[int]) -> int:
def _rob(i: int) -> int:
if i < 0:
return 0
return max(_rob(i - 1), _rob(i - 2) + nums[i])
return _rob(len(nums) - 1)
이런 유형의 문제를 보면 바로 다이나믹 프로그래밍을 떠올릴 수 있을 것 같다.
바로 옆집은 훔칠 수 없으니 현재 집과 옆집 숫자 중의 최댓값을 구하고, 한 집 건넛집까지의 최댓값과 현재 집의 숫자값과의 합을 구해서 두 수 중 더 높은 값이 정답이 된다.
<네 집에서 털 수 있는 최댓값을 구하는 경우①>
그림의 입력값은 nums = [8, 7, 3, 9]
, i = 3
인 경우다.
첫 집과 세 번째 집을 훔치는 경우라면, 이웃한 두 집은 연속해서 털 수 없으므로, 첫 집(8)과 옆집(7) 중 큰 값인 8
이 최댓값이다.
한 집 건넛집을 계산하면, 첫 집(8)과 세 번째 집(3)과 합인 8 + 3 = 11
이 최댓값이다.
하지만, 네 번째 집을 표적으로 한다면, 첫 집(8)과 이웃집(7)의 최댓값인 8
에 네번째 집에서 훔칠 수 있는 9
를 더해 최댓값은 17이 되므로 이 네집 중 가장 큰 금액을 털 수 있는 최댓값인 정답은 17이 된다.
<네 집에서 털 수 있는 최댓값을 구하는 경우②>
그림의 입력값은 nums = [9, 3, 9, 8]
, i = 3
인 경우다.
첫 집(9)과 이웃집(3) 중 더 큰 값은 9
이다.
세 번째 집(9)을 터는 경우 첫 집(9)과의 합인 9 + 9 = 18
이 최댓값이 되고, 이 값은 첫 집과 네번째 집을 터는 9 + 8 = 17
보다 더 크다.
따라서 세 번째 집까지의 값 18
이 가장 큰 값인 정답이 된다.
그럼 브루트 포스 풀이로 구현해보자.
앞서 rob()
함수를 중첩함수로 처리하고, 문제 풀이 함수와 이름이 겹치므로 밑줄을 추가해 내부 함수라는 의미를 부여했다.
그러나 예상대로 이 풀이는 타임아웃으로 풀리지 않는다.
len(nums)
가 10이라면 탐색 횟수가 287회나 된다.
class Solution:
def rob(self, nums: List[int]) -> int:
if not nums:
return 0
if len(nums) <= 2:
return max(nums)
#정렬된 딕셔너리 (원래 해시테이블은 입력순서가 유지되지 않는다)
dp = collections.OrderedDict()
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
for i in range(2, len(nums)):
# 두번째 값이라고 한다면 그 첫번째 값과 세번째 값이 더해지는 것
# 한 칸 이상 떨어진 집만 훔칠 수 있기 때문
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp.popitem()[1]
타뷸레이션으로 풀어본다. 알고리즘은 동일하다.
dp
에 저장하고 두 번 이상 계산하지 않는다.변수명은 dp
이며 여기서는 메모이제이션이 아닌 타뷸레이션으로 풀이했다.
재귀로 구현하는 메모이제이션보다 순회 방식인 타뷸레이션이 좀 더 직관적이어서 이해하기 쉬운 편이다.
결과는 파이썬의 해시 테이블인 딕셔너리에 넣을 것이다.
원래 딕셔너리는 입력 순서가 유지되지 않았으나 파이썬 3/7+부터 입력 순서가 유지된다.
하지만, 여기서는 낮은 버전에서도 순서가 유지될 수 있도록 명시적으로 collections.OrderedDict()
를 사용한다.
가장 마지막 값을 추출하기 위해 popitem()
을 사용한다.
pop()
과 동일한 역할을 하는데, 딕셔너리에도 있지만 반드시 키를 지정해야 하고 해당 키의 아이템을 추출하는 역할을 한다. popitem()
을 사용한다.collections.OrderedDict()
(https://excelsior-cjh.tistory.com/98)
OrderedDict 는 기본 딕셔너리(dictionary)와 거의 비슷하지만, 입력된 아이템들(items)의 순서를 기억하는 Dictionary 클래스이다.
OrderedDict 는 아이템들(items)의 입력(또는 삽입) 순서를 기억하기 때문에 sorted()함수를 사용하여 정렬된 딕셔너리(sorted dictionary)를 만들때 사용할 수 있다. 아래 [예제1]은 sorted dictionary 를 만드는 예제이다.
# 예제1 - sorted()를 이용한 정렬된 OrderedDict 만들기
from collections import OrderedDict
# 기본 딕셔너리
d = {'banana': 3, 'apple': 4, 'pear': 1, 'orange':2}
# 키(key)를 기준으로 정렬한 OrderedDict
ordered_d1 = OrderedDict(sorted(d.items(), key=lambda t:t[0]))
print(ordered_d1)
'''
결과
OrderedDict([('apple', 4), ('banana', 3), ('orange', 2), ('pear', 1)])
'''
# 값(value)를 기준으로 정렬한 OrderedDict
ordered_d2 = OrderedDict(sorted(d.items(), key=lambda t:t[1]))
print(ordered_d2)
'''
결과
OrderedDict([('pear', 1), ('orange', 2), ('banana', 3), ('apple', 4)])
'''
# 키(key)의 길이(len)를 기준으로 정렬한 OrderedDict
ordered_d3 = OrderedDict(sorted(d.items(), key=lambda t:len(t[0])))
print(ordered_d3)
'''
결과
OrderedDict([('pear', 1), ('apple', 4), ('orange', 2), ('banana', 3)])
'''
# ordered_d1에 새로운 아이템 추가
ordered_d1.update({'grape': 5})
print(ordered_d1)
'''
결과
OrderedDict([('apple', 4), ('banana', 3), ('orange', 2), ('pear', 1), ('grape', 5)])
'''
출처: https://excelsior-cjh.tistory.com/98 [EXCELSIOR]
(https://appia.tistory.com/216)
import collections
ordered_Dict1 = collections.OrderedDict()
ordered_Dict1['x'] = 100
ordered_Dict1['y'] = 200
ordered_Dict1['z'] = 300
ordered_Dict2 = collections.OrderedDict()
ordered_Dict2['x'] = 100
ordered_Dict2['z'] = 300
ordered_Dict2['y'] = 200
print("Object1")
print(ordered_Dict1)
print("Object2")
print(ordered_Dict2)
if ordered_Dict1 == ordered_Dict2 :
print(" Dictionary가 동일")
else :
print(" Dictionary가 다름")
Object1
OrderedDict([('x', 100), ('y', 200), ('z', 300)])
Object2
OrderedDict([('x', 100), ('z', 300), ('y', 200)])
Dictionary가 다름
순서와 상관없이, key/value 쌍이 동일하면 동일하다고 인식하는 것이 일반적인 Dictionary입니다.
OrderedDict의 경우 순서까지도 저장되어 있기 때문에 관련해서 순서 또한 비교 대상으로 포함이 됩니다.