본문 바로가기

Dot Algo∙ DS/PS

[BOJ] 백준 1517번 버블소트 (Java)

#1517 버블 소트

난이도 : 골드 1

유형 : 자료 구조 / 좌표 압축 / 세그먼트 트리 || 합병 정렬

 

1517번: 버블 소트

첫째 줄에 N(1≤N≤500,000)이 주어진다. 다음 줄에는 N개의 정수로 A[1], A[2], …, A[N]이 주어진다. 각각의 A[i]는 0≤|A[i]|≤1,000,000,000의 범위에 들어있다.

www.acmicpc.net

▸ 문제

N개의 수로 이루어진 수열 A[1], A[2], …, A[N]이 있다. 이 수열에 대해서 버블 소트를 수행할 때, Swap이 총 몇 번 발생하는지 알아내는 프로그램을 작성하시오.

버블 소트는 서로 인접해 있는 두 수를 바꿔가며 정렬하는 방법이다. 예를 들어 수열이 3 2 1 이었다고 하자. 이 경우에는 인접해 있는 3, 2가 바뀌어야 하므로 2 3 1 이 된다. 다음으로는 3, 1이 바뀌어야 하므로 2 1 3 이 된다. 다음에는 2, 1이 바뀌어야 하므로 1 2 3 이 된다. 그러면 더 이상 바꿔야 할 경우가 없으므로 정렬이 완료된다.

 입력

첫째 줄에 N(1≤N≤500,000)이 주어진다. 다음 줄에는 N개의 정수로 A[1], A[2], …, A[N]이 주어진다. 각각의 A[i]는 0≤|A[i]|≤1,000,000,000의 범위에 들어있다.

 출력

첫째 줄에 Swap 횟수를 출력한다

 

문제 풀이  

📚 조건

  • 배열의 크기 N ( 1<= N <= 500,000)
  • 원소 범위 A[i] ( 0 <= |A[i]| <= 1,000,000,000)

버블 정렬 연산횟수를 구하는 문제이지만 버블 정렬을 사용해서는 안된다. 해당 문제의 N의 범위가 상당히 크므로 버블소트의 시간복잡도는 O(N^2)이기 때문에 시간초과가 발생한다.

 

버블 정렬

배열 [6 4 3 5 2 1]을 버블 정렬을 사용해서 오름차순 시켜보자.

초기 상태

 

버블 정렬은 배열 인덱스 0부터 선형탐색으로 n-1까지 움직여 정렬하는 방식으로 동작한다. 

원소6 연산횟수: 5
원소5,4 연산횟수 : 3
원소4 연산횟수 : 2
원소3 연산횟수 : 2
원소2 연산횟수 : 1

 

 

과정을 보면 원소들은 앞에 있는 원소들 중 자신보다 낮은 수의 만큼 연산하는 것을 알 수 있다.

  • 원소 6은 {4,3,5,2,1}보다 크기 때문에 총 5번
  • 원소 5는 {2,1}보다 크기 때문에 총 2번
  • 원소 4는 {3,2,1}보다 크기 때문에 총 3번
  • 원소 3은 {2,1}보다 크기 때문에 총 2번
  • 원소 2는 {1}보다 크기 때문에 총 1번
  • 원소 1은 0번

→ 연산 횟수는 총 13이다.

 

결국 버블 정렬 연산횟수는 자신보다 앞에 있는 수의 갯수를 세어주면 되는 것을 발견할 수 있다. 이 패턴을 이해했으면 이젠 세그먼트 트리로 해당 문제를 풀이할 수 있게 된다. 

 

세그먼트 트리

세그먼트를 이용해서 앞에 자신보다 큰 값이 몇개나 들어있는지 트리의 구조를 이용해서 구하면 O(logN)으로 탐색가능하다. 해당 N번 반복하면 되므로 총 O(NlogN)으로 로직을 짤 수 있다.

 

좌표 압축하기

원소 범위 A[i] ( 0 <= |A[i]| <= 1,000,000,000) 이기 때문에 트리 원소로 담기에는 너무 부담되는 크기이다. 그래서 해당 배열 원소에 index를 붙여줘서 N의 최대 크기인 50만으로 제한을 해준 다음에 트리에 담아 줄 것이다.

for(int i=0; i<n; i++) {
	elements[i] = Integer.parseInt(st.nextToken());
}
        
// <K, V> = <segment tree index, elements in queue (중복 값 처리)>
Map<Long, Queue<Integer>> pos = new HashMap<>();
for (int i = 0; i < n; i++) {
    pos.computeIfAbsent(elements[i], k -> new LinkedList<>()).offer(i);
}
idx 0 1 2 3 4 5
elements 6 4 3 5 2 1

 

elements배열을 오름차순으로 정렬해준다. 작은 수 부터 세그먼트 트리에 넣어주어야 자신보다 앞의 구간 [idx+1 ~ n-1]에 자신보다 작은 수가 몇 개 있는지 카운트할 수 있기 때문이다. 물론 역으로도 탐색이 가능하다.

 

1. 원소 1 카운트

먼저 원소 1보다 앞에 있는 수는 없으므로 카운트는 0이다. 그리고 원소 1의 elements의 위치는 5이므로 해당 위치에 트리 +1을 업데이트 해준다.

원소1 카운트 (원래 위치5)

 

 

2. 원소 2 카운트

원소 2의 위치는 4이므로 (4,5]의 구간에 수를 카운트해준다. 카운트 +1

