문제 풀이 날짜: 2023.05.15
포스트 작성일: 2023.05.18

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 11659번: 구간 합 구하기 4 (실버3) (문제 링크)

 

키워드

구간 합

 

풀이 접근법

  • 평범하게 중첩 for문으로 합을 구하면 시간초과가 된다. (N ≤ 10^5, M ≤ 10^5, N*M ≤ 10^10)
  • DP와 유사한 발상에서 출발한다. 구했던 값을 또 구할 수도 있기 때문에 미리 배열의 합을 저장해놓는 것이 중요하다.
  • 1부터의 k까지의 합을 미리 배열에 저장해놓고 중간 구간의 부분 합 = 큰 구간의 부분 합 - 작은 구간의 부분 합의 형태로 원하는 답을 구한다. 
    • 예를 들어, 3부터 7까지의 합을 구하고 싶다면 (1부터 7까지의 합) - (1부터 2까지의 합)을 구한다.
    • 이렇게 하면 O(N + M) 시간만에 답을 구할 수 있다.

 

코드

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

public class Q11659 {

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		String[] temp = br.readLine().split(" ");

		int N = Integer.parseInt(temp[0]);
		int M = Integer.parseInt(temp[1]);
		
		int[] numbers = new int[N + 1];
		int[] sum = new int [N + 1];
		
		StringTokenizer st = new StringTokenizer(br.readLine(), " ");
		for(int i = 1; i <= N; i++) {
			numbers[i] = Integer.parseInt(st.nextToken());
			sum[i] = sum[i - 1] + numbers[i];
		}
		
		StringBuilder sb = new StringBuilder();
		while(M-- > 0) {
			temp = br.readLine().split(" ");
			
			int start = Integer.parseInt(temp[0]);
			int end = Integer.parseInt(temp[1]);
			
			int sumOfSections = sum[end] - sum[start - 1];
			
			sb.append(sumOfSections).append('\n');
		}
		
		System.out.print(sb);
	}

}

 

매우 쉬운 문제이지만, 구간 합에 대한 이해가 없다면 시간초과를 받고 헤멜 수도 있다고 생각한다. 이런 스킬은 기초를 잘 알아두면 후에 여러 문제에 응용할 수 있으므로 잘 공부해두는 것이 좋겠다.

 

git 링크

https://github.com/jinju9553/23-CodingTest/blob/main/week8/day2/Q11659.java

문제 풀이 날짜: 2023.05.16
포스트 작성일: 2023.05.18

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 13305번: 주유소 (실버3) (문제 링크)

 

키워드

그리디

 

풀이 접근법

  • 첫 번째 도시에서는 가진 기름이 없기 때문에 무조건 주유를 해야 한다.
  • 마지막 도시에 도착하면 주유할 필요가 없기 때문에 마지막 도시의 기름값은 의미가 없다.
  • 따라서 중간의 어느 도시에서 주유할 지를 선택하는 게 중요하다.
  • 2 ≤ N ≤ 100,000 이기 때문에 O(N²) 시간을 넘지 않는 방식으로 해결해야 한다.
  • 각 도시에서 주유를 할지 말지의 여부를 어떻게 결정할까?
    • 다음 도시의 기름값(cost[i])이 현재 머무르고 있는 도시의 기름값(currCost)보다 크다면 현재 도시에서 다음 도시까지 쓸 기름을 주유한다.
    • 또 그 다음 도시의 기름값을 확인하고, 현재 도시의 기름값과 비교한다. 현재 도시보다 싼 도시가 나올 때까지 누적시킨다. 더 싼 도시를 발견하면 그곳으로 이동해서(currCost = cost[i] 할당) 주유한다. 마지막 도시까지 확인하면 종료한다.
    • 즉, 주유해야 하는 리터 수는 일정하므로 (전체 dist만큼) dist[i]만큼의 기름을 어느 주유소에서 주유할 지를 반복문으로 currCost와 비교하면서 찾으면 된다. (currCost : 역대 방문한 주유소 중 가장 낮았던 주유비를 기록)

 

코드

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

public class Q13305 {

	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		int N = Integer.parseInt(br.readLine());
		
		long[] cost = new long[N];
		long[] dist = new long[N - 1];
		
		StringTokenizer st = new StringTokenizer(br.readLine(), " ");;
		for(int i = 0; i < N - 1; i++) {
			dist[i] = Long.parseLong(st.nextToken()); 
		}
		
		st = new StringTokenizer(br.readLine(), " ");
		for(int i = 0; i < N; i++) {
			cost[i] = Long.parseLong(st.nextToken()); 
		}
		
