[Programmers][2022 KAKAO BLIND RECRUITMENT] 양궁게임
문제 설명
문제 설명
카카오배 양궁대회가 열렸습니다.
라이언
은 저번 카카오배 양궁대회 우승자이고 이번 대회에도 결승전까지 올라왔습니다. 결승전 상대는 어피치
입니다.
카카오배 양궁대회 운영위원회는 한 선수의 연속 우승보다는 다양한 선수들이 양궁대회에서 우승하기를 원합니다. 따라서, 양궁대회 운영위원회는 결승전 규칙을 전 대회 우승자인 라이언에게 불리하게 다음과 같이 정했습니다.
-
어피치가 화살
n
발을 다 쏜 후에 라이언이 화살n
발을 쏩니다. -
점수를 계산합니다.
-
과녁판은 아래 사진처럼 생겼으며 가장 작은 원의 과녁 점수는 10점이고 가장 큰 원의 바깥쪽은 과녁 점수가 0점입니다. ![012022_공채문제_양궁대회_01.png](https://grepp-programmers.s3.ap-northeast-2.amazonaws.com/files/production/2c73b8f8-c938-4b6e-9bc3-e3a3784d6a41/01_2022%E1%84%80%E1%85%A9%E1%86%BC%E1%84%8E%E1%85%A2%E1%84%86%E1%85%AE%E1%86%AB%E1%84%8C%E1%85%A6_%E1%84%8B%E1%85%A3%E1%86%BC%E1%84%80%E1%85%AE%E1%86%BC%E1%84%83%E1%85%A2%E1%84%92%E1%85%AC_01.png)
-
만약, k(k는 1~10사이의 자연수)점을 어피치가 a발을 맞혔고 라이언이 b발을 맞혔을 경우 더 많은 화살을 k점에 맞힌 선수가 k 점을 가져갑니다. 단, a = b일 경우는 어피치가 k점을 가져갑니다.
k점을 여러 발 맞혀도 k점 보다 많은 점수를 가져가는 게 아니고 k점만 가져가는 것을 유의하세요. 또한 a = b = 0 인 경우, 즉, 라이언과 어피치 모두 k점에 단 하나의 화살도 맞히지 못한 경우는 어느 누구도 k점을 가져가지 않습니다.
- 예를 들어, 어피치가 10점을 2발 맞혔고 라이언도 10점을 2발 맞혔을 경우 어피치가 10점을 가져갑니다.
- 다른 예로, 어피치가 10점을 0발 맞혔고 라이언이 10점을 2발 맞혔을 경우 라이언이 10점을 가져갑니다.
-
모든 과녁 점수에 대하여 각 선수의 최종 점수를 계산합니다.
-
-
최종 점수가 더 높은 선수를 우승자로 결정합니다. 단, 최종 점수가 같을 경우 어피치를 우승자로 결정합니다.
현재 상황은 어피치가 화살 n
발을 다 쏜 후이고 라이언이 화살을 쏠 차례입니다.
라이언은 어피치를 가장 큰 점수 차이로 이기기 위해서 n
발의 화살을 어떤 과녁 점수에 맞혀야 하는지를 구하려고 합니다.
화살의 개수를 담은 자연수 n
, 어피치가 맞힌 과녁 점수의 개수를 10점부터 0점까지 순서대로 담은 정수 배열 info
가 매개변수로 주어집니다. 이때, 라이언이 가장 큰 점수 차이로 우승하기 위해 n
발의 화살을 어떤 과녁 점수에 맞혀야 하는지를 10점부터 0점까지 순서대로 정수 배열에 담아 return 하도록 solution 함수를 완성해 주세요. 만약, 라이언이 우승할 수 없는 경우(무조건 지거나 비기는 경우)는 [-1]
을 return 해주세요.
제한사항
-
1 ≤
n
≤ 10 -
info
의 길이 = 11
- 0 ≤
info
의 원소 ≤n
info
의 원소 총합 =n
info
의 i번째 원소는 과녁의10 - i
점을 맞힌 화살 개수입니다. ( i는 0~10 사이의 정수입니다.)
- 0 ≤
-
라이언이 우승할 방법이 있는 경우, return 할 정수 배열의 길이는 11입니다.
- 0 ≤ return할 정수 배열의 원소 ≤
n
- return할 정수 배열의 원소 총합 =
n
(꼭 n발을 다 쏴야 합니다.) - return할 정수 배열의 i번째 원소는 과녁의
10 - i
점을 맞힌 화살 개수입니다. ( i는 0~10 사이의 정수입니다.) - 라이언이 가장 큰 점수 차이로 우승할 수 있는 방법이 여러 가지 일 경우, 가장 낮은 점수를 더 많이 맞힌 경우를 return 해주세요.
- 가장 낮은 점수를 맞힌 개수가 같을 경우 계속해서 그다음으로 낮은 점수를 더 많이 맞힌 경우를 return 해주세요.
- 예를 들어,
[2,3,1,0,0,0,0,1,3,0,0]
과[2,1,0,2,0,0,0,2,3,0,0]
를 비교하면[2,1,0,2,0,0,0,2,3,0,0]
를 return 해야 합니다. - 다른 예로,
[0,0,2,3,4,1,0,0,0,0,0]
과[9,0,0,0,0,0,0,0,1,0,0]
를 비교하면[9,0,0,0,0,0,0,0,1,0,0]
를 return 해야 합니다.
- 0 ≤ return할 정수 배열의 원소 ≤
-
라이언이 우승할 방법이 없는 경우, return 할 정수 배열의 길이는 1입니다.
- 라이언이 어떻게 화살을 쏘든 라이언의 점수가 어피치의 점수보다 낮거나 같으면
[-1]
을 return 해야 합니다.
- 라이언이 어떻게 화살을 쏘든 라이언의 점수가 어피치의 점수보다 낮거나 같으면
입출력 예
n | info | result |
---|---|---|
5 | [2,1,1,1,0,0,0,0,0,0,0] | [0,2,2,0,1,0,0,0,0,0,0] |
1 | [1,0,0,0,0,0,0,0,0,0,0] | [-1] |
9 | [0,0,1,2,0,1,1,1,1,1,1] | [1,1,2,0,1,2,2,0,0,0,0] |
10 | [0,0,0,0,0,0,0,0,3,4,3] | [1,1,1,1,1,1,1,1,0,0,2] |
입출력 예 설명
입출력 예 #1
어피치와 라이언이 아래와 같이 화살을 맞힐 경우,
과녁 점수 | 어피치가 맞힌 화살 개수 | 라이언이 맞힌 화살 개수 | 결과 |
---|---|---|---|
10 | 2 | 3 | 라이언이 10점 획득 |
9 | 1 | 2 | 라이언이 9점 획득 |
8 | 1 | 0 | 어피치가 8점 획득 |
7 | 1 | 0 | 어피치가 7점 획득 |
6 | 0 | 0 | |
5 | 0 | 0 | |
4 | 0 | 0 | |
3 | 0 | 0 | |
2 | 0 | 0 | |
1 | 0 | 0 | |
0 | 0 | 0 |
어피치의 최종 점수는 15점, 라이언의 최종 점수는 19점입니다. 4점 차이로 라이언이 우승합니다.
하지만, 라이언이 아래와 같이 화살을 맞힐 경우 더 큰 점수 차로 우승할 수 있습니다.
과녁 점수 | 어피치가 맞힌 화살 개수 | 라이언이 맞힌 화살 개수 | 결과 |
---|---|---|---|
10 | 2 | 0 | 어피치가 10점 획득 |
9 | 1 | 2 | 라이언이 9점 획득 |
8 | 1 | 2 | 라이언이 8점 획득 |
7 | 1 | 0 | 어피치가 7점 획득 |
6 | 0 | 1 | 라이언이 6점 획득 |
5 | 0 | 0 | |
4 | 0 | 0 | |
3 | 0 | 0 | |
2 | 0 | 0 | |
1 | 0 | 0 | |
0 | 0 | 0 |
어피치의 최종 점수는 17점, 라이언의 최종 점수는 23점입니다. 6점 차이로 라이언이 우승합니다.
따라서 [0,2,2,0,1,0,0,0,0,0,0]
을 return 해야 합니다.
입출력 예 #2
라이언이 10점을 맞혀도 어피치가 10점을 가져가게 됩니다.
따라서, 라이언은 우승할 수 없기 때문에 [-1]
을 return 해야 합니다.
입출력 예 #3
어피치와 라이언이 아래와 같이 화살을 맞힐 경우,
과녁 점수 | 어피치가 맞힌 화살 개수 | 라이언이 맞힌 화살 개수 | 결과 |
---|---|---|---|
10 | 0 | 1 | 라이언이 10점 획득 |
9 | 0 | 1 | 라이언이 9점 획득 |
8 | 1 | 2 | 라이언이 8점 획득 |
7 | 2 | 3 | 라이언이 7점 획득 |
6 | 0 | 0 | |
5 | 1 | 2 | 라이언이 5점 획득 |
4 | 1 | 0 | 어피치가 4점 획득 |
3 | 1 | 0 | 어피치가 3점 획득 |
2 | 1 | 0 | 어피치가 2점 획득 |
1 | 1 | 0 | 어피치가 1점 획득 |
0 | 1 | 0 | 어피치가 0점 획득 |
어피치의 최종 점수는 10점, 라이언의 최종 점수는 39점입니다. 29점 차이로 라이언이 우승합니다.
하지만 라이언이 아래와 같이 화살을 맞힐 경우,
과녁 점수 | 어피치가 맞힌 화살 개수 | 라이언이 맞힌 화살 개수 | 결과 |
---|---|---|---|
10 | 0 | 1 | 라이언이 10점 획득 |
9 | 0 | 1 | 라이언이 9점 획득 |
8 | 1 | 2 | 라이언이 8점 획득 |
7 | 2 | 0 | 어피치가 7점 획득 |
6 | 0 | 1 | 라이언이 6점 획득 |
5 | 1 | 2 | 라이언이 5점 획득 |
4 | 1 | 2 | 라이언이 4점 획득 |
3 | 1 | 0 | 어피치가 3점 획득 |
2 | 1 | 0 | 어피치가 2점 획득 |
1 | 1 | 0 | 어피치가 1점 획득 |
0 | 1 | 0 | 어피치가 0점 획득 |
어피치의 최종 점수는 13점, 라이언의 최종 점수는 42점입니다. 이 경우도 29점 차이로 라이언이 우승합니다.
하지만, 첫 번째 경우와 두 번째 경우를 비교했을 때, 두 번째 경우가 두 경우 중 가장 낮은 점수인 4점을 더 많이 맞혔기 때문에 [1,1,2,3,0,2,0,0,0,0,0]
이 아닌 [1,1,2,0,1,2,2,0,0,0,0]
을 return 해야 합니다.
입출력 예 #4
가장 큰 점수 차이로 이기는 경우 중에서 가장 낮은 점수를 가장 많이 맞힌, 10~3점을 한 발씩 맞히고 나머지 두 발을 0점에 맞히는 경우인 [1,1,1,1,1,1,1,1,0,0,2]
를 return 해야 합니다.
제한시간 안내
- 정확성 테스트 : 10초
문제 풀이
# Backtracking
풀이 과정
2단계라는 사실이 믿기지 않는 문제… 너무 어려웠습니다😂 이 문제의 핵심은 라이언
이 화살을 쏠 때 그 개수는 0
또는 info[i]+1
이라는 것입니다.
처음에는 냅색 알고리즘을 이용해 풀이를 시도했는데, 21번 TC만 통과하지 못하더라구요… 계속 시도해봤는데 어디가 틀린지 모르겠어서 패스…ㅠㅠ (혹시 어디가 틀린지 아시겠는 분은 댓글 부탁드려요…)
그래서 두번째로 백트래킹 알고리즘을 이용했습니다. 각 점수의 화살마다 쏠 수 있는 경우의 수가 0~n개이면 백트래킹을 이용하지 못하겠지만(시간 초과), 여기서는 0 또는 info[i]+1이라는 조건을 찾아낼 수 있기 때문에 이용할 수 있습니다. 이처럼 높은 수준의 문제에서는 문제 속에 숨겨져 있는 조건을 찾아내서 탐색 후보의 수를 줄이는 것이 중요한 것 같습니다.
탐색 알고리즘을 위한 코딩 외에도 base case에서 정답 후보 저장할 때, 마지막에 최종 정답 도출할 때 등 신경써야 할 부분들이 많았던 문제였습니다.
전체 코드
1번 풀이: <span style=color:red>21번 TC만 실패!!!</span>
냅색 알고리즘을 이용한 풀이입니다.
def solution(n, info):
'''점수(0~10)를 아이템으로, 화살 수를 용량으로 생각하여 냅색 알고리즘을 적용한다'''
### '점수차'를 최대화
N, K = 11, n # 점수의 수, 화살 수
weights = [0]+[info[i]+1 for i in range(N)][::-1] # 점수를 얻기 위해 필요한 화살 수
values = [0]+[(10-i)*2 if info[i]>0 else (10-i) for i in range(N)][::-1] # 화살을 투자했을 때 얻는 점수
ans = [[[] for _ in range(K+1)] for _ in range(N+1)] # ans[n][k]: 점수 n까지 탐색, k개의 화살 사용 시 최대 점수를 얻을 때의 화살 투자 순서
knapsack = [[0 for _ in range(K + 1)] for _ in range(N + 1)] # knapsack[n][k]: 점수 n까지 탐색, k개의 화살 사용 시 얻을 수 있는 최대 점수
### knapsack algorithm
for i in range(1, N+1):
for j in range(1, K+1):
weight = weights[i]
value = values[i]
if j < weight:
ans[i][j] = ans[i-1][j]+[0]
knapsack[i][j] = knapsack[i - 1][j]
else:
if_taken = value + knapsack[i - 1][j - weight]
if_not_taken = knapsack[i - 1][j]
if if_taken > if_not_taken: # 이번 점수를 가져오는 것이 더 점수가 높을 때
ans[i][j] = [0]*(i-1-len(ans[i-1][j-weight])) + ans[i-1][j-weight] + [weight]
knapsack[i][j] = value + knapsack[i - 1][j - weight]
elif if_taken < if_not_taken: # 이번 점수를 가져오지 않는 것이 더 점수가 높을 때
ans[i][j] = [0]*(i-1-len(ans[i-1][j])) + ans[i-1][j] + [0]
knapsack[i][j] = knapsack[i-1][j]
else: # 두 경우에 점수가 같을 때
for a, b in zip(ans[i-1][j-weight], ans[i-1][j]): # 가장 낮은 점수를 더 많이 맞춘 경우를 선택
if a > b:
ans[i][j] = ans[i-1][j-weight] + [weight]
break
elif a < b:
ans[i][j] = ans[i-1][j] + [0]
break
else:
ans[i][j] = ans[i-1][j-weight] + [weight]
knapsack[i][j] = value + knapsack[i - 1][j - weight] # 점수는 같으니 아무거나 선택
### 마지막에 점수 비교 필요
ans = ans[N][K][::-1] # 최대 점수를 얻을 수 있는 경우
op_score, my_score = 0, 0 # 상대 점수, 내 점수
for e, (a, b) in enumerate(zip(info, ans)):
if a == b == 0: continue
elif a < b: my_score += 10-e
else: op_score += 10-e
if op_score < my_score:
if sum(ans) < K: ans[-1] = K - sum(ans) # '투자한 화살 수 < 총 화살 수'이면 0점짜리에 남은 화살 수만큼 값을 설정
else:
ans = [-1]
return ans
2번 풀이: 성공
백트래킹을 이용한 풀이입니다.
첫번째 백트래킹 부분에서는 라이언이 가장 큰 점수이득을 얻을 수 있을 때의 슈팅 목록들을 구합니다. 이 때의 점수이득은 어피치가 쏜 화살이 0 일 때
라이언이 점수를 가져가면 n
점, 어피치가 쏜 화살이 0보다 클 때
라이언이 점수를 가져가면 2n
점, 이외에는 0점입니다.
두번째 리턴 부분에서는 백트래킹 부분에서 얻은 슈팅 목록들 중 가장 낮은 점수를 맞춘 횟수가 많은 슈팅 목록을 구하고, 그 때의 슈팅 목록 ans
를 info
와 비교해서 우승자를 가려냅니다.
아, 그리고 이번에 처음 알았는데, sort 함수의 key=lambda x: x
에서 x가 리스트이면 각각의 원소들을 비교해 큰 순서대로 정렬해줍니다…ㄷㄷ
def solution(n, info):
### 라이언이 최대 점수를 얻을 때의 슈팅 목록들을 구함
def backtracking(n, i, s, shots, ans): # 남은 화살 수, 순서, 점수차, 슈팅 순서, 정답 목록
if n==0 or i==11:
if n==0: shots = shots+[0]*(11-i)
if i==11: shots[-1] = n
if len(ans)==0 or ans[0][0] < s:
ans = [[s,shots]]
elif len(ans)==0 or ans[0][0] == s:
ans.append([s,shots])
return ans
for shot in [0,info[i]+1]:
if shot > n: continue
if shot > info[i] and info[i]==0: _s = s+(10-i)
elif shot > info[i] and info[i]!=0: _s = s+2*(10-i)
else: _s = s
ans = backtracking(n-shot, i+1, _s, shots+[shot], ans)
return ans
ans = backtracking(n, 0, 0, [], [])
### 가장 낮은 점수를 더 맞춘 슈팅 목록을 구하고, 어피치의 슈팅 목록과 비교
ans = sorted(ans,key=lambda x:x[1][::-1])[-1][1]
op_score, my_score = 0, 0
for e, (a, b) in enumerate(zip(info, ans)):
if a == b == 0: continue
elif a < b: my_score += 10-e
else: op_score += 10-e
ans = ans if op_score < my_score else [-1]
return ans
정리
- 높은 수준의 문제에서는 문제 속에 숨겨져 있는 조건을 찾아내서 탐색 후보의 수를 줄이는 것이 중요하다!!!
Leave a comment