그리고나서 4번 위치에 해당하는 트리를 업데이트 해준다. 중요한 건 카운트를 먼저 해주고나서 트리를 업데이트해줘야 한다.

원소2 카운트 (원래 위치 4)

 

 

3. 원소 3 카운트

원소 3의 위치는 2이므로 [3,5] 구간을 카운트해준 다음 트리를 업데이트 해준다. 카운트 +2  (3번노드)

원소3 카운트 (원래 위치 2)

3. 원소 4 카운트

원소 4의 위치는 1이므로 [2,5] 구간을 카운트해준 다음 트리를 업데이트 해준다. 카운트 +3 (5번, 3번노드)

원소 4 카운트 (원래 위치1)

 

3. 원소 5 카운트

원소 5의 위치는 3이므로 [4,5] 구간을 카운트해준 다음 트리를 업데이트 해준다. 카운트 +2 (7번, 13번노드)

 

원소 5 카운트 (원래 위치 3)

3. 원소 6 카운트

원소 6의 위치는 0이므로 [1,5] 구간을 카운트해준 다음 트리를 업데이트 해준다. 카운트 +5 (3번, 5번, 9번 노드)

원소 6 카운트 (원래 위치 0)

 

 

연산과정 테이블

elements order idx 구간 (idx+1 ~ n-1) 연산횟수
1 5 6~5 0 0
2 5~5 +1
3 3~5 +2
4 2~5 +3
5 4~5 +2
6 0 1~5 +5

 

 

풀이 코드 (세그먼트 트리) - 2024.01 update 중복 요소 처리 

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

public class Main {

	static int n;
	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		n = Integer.parseInt(br.readLine());
		long[] elements = new long[n];
		StringTokenizer st = new StringTokenizer(br.readLine());
		for(int i=0; i<n; i++) {
			elements[i] = Integer.parseInt(st.nextToken());
		}

		// <K, V> = <segment tree index, elements in queue (중복 값 처리)>
		Map<Long, Queue<Integer>> pos = new HashMap<>();
		for (int i = 0; i < n; i++) {
			pos.computeIfAbsent(elements[i], k -> new LinkedList<>()).offer(i);
		}

		long[] index = elements.clone();
		Arrays.sort(index);

		long[] tree = new long[getTreeSize()];
		long ans =0;
		for(int i=0; i<n; i++)	{
			int idx = pos.get(index[i]).poll();
			ans += sum(tree,0, n-1, 1, idx+1, n-1);
			update(tree, 0, n-1, 1, idx, 1);
		}
		System.out.println(ans);
	}
	
	static int getTreeSize() {
		int h = (int)Math.ceil(Math.log(n)/Math.log(2))+1;
		return (int)Math.pow(2, h);
	}
	
	static long sum(long[] tree, int start, int end, int node, int left, int right) {
		if(end < left || right < start ) return 0;
		if(left <= start && end <= right) {
			return tree[node];
		
		}
		
		int mid = (start+end)/2;
		return sum(tree, start, mid, node*2, left, right) + sum(tree, mid+1, end, node*2+1, left, right);
	}
	
	static void update(long[] tree, int start, int end, int node, int idx, int dif) {
		if(start == end ) {
			tree[node] = dif;
			return;
		}
		int mid = (start+end)/2;
		if(idx <= mid) update(tree, start, mid, node*2, idx, dif);
		else update(tree, mid+1, end, node*2+1, idx, dif);
		
		tree[node] = tree[node*2]+tree[node*2+1];
	}
	
}

 

 

합병 정렬

합병 정렬도 위와 같은 방식으로 로직을 설계해주면 O(NlogN)으로 버블 정렬의 연산횟수를 구할 수 있다.

 

주어진 배열을 모두 divide한 후 다시 merge하는 과정에서 elements[i] > elements[j] 앞에 자신보다 큰 수 있는 경우 그만큼 count해주면 된다.

합병 정렬로 버블 정렬 연산횟수 구하는 과정

 

 

풀이 코드 (합병 정렬)

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

public class Main {

	static long ans;
	static long[] elements, sorted;
	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		int n = Integer.parseInt(br.readLine());
		elements = new long[n];
		StringTokenizer st = new StringTokenizer(br.readLine());
		for(int i=0; i<n; i++) {
			elements[i] = Integer.parseInt(st.nextToken());
		}
		
		sorted = new long[n];
		divide(0,n-1);
		System.out.println(ans);
		
		
	}
	static void divide(int low, int high) {
		
		if(low < high) {
			int mid = (low+high)/2;
			
			divide(low, mid);
			divide(mid+1, high);
			
			merge(low, mid, high);
		}
	}
	
	static void merge(int low, int mid, int high) {
		int i = low;  // 왼쪽 시작 인덱스 
		int j=  mid+1; // 오른쪽 시작 인덱스 
		int k = low; // sorted배열 인덱스
		
		while(i<= mid && j <= high) {
			if(elements[i] <= elements[j]){
				sorted[k] = elements[i];
				i++;
			}else { // elements[i] > elements[j] 앞에 자신보다 큰 수 있는 경우
				sorted[k] = elements[j];
				j++;
				ans += (mid+1-i); // 연산횟수 카운트
			}
			k++;
		}
		
		// 나머지 합병과정 처리
		while(i <= mid) {
			sorted[k] = elements[i];
			i++;
			k++;
		}
		
		while(j <= high) {
			sorted[k] = elements[j];
			j++;
			k++;
		}
		
		for(int m=low; m<high+1; m++) {
			elements[m] = sorted[m];
		}
		
	}
	
}