이전문제와 비슷한 문제

익명함수의 개념만 알면 어렵지 않은 문제였다.


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Comparator;
import java.util.StringTokenizer;

public class Main {

	public static void main(String[] agrs) throws IOException {

		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

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

		String[][] arr = new String[N][100];

		for (int i = 0; i < N; i++) {
			StringTokenizer st = new StringTokenizer(br.readLine());

			arr[i][0] = st.nextToken();
			arr[i][1] = st.nextToken();
		}

		Arrays.sort(arr, new Comparator<String[]>() {

			@Override
			public int compare(String[] o1, String[] o2) {
				return Integer.parseInt(o1[0]) - Integer.parseInt(o2[0]);
			}

		});

		for (int i = 0; i < N; i++) {
			System.out.println(arr[i][0] + " " + arr[i][1]);
		}

	}

}

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

 

10814번: 나이순 정렬

온라인 저지에 가입한 사람들의 나이와 이름이 가입한 순서대로 주어진다. 이때, 회원들을 나이가 증가하는 순으로, 나이가 같으면 먼저 가입한 사람이 앞에 오는 순서로 정렬하는 프로그램을

www.acmicpc.net

 

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

 

11650번: 좌표 정렬하기

첫째 줄에 점의 개수 N (1 ≤ N ≤ 100,000)이 주어진다. 둘째 줄부터 N개의 줄에는 i번점의 위치 xi와 yi가 주어진다. (-100,000 ≤ xi, yi ≤ 100,000) 좌표는 항상 정수이고, 위치가 같은 두 점은 없다.

www.acmicpc.net


 

문제 로직은 굉장히 단순했지만

람다식에 대한 개념?이 기억이 잘안나서 오래 걸렸던 문제였다.

나중에 다시 공부해야겠다

참고 블로그

https://mangkyu.tistory.com/113

 

[Java] 람다식(Lambda Expression)과 함수형 인터페이스(Functional Interface) - (2/5)

1. 람다식(Lambda Expression) 이란? Stream 연산들은 매개변수로 함수형 인터페이스(Functional Interface)를 받도록 되어있다. 그리고 람다식은 반환값으로 함수형 인터페이스를 반환하고 있다. 그렇기 때문

mangkyu.tistory.com

https://st-lab.tistory.com/243

 

자바 [JAVA] - Comparable 과 Comparator의 이해

아마 이 글을 찾아 오신 분들 대개는 Comparable과 Comparator의 차이가 무엇인지 모르거나 궁금해서 찾아오셨을 것이다. 사실 알고보면 두 개는 그렇게 어렵지 않으나 아무래도 자바를 학습하면서 객

st-lab.tistory.com

https://mangkyu.tistory.com/113

 

[Java] 람다식(Lambda Expression)과 함수형 인터페이스(Functional Interface) - (2/5)

1. 람다식(Lambda Expression) 이란? Stream 연산들은 매개변수로 함수형 인터페이스(Functional Interface)를 받도록 되어있다. 그리고 람다식은 반환값으로 함수형 인터페이스를 반환하고 있다. 그렇기 때문

mangkyu.tistory.com


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

public class Main {

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

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

		int[][] arr = new int[N][2];

		for (int i = 0; i < N; i++) {
			st = new StringTokenizer(br.readLine());

			arr[i][0] = Integer.parseInt(st.nextToken());
			arr[i][1] = Integer.parseInt(st.nextToken());
		}

		Arrays.sort(arr, (o1, o2) -> {
			if (o1[0] == o2[0]) {
				return o1[1] - o2[1];
			} else {
				return o1[0] - o2[0];
			}
		});

		for (int i = 0; i < N; i++) {
			System.out.println(arr[i][0] + " " + arr[i][1]);
		}

	}

}

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

 

2089번: -2진수

-2진법은 부호 없는 2진수로 표현이 된다. 2진법에서는 20, 21, 22, 23이 표현 되지만 -2진법에서는 (-2)0 = 1, (-2)1 = -2, (-2)2 = 4, (-2)3 = -8을 표현한다. 10진수로 1부터 표현하자면 1, 110, 111, 100, 101, 11010, 110

www.acmicpc.net

 


단순한게 최고라는 생각에 일반적인 2진법으로 나누었을때랑 같은 방법으로 -2를 나누어보았다.

예를들어

-13 / -2 = 6.5 -> 올림 7 (나머지 1)

7 / -2 = -3.5 -> 올림 -3 (나머지 1)

-3 / -2 = 1.5 -> 올림 2 (나머지 1)

2 / -2 = -1 -> -1 (나머지 0)

-1 / -2 = 0.5 -> 1 (나머지 1)

1. 마지막 몫이 1일때까지 반복되야 함.

2. 나머지는 항상 양수

3. 음수에서 계산이 되기때문에 소수점에서 올림 (ceil)

