섹션 2. 코딩테스트 - 06. 그리디
⬛ 06. 그리디 알고리즘
⬛ 06-1. 그리디 알고리즘
🟦 그리디(Greedy) 알고리즘
- 현재 상태에서 볼 수 있는 최적해가 전체의 최적해라고 가정하며 근시안적으로 보는 알고리즘
- 전체를 보지 않고, 근시안적 선택(현 상황에서의 부분적 최적해)가 최종 문제의 최적해를 얻는 일이라고 가정하며 답을 찾아가는 알고리즘
- 항상 최적해를 보장하지는 못한다.
🟧 그리디 알고리즘 수행 과정
1) 해 선택 : 현재 상태의 부분적 최적해 선택
2) 적절성 검사 : 그 최적해가 전체 문제 제약 조건 벗어나지 않는지 검사
3) 해 검사 : 현재까지 선택한 해 집합들이 전체 문제 해결 가능한지 검사.
- 만약 전체 문제 해결 못한다면 다시 1)로 돌아가 이 과정을 반복
🟦 백준 11047번. 동전 개수의 최솟값 구하기
문제
준규가 가지고 있는 동전은 총 N종류이고, 각각의 동전을 매우 많이 가지고 있다.
동전을 적절히 사용해서 그 가치의 합을 K로 만들려고 한다. 이때 필요한 동전 개수의 최솟값을 구하는 프로그램을 작성하시오.
입력
첫째 줄에 N과 K가 주어진다. (1 ≤ N ≤ 10, 1 ≤ K ≤ 100,000,000)
둘째 줄부터 N개의 줄에 동전의 가치 Ai가 오름차순으로 주어진다. (1 ≤ Ai ≤ 1,000,000, A1 = 1, i ≥ 2인 경우에 Ai는 Ai-1의 배수)
출력
첫째 줄에 K원을 만드는데 필요한 동전 개수의 최솟값을 출력한다.
풀이 - 그리디 알고리즘
- 어쨋든 최소 동전 개수로 K원을 만드는 게 목표이다 보니, 큰 동전부터 역순으로 확인해야 한다.
- 1) K보다 작거나 같은 동전 값 발견 시,
- 2) K /arr[i] 해당 동전으로 나눈 ‘몫’은 동전개수++ 누적시키고
- 3) K % arr[i] 해당 동전 나누고 남은 ‘나머지’ 금액은 K원 갱신시키고
- 4) 반복하고 탈출한 동전개수를 최종 출력시킨다.
package to_0614_1;
import java.util.Scanner;
/* 11047번. 동저 개수 최솟값 - 그리디 알고리즘
* */
public class Main {
//실행 메인
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner kb =new Scanner(System.in);
int N = kb.nextInt();//동전 개수
int K = kb.nextInt(); //목표 금액
int arr[] = new int[N];//동전 묶음
//입력 받기
for(int i=0; i<N; i++) arr[i] = kb.nextInt();
int answer = 0;//최소 동전 개수 저장용
//그리디 알고리즘
//일단 최소 동전 쓰려면 역순 큰 값부터 대조
for(int i=N-1; i>=0; i--) {
if(arr[i] <= K) { // 각 동전값이 K보다 작거나 같다 == 만들 수 있다이므로
answer += K/arr[i];//그 나눈 몫은 동전 개수로 누적
K = K%arr[i];//나머지는 K값으로 갱신 처리
}
}
//정답 출력
System.out.println(answer);
}
}
🟦 백준 1715번. 카드 정렬하기
문제
정렬된 두 묶음의 숫자 카드가 있다고 하자. 각 묶음의 카드의 수를 A, B라 하면 보통 두 묶음을 합쳐서 하나로 만드는 데에는 A+B 번의 비교를 해야 한다. 이를테면, 20장의 숫자 카드 묶음과 30장의 숫자 카드 묶음을 합치려면 50번의 비교가 필요하다.
매우 많은 숫자 카드 묶음이 책상 위에 놓여 있다. 이들을 두 묶음씩 골라 서로 합쳐나간다면, 고르는 순서에 따라서 비교 횟수가 매우 달라진다. 예를 들어 10장, 20장, 40장의 묶음이 있다면 10장과 20장을 합친 뒤, 합친 30장 묶음과 40장을 합친다면 (10 + 20) + (30 + 40) = 100번의 비교가 필요하다. 그러나 10장과 40장을 합친 뒤, 합친 50장 묶음과 20장을 합친다면 (10 + 40) + (50 + 20) = 120 번의 비교가 필요하므로 덜 효율적인 방법이다.
N개의 숫자 카드 묶음의 각각의 크기가 주어질 때, 최소한 몇 번의 비교가 필요한지를 구하는 프로그램을 작성하시오.
입력
첫째 줄에 N이 주어진다. (1 ≤ N ≤ 100,000) 이어서 N개의 줄에 걸쳐 숫자 카드 묶음의 각각의 크기가 주어진다. 숫자 카드 묶음의 크기는 1,000보다 작거나 같은 양의 정수이다.
출력
첫째 줄에 최소 비교 횟수를 출력한다.
풀이 - 그리디 알고리즘
- 카드 묶음의 카드 개수가 ‘작은 순서대로’ 먼저 합치는 것이 전체 비교횟수를 줄일 수 있는 방법이다.
- 따라서 그리디로 풀려면, 현재 데이터 중 가장 작은 카드 개수 묶음 2개를 뽑고, 그 2개를 합친 새 카드 묶음을 다시 데이터에 넣은 뒤 정렬시키는 과정을 반복해야 한다.
- ‘우선순위 큐’도 사용해야 한다.
- while(pq.size()≠1) ,즉. 우선순위 큐 사이즈 1이 아닌 동안 반복하는 이유는 : 두 개 카드 묶음끼리 비교하는 건데 1개가 됐다는 것은 더 이상 비교할 카드 없다는 뜻이므로.
import java.util.PriorityQueue;
import java.util.Scanner;
/* 1715번. 카드 정렬하기 */
public class Main {
//실행 메인
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner kb= new Scanner(System.in);
int N = kb.nextInt();//최초 카드 묶음의 개수
//데이터값 자동 오름차순 정렬될 우선순위 큐에 값 저장시킬 것
PriorityQueue<Integer> pq = new PriorityQueue<>();
for(int i=0; i<N; i++) {
int data = kb.nextInt();
pq.add(data);
}
int sum = 0;//비교 횟수 합 더할 것
while(pq.size()!=1) { //내부 카드묶음 1개 되면 더이상 비교 불가하므로
int a = pq.poll();//첫 데이터 뽑고
int b = pq.poll();//다음 데이터 뽑고
sum += a+b;//두 데이터 묶음을 sum에 누적
//합친 카드묶음은 새 데이터로 우선순위큐에 저장
pq.add(a+b);
}
//탈출한 sum 이 최종 답
System.out.println(sum);
}
}
⬛ 우선순위 큐 | Priority Queue
- 큐(Queue)는 FIFO 의 형태로 데이터를 처리한다.
- Priority Queue 는 우선순위를 먼저 결정한 뒤, 그 우선순위가 높은 요소부터 먼저 나가는 자료구조이다.
- 기본적으로 오름차순 (작은→큰) 값의 순서로 우선순위 배정된다.
import java.util.PriorityQueue;//import//int형 priorityQueue 선언 (우선순위가 내부 값 오름차순)
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
//int형 priorityQueue 선언 (우선순위가 내부 값 내림차순)
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(Collections.reverseOrder());
//String형 priorityQueue 선언 (우선순위가 내부 값 오름차순)
PriorityQueue<String> priorityQueue = new PriorityQueue<>();
//String형 priorityQueue 선언 (우선순위가 내부 값 내림차순)
PriorityQueue<String> priorityQueue = new PriorityQueue<>(Collections.reverseOrder());
🟦 백준 1744번. 수 묶어서 최대 만들기
문제
길이가 N인 수열이 주어졌을 때, 그 수열의 합을 구하려고 한다. 하지만, 그냥 그 수열의 합을 모두 더해서 구하는 것이 아니라, 수열의 두 수를 묶으려고 한다. 어떤 수를 묶으려고 할 때, 위치에 상관없이 묶을 수 있다. 하지만, 같은 위치에 있는 수(자기 자신)를 묶는 것은 불가능하다. 그리고 어떤 수를 묶게 되면, 수열의 합을 구할 때 묶은 수는 서로 곱한 후에 더한다.
예를 들면, 어떤 수열이 {0, 1, 2, 4, 3, 5}일 때, 그냥 이 수열의 합을 구하면 0+1+2+4+3+5 = 15이다. 하지만, 2와 3을 묶고, 4와 5를 묶게 되면, 0+1+(23)+(45) = 27이 되어 최대가 된다. (== 묶는 순서에 따라서 결과가 달라진다.)
수열의 모든 수는 단 한번만 묶거나, 아니면 묶지 않아야한다.
수열이 주어졌을 때, 수열의 각 수를 적절히 묶었을 때, 그 합이 최대가 되게 하는 프로그램을 작성하시오.
입력
첫째 줄에 수열의 크기 N이 주어진다. N은 50보다 작은 자연수이다. 둘째 줄부터 N개의 줄에 수열의 각 수가 주어진다. 수열의 수는 -1,000보다 크거나 같고, 1,000보다 작거나 같은 정수이다.
출력
수를 합이 최대가 나오게 묶었을 때 합을 출력한다. 정답은 항상 231보다 작다.
풀이 - 그리디 알고리즘
양수의 경우 가능한 큰 수들끼리 우선적으로 묶어야 결과값이 커진다는 것.
음수 간의 곱은 양수가 되므로 (음수는 작은값 우선으로 묶어야) 결과값이 커진다는 것을 이용하여 풀어야 한다.
일단 이 문제는 수의 유형을 4가지로 분리해서 저장해야 힘
1) 1보다 큰 양의 정수 : PQ에 큰 값 우선(내림차순) 정렬
2) 음의 정수 : PQ에 작은값 우선(오름차순) 정렬,
3) 0과 1은 카운팅
그리고, while()문으로 pQ사이즈가 1보다 클때까지 두 개의 수를 뽑아 곱해서 sum에 누적하되, 만약 크기 1이 되어 탈출했다면(즉, 그 개수가 홀수개 였다면).
(1) 양수는 isEmpty() 될때까지 sum에 누적하면 되고
(2) 음수는
1) 0 카운팅이 존재하면. 남은 음수와 0을 곱한 값을, 어차피 0 더하니 0 카운팅이 존재하지 않을 경우만 생각하면 된다.
2) 0카운팅이 존재하지 않으면 그냥 남은 음수를 sum에 누적시킨다.
마지막에는 one 카운팅한 값을 그대로 sum에 더하고 출력한다.
import java.util.Collections;
import java.util.PriorityQueue;
import java.util.Scanner;
/*1744번. 수 묶어서 최대 만들기 */
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner kb = new Scanner(System.in);
int N = kb.nextInt();
//유형별 수 나눔
//양수는 큰수 우선 -> 역순(내림차순)
PriorityQueue<Integer> plusQ = new PriorityQueue<>(Collections.reverseOrder());
//음수는 작은 수 우선 -> 더 작은 수 끼리 곱이 더 크므로 (오름차순)
PriorityQueue<Integer> minusQ = new PriorityQueue<>();
//0과 1은 카운팅
int zero =0;
int one =0;
//입력 받기
for(int i=0; i<N; i++) {
int input = kb.nextInt();
if(input == 1) one++;
else if(input ==0) zero++;
else if(input > 1) plusQ.add(input);
else if(input <0) minusQ.add(input);
}
int sum = 0;//얘가 최대가 되어야 함
//1) 양수들은 최댓값 우선 뽑아서 곱한 뒤 해결
while(plusQ.size() > 1) { //짝수이면 지들끼리 곱한 뒤 합하면 됨
int a = plusQ.poll();
int b = plusQ.poll();
sum += (a*b);
}
//만약 홀수개라 마지막에 1개 남은 경우
if(!plusQ.isEmpty()) {
int x = plusQ.poll();
sum += x;
}
//2) 음수 처리
while(minusQ.size() > 1 ) {
int a = minusQ.poll();
int b = minusQ.poll();
sum += (a*b);
}
if(!minusQ.isEmpty()) {
int x = minusQ.poll();
if(zero >= 1) {
sum += (0 * x);
zero--;
}else {
sum += x;
}
}
//3) 남은 1 처리
sum += one;
//최종 최댓값 출력
System.out.println(sum);
}
}
🟦 백준 1931번. 회의실 배정
문제
한 개의 회의실이 있는데 이를 사용하고자 하는 N개의 회의에 대하여 회의실 사용표를 만들려고 한다. 각 회의 I에 대해 시작시간과 끝나는 시간이 주어져 있고, 각 회의가 겹치지 않게 하면서 회의실을 사용할 수 있는 회의의 최대 개수를 찾아보자. 단, 회의는 한번 시작하면 중간에 중단될 수 없으며 한 회의가 끝나는 것과 동시에 다음 회의가 시작될 수 있다. 회의의 시작시간과 끝나는 시간이 같을 수도 있다. 이 경우에는 시작하자마자 끝나는 것으로 생각하면 된다.
입력
첫째 줄에 회의의 수 N(1 ≤ N ≤ 100,000)이 주어진다. 둘째 줄부터 N+1 줄까지 각 회의의 정보가 주어지는데 이것은 공백을 사이에 두고 회의의 시작시간과 끝나는 시간이 주어진다. 시작 시간과 끝나는 시간은 231-1보다 작거나 같은 자연수 또는 0이다.
출력
첫째 줄에 최대 사용할 수 있는 회의의 최대 개수를 출력한다.
풀이 1) - 그리디 알고리즘 & Time 클래스 생성
- 1개의 회의실에 회의가 겹치지 않게 최대한 많은 회의를 배정해야 하는 문제이다.
- 현재 회의의 종료 시간이 빠를수록 다음 회의와 겹치지 않게 시작하는 게 유리하다.
- 그러므로 종료 시간 빠른 순으로 우선 정렬하고, 만약 종료시간이 같은 회의의 경우 시작 시간 빠른 것 우선으로 정렬하도록 조건을 설정한다.
- 그런 뒤, 차례로 순회하면서 현재 회의 종료시간과 같거나 큰 시작 시간 발견 시 카운팅++ 후, 종료 시간을 해당 회의의 종료 시간으로 갱신처리한다.
🏓첫 번째 풀이는 Comparable
package to_0614_4;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Scanner;
/* 1931번. 회의실 배정하기 */
class Time implements Comparable<Time>{
int s; //시작 시간
int e; //끝 시간
Time(int s, int e ){
this.s = s;
this.e = e;
}
@Override
public int compareTo(Time o) {
// TODO Auto-generated method stub
if(this.e == o.e){//만약 종료시간이 두 객체 모두 같다면.
return this.s - o.s;//빠른 시작 시간 우선
}
return this.e - o.e; //기본적으로 빠르종료 시간 우선 정렬
}
}
public class Main {
//실행 메인
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner kb = new Scanner(System.in);
int N = kb.nextInt(); //회의실 개수
ArrayList<Time> arr = new ArrayList<>();
for(int i=0; i<N; i++) {
int a = kb.nextInt();
int b = kb.nextInt();
arr.add(new Time(a, b)); //객체에 담고
}
int cnt =0;
//재정의한 기준으로 정렬
Collections.sort(arr);
//기본 종료 시간은 0 으로 세팅해놓고
int et = 0;
for(Time x : arr) {
if(x.s >= et) { //종료시간 기준 정렬된 상태에서 가장 먼저 나타난 시작시간 회의를 발견시
cnt++;//회의 배정++
et = x.e;//현재 선택한 회의의 종료시간으로 et를 갱신
}
}
System.out.println(cnt);
}
}
🏓두 번째 풀이는 2차원 배열을 활용하여 정렬하는 방식이다.
- int[][]A = new int[N][2] ;에 저장한 다음, A[i][0]은 시작, A[i][1]은 종료 시간을 담아둔다.
Arras.sort(A, new Comparator<int[]>() {
@override
public int compare(int[] A, int[] B){
//만약 종료 시간이 같을 경우 , 시작시간 오름차순 정렬
if(A[1] == B[1] ) return A[0] - B[0];
return A[1]-B[1]; //기본 종료 시간 빠른 순 정렬 오름차순
}
});
풀이 2) - 그리디 알고리즘 & 2차원 배열
import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;
/*1931번. 회의실 배정 */
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner kb= new Scanner(System.in);
int N = kb.nextInt();
int[][] A = new int[N][2];
//입력받고
for(int i=0; i<N; i++) {
int s = kb.nextInt();
int e = kb.nextInt();
A[i][0] = s; //시작
A[i][1] = e; //종료
}
//정렬
Arrays.sort(A, new Comparator <int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
// TODO Auto-generated method stub
if(o1[1] == o2[1]) {//종료시간 같다면
return o1[0] - o2[0];//시작 시간 빠르 순
}
//기본적으로 종료시간 우선 정렬
return o1[1] - o2[1];
}
});
int et = 0;
int cnt = 0;//카운팅
for(int i=0; i<N; i++) {
if(et <= A[i][0]) {
//현재 종료시간 보다 크거나 같은 시작시간 회의 발견ㅅ ㅣ
cnt++;
et = A[i][1];//종료시간을 해당 회의의 종료시간으로 갱신
}
}
System.out.println(cnt);
}
}
🟦 백준 1541번. 잃어버린 괄호
문제
세준이는 양수와 +, -, 그리고 괄호를 가지고 식을 만들었다. 그리고 나서 세준이는 괄호를 모두 지웠다.
그리고 나서 세준이는 괄호를 적절히 쳐서 이 식의 값을 최소로 만들려고 한다. (== 묶는 순서에 따라서 결과가 달라진다.)
괄호를 적절히 쳐서 이 식의 값을 최소로 만드는 프로그램을 작성하시오.
입력
첫째 줄에 식이 주어진다. 식은 ‘0’~‘9’, ‘+’, 그리고 ‘-’만으로 이루어져 있고, 가장 처음과 마지막 문자는 숫자이다. 그리고 연속해서 두 개 이상의 연산자가 나타나지 않고, 5자리보다 많이 연속되는 숫자는 없다. 수는 0으로 시작할 수 있다. 입력으로 주어지는 식의 길이는 50보다 작거나 같다.
출력
첫째 줄에 정답을 출력한다.
풀이 - 그리디 알고리즘
- 가장 작은 최솟값을 만들려면 가능한 큰 수를 빼야한다.
- (-) 를 기준으로 분리해서 괄호로 합친 각자 값들을 모두 더한 뒤, 빼기를 실행하면 최솟값이 만들어진다.
- ex) 100 - (40+50) - (31+32+90)
package to_0614_6;
import java.util.Scanner;
public class Main {
static int mySum(String x) {
int sum = 0;
for(String a : x.split("[+]")) {
sum += Integer.parseInt(a);
}
return sum;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner kb =new Scanner(System.in);
String x = kb.next();
//- 기준으로 구분해놓고
String[] sArr = x.split("-");
int sum =0;
for(int i=0; i<sArr.length; i++) {
int tmp = mySum(sArr[i]);
if(i ==0) {
sum += tmp;
}else {
sum -= tmp;
}
}
System.out.println(sum);
}
}
'알고리즘 이론 [개념] > [개념] 코테 알고리즘 공부 - 시즌 2' 카테고리의 다른 글
섹션 3. 코딩테스트 [실전편] - 08. 그래프 - (1) 이분 그래프, 유니온 파인드 (0) | 2023.06.21 |
---|---|
섹션 2. 코딩테스트 - 07. 정수론 -(1) 소수, 오일러 피, 유클리드 호제법 (0) | 2023.06.15 |
섹션 2. 코딩테스트 - 05. 탐색 - (2) 이분 탐색 (0) | 2023.06.13 |
섹션 2. 코딩테스트 - 05. 탐색 파트 - (1) DFS, BFS (0) | 2023.06.08 |
섹션 2. 코딩테스트 - 04. 정렬 파트 (0) | 2023.06.05 |