링크와 스타트

조소복·2022년 10월 3일
0

문제

오늘은 스타트링크에 다니는 사람들이 모여서 축구를 해보려고 한다. 축구는 평일 오후에 하고 의무 참석도 아니다. 축구를 하기 위해 모인 사람은 총 N명이다. 이제 스타트 팀과 링크 팀으로 사람들을 나눠야 한다. 두 팀의 인원수는 같지 않아도 되지만, 한 명 이상이어야 한다.

BOJ를 운영하는 회사 답게 사람에게 번호를 1부터 N까지로 배정했고, 아래와 같은 능력치를 조사했다. 능력치 Sij는 i번 사람과 j번 사람이 같은 팀에 속했을 때, 팀에 더해지는 능력치이다. 팀의 능력치는 팀에 속한 모든 쌍의 능력치 Sij의 합이다. Sij는 Sji와 다를 수도 있으며, i번 사람과 j번 사람이 같은 팀에 속했을 때, 팀에 더해지는 능력치는 Sij와 Sji이다.

N=4이고, S가 아래와 같은 경우를 살펴보자.

예를 들어, 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개의 줄에 S가 주어진다. 각 줄은 N개의 수로 이루어져 있고, i번 줄의 j번째 수는 Sij 이다. Sii는 항상 0이고, 나머지 Sij는 1보다 크거나 같고, 100보다 작거나 같은 정수이다.

출력

첫째 줄에 스타트 팀과 링크 팀의 능력치의 차이의 최솟값을 출력한다.

예제 입력 1

4
0 1 2 3
4 0 5 6
7 1 0 2
3 4 5 0

예제 출력 1

0

예제 입력 2

6
0 1 2 3 4 5
1 0 2 3 4 5
1 2 0 3 4 5
1 2 3 0 4 5
1 2 3 4 0 5
1 2 3 4 5 0

예제 출력 2

2

예제 입력 3

8
0 5 4 5 4 5 4 5
4 0 5 1 2 3 4 5
9 8 0 1 2 3 1 2
9 9 9 0 9 9 9 9
1 1 1 1 0 1 1 1
8 7 6 5 4 0 3 2
9 1 9 1 9 1 0 9
6 5 4 3 2 1 9 0

예제 출력 3

1

예제 입력 4

5
0 3 1 1 1
3 0 1 1 1
1 1 0 1 1
1 1 1 0 1
1 1 1 1 0

예제 출력 4

0

문제 풀이

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.StringTokenizer;

public class Main {
    static boolean[] selected;
    static int[][] s;
    static int N;
    static int result=Integer.MAX_VALUE;
    public static void main(String[] args) throws IOException {
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));

        N=Integer.parseInt(br.readLine());

        StringTokenizer st;
        s=new int[N][N];

        for(int i=0;i<N;i++){
            st=new StringTokenizer(br.readLine());
            for(int j=0;j<N;j++){
                s[i][j]=Integer.parseInt(st.nextToken());
            }
        }

        selected=new boolean[N];
        //두 팀으로 나누기
        //부분집합
        subset(0);

        System.out.print(result);
    }

    private static void subset(int index) {
        if(index==N){
            ArrayList<Integer> start=new ArrayList<>();
            ArrayList<Integer> link=new ArrayList<>();
            //두 팀으로 나눴는지 확인 후 능력치 구하기
            for(int i=0;i<N;i++){
                if(selected[i]){
                    start.add(i);
                }else{
                    link.add(i);
                }
            }

            //두 팀으로 나뉘어지지 않은 경우
            if(start.size()==0 || link.size()==0) return;

            //능력치 계산해오기
            int start_s=stat(start);
            int link_s=stat(link);

            if(result> Math.abs(start_s-link_s)) result=Math.abs(start_s-link_s);

            return;
        }

        selected[index]=true;
        subset(index+1);

        selected[index]=false;
        subset(index+1);
    }

    //이중for문으로 각 팀의 능력치 구하기
    private static int stat(ArrayList<Integer> team) {
        int sum=0;

        for(int i=0;i<team.size();i++){
            int a=team.get(i);
            for(int j=i;j< team.size();j++){
                int b=team.get(j);
                sum+=s[a][b]+s[b][a];
            }
        }

        return sum;
    }

}