4. 마지막 1일때 몫을 따로 append 해주고 순서를 reverse 해야한다. 

위와 같은 방법으로 코드를 구현하였다.


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

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

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

		if (N == 0) {
			System.out.println(0);
		} else {
			while (N != 1) {

				sb.append(Math.abs(N % -2));
				N = (int) Math.ceil((double) N / (-2));

			}

			sb.append(N);

			sb.reverse();
			System.out.println(sb);
		}

	}
}

StringTokenizer 클래스란?

BufferedReader 클래스의 메서드로 입력을 읽어들이면, 라인 단위로 읽어들일 수밖에 없어요.

거기서 머 스페이스 기준으로 문자열을 분리한다던가 필요할때가 많겠죠?

 

BufferedReader 클래스가 아니더라도, 컴마로 구분되는 문자열들을 분리한다던가, 

특정 문자에 따라 문자열을 나누고 싶을 때에 StringTokenizer를 이용하실 수 있습니다. 

 

String : 문자열을

Tokenizer : 토큰화한다.

즉 토큰은 분리된 문자열 조각으로, 스트링토크나이저 클래스는 하나의 문자열을 여러 개의 토큰으로 분리하는 클래스인거죠.

 

StringTokenizer 생성자

StringTokenizer를 생성하는 방식에는 3가지가 있습니다.

생성자 설명
public StringTokenizer(String str); 절달된 매개변수 str을 기본(default) delim으로 분리합니다. 기본 delimiter는 공백 문자들인 " \t\n\r\t"입니다. 
public StringTokenizer(String str,String delim); 특정 delim으로 문자열을 분리합니다.
public StringTokenizer(String str,String delim,boolean returnDelims); str을 특정 delim으로 분리시키는데 그 delim까지 token으로 포함할지를 결정합니다. 그 매개변수가 returnDelims로 true일시 포함, false일땐 포함하지 않습니다.

StringTokenizer 메서드

앞에서 nextToken() 이용해 토큰값을 가져올 수 있다는 건 대충 알았어요.

이 외에 이 클래스에서 지원하는 메서드들은 뭐가 있을까요?

리턴값 메서드명 역할
boolean hasMoreTokens() 남아있는 토큰이 있으면 true를 리턴, 더 이상 토큰이 없으면 false 리턴
String nextToken() 객체에서 다음 토큰을 반환
String nextToken(String delim) delim 기준으로 다음 토큰을 반환
boolean hasMoreElements() hasMoreTokens와 동일한대 엘레먼트보다 토큰으로 된 메서드를 주로 씀
Object nextElement() nextToekn 메서드와 동일하지만 문자열이 아닌 객체를 리턴
int  countTokens() 총 토큰의 개수를 리턴

자주 사용하는 메서드는 hasMoreTokens, countTokens 그리고 nextToken 정도 되겠습니다.

 

StringTokenizer와 Split 차이?

둘 다 모두 문자열 파싱하는데 사용할 수 있습니다.

 

■ StringTokenizer는 java.util에 포함되어 있는 클래스, split는 String클래스에 속해있는 메소드이다.

■ StringTokenizer는 문자 또는 문자열로 문자열을 구분한다면, split는 정규표현식으로 구분합니다. 

■ StringTokenizer는 빈 문자열을 토큰으로 인식하지 않지만 split는 빈 문자열을 토큰으로 인식하는 차이가 있습니다.

■ Stringtokenizer는 결과값이 문자열이라면 split는 결과 값이 문자열 배열입니다. 따라서 StringTokenizer를 이용할 경우, 전체 토큰을 보고 싶다면 반복문을 이용해서 하나하나 뽑을 수 밖에 없어요.

■ 배열에 담아 반환하는 Split는 데이터를 바로바로 잘라서 반환해주는 StringTokenizer보다 성능이 약간 뒤쳐지겠죠? 그러나 데이터양이 많은 경우 거기서 거기기 때문에 크게 신경쓸 필요는 없습니다.

문자열을 다루를 대표적인 클래스 String, StringBuffer, StringBuilder 차이점

연산이 많지 않을때는 위에 나열된 어떤 클래스를 사용하더라도 이슈가 발생할 가능성은 거의 없다. 그러나 연산횟수가 많아지거나 멀티쓰레드, Race condition 등의 상황이 자주 발생 한다면 각 클래스의 특징을 이해하고 상황에 맞는 적절한 클래스를 사용해야 한다.
 

