반응형

https://www.acmicpc.net/problem/2751

 

2751번: 수 정렬하기 2

첫째 줄에 수의 개수 N(1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄부터 N개의 줄에는 수가 주어진다. 이 수는 절댓값이 1,000,000보다 작거나 같은 정수이다. 수는 중복되지 않는다.

www.acmicpc.net

해당 문제는 수의 개수가 100만까지 있다. N^2로 하면 시간초과가 날게 뻔하다. 정렬 알고리즘 중에서 최대 NlogN까지의 시간복잡도를 갖는 애로 가야한다.

 

1) 퀵소트로 풀어봤다.

#include <stdio.h>

void quick (int arr[], int start, int end)
{
	if (start>=end)
    {
    	return;
    }
    
    int key = start;
    int i = start+1;
    int j = end;
    int change;
    
    while (i<=j)
    {
    	while (arr[key]<=arr[i])
        {
        	i++;
        }
        
        while (arr[j]>arr[key] && j>key)
        {
        	j--;
        }
        
        if (i>j)
        {
        	change = arr[j];
            arr[j] = arr[key];
            arr[key] = change;
        }
        
        else
        {
        	change = arr[j];
            arr[j] = arr[i];
            arr[i] = change;
        }
    }
    
    quick (arr,start,j-1);
    quick (arr,j+1,end);
}

int main()
{
	int i,number = 0;
	scanf("%d",&number);
	int arr[number];
	for (i=0; i<number; i++)
	{
		scanf("%d",&arr[i]);
	}
	quick (arr, 0 , number-1);
	for (i=0; i<number; i++)
	{
		printf("%d\n",arr[i]);
	}
	return 0;
}

근데 퀵소트로 푸니까 시간초과가 나왔다. 아마 퀵소트의 경우는 Best, Avg케이스는 NlogN이지만 Worst케이스는 N^2가 나올 수 있기 때문인 것 같다.

 

따라서, NlogN 정렬 중 합병정렬이나 힙 정렬로 접근해보고자 한다.

 

 

-> 합병정렬

#include <stdio.h>

int sorted[1000000];

void merge (int arr[], int start, int mid, int end)
{
	int i = start;
	int j = mid+1;
	int k = start;
	
	while(i<=mid && j<=end)
	{
		if (arr[i]<=arr[j])
		{
			sorted[k] = arr[i];
			i++;
		}
		else
		{
			sorted[k] = arr[j];
			j++;
		}
		k++;
	}
	
	if (i>mid)
	{
		int t;
		for (t=j; t<=end; t++)
		{
			sorted[k] = arr[t];
			k++;
		}
	}
	
	else
	{
		int t;
		for (t=start; t<=mid; t++)
		{
			sorted[k] = arr[t];
			k++;
		}
	}
	int t;
	for (t=start; t<=end; t++)
	{
		arr[t] = sorted[t];
	}
	
}

void merge_sort (int arr[], int start, int end)
{
	if (start<end)
	{
		int mid = (start+end)/2;
		merge_sort (arr,start,mid);
		merge_sort (arr,mid+1,end);
		merge (arr,start,mid,end);
	}
}

int main()
{
	int i,number = 0;
	scanf("%d",&number);
	int arr[number];
	
	
	for (i=0; i<number; i++)
	{
		scanf("%d",&arr[i]);
	}
	
	merge_sort (arr,0,number-1);
	for (i=0; i<number; i++)
	{
		printf("%d\n",arr[i]);
	}
	
	return 0;
}
반응형
반응형

 

풀이에 사용한 언어 : C언어

그리디 알고리즘
- 현재 주어진 Step만을 기준으로 하여, 최적의 해를 구하는 알고리즘이다.
다만, 매 Step마다 최적의 해를 구했다해도 최종 결과는 최적의 해가 아닐 수 있다. 그래서 불완전한 알고리즘이며 특수한 경우에만 그리디 알고리즘을 쓴다.

대표적인 예제가 거스름돈이다. 우리가 맥도날드에서 상하이스파이시버거세트를 산다하자. 한 때, 굉장히 좋아했는데 이사한 집 근처에 맥날이 없어서 가격이 기억안나지만 대충 5500원이였던 것 같다.
10000원을 지불하고 4500원을 거슬러 받는다 하면, 일반적으로 1000원 4장, 500원 1개로 총 5번의 거스름을 받는게 맞다. 화폐는 그 단위가 5000,1000,500,100... 이런 식으로 정해져 있고 내림차순 (혹은 오름차순)이기 때문에 그리디를 적용해도 괜찮다.