문제를 풀기위해 생각해야할 순서는 다음과 같다.

  • 두 팀으로 나누기
  • 팀에 속한 능력치의 합을 구하기
    - 이때, 이중 for문을 이용하여 능력치의 쌍을 구한다.
  • 각 팀의 능력치의 차가 최소인 경우 찾기

두 팀으로 나누기

private static void subset(int index) {
    if(index==N){
        ArrayList<Integer> start=new ArrayList<>();
        ArrayList<Integer> link=new ArrayList<>();
        //두 팀으로 나눴는지 확인 후 능력치 구하기
        for(int i=0;i<N;i++){
            if(selected[i]){
                start.add(i);
            }else{
                link.add(i);
            }
        }

        //두 팀으로 나뉘어지지 않은 경우
        if(start.size()==0 || link.size()==0) return;

        //능력치 계산해오기
        int start_s=stat(start);
        int link_s=stat(link);

        if(result> Math.abs(start_s-link_s)) result=Math.abs(start_s-link_s);

        return;
    }

    selected[index]=true;
    subset(index+1);

    selected[index]=false;
    subset(index+1);
}

두 팀으로 나눌때에는 부분집합을 이용하여 나눠준다. 각 팀에 몇 명씩 있어야하는지 정해져있는 것이 아니기 때문에 한 팀에 팀원이 아무도 없는 경우를 제외하고 모든 경우를 구해준다.

스타트 팀에 속하게 된 경우 selected[i]=true라고 생각하여 배열에 넣어주고 방문하지 않은 경우 링크 팀에 넣어준다.

이때, 스타트팀과 링크팀 중 크기가 0인 경우는 아무 팀원이 없다는 뜻이기 때문에 두 팀으로 나누어지지 않은 경우이므로 return하여 함수를 끝낸다.

그것이 아니라면 각 팀의 능력치의 합을 구하여 최소값을 구한다.


각 팀의 능력치 합 구하기

private static int stat(ArrayList<Integer> team) {
    int sum=0;

    for(int i=0;i<team.size();i++){
        int a=team.get(i);
        for(int j=i;j< team.size();j++){
            int b=team.get(j);
            sum+=s[a][b]+s[b][a];
        }
    }

    return sum;
}

ArrayList로 넘어온 팀의 정보를 이용하여 각 팀원들의 능력치 합을 구한다.

예를 들어 team에 3번, 4번, 5번 팀원이 들어왔다고 했을 때, 구해야하는 능력치는

s[3][4], s[3][5], s[4][5], s[4][3], s[5][3], s[5][4]

이다.

s[4][5]까지는 이중 for문을 이용하여 쉽게 구할 수 있고, 이후의 능력치는 앞서 구한 능력치 쌍의 인덱스를 반대로 해주면 된다.

즉, s[4][5]까지만 for문을 돌려주면 되는 것이다.

sum에 s[a][b]+s[b][a]의 값을 더해주는 것을 반복하면 해당 팀의 능력치 합을 구할 수 있다.

반환된 sum값을 이용하여 두 팀의 능력치가 최소가 되는 경우를 구하여 답을 출력한다.


처음에 부분집합을 이용해야할 것을 떠올렸지만 코드가 길어지면서 잘하고 있는 건지 의문이 들어 시간이 좀 걸렸다.

결국 처음에 생각했던 로직대로 차근차근 풀어 정답을 맞출 수 있었다. 내 코드가 의심돼도 끝까지 해보는 것이 좋겠다는 생각이 들었다.

profile
개발을 꾸준히 해보자

0개의 댓글