String  vs  StringBuffer/StringBuilder

 
String과 StringBuffer/StringBuilder 클래스의 가장 큰 차이점은 String은 불변(immutable)의 속성을 갖는다는 점입니다.
String str = "hello";   // String str = new String("hello");
str = str + " world";  // [ hello world ]
직관적이어서 가장 많이 사용할 듯한 위의 예제에서 "hello" 값을 가지고 있던 String 클래스의 참조변수 str이 가리키는 곳에 저장된 "hello"에 "world" 문자열을 더해 "hello world"로 변경한 것으로 착각할 수 있습니다.
하지만 기존에 "hello" 값이 들어가있던 String 클래스의 참조변수 str이 "hello world"라는 값을 가지고 있는 새로운 메모리영역을 가리키게 변경되고 처음 선언했던 "hello"로 값이 할당되어 있던 메모리 영역은 Garbage로 남아있다가 GC(garbage collection)에 의해 사라지게 되는 것 입니다. String 클래스는 불변하기 때문에 문자열을 수정하는 시점에 새로운 String 인스턴스가 생성된 것이지요.
위와 같이 String은 불변성을 가지기 때문에 변하지 않는 문자열을 자주 읽어들이는 경우 String을 사용해 주시면 좋은 성능을 기대할 수 있습니다. 그러나 문자열 추가,수정,삭제 등의 연산이 빈번하게 발생하는 알고리즘에 String 클래스를 사용하면 힙 메모리(Heap)에 많은 임시 가비지(Garbage)가 생성되어 힙메모리가 부족으로 어플리케이션 성능에 치명적인 영향을 끼치게 됩니다. (+연산에 내부적으로 char배열을 사용함)
이를 해결하기 위해 Java에서는 가변(mutable)성을 가지는 StringBuffer / StringBuilder 클래스를 도입했습니다.
String 과는 반대로 StringBuffer/StringBuilder 는 가변성 가지기 때문에 .append() .delete() 등의 API를 이용하여 동일 객체내에서 문자열을 변경하는 것이 가능합니다. 따라서 문자열의 추가,수정,삭제가 빈번하게 발생할 경우라면 String 클래스가 아닌 StringBuffer/StringBuilder를 사용하셔야 합니다.
StringBuffer sb= new StringBuffer("hello");
sb.append(" world");

| StringBuffer  vs  StringBuilder

 

그렇다면 동일한 API를 가지고 있는 StringBuffer, StringBuilder의 차이점은 무엇일까요?
가장 큰 차이점은 동기화의 유무로써 StringBuffer는 동기화 키워드를 지원하여 멀티쓰레드 환경에서 안전하다는 점(thread-safe) 입니다.  참고로 String 불변성을 가지기때문에 마찬가지로  멀티쓰레드 환경에서의 안정성(thread-safe)을 가지고 있습니다. 
반대로 StringBuilder는 동기화를 지원하지 않기때문에 멀티쓰레드 환경에서 사용하는 것은 적합하지 않지만 동기화를 고려하지 않는 만큼 단일쓰레드에서의 성능은 StringBuffer 보다 뛰어납니다.
 

| 정리

 
마지막으로 각 클래스별 특징을 정리해 보겠습니다. 컴파일러에서 분석 할때 최적화에 따라 다른 성능이 나올 수도 있지만 일반적인 경우에는 아래와 같은 경우에 맞게 사용하시면 될 것 같네요.
 
String                :  문자열 연산이 적고 멀티쓰레드 환경일 경우
StringBuffer     :  문자열 연산이 많고 멀티쓰레드 환경일 경우
StringBuilder   :  문자열 연산이 많고 단일쓰레드이거나 동기화를 고려하지 않아도 되는 경우  
String, StringBuffer, StringBuilder 비교

* StringBuffer와 StringBuilder는 성능으로 따졌을 때 2배의 속도차이가 있다고 하지만 참고사이트의 속도 차이 실험 결과 append()연산이 약 1억6천만번 일어날 때 약 2.6초의 속도차이를 보인다고 합니다.

(String은 +연산이 16만번이상 넘어가게 되면 10초이상 걸리면서 못 쓸정도의 성능을 보입니다.)

따라서 문자열연산이 많지만 엄청나게 일어나지 않는 환경이라면 StringBuffer를 사용해서 thread-safe한 것이 좋다는 생각입니다.

* JDK1.5이상부터 String에서 +연산으로 작성하더라도 StringBuilder로 컴파일하게 만들어 놨다지만 여전히 String클래스의 객체 생성하는 부분을 동일하므로 StringBuffer,StringBuilder 사용이 필요함.

+ StringBuffer, StringBuilder의 경우 buffer size를 초기에 설정해야하는데 이런 생성, 확장 오버로드가 걸려 버퍼사이즈를 잘못 초기화할 경우 성능이 좋지 않을 수 있음.

+ String클래스가 컴파일러분석단계에서 최적화될 가능성이 있기때문에 간혹 성능이 잘나오는 경우도 있음. 문자열 연산이 많지 않은 경우는 그냥 사용해도 무방.

 


소수 구하는 방법처럼 제곱근까지만 for문을 돌렸다.

소인수분해로 나누어지는 정수는 2부터 시작이고 2가 제일 작은 숫자기때문에