아래 코드 내용은, 돈을 입력받았을 때, (예를 들면 3100원이라 하자) 500원,100원,50원,10원만 갖고 있을 때 가장 최소한으로 거스름을 한 결과를 구현하고자 했다.

물론 아래 코드는 실행해보면 잘못되었음을 알 수 있다.

#include <stdio.h>

int main()
{
	int money;
	scanf("%d",&money);
	int n,m,k,l=0;
	
	while(money>0)
	{
		money = money%500;
		n = money/500;
		
		if (money<500)
		{
			money = money%100;
			m = money/100;
			if (money<100)
			{
				money = money%50;
				k = money/50;
				if (money<50)
				{
					money = money%10;
					l = money/10;
				}
			}
		}
		printf("%d",n+m+k+l);
		
	}
	
	return 0;
}

그럼 왜 틀렸는지 분석을 해봤는데, 단순히 코드의 순서가 잘못되었다고 판단 중이다.
보면 %연산자를 먼저한 값을 money에 집어넣고 그 다음 몫연산자를 카운팅했으니 값이 이상하게 나올 수 밖에 없다.
또한, if문은 필요가 없다. 반복을 수행하면서 알아서 500원 이하면 100으로 넘어갈테니까.

#include <stdio.h>

int main()
{
	int cnt = 0;
	int money;
	scanf("%d",&money);
	
	while (money>0)
	{
		cnt += money/500;
		money = money%500;
		
		cnt += money/100;
		money = money%100;
		
		cnt += money/50;
		money = money%50;
		
		cnt += money/10;
		money = money/10;
	}
	
	printf("%d",cnt);
	
	return 0;
}

그래서 재구성해본 결과는 이러하다.

잘 나온다. 4700원을 먼저 500원으로 거스르면 9번이고 나머지 200원은 100원으로 거스르면 2번이니까 총합 11번이다.
결과는 같다.

반응형
반응형

알고리즘 공부를 시작하다보면 가장 먼저 마주하는 용어가 '시간 복잡도' 입니다. 이 시간복잡도가 어떤 꼴로 나오냐에 따라 알고리즘의 좋고나쁨을 알 수 있기 때문입니다.

 

우리 모두 수학을 싫어하는건 알지만 그래프를 하나 보도록 하겠습니다.

시간복잡도 그래프

너무 잘 나와있어서 설명이 필요없는 그래프긴 하나, 살펴보겠습니다.

X축은 Example Scale이라 보시면 되겠습니다. 얼마나 많은 양을 처리해야하는지인데요. Y축을 보면 그 양을 처리하는데 걸리는 시간이 나와있습니다.


더보기

어? 그러면 똑같은 문제의 양이 주어졌어도 푸는데 걸리는 시간이 짧을수록 좋겠네?

맞습니다. 위 그래프에 따르면 지수 함수에 가까워질 때 비효율적이고  로그 함수에 가까워질 때 효율적임을 보입니다.


더보기

그러면 어떤 이유에서 각각의 알고리즘의 꼴이 지수함수가 되고 로그함수가 되는거죠?

이 질문에 대한 설명을 위해 오늘 처음으로 만나볼 알고리즘은 '거품 정렬 (Bubble Sort)' 입니다.

왜 이름이 거품 정렬이냐면 정렬을 위해 숫자들이 바쁘게 움직이며 자리를 찾아가는 모습이  비눗방울이 날아다니는것 같다해서 붙여진 이름입니다. 

거품 정렬 <출처 - 👨🏻‍💻 Tech Interview (gyoogle.dev) 김규석님>

저는 봐도 잘 와닿진 않는데, 암튼 그렇게 이름이 붙여졌답니다.

거품 정렬은 가장 간단한 알고리즘입니다. 이 알고리즘은 주어진 데이터들을 짝지어서 모두 한번씩 비교를 해서 크고작음을 판단합니다. 여기까지만 읽어봐도 거품 정렬이 비효율적이구나라는 것이 느껴지실겁니다. 