		long sum = 0;
		long currCost = cost[0];
		sum += currCost * dist[0];
		for(int i = 1; i < N - 1; i++) {
			if(currCost > cost[i]) { //더 싼값에 주유할 수 있다면
				currCost = cost[i];
			}
			sum += currCost * dist[i];
		}
		
		System.out.print(sum);
	}

}

매 순간마다 최선의 선택을 한다는 그리디 알고리즘인 만큼, 매 순간마다 가장 비용이 작은 주유소를 택하는 발상이 중요한 문제였다. 필요한 비용을 계속 누적시켜야 하는 문제이고, 입력으로 주어지는 가격 또한 큰 편이다. 오버플로우가 일어나지 않도록 무언가를 누적시켜야 하는 변수를 long 타입으로 선언해주었다.  

 

git 링크

https://github.com/jinju9553/23-CodingTest/blob/main/week8/day3/Q13305.java

 

문제 풀이 날짜: 2023.05.18
포스트 작성일: 2023.05.18

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

Software Expert Academy 1208번 : Flatten (D3) (문제 링크)

 

키워드

완전탐색, 그리디

 

풀이 접근법

  • 입력 tc의 개수는 무조건 10개로 고정이다. 입력 배열의 크기도 항상 100으로 고정이다.
  • 완전 탐색으로 풀이하였다. 그리디한 발상을 이용하기도 하였다.
  • 입력 배열을 정렬하는 것이 핵심이다. 매 순간마다 가장 왼쪽 & 오른쪽 끝 값을 이용하여 덤프 작업을 수행하면 되며, 이렇게 하면 매 반복마다 가장 높이가 높은 곳과 낮은 곳을 일일이 찾을 필요가 없다.
  • 가장 왼쪽의 작은 값을 가리키는 lowerBound와 가장 오른쪽의 큰 값을 가리키는 upperBound 계속 찾아주며 덤프 작업을 수행한다. 같은 값이 여러개 있다면 lowerBound는 같은 값들 중 가장 오른쪽 것을 가리키고, upperBound 또한 가장 오른쪽 것을 가리킨다.
    • 풀이 당시에는 가장 작은 값을 가리키는 인덱스, 가장 큰 값을 가리키는 인덱스라는 의미로 lowerBound와 upperBound라는 이름을 썼으나 이분탐색에서 쓰는 개념과 혼동될 여지가 있다. lowerIndex, upperIndex 등으로 바꾸어 쓰는 게 좋을 듯하다.

 

코드

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

public class SWEA1208 {
	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		int T = 10;
		int N = 100;
		
		int[] boxes = new int[N];

		StringBuilder sb = new StringBuilder();
		for(int test_case = 1; test_case <= T; test_case++) {
			int dump = Integer.parseInt(br.readLine());
			
			StringTokenizer st = new StringTokenizer(br.readLine(), " ");
			for(int i = 0; i < N; i++) {
				boxes[i] = Integer.parseInt(st.nextToken());
			}
			
			Arrays.sort(boxes);
			
			int lowerBound = 0;
			int upperBound = N - 1;
			for(int i = 0; i < dump; i++) {
				//1.dump 작업을 수행한다.
				boxes[lowerBound] += 1;
				boxes[upperBound] -= 1;
				
				//2.다음 lowerBound를 찾는다.
				for(int j = 0; j < N - 1; j++) {
					if(boxes[j] < boxes[j + 1]) {
						lowerBound = j;
						break;
					}
				}
				
				//3.다음 upperBound를 찾는다.
				if(boxes[upperBound] < boxes[upperBound - 1]) {
					upperBound = upperBound - 1;
				}
				else { // upperBound >= upperBound - 1
					upperBound = N - 1;
				}
			}
			
			Arrays.sort(boxes);
			
			int result = boxes[N - 1] - boxes[0];
			sb.append("#" + test_case + " ").append(result).append('\n');
		}
        
        System.out.print(sb);
	}
}

lowerBound와 upperBound를 찾는 방식이 조금 깔끔하지 못하다. 입력의 크기가 더 커진다면 이러한 방식은 사용할 수 없을 것이다. 풀이를 마친 후 다른 분들의 풀이를 참고해보니, 덤프 작업을 수행할 때마다 배열을 정렬하는 방법도 있었다. 그 방식을 사용한다면 코드를 더욱 깔끔하게 줄일 수 있을 듯하다.

 

git 링크

https://github.com/jinju9553/23-CodingTest/blob/main/week8/day5/SWEA1208.java

문제 풀이 날짜: 2023.05.18
포스트 작성일: 2023.05.18

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

