728x90
반응형

https://programmers.co.kr/learn/courses/30/lessons/12921

 

코딩테스트 연습 - 소수 찾기

1부터 입력받은 숫자 n 사이에 있는 소수의 개수를 반환하는 함수, solution을 만들어 보세요. 소수는 1과 자기 자신으로만 나누어지는 수를 의미합니다. (1은 소수가 아닙니다.) 제한 조건 n은 2이상

programmers.co.kr

[문제 설명]

1부터 입력받은 숫자 n 사이에 있는 소수의 개수를 반환하는 함수, solution을 만들어 보세요.

소수는 1과 자기 자신으로만 나누어지는 수를 의미합니다.
(1은 소수가 아닙니다.)

[제한 조건]

  • n은 2이상 1000000이하의 자연수입니다.

입출력 예

n result
10 4
5 3

입출력 예 설명

입출력 예 #1
1부터 10 사이의 소수는 [2,3,5,7] 4개가 존재하므로 4를 반환

입출력 예 #2
1부터 5 사이의 소수는 [2,3,5] 3개가 존재하므로 3를 반환

 

[문제 개요]

 소수는 자신과 1이외의 정수로 나누어떨어지지 않는 정수이다. 예를 들어 소수인 13은 2,3,...,12 가운데 어떤 정수로도 나누어 떨어지지 않는다. 그러므로 어떤 정수 n에 대하여 아래의 조건을 만족하면 소수임을 알 수 있다. 

2부터 n-1까지의 어떤 정수로도 나누어떨어지지 않는다.

만약 나누어떨어지는 정수가 하나 이상 존재하면 그 수는 합성수(composite number)이다. 

 

[풀이 과정]

알고리즘 개선 과정 : [풀이 1] -> [풀이 2] -> [풀이 3],[풀이 4]

 

[풀이 1]

첫번째 풀이는 제일 처음에 풀었던 방식인데 테스트 12부터 시간 초과가 나고 효율성에서 0점을 받았다.. 

 

최대공약수 함수 gcd를 먼저 만들어 놓는다. 판별하고 싶은 수(n) 전까지 모두 gcd에서 return 1이 나오면 된다고 생각해서 풀었던 방식이다. temp 변수를 0으로 초기화 해두어서 gcd가 1이 나올경우 temp++해준다. 만약 temp가 n-1이 된다면 이 수를 '소수' 라고 판단해주는것이다. (할때부터 시간 초과가 날 것 같았고, 그냥 머릿속에 떠오른 대로 푼 방식이다)

여기서 시간 복잡도는 O(N) 이다. 

#include <string>
#include <vector>

using namespace std;
int gcd(int a, int b){
    int c;
    while(b!=0){
        c=a%b;
        a=b;
        b=c;
    }
    return a;
}
int solution(int n) {
    int answer = 0;
    for(int i=2;i<=n;i++){
        int temp=0;
        for(int j=1;j<i;j++){
            if(gcd(i,j)==1){
                temp++;
            }else break;
        }
        if(temp==(i-1)) answer++;
    }
    return answer;
}

비슷한 경우 : 1000이하의 소수를 나열하는 프로그램.

#include <iostream>
using namespaces std;

int main(void){
    for(int n=2;n<=1000;n++){
    	for(int i=2;i<n;i++){
        	if(n%i==0)//나누어떨어지면 소수가 아니다
            	break;//더 이상의 반복은 불필요
        }
        if(n==i)
        	cout<<n<<endl;
    }
    return 0;
}

for문의 반복이 종료된 시점에서 변수의 값은 아래와 같다.


n이 합성수(소수가 아닌) 경우 : n보다 작은 값 (for문이 중단됨)

[풀이 2] : 테스트 15까지 통과했지만 효율성 0점..

위의 상자에서 힌트를 얻었다. n이 2또는 3으로 나누어 떨어지지 않으면 2X2인 4 또는 2X3인 6으로도 나누어떨어지지 않는다. 그러므로 이 프로그램은 불필요한 나눗셈을 실행하고 있음을 알 수 있다. 즉, 정수 n이 소수인지의 여부는 아래 조건을 만족하는지 조사하면 된다.

2부터 n-1까지의 어떤 소수로도 나누어떨어지지 않는다.

예를 들어, 7이 소수인지는 7보다 작은 소수 2,3,5로 나눗셈을 실행하면 충분하다. 이 아이디어를 이용하여 시간을 줄여보겠다.

#include <string>
#include <vector>

using namespace std;

