BAEKJOON 14889
문제
오늘은 스타트링크에 다니는 사람들이 모여서 축구를 해보려고 한다. 축구는 평일 오후에 하고 의무 참석도 아니다. 축구를 하기 위해 모인 사람은 총 N명이고 신기하게도 N은 짝수이다. 이제 N/2명으로 이루어진 스타트 팀과 링크 팀으로 사람들을 나눠야 한다.
BOJ를 운영하는 회사 답게 사람에게 번호를 1부터 N까지로 배정했고, 아래와 같은 능력치를 조사했다. 능력치 Sij는 i번 사람과 j번 사람이 같은 팀에 속했을 때, 팀에 더해지는 능력치이다. 팀의 능력치는 팀에 속한 모든 쌍의 능력치 Sij의 합이다. Sij는 Sji와 다를 수도 있으며, i번 사람과 j번 사람이 같은 팀에 속했을 때, 팀에 더해지는 능력치는 Sij와 Sji이다.
N=4이고, S가 아래와 같은 경우를 살펴보자. S = [[0,1,2,3], [4,0,5,6], [7,1,0,2], [3,4,5,0]] 예를 들어, 1, 2번이 스타트 팀, 3, 4번이 링크 팀에 속한 경우에 두 팀의 능력치는 아래와 같다.
스타트 팀: S12 + S21 = 1 + 4 = 5
링크 팀: S34 + S43 = 2 + 5 = 7
1, 3번이 스타트 팀, 2, 4번이 링크 팀에 속하면, 두 팀의 능력치는 아래와 같다.
스타트 팀: S13 + S31 = 2 + 7 = 9
링크 팀: S24 + S42 = 6 + 4 = 10
축구를 재미있게 하기 위해서 스타트 팀의 능력치와 링크 팀의 능력치의 차이를 최소로 하려고 한다. 위의 예제와 같은 경우에는 1, 4번이 스타트 팀, 2, 3번 팀이 링크 팀에 속하면 스타트 팀의 능력치는 6, 링크 팀의 능력치는 6이 되어서 차이가 0이 되고 이 값이 최소이다.
입력
첫째 줄에 N(4 ≤ N ≤ 20, N은 짝수)이 주어진다. 둘째 줄부터 N개의 줄에 S가 주어진다. 각 줄은 N개의 수로 이루어져 있고, i번 줄의 j번째 수는 Sij 이다. Sii는 항상 0이고, 나머지 Sij는 1보다 크거나 같고, 100보다 작거나 같은 정수이다.
나의 처음 코드
주어진 선수들의 인원 수의 절반씩 팀을 만들어줘야 한다. 처음에는 어떻게 4개를 고를까 생각하다가, 이것도 여느 백트래킹과 같은 문제라는 것을 깨달았다. 그래서 check_player로 depth가 깊어질 수록 True인 선수들만 짝지어주다가 check_player에 남아있는 True의 갯수가 총인원의 절반일 때 return을 해주었다. 코드는 아래와 같다.
num_of_player = int(input()) #총 인원수
player_state = [list(map(int, input().split())) for i in range(num_of_player)] #2차원 배열 생성
check_player = [True] * num_of_player #팀이 있는 선수 = False , 없는 선수 = True
min = 999
# 스탯을 구하는 함수 Sij와 Sji의 합들
def get_value(num_list : list):
sum = 0
for i in num_list:
for j in num_list:
if i != j:
sum += player_state[i][j]
return sum
# 백트래킹 부분
def find_team(team1 : list):
global min
#True의 갯수가 선수의 절반일 때. 즉, 절반의 선수들이 팀이 짜여져있을 때
if check_player.count(True) == num_of_player//2:
#스타트팀과 링크팀의 합을 구하는 부분
team1 = [i for i,v in enumerate(check_player) if v == False]
team2 = [i for i,v in enumerate(check_player) if v]
diff = abs(get_value(team1) - get_value(team2))
min = diff if min > diff else min
return
#다른 백트래킹 부분과 같다.
for i, v in enumerate(check_player):
if v == False:
continue
check_player[i] = False
find_team(team1)
check_player[i] = True
find_team([])
print(min)
결과는 시간초과. 답은 모두 맞게 나오는데, pypy3로 해도 시간초과가 나왔다. 빅오를 구해보니, 백트래킹 부분에서 쓸데없는 작업으로 인해 완전 커지는걸 볼 수 있었다. 따라서 쓸데없는 부분들을 없애고, 기존에 했던 작업들을 스킵하는 과정으로 새로 만들었다.
예를 들어, 총 1, 2, 3, 4의 선수들이 있다고 해보자. 그러면 (1, 3)이 한팀일 때 값을 구하면 (3, 1)일 때 값을 구하지 않아도 되는 것이다. 즉, 순열보다는 조합이라는 뜻이다. 이것을 고려해서 다시 짜보았다.
코드는 아래와 같다.
num_of_player = int(input())
player_state = [list(map(int, input().split())) for i in range(num_of_player)]
check_player = [True] * num_of_player
min = 999
#처음 코드와 동일
def get_value(num_list : list):
sum = 0
for i in num_list:
for j in num_list:
if i != j:
sum += player_state[i][j]
return sum
#매개변수로 my_check_player가 추가 되었다. (= 총 선수들의 인덱스)
def find_team(team1 : list, my_check_player : list):
global min
#처음 코드와 동일
if len(team1) == num_of_player//2:
diff = abs(get_value(team1) - get_value(my_check_player))
min = diff if min > diff else min
return
#if문 2개로 굳이 확인 안해줘도 되는 부분들을 띄어넘는 작업을 해주었다.
for i, v in enumerate(my_check_player):
#중복확인을 맞기 위한 ifans
if len(team1) != 0 and team1[-1] > v:
continue
#조합문제이므로 절반만 확인해도 답을 구할 수 있다.
if len(team1) != 0 and team1[0] == num_of_player//2:
break
team1.append(v)
my_check_player.pop(i)
find_team(team1, my_check_player)
team1.pop(-1)
my_check_player.insert(i, v)
find_team([], [i for i in range(num_of_player)] )
print(min)
결과는 맞았습니다. 조금 더 간결하게 짤 수도 있다. if문을 없애고, for문에서 제어를 해줘도 된다. 이 문제 코드의 리팩토링은 나중에 다시 해보도록 하겠다! 앞으로 갈 길이 멀기에!