주어진 숫자는 제곱근 숫자 이하의 인수 개수만 가지기 때문 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

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());

		for (int i = 2; i <= Math.sqrt(N); i++) {
			while (N % i == 0) {
				System.out.println(i);
				N /= i;
			}
		}

		if (N != 1)
			System.out.println(N);
	}
}

 

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


소수 구하는 다른 방법인 에라토스테네스 방법도 공부해야 할듯

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

public class Main {

	static int N, M;

	public static void main(String[] args) throws IOException {

		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

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

		ArrayList<Integer> al = new ArrayList<Integer>();
		int sum = 0;

		for (int i = N; i <= M; i++) {
			boolean flag = true;

			if(i == 1)
				continue;
			
			for (int j = 2; j <= Math.sqrt(i); j++) {

				if (i % j == 0) {
					flag = false;
					break;
				}
			}

			if (flag) {
				
				sum += i;
				al.add(i);
			}
		}
		
		Collections.sort(al);
		
		if(al.isEmpty()) {
			System.out.println("-1");
		}
		else {
			System.out.println(sum);
			System.out.println(al.remove(0));
		}

	}

}

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


for 문으로 해당되는 숫자까지 무작정 나누어서 0으로 푸는 방법도 있지만,

당연히(?) 비효율적이기 때문에 그렇게 하는 방법보다는

number의 제곱근까지만 나누어주면 되는 방법을 이용하여 풀었다.

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

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());

		StringTokenizer st = new StringTokenizer(br.readLine());

		int number = 0;
		int count = 0;

		for (int i = 0; i < N; i++) {
			number = Integer.parseInt(st.nextToken());

			if (number == 1)
				continue;

			boolean flag = true;

			for (int j = 2; j <= Math.sqrt(number); j++) {
				if (number % j == 0) {
					flag = false;
					break;
				}
			}

			if (flag) {
				count++;
			}
		}
		System.out.println(count);

	}

}

 

  • 들어가기 전에



소수 [Prime Number]

 

소수의 정의는 1보다 큰 자연수 중 1 과 그 수 자기 자신만을 약수로 갖는 자연수를 의미한다는 점은 누구나 알고 있을 것이다.

 

즉, 소수의 약수는 2개만을 갖고, 그 중 하나는 반드시 1 이며, 나머지는 자기 자신만을 약수로 갖기 때문에 만약 1 보다 크고 자기 자신의 수보다 작은 자연수를 약수로 갖게 된다면 이는 합성수라고 한다.

 

그리고 위의 개념을 확장해본다면 합성수는 3개 이상인 자연수의 곱으로 이루어져 있으며 소수로 이루어진 곱으로 표현할 수 있다.

그리고 이를 소인수분해라고 한다.

 

더욱 개념을 넓혀볼까.

소수는 1과 자기 자신만을 약수로 갖고, 합성수는 3개 이상인 자연수의 곱으로 이루어져 있으며 소수들로 이루어진 곱으로 표현할 수 있다고 했다.

1 보다 큰 자연수는 모두 소수 또는 합성수로 이루어져 있으니 1 보다 큰 모든 자연수는 소수들의 곱으로 표현할 수 있다.

 

이렇듯 소수는 수학적으로도 매우 중요한 개념이다.

 

 

그렇다면 왜 개발자 입장에서 또는 프로그래밍의 세계에서 소수가 중요한 이유는 무엇일까.

 

바로 "암호" 때문이다.

 

실제로 우리 일상생활에서도 많이 쓰이는 암호 또한 소수를 이용하고 있다.

대표적으로 'RSA 암호화 방식'이 있다.

 

 

위 암호화 방식의 가장 근본적인 접근 방식은 이렇다.

 

"임의의 수들의 곱은 구하기 쉽지만 역으로 소인수 분해하는 것은 어렵다."

 

즉, 𝑝 × 𝑞 = 𝐍 를 만들기는 쉽지만,  𝐍 을 역으로 소인수 분해하여 𝑝 × 𝑞 를 만족하는 수를 찾기가 어렵다는 것이다.

 

 

 

예로 들어보자.

 

77 을 소인수분해 하면?

7 × 11 이 바로 나온다.

 

 

 

그렇다면 조금 더 큰 수라면 어떨까?

12126 을 소인수 분해 해보자.

 

먼저 직관적으로는 2로 나누어 떨어지겠다.

 

  P × 2 = 12126      ...     P = 6063

 

그리고 각 자릿수의 합 ( 6 + 0 + 6 + 3 ) 은 15 로 3의 배수이니 3으로 나누어 떨어지겠다.

 

  P × 2 × 3 = 12126     ...     P = 2021

 

 

그리고 2021 이 수를 소인수 분해를 해보자.

한 번에 눈에 들어오는가?

 

2021 을 소인수 분해하면 43 과 47 이다.

즉, 2부터 하나씩 대입해보려 하면 43 까지 대입해보아야 한다.

