3월 25, 2024

[백준] 1517번 버블 소트 문제 Merge Sort로 풀어보기 (버블 소트로는 풀 수 없는 이유?)

1. 문제

www.acmicpc.net/problem/1517

문제는 위의 링크에 들어가면 확인할 수 있다.


2. 풀이

이 문제는 버블 소트를 할 때 수를 바꾸는 과정이 몇 번 있는지를 묻는 문제이다. 하지만 이것을 실제 Bubble Sort대로 풀면 시간초과가 나게 될 것이다. 왜냐하면 버블소트는 시간복잡도 O(N^2) 이기 때문에 문제에서 주어진 조건을 초과하게 된다. 

 

따라서 이 문제는 시간복잡도 O(NlogN)인 merge sort를 활용하여서 풀 수 있다. 

 

버블 소트는 index i, j에 대해서 i<j인데 a[i] > a[j] 일때 두 수를 바꾸어주는 알고리즘을 의미한다. 

그래서 이를 merge sort라고 생각하면, 두 그룹을 합쳐줄 때 버블 소트에서 두 수를 바꾸어주는 count를 세어 줄 수 있다.

 

다시 말해서,


위와 같은 두 그룹을 merge sort를 통해서 합쳐준다고 생각하면 첫 그룹의 7과 두번째 그룹의 1을 비교해서 1이 먼저 앞으로 들어가게 된다. 

Bubble Sort라고 생각해보면 7, 9, 1, 3이 다 합쳐져있었을 것이고 1을 기준으로 7과 1이 한번 교환되었을 것이고, 9과 1이 한번 더 교환되었을 것이다. 그러면 1 기준으로 두 번의 숫자 교환이 이루어졌던 것이다. 

우리는 merge sort를 통해서 구현하고 있었으니 이를 merge sort로 생각해보면 두 번째 그룹인 1이 가장 처음에 들어갈 때 앞에 남아있는 그룹의 원소 개수만큼 숫자의 교환이 이루어지는 것이다. 여기서는 1이 가장 먼저 정렬될 때 첫 번째 그룹에 7,9 이렇게 두 개의 숫자가 남아있으니 답에는 2가 더해지게 되는 것이다. 마찬가지로 3의 경우에도 3이 정렬될 때 첫 번째 그룹에 두 개의 숫자가 남아있으니 답에는 추가로 2만큼 더해주어야 한다.

 


3. 코드

이를 코드로 구현한 것은 아래와 같다.

 

import java.util.*;
import java.io.*;

public class Main{
    static int a[];
    public static long go(int start, int end){
        if (start==end){
            return 0;
        }
        int mid=(start+end)/2;
        long ans=go(start, mid)+go(mid+1, end);
        int [] tmp=new int[end-start+1];
        { //두 그룹을 합칠 때 merge
            int i=start;
            int j=mid+1;
            int k=0;
            while(i<=mid || j<=end){
                if (i<=mid && (j>end||a[i]<=a[j])){
                    tmp[k++]=a[i++];
                }else{
                    ans+=(long)(mid-i+1);  //두번째 그룹의 숫자가 들어갈 때 
                    tmp[k++]=a[j++];
                }
            }
            
        }
        for (int i=start; i<=end; i++) {
            a[i] = tmp[i-start];
        }
        return ans;
    }
    public static void main(String[] args) throws IOException{
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        int n=Integer.valueOf(br.readLine());
        a=new int[n];
        String s[]=br.readLine().split(" ");
        for(int i=0; i<n; i++){
            a[i]=Integer.valueOf(s[i]);
        }
        System.out.println(go(0, n-1));
    }
}

merge sort의 구현을 그대로 따라주었고 다른 점은 두 번째 그룹의 숫자를 tmp라는 배열에 담을 때 첫 번째 그룹의 원소개수를 정답에 추가해주어야 한다는 점이다. 


3월 25, 2024

[백준] 11728번 배열 합치기 Merge Sort 풀이법

1. 문제

www.acmicpc.net/problem/11728

문제는 위의 링크에 들어가면 확인할 수 있다.


2. merge sort

merge sort 중에서 합치는 부분을 코드로 구현해야 하는 문제이다.

Merge Sort에 관한 내용은 이전 포스팅에서 정말 자세하게 다루었다.

https://www.programmingstory.com/2024/02/merge-sort.html