만약 방대한 데이터를 다루게 될 때, 거품 정렬을 쓴다면 언제 그걸 다 일일이 비교하며 정렬해갈까요.

 

그래서 거품 정렬은 현업에서는 절대 안씁니다. 그러면 거품 정렬에 대한 시간복잡도 형태를 구해보도록 하겠습니다.

1부터 10까지 무작위로 나열된 데이터가 주어졌다고 생각합시다. 이를 오름차순 정렬하려고 해요. 만약 거품 정렬을 적용한다면  왼쪽에서 오른쪽으로 진행하면  내 오른쪽에 있는 숫자가 나보다 작다! 하면 자리를 바꿉니다.  이걸 오른쪽 끝까지 도달할 때까지 계속 반복하구요. 다시 왼쪽에서부터 시작해서 정렬을 또 반복합니다.

 

그러면 3 1 5 7 9 10 6 2 4 8 이라는 데이터에서 좀 더 우리가 진행해보면

3 1 5 7 9 6 10 2 4 8

3 1 5 7 9 6 2 10 4 8

3 1 5 7 9 6 2 4 10 8

3 1 5 7 9 6 2 4 8 10  으로 됩니다.  그러면 이제 다시 3으로 돌아가서 또 정렬을 반복하는겁니다.

 

즉, 데이터의 개수가 N개라 할 때 한번의 반복이 끝나더라도 또 N-1.. N-2... N-3... N-4...  마지막 1번의 반복.  이렇게가야 1 2 3 4 5 6 7 8 9 10 으로 정렬이 완성될 것입니다.

그림판 크기가 작아서 여기선 7개의 데이터 (민트색) 까지만 그려놨는데요. 7개의 데이터들이 자기 자신을 제외한 남은 데이터 모두와 비교분석을 하는데까지 걸리는 횟수는 총 21번입니다. 즉,  N(N-1)/2의 공식이 나오게 됩니다. N에다가 7을 넣으면  7개의 데이터를 분석하는데 걸린 횟수가 21. 라고 나오는거죠.

 

즉, 데이터 개수에 '제곱' 꼴이 되는 것입니다. 따라서 거품 정렬의 시간복잡도는 N^2입니다.

  Best Average Worst
1. Bubble Sort (New!) N^2 N^2 N^2

 

그러면 이제 소스코드를 한번 보겠습니다.

정렬된 모습 (거품 정렬) / 사용 언어 : C

// # 0. Bubble Sort

#include <stdio.h>  // 사용 언어 : C


int main()

{
	int i,j,change;
	int num[10] = {3,9,1,4,10,7,8,2,5,6};
	
	for (i=0; i<10; i++)
	{
		for (j=0; j<i; j++)
		{
			if (num[i]<num[j])
			{
				change = num[i];
				num[i] = num[j];
				num[j] = change;
			}
		}
	}
	
	for (i=0; i<10; i++)
	{
		printf("%d ",num[i]);
	}
	
	
	return 0;
}

눈여겨 볼 점은 이중 for문과 if함수 블록에 있는 Swap구문입니다.

 

1) Outer Loop의 for문은 데이터가 총 10개이므로 10번 반복을 한다고 보시면 되구요.

2) Inner Loop의 for문은 두 수를 묶어서 비교를 해가는 것이므로 조건식을 j<i라고 해두었습니다.

 

i = 0인 경우엔 두 숫자가 아니므로 실행이 안되고,  i=1인 경우엔 j=0이 실행됩니다. 그러면 

num[i]는 -> num[1] / num[j]는 -> num[0]가 되니까  num[i]<num[j]인 경우는  왼쪽에 있는 숫자가 오른쪽에 있는 숫자보다 클 경우를 의미합니다.

 

즉, 오름차순 정렬을 원할 경우 num[i]<num[j]에 해당하는 애들을 swap시켜라! 라는 겁니다.

swap을 시키기 위해서는 두 수도 있지만 이 두 수를 swap시켜줄 매개체가 필요합니다. 그게 change 변수입니다.

change = num[i];
num[i] = num[j];
num[j] = change;

swap구문의 가장 기초적인 틀입니다. 어려울게 없이 매개체를 이용해서 두 수의 자리를 바꿔준 것입니다.

반응형

+ Recent posts