그리고 43과 47 은 각각 소수로 더이상의 소인수분해가 불가능 하다.

 

뭐 이정도 까진.. 컴퓨터의 대단한 연산능력에 맏기면 금방 풀릴 수 있겠다.

 

그런데 다음과 같은 수라면??

 

114381625757888867669235779976146612010218296721242362562561842935706935245733897830597123563958705058989075147599290026879543541

 

이 수는 실제로 RSA-129 의 공개키였고 현재는 풀린 129자리의 수다.

 

 

위 수를 여러분의 컴퓨터로 소인수 분해를 할 수 있겠는가?

 

일단 정답부터 말하자면 다음과 같다.

 

RSA-129 =

3490529510847650949147849619903898133417764638493387843990820577 × 32769132993266709549961988190834461413177642967992942539798288533

 

위의 두 수는 반드시 소수다.

 

위 문제를 컴퓨터로 푼다면 쉽게 풀릴 것 같지만 이는 오산이다.

실제로 위 문제를 풀기위해서 약 1600대의 컴퓨터와 600명의 사람이 모여 6개월동안 진행했다.

 

이 마저도 129자리 소수를 푸는데 저러한 시간이 걸렸는데 현재 암호방식은 RSA-2048로 617 자리수의 공개키가 있다.

 

617 자리의 수는 얼마나 오래 걸릴지 가늠이 안된다.

그래서 실제로도 대부분의 인터넷뱅킹도 RSA-2048 을 쓰고 있다.

 

 

혹시 독자 분들 중에 위 키를 푼다면 혹시 모를까... 세계 유명인사가 될 테니..

RSA-2048 의 공개키를 공유하겠다.

 

RSA-2048 =

25195908475657893494027183240048398571429282126204032027777137836043662020707595556264018525880784406918290641249515082189298559149176184502808489120072844992687392807287776735971418347270261896375014971824691165077613379859095700097330459748808428401797429100642458691817195118746121515172654632282216869987549182422433637259085141865462043576798423387184774447920739934236584823824281198163815010674810451660377306056201619676256133844143603833904414952634432190114657544454178424020924616515723350778707749817125772467962926386356373289912154831438167899885040445364023527381951378636564391212010397122822120720357

 

(화이팅입니다...)

 

 

 

 

 

 

서론이 너무 길었다.. 여튼 소수는 우리 일상생활에서도 긴밀하게 쓰이고 있다.

그런김에 소수를 구하고 판별하는 몇가지 방법을 같이 배워보고자 한다.

 

 

각 방법마다 소수를 판별하는 알고리즘과,

N 이하의 소수를 모두 구하는 알고리즘을 소개하겠다.

 

 

 

 

 

 


 

 

 

 

  • 방법 1

 

 

 

" N 보다 작은 자연수들로 모두 나눠본다. "

 

 

 

가장 기본적인 방법이라 할 수 있겠다.

 

임의의 수 N 이 1 과 N 을 제외한 다른 수를 약수로 갖고 있다면 그 수는 소수가 아니고, 만약 다른 약수가 없다면 그 수는 소수일 것이다.

 

 

 

 

[ 소수 판별 알고리즘 ]

 
import java.util.Scanner;
 
 
 
public class Prime_1 {
 
public static void main(String[] args) {
 
 
 
Scanner in = new Scanner(System.in);
 
 
 
is_prime(in.nextInt());
 
 
 
}
 
 
 
// 소수 판별 메소드
 
public static void is_prime(int number) {
 
 
 
// 0 과 1 은 소수가 아니다
 
if(number < 2) {
 
System.out.print("소수가 아닙니다");
 
return;
 
}
 
 
 
// 2 는 소수다
 
if(number == 2) {
 
System.out.print("소수입니다");
 
return;
 
}
 
 
 
 
 
for(int i = 2; i < number; i++) {
 
 
 
// 소수가 아닐경우
 
if(number % i == 0) {
 
System.out.print("소수가 아닙니다");
 
return;
 
}
 
}
 
// 위 반복문에서 약수를 갖고 있지 않는경우 소수다.
 
System.out.print("소수입니다");
 
return;
 
}
 
 
 
 
 
}

 

 

2 이상 N 미만의 수 중에 나누어 떨어지는 수가 존재한다면 소수가 아님을 이용한 소수 판별법이다.

 

또한, 위 알고리즘의 시간복잡도는 당연히 N 이하의 수까지 모든 수를 검사하므로 O(N) 이다.

 

 

 

 

 

[ N 이하의 모든 소수 구하는 알고리즘 ] 

위를 응용하여 N 이하의 소수를 모두 구하는 알고리즘은 다음과 같을 것이다.

 
// 소수만 출력
 
 
 
import java.util.Scanner;
 
 
 