Software Expert Academy 1206번: View (D3) (문제 링크)

 

키워드

완전탐색

 

풀이 접근법

  • 입력 tc의 개수는 무조건 10개로 고정이다.
  • 단순 완전탐색으로 풀이하였다. N ≤ 1000이며, O(N) 시간 안에 해결이 가능하다.
  • 조망권을 측정할 빌딩(i)과 그 좌우에 있는 각각 두 개의 빌딩(i - 2, i - 1, i + 1, i +2)과 높낮이를 비교한다.
    • 좌우 네 개의 빌딩보다 i번째 빌딩이 높으면 조망권을 확보할 수 있는 건물이다.
    • 좌우 네 개의 빌딩 중에서 i보다 낮으면서도 높이가 최대인 것을 찾아(secondHeight) 둘의 높이 차를 구한다. (height[i] - secondHeight) 이것이 조망권이 확보된 세대 수이다.
    • 이를 반복문 끝까지 누적시키면 우리가 찾는 답이 된다.

 

코드

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

public class SWEA1206 {

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

		StringBuilder sb = new StringBuilder();
		for(int test_case = 1; test_case <= T; test_case++) {
			int N = Integer.parseInt(br.readLine());
			
			int[] height = new int[N];
			
			StringTokenizer st = new StringTokenizer(br.readLine(), " ");
			for(int i = 0; i < N; i++) {
				height[i] = Integer.parseInt(st.nextToken());
			}
			
			int result = 0;
			for(int i = 2; i < N - 2; i++) {
				int secondHeight = -1;
				
                		//좌우 네 개의 빌딩을 탐색
				for(int j = -2; j <= 2; j++) {
					if(j == 0) {
						continue;
					}
					
                    			//하나라도 i번째 빌딩보다 높으면 탐색을 멈춘다.
					if(height[i + j] > height[i]) {
						secondHeight = -1;
						break;
					}
					
					secondHeight = Math.max(height[i + j], secondHeight);
				}
				
                		//i번째 빌딩이 가장 높다면
				if(secondHeight > -1) {
					result += (height[i] - secondHeight);
				}
			}
			
			sb.append("#" + test_case + " ").append(result).append('\n');
		}
        
        System.out.print(sb);
	}

}

 

입력의 크기가 작기 때문에 웬만한 완전탐색으로 풀이가 가능하지 않을까 싶다. 입력의 크기가 지금보다 더 커진다면 다른 방법을 고민해보아야 할 듯하다.

 

git 링크

https://github.com/jinju9553/23-CodingTest/blob/main/week8/day5/SWEA1206.java

문제 풀이 날짜: 2023.05.03
포스트 작성일: 2023.05.06

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

프로그래머스 코딩테스트 고득점Kit 해시 편 : 베스트앨범 (Lv.3) (문제 링크)

 

키워드

해시맵, 우선순위 큐

 

풀이 접근법

  • 주어진 조건에 부합하는 노래를 ‘두 곡’만 선정하는데, 만약 조건에 부합하는 것이 오직 한 곡만 존재한다면 한 곡만 수록해야 한다. 조건은 다음과 같다.
  1. 속한 노래가 많이 재생된 장르를 먼저 수록
    • 각 노래별로 총 재생수를 집계하고, 해당 장르의 곡 목록을 탐색한다 ⇒ 정렬 필요
  2. 장르 내에서 많이 재생된 노래를 먼저 수록
    • 그 장르 내에서 가장 재생수가 높은 노래 탐색 ⇒ 정렬 필요
  3. 장르 내에서 재생 횟수가 같은 노래 중에서는 고유 번호가 낮은 노래를 먼저 수록
    • 별도의 Comparator를 이용한 정렬 기준이 필요하다.
  • 장르별 재생수를 집계한 다음에 수록은 어떻게 할까?
    • 우선순위 큐에 모든 장르를 넣고, 재생수가 높은 장르를 하나씩 뽑는다. (poll) ⇒ Entry 자료구조 필요
    • 해당 장르의 노래들로 Entry<장르, 재생수> 리스트를 만들어 사전순의 반대로 정렬한다.
    • 앨범 등록을 마친 장르는 자연히 우선순위 큐에서 삭제된 상태이다.
    • 큐에 더 이상 들어있는 것이 없다면 종료한다.

 

코드

import java.util.*;