int solution(int n) {
    int answer = 1;
    vector<int> prime;// 소수를 저장하는 배열
    prime.push_back(2);// 2는 소수 -> answer=1에서 시작
    for(int i=3;i<=n;i+=2){// 홀수만을 대상으로 한다
        int j;
        for(j=0;j<prime.size();j++){// 이미 구한 소수 배열의 크기까지 반복문
            if(i%prime[j]==0)// 나누어 떨어지면 소수가 아니다
                break;
        }
        if(j==prime.size()) {// 마지막까지 나누어떨어지지 않으면
            prime.push_back(i);// 배열에 저장한다.
            answer++;// 소수 개수는 증가시켜줌
        }
    }
    return answer;
}

소수를 구하는 과정에서 그때까지 구한 소수를 배열 prime의 요소로 저장한다. 이 때, n이 소수인지를 판단할 때 쌓아 놓은 소수로 나눗셈을 한다. 먼저 2는 소수이므로 prime에 저장한다. 3이상의 소수는 이중 for문으로 구한다. 바깥쪽 for문은 n의 값을 2씩 증가하여(짝수는 2의 배수이므로 해줄 필요 x) 3,5,7,9...,999로 홀수 값만을 생성한다.

 

[풀이 3] : 효율성까지 통과

100을 두 숫자의 곱셈으로 나타내면 어떻게 될까 -> ① 2x50 ② 4X25 ③ 5X20 ④ 10X10 ⑤ 20X5 ⑥ 25X4 ⑦ 50X2

여기서 ① ~ ④까지의 나눗셈만 수행하면 된다.이것은 넓이가 100인 직사각형의 가로,세로 길이와 같다.

예를 들어, 2X50과 50X2 는 가로, 세로가 다르지만 같은 직사각형으로 볼 수 있다. 100은 10X10을 중심으로 대칭 형태를 이루고 있다. 이는 소수임을 판단할때, 정사각형의 한 변의 길이까지만 소수로 나눗셈을 시도하면 된다는 뜻이다. ( 나머지는 대칭이라고 보면 된다) 이 과정에서 한 번도 나누어떨어지지 않으면 소수라고 판단할 수 있게 되는 것이다. 이러한 성질을 이용하여 제곱근을 한 변으로 하는 이후의 직사각형에 대한 계산량을 줄이는 것이 핵심이다. 

 즉, 어떤 정수 n은 다음 조건을 만족하면 소수라고 판단할 수 있다.

n이 소수일 조건은 n의 제곱근 이하의 어떤 소수로도 나누어떨어지지 않는다.

[풀이2]에서 prime[i]를 n의 제곱근 이하의 수들만 판단하면 된다는 것이다.

 예를 들어 첫 번째 경우에서 10이하의 소수를 구할려면 10의 제곱근은 3.xxx 이므로 3이하의 소수로 나누어 떨어지는지 판단하면 된다. 

#include <string>
#include <vector>

using namespace std;

int solution(int n) {
    vector<int> prime;// 소수를 저장하는 배열
    prime.push_back(2);// 2는 소수
    for(int i=3;i<=n;i+=2){// 홀수만을 대상으로 한다
        bool flag=true;//나누어 떨어지는지 확인할 변수
        for(int j=0;prime[j]*prime[j]<=i;j++){// prime[j]의 제곱이 i이하인가 판단
            if(i%prime[j]==0){// 나누어 떨어지면 소수가 아니다
                flag=false;
                break;
            }
        }
        if(flag) {// 마지막까지 나누어떨어지지 않으면
            prime.push_back(i);// 배열에 저장한다.
        }
    }
    return prime.size();
}

 

[풀이 4]

4번째 풀이는 알고리즘에 '에라토스테네스의 체' 라는 효율적인 방법으로 풀었다. 에라토스테네스의 체는 2부터 소수를 구하고자 하는 구간의 모든 수를 나열하여, 자기 자신을 제외한 배수들을 모두 지운다. 예를 들어 2부터 시작할 때, 2는 자기 자신을 제외한 2의 배수를 모두 지운다. 그 다음, 남아있는 수 가운데 3은 소수이므로 자기 자신을 제외한 3의 배수를 모두 지운다. 앞서 [풀이 3]에서 제곱근 만큼만 하면 나머지는 대칭이므로 위의 과정을 구하려는 n의 제곱근 이하의 자연수만큼만 반복하면 구하는 구간의 모든 소수가 남는다. 

#include <string>
#include <vector>
using namespace std;

int solution(int n)
{
    bool flag[100000];
    int answer = 0;
    for(int i=2;i<= n;i++){
    	flag[i] = true;
    }
    
	for(int i=2;i*i<=n;i++)
	{
		if (flag[i]){
        	for (int j=i*i;j<=n;j+=i){
            	flag[j] = false;
            }
        }
    }
    for(int i=2;i<=n;i++)
    {
    	if(flag[i]) answer++;
    }
    return answer;
}

 

728x90
반응형

+ Recent posts