public class Prime_1 {
 
public static void main(String[] args) {
 
 
 
Scanner in = new Scanner(System.in);
 
 
 
int N = in.nextInt();
 
 
 
// 0 ~ N 까지 수 중 소수를 구하는 반복문
 
for(int i = 0; i <= N; i++) {
 
make_prime(i);
 
}
 
 
 
}
 
 
 
// 소수 생성 메소드
 
public static void make_prime(int number) {
 
 
 
// 0 과 1 은 소수가 아니므로 종료
 
if(number < 2) {
 
return;
 
}
 
 
 
// 2 는 소수다
 
if(number == 2) {
 
System.out.println(number);
 
return;
 
}
 
 
 
 
 
for(int i = 2; i < number; i++) {
 
 
 
// 소수가 아닐경우 종료
 
if(number % i == 0) {
 
return;
 
}
 
}
 
// 위 반복문에서 약수를 갖고 있지 않는경우 소수다.
 
System.out.println(number);
 
return;
 
}
 
 
 
 
 
}

 

 

 

위 알고리즘은 소수 판별 알고리즘을 N 번 반복하여 각 수마다 소수 판별 한 뒤 소수만 출력하도록 한 알고리즘이다.

 

즉 O(N) 알고리즘을 N 번 반복하므로 위 방법의 N 이하의 소수를 모두 구하는 알고리즘의 시간복잡도는 O(N²) 이다.

 

 

 





 




  • 방법 2

 

 

 

" √N 이하의 자연수들로 모두 나눠본다. "

 

 

 

방법 1 의 방법에서 약간 업그레이드 된 알고리즘이다.

 

바로 N 을 √N 이하의 자연수들만 나누는 방법이다.

 √N 이하의 자연수들만 나누면 되는지는 생각보다 매우 쉽게 알 수 있다.

 

 

생각해보자. 소수를 판별한다는 것은 결국 1 과 자기 자신을 제외한 다른 자연수를 약수로 갖고 있으면 안된다는 의미다.

 

 임의의 자연수 𝐍 (𝐍 > 0) 이 있다고 가정하자.

 𝑝 × 𝑞 = 𝐍 을 만족할 때 우리는 아래와 같은 부등식을 완성할 수 있다.

 

( 1 ≤  𝑝 , 𝑞 ≤ 𝐍 )

 

그리고 𝑝 와 𝑞 중 하나는 √N 보다 작거나 같다.

 

예로들어  𝐍 = 16 라고 하자.

그러면 아래와 같이 두 수의 곱으로 표현할 수 있다.

 

1 × 16

2 × 8

4 × 4

8 × 2

16 × 1

 

여기서 볼 수 있듯이 만약 𝑝 가 증가한다면 자연스레 𝑞 가 감소하고,

반대로 𝑝 가 감소한다면 자연스레 𝑞 가 증가한다.

 

그리고 𝑝 와 𝑞 는 𝐍의 약수이기 때문에 결국 𝐍 을 임의의 수로 나누게 되면 임의의 수가 √N 보다 작다면 결국 나머지는 √N 보다 클 수 밖에 없다.

 

결과적으로 𝑝 와 𝑞 중 하나는 반드시 √N 보다 작거나 같다.

 

즉, √N 이하의 자연수 중에 나누어 떨어지는 수가 있다면 이는 1 과 N 을 제외한 다른 자연수가 N 의 약수라는 의미이므로 소수가 아니게 되는 것이다.

 

그럼 이를 토대로 알고리즘을 짜보자.

 

 

 

 

[ 소수 판별 알고리즘 ]

 
import java.util.Scanner;
 
 
 
public class Prime_2 {
 
public static void main(String[] args) {
 
 
 
Scanner in = new Scanner(System.in);
 
 
 
is_prime(in.nextInt());
 
 
 
}
 
 
 
// 소수 판별 메소드
 
public static void is_prime(int number) {
 
 
 
// 0 과 1 은 소수가 아니다
 
if(number < 2) {
 
System.out.print("소수가 아닙니다");
 
return;
 
}
 
 
 
// 2 는 소수다
 
if(number == 2) {
 
System.out.print("소수입니다");
 
return;
 
}
 
 
 
// 제곱근 함수 : Math.sqrt()
 
for(int i = 2; i <= Math.sqrt(number); i++) {
 
 
 
// 소수가 아닐경우
 
if(number % i == 0) {
 
System.out.print("소수가 아닙니다");
 
return;
 
}
 
}
 
// 위 반복문에서 약수를 갖고 있지 않는경우 소수다.
 
System.out.print("소수입니다");
 
return;
 
}
 
 
 
 
 
}

 

 

2 이상 √N 이하의 수 중에 나누어 떨어지는 수가 존재한다면 소수가 아님을 이용한 소수 판별법이다.

 

또한, 위 알고리즘의 시간복잡도는 당연히 √N 이하의 수까지 모든 수를 검사하므로 O(√N) 이다.

 

 




 