class Solution {
    public int[] solution(String[] genres, int[] plays) {
        int[] answer = {};

        HashMap<String, Integer> map = new HashMap<>();

        //장르별 총 재생횟수 집계
        for(int i = 0; i < genres.length; i++) {
            String genre = genres[i];
            int play = plays[i];

            map.put(genre, map.getOrDefault(genre, 0) + play);
        }

        PriorityQueue<Map.Entry<String, Integer>> pq = new PriorityQueue<>(
            (o1, o2) -> Integer.compare(o2.getValue(), o1.getValue())); //내림차순

        for(Map.Entry<String, Integer> entry : map.entrySet()) {
            pq.add(entry);
        }

        ArrayList<Integer> result = new ArrayList<>();

        //pq에서 재생수가 높은 장르 순으로 뽑으며 앨범에 수록한다.
        while(!pq.isEmpty()) {
            Map.Entry<String, Integer> entry = pq.poll();
            String genre = entry.getKey();

		//poll()로 뽑은 장르의 노래들만 모은다.
            ArrayList<int[]> list = new ArrayList<>();
            for(int i = 0; i < genres.length; i++) {
                if(genre.equals(genres[i])) { 
                    int[] a = {i, plays[i]};
                    list.add(a);
                }
            }

            Collections.sort(list, (o1, o2) -> (Integer.compare(o2[1], o1[1])));

            int avaliableSize = 0;
            if(list.size() >= 2) {
                avaliableSize = 2;
            }
            else if (list.size() == 1) {
                avaliableSize = 1;
            }

            for(int i = 0; i < avaliableSize; i++) {
                int num = (list.get(i))[0];
                result.add(num);
            }
        }

        answer = new int[result.size()];
        for(int i = 0; i < result.size(); i++) {
            answer[i] = result.get(i);
        }

        return answer;
    }
}

해시 문제집에 있었지만 해시 그 자체보다는 여러 자료구조를 알아야 풀기 쉬운 문제인 것 같다. 시간을 정해놓고 풀었기 때문에 급하게 마무리했으나, 개인적으로 코드를 더 깔끔하게 짰으면 좋았을 텐데 하는 아쉬움이 있다. Stream을 사용하면 몇몇 코드를 깔끔하게 다듬을 수 있을 듯하다. 

 

git 링크

https://github.com/jinju9553/23-CodingTest/blob/main/week7/day1/BestAlbum.java

 

문제 풀이 날짜: 2023.05.05

포스트 작성일: 2023.05.06

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

프로그래머스 코딩테스트 고득점Kit 그리디 편 : 구명보트 (Lv.2) (문제 링크)

 

키워드

그리디, 투포인터

 

풀이 접근법

  • 보트에는 한 번에 최대 2명씩만 탈 수 있다는 점에 유의하자.
  • 입력의 수는 1 ≤ n ≤ 50,000 이므로, 50,000 × 50,000 = 2,500,000,000 으로 10^9 시간을 초과한다. 따라서 O(n²) 시간을 넘지 않는 방식으로 풀이해야 한다.
  • 보트의 무게 제한을 넘기지 않으려면 가능한 가벼운 사람과 무거운 사람을 함께 태워 보내야 한다. ⇒ 주어진 인원의 무게를 오름차순으로 정렬해서 양 끝을 탐색한다.
  • 배열을 정렬하였으므로 왼쪽 포인터와 오른쪽 포인터가 가리키는 사람을 함께 태울 수 있는지의 여부를 체크한다.
  • 만약 두 사람을 함께 태울 수 없다면 맨 오른쪽의 무거운 사람을 먼저 보트 하나에 태워보내고(right += 1) 다음 사람들의 무게를 체크한다.
  • 둘 다 태울 수 있다면 해당 2인을 태우고 양쪽의 인덱스를 모두 증가시킨다. (left += 1 & right += 1)
  • 두 포인터가 만나는 지점까지 탐색하면 종료된다. (같은 사람을 가리키게 되면 종료)

 

코드

import java.util.*;

class Solution {
    public int solution(int[] people, int limit) {
        int answer = 0;

        Arrays.sort(people);

        int left = 0;
        int right = people.length - 1;

        while(left <= right) {
            if(people[left] + people[right] <= limit) { 
                left += 1;
            }
            right -= 1;
            answer += 1;
        }

        return answer;
    }
}

 

처음에는 배열을 정렬한 뒤 맨 앞부터 두 명씩 가벼운 사람을 태우는 방식으로 풀었는데 대부분의 테스트 케이스에서 틀렸다. 반례를 찾아보니 인접한 무게의 사람들끼리 태우는 것으로는 해를 구할 수 없어, 투포인터로 바꿔 풀었다. 

 

Git 링크

https://github.com/jinju9553/23-CodingTest/blob/main/week7/day3/Lifeboat.java

 

+ Recent posts