위의 포스팅에서 합치는 코드 또한 다루었기 때문에 한번 읽어보면 이 문제를 푸는데에도 큰 도움이 될 것이다.


3. 풀이

다른 점은 위 포스팅에서는 start, end 라고 해서 두 그룹을 합쳐서 처음과 끝 index를 따로 표시해주었는데 이번에는 두 배열을 서로 다른 배열로 입력받아놓았다는 점이다. 

그래도 알고리즘 자체는 완전히 같다.

 

합치는 코드 부분만 우선 살펴보겠다.

 //합치는 코드
        int i=0; //첫번째 그룹 index
        int j=0; //두번째 그룹 index
        int k=0; //현재 저장하는 c index
        while(i<n && j<m){
            if (a[i]<=b[j]){
                c[k++]=a[i++];
            }
            else {
                c[k++]=b[j++];
            }
        }
        while(i<n){
            c[k++]=a[i++];
        }
        while(j<m){
            c[k++]=b[j++];
        }

 

여기서 c라는 배열에다가 정렬된 것을 저장해놓을 것이고, 두 그룹의 index별로 대소관계를 비교해 먼저 c 배열에 들어와야 할 요소를 판단한다. 

마지막에 while문은 만약 하나의 그룹의 원소들은 모두 다 c 배열에 저장이 되었는데 다른 그룹의 원소들이 남아있을 경우를 대비하여 c 배열에 순서대로 저장하는 코드를 추가한 것이다.


4. 전체 코드

입력받고 출력하는 것까지 전체 코드는 아래와 같다.

import java.util.*;
import java.io.*;

public class Main{
    public static void main(String[] args) throws IOException{
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        String[] line = br.readLine().split(" ");
        int n = Integer.valueOf(line[0]);
        int m = Integer.valueOf(line[1]);
        int[] a = new int[n];
        line = br.readLine().split(" ");
        for (int i=0; i<n; i++) {
            a[i] = Integer.valueOf(line[i]);
        }
        int[] b = new int[m];
        line = br.readLine().split(" ");
        for (int i=0; i<m; i++) {
            b[i] = Integer.valueOf(line[i]);
        }
        int[] c = new int[n+m]; //저장할 공간
        
        //합치는 코드
        int i=0; //첫번째 그룹 index
        int j=0; //두번째 그룹 index
        int k=0; //현재 저장하는 c index
        while(i<n && j<m){
            if (a[i]<=b[j]){
                c[k++]=a[i++];
            }
            else {
                c[k++]=b[j++];
            }
        }
        while(i<n){
            c[k++]=a[i++];
        }
        while(j<m){
            c[k++]=b[j++];
        }
        StringBuilder sb = new StringBuilder();
        for (int in=0; in<n+m; in++) {
            sb.append(c[in] + " ");
        }
        System.out.println(sb);
    }
}

 

입력이 많기 때문에 Scanner 를 사용하는 대신 BufferedReader를 사용했고, 출력또한 매번 System.out.print()를 사용하는 것이 아닌, StringBuilder를 사용해서 한꺼번에 출력하여 시간을 줄였다. 


2월 13, 2024

Merge Sort (머지 소트) 알고리즘 소개 (합치는 코드, 분할코드, 시간복잡도)

 Merge Sort는 분할 정복 알고리즘의 하나의 종류로 N개를 효율적으로 정렬할 때 사용하는 소팅 방법이라고 할 수 있다.

예를 들어, N개의 숫자가 있고 이것을 정렬하고 싶을 때 매 단계마다 N개를 N/2, N/2개로 나누어서 각각을 정렬하는 것이다. 이렇게 분할하는 과정은 1개의 숫자가 될때까지 진행하고 1개씩 모두 나뉘어졌다면, 이제 정렬한 결과를 다시 하나로 합치는 과정을 반복한다.


 

MergeSort 예시로 쉽게 이해하기

 

예를 들어

초기 상태

위와 같은 배열을 정렬하고 싶다면, 이를 두개씩 두 그룹으로 먼저 나누는 것이다.




반으로 분할

하나의 그룹의 개수가 1개가 될때까지 분할을 계속해야 하기 때문에 한 번 더 진행해준다. 


분할 한 번 더 진행