[ N 이하의 모든 소수 구하는 알고리즘 ] 

위를 응용하여 N 이하의 소수를 구하는 알고리즘은 다음과 같을 것이다.

 
// 소수만 출력
 
 
 
import java.util.Scanner;
 
 
 
public class Prime_2 {
 
public static void main(String[] args) {
 
 
 
Scanner in = new Scanner(System.in);
 
 
 
int N = in.nextInt();
 
 
 
// 0 ~ N 까지 수 중 소수를 구하는 반복문
 
for(int i = 0; i <= N; i++) {
 
make_prime(i);
 
}
 
 
 
}
 
 
 
// 소수 생성 메소드
 
public static void make_prime(int number) {
 
 
 
// 0 과 1 은 소수가 아니므로 종료
 
if(number < 2) {
 
return;
 
}
 
 
 
// 2 는 소수다
 
if(number == 2) {
 
System.out.println(number);
 
return;
 
}
 
 
 
 
 
// 제곱근 함수 : Math.sqrt()
 
for(int i = 2; i <= Math.sqrt(number); i++) {
 
 
 
// 소수가 아닐경우 종료
 
if(number % i == 0) {
 
return;
 
}
 
}
 
// 위 반복문에서 약수를 갖고 있지 않는경우 소수다.
 
System.out.println(number);
 
return;
 
}
 
 
 
 
 
}

 

 

 

위 알고리즘은 소수 판별 알고리즘을 N 번 반복하여 각 수마다 소수 판별 한 뒤 소수만 출력하도록 한 알고리즘이다.

 

즉 O(√N) 알고리즘을 N 번 반복하므로 위 방법의 N 이하의 소수를 모두 구하는 알고리즘의 시간복잡도는 O(N√N) 이다.

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

  • 방법 3 : 에라토스테네스의 체




소수를 구하는 대표적인 방법 중 하나다.

" k=2 부터 √N 이하까지 반복하여 자연수들 중 k를 제외한 k의 배수들을 제외시킨다"

 

 

즉, 방법으로 보면 다음과 같다.

 

 

 

 

k = 2 이면 2 를 제외한 2의 배수를 모두 지워주고,

k = 3 이면 3 을 제외한 3의 배수를 모두 지워주고,

(4는 이미 k = 2 에서 제외되어 넘어간다.)

k = 5 이면 5 를 제외한 5의 배수를 모두 지워주고..

 

이렇게 하여 k = √N 까지 반복하는 방법이다.

 

 

이 방법은 소수를 구하는 방법이랑 판별하는 방법이 동일하기 때문에 하나로 묶어서 설명하겠다.

 

 

 

 

 

 

 

[ N 이하의 모든 소수를 구하는 알고리즘 ] 

 
import java.util.Scanner;
 
 
 
public class Prime_3 {
 
 
 
public static boolean[] prime; // 소수를 체크할 배열
 
public static void main(String[] args) {
 
 
 
Scanner in = new Scanner(System.in);
 
 
 
int N = in.nextInt();
 
 
 
make_prime(N);
 
 
 
for(int i = 0; i < prime.length; i++) {
 
if(prime[i] == false) { // 소수(false)일 경우 출력
 
System.out.println(i);
 
}
 
}
 
}
 
 
 
// N 이하 소수 생성 메소드
 
public static void make_prime(int N) {
 
 
 
prime = new boolean[N + 1]; // 0 ~ N
 
 
 
/*
 
소수가 아닌 index = true
 
소수인 index = false
 
*/
 
 
 
// 2 미만의 N 을 입력받으면 소수는 판별할 필요 없으므로 바로 return
 
if(N < 2) {
 
return;
 
}
 
 
 
prime[0] = prime[1] = true;
 
 
 
 
 
// 제곱근 함수 : Math.sqrt()
 
for(int i = 2; i <= Math.sqrt(number); i++) {
 
 
 
// 이미 체크된 배열이면 다음 반복문으로 skip
 
if(prime[i]==true) {
 
continue;
 
}
 
 
 
// i 의 배수들을 걸러주기 위한 반복문
 
for(int j = i*i; j < prime.length; j = j+i) {
 
prime[j] = true;
 
}
 
}
 
 
 
}
 
 
 
}

 

 

 

언뜻 보기에는 이중for문이라 시간복잡도가 O(N²) 일 것 같지만 그렇지 않다.

 

 

 

1 ~ 𝑥 까지의 수가 있는 칸을 체크하는 횟수를 대략적으로 따진다면 아래와 같을 것이다.

(n = 1 부터 시작한다고 할 때)

 

(𝑥) + (𝑥/2) + (𝑥/3) + (𝑥/4) + (𝑥/5) + (𝑥/6) + ⋯ + 1

 

그러면 위 식을 다음과 같이 표현 할 수 있다.

𝑥(1 + 1/2 + 1/3 +  ⋯ + 1/𝑥) 

 

