민호는 다단계 조직을 이용하여 칫솔을 판매하고 있습니다. 판매원이 칫솔을 판매하면 그 이익이 피라미드 조직을 타고 조금씩 분배되는 형태의 판매망입니다. 어느정도 판매가 이루어진 후, 조직을 운영하던 민호는 조직 내 누가 얼마만큼의 이득을 가져갔는지가 궁금해졌습니다. 예를 들어, 민호가 운영하고 있는 다단계 칫솔 판매 조직이 아래 그림과 같다고 합시다.
민호는 center이며, 파란색 네모는 여덟 명의 판매원을 표시한 것입니다. 각각은 자신을 조직에 참여시킨 추천인에 연결되어 피라미드 식의 구조를 이루고 있습니다. 조직의 이익 분배 규칙은 간단합니다. 모든 판매원은 칫솔의 판매에 의하여 발생하는 이익에서 10% 를 계산하여 자신을 조직에 참여시킨 추천인에게 배분하고 나머지는 자신이 가집니다. 모든 판매원은 자신이 칫솔 판매에서 발생한 이익 뿐만 아니라, 자신이 조직에 추천하여 가입시킨 판매원에게서 발생하는 이익의 10% 까지 자신에 이익이 됩니다. 자신에게 발생하는 이익 또한 마찬가지의 규칙으로 자신의 추천인에게 분배됩니다. 단, 10% 를 계산할 때에는 원 단위에서 절사하며, 10%를 계산한 금액이 1 원 미만인 경우에는 이득을 분배하지 않고 자신이 모두 가집니다.
예를 들어, 아래와 같은 판매 기록이 있다고 가정하겠습니다. 칫솔의 판매에서 발생하는 이익은 개당 100 원으로 정해져 있습니다.
판매원 | 판매 수량 | 이익금 |
---|---|---|
young | 12 | 1,200 원 |
john | 4 | 400 원 |
tod | 2 | 200 원 |
emily | 5 | 500 원 |
mary | 10 | 1,000 원 |
판매원 young 에 의하여 1,200 원의 이익이 발생했습니다. young 은 이 중 10% 에 해당하는 120 원을, 자신을 조직에 참여시킨 추천인인 edward 에게 배분하고 자신은 나머지인 1,080 원을 가집니다. edward 는 young 에게서 받은 120 원 중 10% 인 12 원을 mary 에게 배분하고 자신은 나머지인 108 원을 가집니다. 12 원을 edward 로부터 받은 mary 는 10% 인 1 원을 센터에 (즉, 민호에게) 배분하고 자신은 나머지인 11 원을 가집니다. 이 상태를 그림으로 나타내면 아래와 같습니다.
그 후, 판매원 john 에 의하여 400 원의 이익이 발생합니다. john 은 10% 인 40 원을 센터에 배분하고 자신이 나머지인 360 원을 가집니다. 이 상태를 그림으로 나타내면 아래와 같습니다.
또 그 후에는 판매원 tod 에 의하여 200 원 이익이 발생하는데, tod 자신이 180 원을, 추천인인 jaimie 가 그 중 10% 인 20 원을 받아서 18 원을 가지고, jaimie 의 추천인인 mary 는 2 원을 받지만 이것의 10% 는 원 단위에서 절사하면 배분할 금액이 없기 때문에 mary 는 2 원을 모두 가집니다. 이 상태를 그림으로 나타내면 아래와 같습니다.
그 다음으로 emily 가 칫솔 판매를 통하여 얻은 이익 500 원은 마찬가지의 규칙에 따라 emily 에게 450 원, mary 에게 45 원, 그리고 센터에 5 원으로 분배됩니다. 이 상태를 그림으로 나타내면 아래와 같습니다.
마지막으로, 판매원 mary 는 1,000 원의 이익을 달성하고, 이 중 10% 인 100 원을 센터에 배분한 후 그 나머지인 900 원을 자신이 가집니다. 이 상태를 그림으로 나타내면 아래와 같습니다.
위와 같이 하여 모든 조직 구성원들의 이익 달성 현황 집계가 끝났습니다. 지금까지 얻은 이익을 모두 합한 결과를 그림으로 나타내면 아래와 같습니다.
이 결과가 민호가 파악하고자 하는 이익 배분 현황입니다.
각 판매원의 이름을 담은 배열 enroll, 각 판매원을 다단계 조직에 참여시킨 다른 판매원의 이름을 담은 배열 referral, 판매량 집계 데이터의 판매원 이름을 나열한 배열 seller, 판매량 집계 데이터의 판매 수량을 나열한 배열 amount가 매개변수로 주어질 때, 각 판매원이 득한 이익금을 나열한 배열을 return 하도록 solution 함수를 완성해주세요. 판매원에게 배분된 이익금의 총합을 계산하여(정수형으로), 입력으로 주어진 enroll에 이름이 포함된 순서에 따라 나열하면 됩니다.
“-“
가 기입됩니다. 위 예제에서는 john 과 mary 가 이러한 예에 해당합니다.판매원들의 정보를 보면 피라미드 형태의 조직 구조로,
자신의 추천인 → 추천인의 추천인 → ... → center의 형태이다.
enroll
과 referral
을 보고 자식-부모 노드의 관계라고 생각해서, 이를 parent[자식] = 부모
형태의 조직구조로 표현하고자 했다.
단, enroll
과 referral
의 원소들은 str
타입이므로 딕셔너리로 초기화했다.(int
타입이라면 리스트로 초기화 → 유니온-파인드)
조직구조에서 부모를 타고타고 올라가면서 이익을 계산하면 된다.
또 다른 방법으로는, 자식-부모 노드의 관계 구조이므로 유니온-파인드(union-find
)를 사용하면 된다.
from collections import defaultdict
def solution(enroll, referral, seller, amount):
amount = [p * 100 for p in amount]
profit = defaultdict(int) # 판매원의 총 이익
parent = defaultdict(str) # 판매원의 추천인 정보(조직 구조)
# 총 이익 초기화
for p in enroll:
profit[p] = 0
# 조직 구조 초기화
for e, r in zip(enroll, referral):
parent[e] = r
# 이익 계산하기
for i, s in enumerate(seller):
to_parent = amount[i] // 10 # 추천인에게 분배할 이익
profit[s] += amount[i] - to_parent # 자신의 이익 = 판매 이익 - 분배 이익
while 1:
s = parent[s] # 추천인
if s == '-' or to_parent == 0: # '-' 즉, center거나 줄 이익이 없으면
break # 종료
else:
profit[s] += to_parent - to_parent // 10 # 추천인의 이익 갱신
to_parent //= 10 # 추천인의 추천인(다단계)
# 총 이익 반환(단, enroll의 순서를 따른다.)
return list(profit.values())
테스트 1 〉 통과 (0.03ms, 10.2MB)
테스트 2 〉 통과 (0.07ms, 10.3MB)
테스트 3 〉 통과 (0.05ms, 10.2MB)
테스트 4 〉 통과 (0.12ms, 10.3MB)
테스트 5 〉 통과 (1.40ms, 10.2MB)
테스트 6 〉 통과 (3.32ms, 12.4MB)
테스트 7 〉 통과 (2.44ms, 12.4MB)
테스트 8 〉 통과 (6.91ms, 12.6MB)
테스트 9 〉 통과 (27.02ms, 13.5MB)
테스트 10 〉 통과 (253.26ms, 24MB)
테스트 11 〉 실패 (시간 초과)
테스트 12 〉 실패 (시간 초과)
테스트 13 〉 실패 (시간 초과)
테스트 11, 12, 13 시간초과
문제에서 아래의 조건이 있다.
✅ '10%를 계산한 금액이 1원 미만인 경우에는 이득을 분배하지 않고 자신이 모두 가진다'
계산한 분배 이익이 0원이라면 어차피 0원을 더할거니 크게 문제가 없다고 생각해서, 굳이 처리하는 코드를 넣지 않아서... → 시간초과가 발생했다.
극단적으로 문제의 예시가 주어져서 조직구조가 하나의 연결리스트 형태라고 생각해보자.
그리고 enroll
의 크기는 주어진 최대 크기인 , seller
의 크기는 주어진 최대 크기인 이고, 모든 판매원이 리프노드라고 생각해보자.
⛔️ 분배 이익이 0원일 때를 처리하는 코드를 넣지 않으면... 어떻게 될까?
현재 탐색하는 판매원은 center
즉, '-'
까지 의 while문이 실행된다.
seller
의 크기는 이다.
즉, 시간복잡도가 으로 이므로 시간초과가 발생하는 것이 자명하다.
꺼진 불도 다시보자..
from collections import defaultdict
def solution(enroll, referral, seller, amount):
amount = [p * 100 for p in amount]
profit = defaultdict(int) # 판매원의 총 이익
parent = defaultdict(str) # 판매원의 추천인 정보(조직 구조)
# 총 이익 초기화
for p in enroll:
profit[p] = 0
# 조직 구조 초기화
for e, r in zip(enroll, referral):
parent[e] = r
# 이익 계산하기
for i, s in enumerate(seller):
to_parent = amount[i] // 10 # 추천인에게 분배할 이익
profit[s] += amount[i] - to_parent # 자신의 이익 = 판매 이익 - 분배 이익
while 1:
s = parent[s] # 추천인
if s == '-' or to_parent == 0: # '-' 즉, center거나 줄 이익이 없으면
break # 종료
else:
profit[s] += to_parent - to_parent // 10 # 추천인의 이익 갱신
to_parent //= 10 # 추천인의 추천인(다단계)
# 총 이익 반환(단, enroll의 순서를 따른다.)
return list(profit.values())
테스트 1 〉 통과 (0.02ms, 10.2MB)
테스트 2 〉 통과 (0.08ms, 10.2MB)
테스트 3 〉 통과 (0.06ms, 10.3MB)
테스트 4 〉 통과 (0.22ms, 10.2MB)
테스트 5 〉 통과 (0.92ms, 10.3MB)
테스트 6 〉 통과 (3.52ms, 12.5MB)
테스트 7 〉 통과 (3.69ms, 12.4MB)
테스트 8 〉 통과 (3.32ms, 12.4MB)
테스트 9 〉 통과 (17.22ms, 13.5MB)
테스트 10 〉 통과 (152.10ms, 24.1MB)
테스트 11 〉 통과 (132.15ms, 23.9MB)
테스트 12 〉 통과 (145.58ms, 23.8MB)
테스트 13 〉 통과 (98.77ms, 23.8MB)
분배 이익이 0원일 때 while문을 종료하는 코드를 추가했다.
정상적으로 시간초과 발생없이 모든 테스트케이스를 통과했다.
정답률이 39%라 그런지 Lv.3 치고는 쉬운 문제였다.