분할을 한 번 더 진행해주었더니 각 그룹의 원소 개수가 1개씩 잘 분할이 되었다. 그러면 이제 더 이상 분할할 수 없기 때문에 합치는 과정을 진행해준다. 합칠 때 정렬을 하면서 합쳐주는 것이다.


 


합치는 과정

위의 블락을 합치는 과정에서 정렬을 해주었다. 특히 1과 3은 원래 3, 1 순서였는데 오름차순으로 정렬하면서 1,3으로 정렬되었다. 원래 개수인 4개가 모두 합쳐질 때까지 합치는 과정을 진행해주어야 하기 때문에 한 번 더 진행해준다.


최종 정렬된 모습

두 그룹을 합치면서 순서를 정렬해주어, 최종적으로 원래 개수인 4개 숫자가 모두 오름차순으로 정렬된 것을 볼 수 있다. 

 



합쳐주는 알고리즘 구현


그렇다면, 위에서 그림으로 설명할 때는 합치는 알고리즘을 따로 구현하지 않고 말과 그림으로만 설명했는데 코드로는 어떻게 작성할 수 있을까?


예시 그림

 

만약 위와 같이 start와 end가 있고 이를 합쳐야 한다고 가정해보자.

그렇다면 4개의 원소를 정렬해서 저장할 배열 하나가 추가적으로 필요하다.

저장할 배열

그러면 위와 같이 정렬된 배열을 저장할 새로운 배열을 만들어주고 여기에 정렬된 결과를 저장하면 된다. 다 저장한 이후에는 원래 배열에다가 하나씩 옮겨주면 된다. 

 

이 합치는 과정을 코드로 한번 구현해보았다.

 

void merge(int start, int end){
  int mid=(start+end)/2;
  int i=start; //첫번째 그룹의 index
  int j=mid+1; //두번째 그룹의 index
  int k=0; //저장할 배열의 index
  while(i<=mid && j<=end){
   if (a[i]<=a[j]) b[k++] = a[i++]; //첫번째 그룹 수가 더 작아서 먼저 들어가야 할 경우
   else {
    b[k++]=a[j++]; 
   }
  }
  while(i<=mid) b[k++] =a[i++]; //두번째 그룹은 다 정렬되어 추가되었는데 첫번째 그룹이 다 안들어갔을 경우
  while(j<=end) b[k++] =a[j++]; //두번째 그룹이 다 안들어갔을 경우
  
  for(int i=start; i<=end; i++){
   a[i]=b[i-start]; //다시 원래 배열에 정렬된 값을 저장해줌
  }
}

두 그룹의 값을 비교하면서 말 그대로 정렬해주는 과정이다. 만약 다 정렬된 다음에 한 그룹의 숫자가 남았다면 차례대로 넣어주면 된다. 

 

이렇게 합쳐주고 나서 이후에 다시 원래 index에 정렬된 숫자를 넣어주는 과정이 필요하다. 즉 위의 코드에서 b 배열은 임시로 정렬된 숫자를 저장하기 위해 만들어준 배열인 것이다. 



분할하는 알고리즘 구현

 

위와 같은 과정이 분할을 한 뒤에 합치는 과정이었다면, 분할을 하는 과정의 코드는 어떻게 구현할 수 있을까?

void divide(int start, int end){
 if (start==end) return; //수의 개수가 1개면 이제 그만 분할함
 int mid=(start+end)/2; //반반 나눔
 divide(start, mid); //그 중 왼쪽
 divide(mid+1, end);  // 그 중 오른쪽 그룹
 merge(start, end); //이후 둘을 합침 (위에서 작성한 merge 함수 호출)
}

이렇게 재귀적으로 mid를 기준으로 왼쪽 오른쪽 그룹을 나누어서 결국 그룹의 수가 1개가 될때까지 divide를 실행해준다. 그런 다음에 merge 함수를 호출해서 divide한 그룹을 다시 정렬하면서 합쳐준다. 



MergeSort의 시간복잡도  : O(nlogn) 

 

계속 반씩 나누어서 divide를 해주기 때문에 기본적으로 O(logN)의 시간복잡도를 가지고 있고, 거기에 추가하여서 합치는 과정이 O(N)의 시간복잡도를 갖기 때문에 전체적으로 MergeSort는 O(nlogn)의 시간복잡도를 가지고 있다고 할 수 있다.