위의 x로 묶인 괄호 안의 수열은 조화 수(Harmonic Number)라고 하는데, 쉽게 말해서 조화 수열에서 부분 합을 말한다고 생각하면 된다.

그리고 다음과 같이 발산하게 된다고 한다.

 

 

이 때 감마는 상수 값이고 O(1/x)는 우리가 생각하는 big O와 같은 표기로 수학에서는 함수 성장률의 상한선이다. 그리고 위의 조화수는 대략 자연로그의 형태를 따라간다.

(자세한 건 아래 링크를 참고하시기 바란다.)

https://en.wikipedia.org/wiki/Harmonic_number

 

즉, N 이하의 소수에 대하여 체에 거르는 시간이 logN 이므로 단순하게 체에 거르는 것만 해도 시간 복잡도는 O(N㏒N) 이다.

( ㏒ 는 자연로그 𝑙𝑛 으로 본다 )

 

그런데 우리는 여기서 더 나아가 이미 체크된 배열은 검사하지 않고 다음 반복문으로 넘어간다.

(𝑥/2) + (𝑥/3) + (𝑥/4) + (𝑥/5) + (𝑥/6) + (𝑥/7)+ (𝑥/8)+ (𝑥/9) + ⋯ + (𝑥/𝑥-1) 

 

 

 

우리는 앞서 조화수를 통해 점근적 시간 복잡도 O(NlogN) 이라는 시간 복잡도를 얻었다.

이 의미는 x개의 수에 대해 2일 때 체크하는 개수인 (x/2), 3일 때 체크하는 개수인 (x/3), ... 이렇게 체크를 하게 된다.

 

하지만 우리가 알고싶은 것은 이미 중복되는 수들은 검사하지 않는다는 것이다. 이 의미는 무엇일까? 결국 검사하는 수는 소수로 판정 될 때 그의 배수들을 지우는 것이라는 것이다. 이 말은 구간 내의 소수의 개수를 알아야 한다는 뜻이기도 하다.

 

근데, 소수가 규칙성이 있는가? 이 것은 아직까지도 풀지못한 것이다. 이를 찾고자 가우스는 15살 때 하나하나씩 구하면서 x보다 작거나 같은 소수의 밀도에 대해 대략 1 / ln(x) 라는 것을 발견하게 되는데 증명을 못했다. (이를 가우스의 소수 정리라고 한다)

 

즉, 위를 거꾸로 말하면 x번째 소수는 xlog(x) 라는 의미가 되지 않겠는가? 즉, 우리는 앞서 1/x 의 합을 구했지만, 실제로 중복되는 수가 제외된다면 x는 소수만 된다는 의미고, 이는 소수의 역수 합이다. (1/2 + 1/3 + 1/5 + 1/7 + ⋯ ) 이런식으로 말이다.

 

그러면 시간 복잡도를 도출해낼 수 있다.

바로 O(Nlog(log N)) 의 시간 복잡도를 갖게 된다.

www.acmicpc.net/problem/1712


문제

월드전자는 노트북을 제조하고 판매하는 회사이다. 노트북 판매 대수에 상관없이 매년 임대료, 재산세, 보험료, 급여 등 A만원의 고정 비용이 들며, 한 대의 노트북을 생산하는 데에는 재료비와 인건비 등 총 B만원의 가변 비용이 든다고 한다.

예를 들어 A=1,000, B=70이라고 하자. 이 경우 노트북을 한 대 생산하는 데는 총 1,070만원이 들며, 열 대 생산하는 데는 총 1,700만원이 든다.

노트북 가격이 C만원으로 책정되었다고 한다. 일반적으로 생산 대수를 늘려 가다 보면 어느 순간 총 수입(판매비용)이 총 비용(=고정비용+가변비용)보다 많아지게 된다. 최초로 총 수입이 총 비용보다 많아져 이익이 발생하는 지점을 손익분기점(BREAK-EVEN POINT)이라고 한다.

A, B, C가 주어졌을 때, 손익분기점을 구하는 프로그램을 작성하시오.

입력

첫째 줄에 A, B, C가 빈 칸을 사이에 두고 순서대로 주어진다. A, B, C는 21억 이하의 자연수이다.

출력

첫 번째 줄에 손익분기점 즉 최초로 이익이 발생하는 판매량을 출력한다. 손익분기점이 존재하지 않으면 -1을 출력한다.

 

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

public class Main {

	static int A,B,C,N;
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		
		A = Integer.parseInt(st.nextToken()); 
		B = Integer.parseInt(st.nextToken()); 
		C = Integer.parseInt(st.nextToken()); 
		
		if(C-B>0) {
			N = (A / (C-B)) +1;
		} else {
			N = -1;
		}
		System.out.println(N);

	}
}

복잡해보이지만 수학적으로 접근하면 간단하게 풀 수 있는 문제였다.

+ Recent posts