안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 문자열 문제 - 디지털시계 코틀린 풀이를 진행하고자 합니다.

 

문제 출처: https://www.acmicpc.net/problem/1942

 

1942번: 디지털시계

디지털시계는 일반적으로 시각을 “hh:mm:ss”의 형태로 표현한다. hh는 00 이상 23 이하의 값을, mm과 ss는 00 이상 59 이하의 값을 가질 수 있다. 이러한 형태의 시각에서 콜론(“:”)을 제거하면 “hhm

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*

const val LAST_TIME_SECONDS_OF_DAY: Int = 3600 * 23 + 59 * 60 + 59
const val START_TIME_SECONDS_OF_DAY: Int = 0

fun main() = BufferedReader(InputStreamReader(System.`in`)).use {

    val sb = StringBuilder()
    repeat(3) {
        val (startTimeSeconds, endTimeSeconds) = readlnOrNull()!!.split(" ").map { it.toTimeSeconds() }

        val result = when {
            startTimeSeconds <= endTimeSeconds -> calculateMultipleOfThreeBetween(startTimeSeconds, endTimeSeconds)
            else -> calculateMultipleOfThreeBetween(startTimeSeconds, LAST_TIME_SECONDS_OF_DAY) +
                    calculateMultipleOfThreeBetween(START_TIME_SECONDS_OF_DAY, endTimeSeconds)
        }

        sb.append(result).append("\n")
    }

    println(sb)
}

private fun calculateMultipleOfThreeBetween(startTimeSeconds: Int, endTimeSeconds: Int): Int {
    var targetTimeSeconds = startTimeSeconds
    var result = 0

    while (targetTimeSeconds <= endTimeSeconds) {

        val longOfTime = targetTimeSeconds.transformTimeToLiteralNumber()
        if (longOfTime % 3 == 0) {
            result++
        }

        if (targetTimeSeconds == LAST_TIME_SECONDS_OF_DAY) {
            break
        }

        targetTimeSeconds++
    }

    return result
}

fun Int.transformTimeToLiteralNumber(): Int {
    val hours = this / 3600
    val minutes = (this % 3600) / 60
    val seconds = this % 60
    return hours * 10000 + minutes * 100 + seconds
}

fun String.toTimeSeconds(): Int {
    val (hours, minutes, seconds) = this.split(":").map { it.toInt() }
    return hours * 3600 + minutes * 60 + seconds
}

 

 

2. 풀이 중점사항

 

- 시작 시간과 종료 시간의 대수 비교

시작 시간이 종료 시간보다 클 경우 (ex: 23:59:59, 00:00:01) 시작 시간 ~ 23:59:59, 00:00:00 ~ 종료 시간 으로 구획을 나눠서 진행해야합니다.

만약, 시작 시간보다 종료 시간이 클 경우는 시작 시간 <= 종료 시간이 될 때 까지 순회하면 됩니다.

val result = when {
    startTimeSeconds <= endTimeSeconds -> calculateMultipleOfThreeBetween(startTimeSeconds, endTimeSeconds)
    else -> calculateMultipleOfThreeBetween(startTimeSeconds, LAST_TIME_SECONDS_OF_DAY) +
            calculateMultipleOfThreeBetween(START_TIME_SECONDS_OF_DAY, endTimeSeconds)
}

 

-시작 시간과 종료 시간 사이의 범위구하기

시작 시간과 종료 시간은 초(단위)로 바꿔서 구할 수 있습니다. 

예를 들어 00:00:00 ~ 00:01:03이 있다면 이를 초(단위)로 표현하면 0 ~ 63이 됩니다.

 

fun String.toTimeSeconds(): Int {
    val (hours, minutes, seconds) = this.split(":").map { it.toInt() }
    return hours * 3600 + minutes * 60 + seconds
}

 

0 ~ 63까지 순회하되, 순회한 숫자를 시간 문자 그대로의 숫자로 바꿔주어야 합니다.

0 -> 00:00:00 -> 0

63 -> 00:01:03 -> 103

이를 위해 시간, 분, 초로 바꾼 후 각 단위에 맞게 10의 제곱승을 곱해줄 수 있습니다.

 

fun Int.transformTimeToLiteralNumber(): Int {
    val hours = this / 3600
    val minutes = (this % 3600) / 60
    val seconds = this % 60
    return hours * 10000 + minutes * 100 + seconds
}

 

이렇게 구한 문자 그대로의 시간을 표현하는 정수를 3의 배수인지 파악하고 결과를 리턴하면 결과를 얻을 수 있습니다!

private fun calculateMultipleOfThreeBetween(startTimeSeconds: Int, endTimeSeconds: Int): Int {
    var targetTimeSeconds = startTimeSeconds
    var result = 0

    while (targetTimeSeconds <= endTimeSeconds) {

        val longOfTime = targetTimeSeconds.transformTimeToLiteralNumber()
        if (longOfTime % 3 == 0) {
            result++
        }

        if (targetTimeSeconds == LAST_TIME_SECONDS_OF_DAY) {
            break
        }

        targetTimeSeconds++
    }

    return result
}

 

 

이상으로 백준 디지털시계 문제 풀이를 마치도록 하겠습니다. 감사합니다!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 문자열 문제 - 경고 풀이를 진행하도록 하겠습니다.

 

문제 출처: https://www.acmicpc.net/problem/3029

 

3029번: 경고

첫째 줄에 현재 시간이 hh:mm:ss 형식으로 주어진다. (시, 분, 초) hh는 0보다 크거나 같고, 23보다 작거나 같으며, 분과 초는 0보다 크거나 같고, 59보다 작거나 같다. 둘째 줄에는 나트륨을 던질 시간

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*

fun main() = BufferedReader(InputStreamReader(System.`in`)).use {

    val startTime = readlnOrNull()?.timeToSeconds() ?: throw IllegalArgumentException()
    val endTime = readlnOrNull()?.timeToSeconds() ?: throw IllegalArgumentException()

    val result = when {
        endTime - startTime <= 0 -> (endTime + 24 * 3600 - startTime).toTimeString()
        else -> (endTime - startTime).toTimeString()
    }

    println(result)
}


fun String.timeToSeconds(): Int {
    val (hour, minute, seconds) = this.split(":").map { it.toInt() }
    return hour * 3600 + minute * 60 + seconds
}

fun Int.toTimeString(): String {
    val hours = this / 3600
    val minutes = (this % 3600) / 60
    val seconds = this % 60

    return String.format("%02d:%02d:%02d", hours, minutes, seconds)
}

 

2. 풀이 과정

 

- 확장 함수 사용

코틀린 확장함수를 사용하여 String, Int 등 기본 타입에 추가로 커스텀한 함수를 만들 수 있습니다.

timeToSeconds()라는 확장함수를 정의하여 시간, 분, 초를 초단위 정수로 변환하였습니다.

 

toTimeString()은 정수를 필요한 포맷인 00:00:00 형태로 바꿔주는 함수입니다.

만약 숫자를 그대로 출력하면 2자리가 아닌 한자리 숫자(ex 8)는 그대로 한자리가 됩니다.

String.format("%02d:%02d:%02d", hours, minutes, seconds)로 필요한 공백은 0으로 만들어서 리턴할 수 있습니다.

 

- when 사용

endTime - startTime이 0이하인 경우는 endTime에 24시간을 더 한 후 startTime에서 빼면 시간 차이를 구할 수 있습니다.

문제에서 최소 1초는 대기한다고 하였으므로 만약 startTime과 endTime이 시간이 같다면 endTime은 날짜가 하루 지난 것이라고 보아야 합니다.

 

이상으로 백준 경계 문제 풀이를 마치도록 하겠습니다.

감사합니다!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 별자리 만들기 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/4386

 

4386번: 별자리 만들기

도현이는 우주의 신이다. 이제 도현이는 아무렇게나 널브러져 있는 n개의 별들을 이어서 별자리를 하나 만들 것이다. 별자리의 조건은 다음과 같다. 별자리를 이루는 선은 서로 다른 두 별을 일

www.acmicpc.net

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;
import static java.lang.Double.parseDouble;
public class Main {
    
    static int n; // n개의 별
    static int[] parent; // 부모 노드
    static int[] count; // 자식 노드 개수
    
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine());
        parent = new int[n]; // 초기화
        count = new int[n];
        double[][] star = new double[n][2];
        
        for (int i = 0; i < n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            double x = parseDouble(st.nextToken());
            double y = parseDouble(st.nextToken());
            star[i][0] = x;
            star[i][1] = y;
            parent[i] = i; // 초기화는 부모 노드 = 자식 노드
            count[i] = 1; // 자신을 포함해야하므로 개수 1로 초기화
        }
        if (n == 1) {
            System.out.println(0.00); // n == 1이라면 거리 측정 불가 0
            return;
        }
        
        System.out.printf("%.2f", getDistance(star));
    }

    // 우선순위 큐에 거리를 입력한 후 하나씩 빼며 유니온 파인드 진행
    // count[p1]이 n개를 만족한다면 모든 자식 노드가 하나의 루트를 공유하므로 mst 완성
    static double getDistance(double[][] star) {
        Queue<Star> queue = new PriorityQueue<>(Comparator.comparing((Star s) -> s.distance));
        
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                double distance = Math.sqrt(Math.pow((star[i][0] - star[j][0]), 2) + Math.pow((star[i][1] - star[j][1]), 2));
                queue.add(new Star(i, j, distance)); // 우선순위 큐에 별 간의 거리 입력
            }
        }
        
        double distance = 0.0;
        while(!queue.isEmpty()) {
            Star s = queue.poll();
            int p1 = find(s.idx1);
            int p2 = find(s.idx2);

            if (p1 != p2) { // 두 부모가 다른 경우에만 유니온 (사이클 방지)
                union(p1, p2); // 유니온
                distance += s.distance; // 거리 업데이트
            }

            if (count[p1] == n) break; // 만약 개수가 n개가 된다면 탐색 종료
        }
        
        return distance; // 거리 리턴
    } 
    
    
    static int find(int child) { // 경로 압축으로 find
        if (parent[child] == child) return child;
        return parent[child] = find(parent[child]);
    }
    
    static void union(int p1, int p2) { // 유니온
        parent[p2] = p1;
        count[p1] += count[p2]; // p2를 부모로 가지는 자식 노드들의 개수를 count[p1]에 더해주기
        count[p2] = 0;
    }
    
    static class Star {
        int idx1; // 별 첫번째
        int idx2; // 별 두번째
        double distance; // 두 별 간의 거리
        
        Star (int idx1, int idx2, double distance) {
            this.idx1 = idx1;
            this.idx2 = idx2;
            this.distance = distance;
        }
    }
}

 

2. 풀이 중점 사항

 

이번 문제는 MST 문제로, 두 별사이의 거리를 우선순위 큐에 저장한 후, 모든 노드가 하나의 경로로 이어질 수 있도록 유니온 파인드를 적용하는 방법으로 해결할 수 있었습니다.

 

우선순위 큐

 

MST에서 크루스칼 알고리즘을 사용한다면 우선순위 큐를 활용하여 간선의 가중치를 오름차순 형태로 빼낼 수 있습니다.

다른 문제와 달리 이 문제는 간선의 길이를 제공하지 않으므로 두 별자리의 계산 공식 (두 점 사이의 거리)를 활용하여 값을 구하고 이를 우선순위 큐에 넣는 방법을 활용해야 합니다.

 

Queue<Star> queue = new PriorityQueue<>(Comparator.comparing((Star s) -> s.distance));

for (int i = 0; i < n; i++) {
    for (int j = i + 1; j < n; j++) {
        double distance = Math.sqrt(Math.pow((star[i][0] - star[j][0]), 2) + Math.pow((star[i][1] - star[j][1]), 2));
        queue.add(new Star(i, j, distance)); // 우선순위 큐에 별 간의 거리 입력
    }
}

 

람다를 활용하여 distance로 정렬할 수 있도록 한 후, 두 점사이의 거리 공식을 활용하고 큐에 넣습니다.

 

 

유니온 파인드

 

static int find(int child) { // 경로 압축으로 find
    if (parent[child] == child) return child;
    return parent[child] = find(parent[child]);
}

static void union(int p1, int p2) { // 유니온
    parent[p2] = p1;
    count[p1] += count[p2]; // p2를 부모로 가지는 자식 노드들의 개수를 count[p1]에 더해주기
    count[p2] = 0;
}

 

find(), union()은 정형화된 메서드입니다. 경로압축으로 자식 노드가 가리키는 부모 노드를 찾으면서 업데이트합니다.

union()은 찾은 두 부모를 하나로 연결하는 과정으로, parent[p2]의 값을 p1으로 업데이트하여, p2의 부모가 p1이 되도록 합니다.

 

하단에 count[]코드는 n개의 별자리를 만족시키면 종료해야 합니다. 이 경우, 부모 노드가 가지고 있는 자식 노드들의 개수를 합치는 (더 커지는) 부모 노드에 업데이트해주어야 합니다. 

 

따라서, p1에 p2의 자식 노드들의 개수를 더해주고, p2는 더 이상 루트 노드가 아니므로 0으로 업데이트합니다.

 

double distance = 0.0;
while(!queue.isEmpty()) {
    Star s = queue.poll();
    int p1 = find(s.idx1);
    int p2 = find(s.idx2);

    if (p1 != p2) { // 두 부모가 다른 경우에만 유니온 (사이클 방지)
        union(p1, p2); // 유니온
        distance += s.distance; // 거리 업데이트
    }

    if (count[p1] == n) break; // 만약 개수가 n개가 된다면 탐색 종료
}

return distance; // 거리 리턴

 

만약 이미 부모가 같은 경우는, distance를 증가시키면 안 됩니다 (사이클)

이미 같은 부모를 공유한다는 의미는 두 별자리에 대한 distance를 더했음을 의미합니다.

따라서, 이 경우는 패스하고 두 부모가 다른 경우에만 적용할 수 있도록 하였습니다.

 

이상으로 백준 별자리 만들기 풀이를 마치도록 하겠습니다.

감사합니다!!!

 

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 LIS 문제 - 가장 긴 증가하는 부분 수열5 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/14003

 

14003번: 가장 긴 증가하는 부분 수열 5

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

public class Main {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = parseInt(br.readLine());
        int[] arr = new int[n]; // 입력 배열
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        for (int i = 0; i < n; i++) arr[i] = parseInt(st.nextToken()); // 배열 초기화
        List<Integer> result = search(arr);
        
        StringBuilder sb = new StringBuilder();
        sb.append(result.size()).append("\n");
        for (Integer i : result) sb.append(i).append(" ");
        System.out.print(sb);
    }
    
    static List<Integer> search(int[] arr) {
        List<Integer> dp = new ArrayList<>();
        
        int[] idxs = new int[arr.length]; // 인덱스 저장
        int[] preIdxs = new int[arr.length]; // 다음으로 이동할 임시 인덱스
        
        for (int i = 0; i < arr.length; i++) {
            if (dp.isEmpty() || arr[i] > dp.get(dp.size() - 1)) {
                if (!dp.isEmpty()) preIdxs[i] = idxs[dp.size() - 1]; // dp가 비지 않은 경우 업데이트하는 인덱스와 다음으로 이동할 인덱스 업데이트
                dp.add(arr[i]); // dp에 추가
                idxs[dp.size() - 1] = i; // 인덱스 업데이트
            } else { // 이분 탐색
                int left = 0, right = dp.size() - 1;
                while (left < right) {
                    int mid = left + (right - left) / 2;
                    if (dp.get(mid) < arr[i]) left = mid + 1;
                    else right = mid;
                }
                dp.set(right, arr[i]);
                idxs[right] = i;
                if (right > 0) preIdxs[i] = idxs[right - 1];
            }            
        }
        
        List<Integer> lis = new ArrayList<>();
        int idx = idxs[dp.size() - 1];
        while (lis.size() < dp.size()) {
            lis.add(arr[idx]);
            idx = preIdxs[idx];
        }
        Collections.reverse(lis); // 마지막 역순 출력
        return lis;
    }
}

 

2. 풀이 중점 사항

 

해당 문제는 이전에 풀었던 LIS 풀이에서 자세하게 설명하였습니다!

해당 풀이에서 그림과 함께 자세하게 설명하여서 링크를 남기도록 하겠습니다!

https://gose-kose.tistory.com/103

 

[Algorithm] 백준 DP 문제 - 줄세우기(2631) 자바 풀이

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다. 이번 포스팅은 줄세우기 자바 풀이를 작성하도록 하겠습니다. 문제 출처: https://www.acmicpc.net/problem/2631 2631번: 줄세우기 KOI 어린이집에는 N명의

gose-kose.tistory.com

이상으로 풀이를 마치겠습니다 감사합니다!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 LCS 문제 - LCS2 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/9252

 

9252번: LCS 2

LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다. 예를 들어, ACAYKP와 CAPCAK의 LCS는 ACAK가 된다.

www.acmicpc.net

 

1. 풀이 소스

 

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

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

        String str1 = br.readLine(); // 문자열 1
        String str2 = br.readLine(); // 문자열 2
        int[][] dp = new int[str1.length() + 1][str2.length() + 1];

        for (int i = 1; i <= str1.length(); i++) { // LCS 공식
            for (int j = 1; j <= str2.length(); j++) {
                if (str1.charAt(i - 1) == str2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }

        StringBuilder sb = new StringBuilder();
        if (dp[str1.length()][str2.length()] == 0) sb.append(0); // 만약 값이 0 -> LCS가 없음
        else {
            sb.append(dp[str1.length()][str2.length()]).append("\n");

            Stack<Character> stack = new Stack<>();
            int row = str1.length(), col = str2.length();
            while (row > 0 && col > 0) { // 역추적 --> LCS 공식에 따라, (행 - 1)과 같다면 행--, (열 - 1)과 같다면 열 --

                if (dp[row][col] == dp[row - 1][col]) row--;
                else if (dp[row][col] == dp[row][col - 1]) col--;
                else { // 행 혹은 열 하나씩 뺀 것과 같지 않다면, 나머지는 대각선
                    stack.push(str1.charAt(row - 1)); // 스택에 넣기
                    row--;
                    col--;
                }
            }

            while(!stack.isEmpty()) sb.append(stack.pop()); // 후입선출로 맨 마지막부터 출력
        }
        System.out.println(sb);
    }
}

 

2. 풀이 중점 사항 

 

LCS 공식 적용

 

for (int i = 1; i <= str1.length(); i++) { // LCS 공식
    for (int j = 1; j <= str2.length(); j++) {
        if (str1.charAt(i - 1) == str2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;
        else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
    }
}

 

이전 LCS 문제에서 풀이했던 방식을 그대로 활용할 수 있습니다. 이전 블로그에서 자세하게 작성하였습니다. 혹시 LCS 공식이 헷갈리신다면 참고 부탁드립니다.!

https://gose-kose.tistory.com/119 

 

[Algorithm] 백준 LCS 문제 - LCS(9251) 자바 풀이

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다. 이번 포스팅은 백준 LCS 문제 풀이를 진행하고자 합니다. 문제 출처: https://www.acmicpc.net/problem/9251 9251번: LCS LCS(Longest Common Subsequence, 최장 공통

gose-kose.tistory.com

 

LCS 역추적하기

 

Stack<Character> stack = new Stack<>();
int row = str1.length(), col = str2.length();
while (row > 0 && col > 0) { // 역추적 --> LCS 공식에 따라, (행 - 1)과 같다면 행--, (열 - 1)과 같다면 열 --

    if (dp[row][col] == dp[row - 1][col]) row--;
    else if (dp[row][col] == dp[row][col - 1]) col--;
    else { // 행 혹은 열 하나씩 뺀 것과 같지 않다면, 나머지는 대각선
        stack.push(str1.charAt(row - 1)); // 스택에 넣기
        row--;
        col--;
    }
}

while(!stack.isEmpty()) sb.append(stack.pop()); // 후입선출로 맨 마지막부터 출력

 

LCS를 만들 때, 만약 두 문자열의 값이 같다면 대각선을 활용하고, 아니라면 행 혹은 열은 하나씩 줄여서 최대값으로 이어 가는 것을 확인할 수 있었습니다. 이를 그대로 활용하여, 최대값을 기준으로 대각선과 값이 같다면 stack에 값을 넣고, row, col을 각각 -- 해줍니다. 만약 행이 하나 작은 것과 같다면, 행을 줄이고, 열이 하나 작은 것과 같다면 열을 줄여서 최대값이 적용된 흐름대로 움직이는 것입니다.

 

이러한 방법으로 LCS의 값을 역추적할 수 있습니다. 이상으로 풀이를 마치겠습니다. 감사합니다!!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 이분탐색 문제 - 공유기 설치 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/2110

 

2110번: 공유기 설치

첫째 줄에 집의 개수 N (2 ≤ N ≤ 200,000)과 공유기의 개수 C (2 ≤ C ≤ N)이 하나 이상의 빈 칸을 사이에 두고 주어진다. 둘째 줄부터 N개의 줄에는 집의 좌표를 나타내는 xi (0 ≤ xi ≤ 1,000,000,000)가

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

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

        n = parseInt(st.nextToken());
        int m = parseInt(st.nextToken());

        house = new int[n];
        for (int i = 0; i < n; i++) house[i] = parseInt(br.readLine());
        Arrays.sort(house);

        int left = 1; // 최소 거리는 1
        int right = house[n - 1] - house[0] + 1; // 최대 거리는 양끝 값 차이 + 1

        while (left < right) { // upper bound로 찾기
            int mid = left + (right - left) / 2; // 두 거리의 중점으로 거리 업데이트

            int count = installWifi(mid);
            if (count < m) right = mid; // 설치된 개수가 m에 만족하지 않는 경우 더 설치 해야하므로 최소 거리 줄이기
            else left = mid + 1; // 최소 거리를 만족하므로 더 길게 가능한지 파악하기 위해 left 업데이트
        }

        System.out.println(left - 1);
    }

    static int installWifi(int distance) {
        int lastLoc = house[0]; // lastLoc은 맨 처음집으로 설정
        int count = 1; // lastLoc은 1번 집 설정하므로 개수 1

        for (int i = 0; i < n; i++) {
            int nowLoc = house[i];

            if (nowLoc - lastLoc >= distance) { // 최소 거리보다 크다면
                count++; // 개수 증가
                lastLoc = nowLoc; // 마지막 위치 업데이트
            }
        }
        return count;
    }
}

 

2. 풀이 중점 사항

 

해당 문제는 이분탐색을 어느 것으로 설정하느냐가 중요한 포인트였습니다.

m개의 공유기를 설치해야 하므로, 공유기 위치를 콤비네이션으로 정의하게 된다면 그 경우의 수가 10억 C 20만 까지 될 수 있으므로 시간초과가 발생할 수 있습니다.

 

따라서, 최단 거리를 이분탐색하여, m개의 공유기를 만족하는 가장 큰 값을 찾는 방법으로 문제를 정의할 수 있습니다.

 

예를 들어 이분탐색한 결과 집 간 인접한 두 공유기의 사이의 최대 거리가 3일 때, 10개의 공유기를 설치할 수 있습니다.

이는 곳 1, 4, 10, 15 .... 혹은 1, 4, 7, 14...로 10개를 설치했음을 의미합니다.

 

m개의 공유기를 만족하기 때문에 mid 값인 3에 +1을 하여 길이를 4로 만들고 다시 탐색하여 공유기 10개를 설치할 수 있는지 판단합니다. 만약 m개를 만족하지 않는다면 right = 4를 설정하여 다시 이분탐색을 진행하는 것입니다.

 

이 원리를 적용하면,

집의 개수 최대 20만,

공유기 개수 최대 20만

이분 탐색 0 ~ 10억 사이의 거리이므로 log10억  = 약 28 ~ 30 사이쯤

 

이분탐색하는 횟수 * 집을 탐색하며 공유기를 설치하는 횟수 

= O(20만 * 30) = O(600만) 정도로 생각할 수 있습니다.

 

3. 코드로 확인하기

 

int left = 1; // 최소 거리는 1
int right = house[n - 1] - house[0] + 1; // 최대 거리는 양끝 값 차이 + 1

 

따라서, 길이의 최소 거리를 의미하는 1과 각 집의 가장 양끝에 있는 거리를 left, right로 설정하여, mid를 구합니다.

mid를 최단 거리로 설정하여, 마지막 설치된 공유기의 길이에서 지금 설치하는 공유기 길이가 최소거리보다 큰 경우에만 업데이트합니다. (만약 최소 거리보다 작다면 카운트 개수가 증가하지 않으므로, m 개수를 만족하지 않는다면 이분탐색을 다시 실행)

 

static int installWifi(int distance) {
    int lastLoc = house[0]; // lastLoc은 맨 처음집으로 설정
    int count = 1; // lastLoc은 1번 집 설정하므로 개수 1

    for (int i = 0; i < n; i++) {
        int nowLoc = house[i];

        if (nowLoc - lastLoc >= distance) { // 최소 거리보다 크다면
            count++; // 개수 증가
            lastLoc = nowLoc; // 마지막 위치 업데이트
        }
    }
    return count;
}

 

while (left < right) { // upper bound로 찾기
    int mid = left + (right - left) / 2; // 두 거리의 중점으로 거리 업데이트

    int count = installWifi(mid);
    if (count < m) right = mid; // 설치된 개수가 m에 만족하지 않는 경우 더 설치 해야하므로 최소 거리 줄이기
    else left = mid + 1; // 최소 거리를 만족하므로 더 길게 가능한지 파악하기 위해 left 업데이트
}

이렇게 얻은 count로 m개에 만족, 불만족하는지 판단한후 upper bound 형식으로 이분탐색을 진행합니다.

이상으로 백준 공유기 설치 자바 풀이를 마치도록 하겠습니다. 감사합니다.! 

 

참고 블로그: https://st-lab.tistory.com/277

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 나무 자르기 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/2805

 

2805번: 나무 자르기

첫째 줄에 나무의 수 N과 상근이가 집으로 가져가려고 하는 나무의 길이 M이 주어진다. (1 ≤ N ≤ 1,000,000, 1 ≤ M ≤ 2,000,000,000) 둘째 줄에는 나무의 높이가 주어진다. 나무의 높이의 합은 항상 M보

www.acmicpc.net

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;
import static java.lang.Integer.parseInt;

public class Main {
    static int[] trees;
    static int m;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = parseInt(st.nextToken());
        trees = new int[n]; // n개의 나무
        
        m = parseInt(st.nextToken());
        st = new StringTokenizer(br.readLine());
        int max = 0;
        for (int i = 0; i < n; i++) {
            trees[i] = parseInt(st.nextToken());
            max = Math.max(trees[i], max); // 가장 긴 나무를 선택하여 자를 위치 선정
        }
        
        System.out.println(find(max));
    }
    
    static long find(int max) {
        long value, left, mid; // 이분 탐색을 위한 long 값 20억 이상 될 수 있음
        long right = max; // right 최대값 선정
        value = left = mid = 0L; // value, left 초기화

        while(left < right) {
            mid = left + (right - left) / 2; // 이분탐색 중간 값 최대 40억

            for (int tree : trees) {
                if (tree > mid) value += tree - mid; // 잘리는 위치보다 큰 경우 더하기
            }
            
            if (value >= m) left = mid + 1; // 목표로 하는 값보다 같거나 크다면 upper bound
            else right = mid; // 아니라면 right
            value = 0; // value 초기화
        }
        
        return right - 1;
    }
}

 

2. 시간 복잡도 계산

 

이번 문제는 앞 서 풀었던 랜선 자르기와 비슷한 문제로 이분 탐색의 upper bound를 설정하여 해결할 수 있습니다.

최대 100만 개의 N을 가지고, 20억 개의 범위를 탐색해야 하므로 일반적으로 for문을 돌린다면 시간초과가 날 수 있습니다.

 

총나무의 수가 최대 100만 개이므로 찾은 최적의 값을 가지고 100만 번을 연산해야 합니다. 만약 아니라면 값을 바꿔서 다시 진행해야 하므로 100만 * 나무를 찾는 횟수가 시간 복잡도가 됩니다.

 

이분 탐색은 logM의 시간 복잡도를 가지므로. 이분 탐색의 upper bound를 활용한다면,

O(N * logM)  = O(100만 * 약 31) = 대략 O(3100만) 의 시간을 가진다고 볼 수 있습니다. 

 

3. upper bound로 계산하기

 

static long find(int max) {
    long value, left, mid; // 이분 탐색을 위한 long 값 20억 이상 될 수 있음
    long right = max; // right 최대값 선정
    value = left = mid = 0L; // value, left 초기화

    while(left < right) {
        mid = left + (right - left) / 2; // 이분탐색 중간 값 최대 40억

        for (int tree : trees) {
            if (tree > mid) value += tree - mid; // 잘리는 위치보다 큰 경우 더하기
        }
        
        if (value >= m) left = mid + 1; // 목표로 하는 값보다 같거나 크다면 upper bound
        else right = mid; // 아니라면 right
        value = 0; // value 초기화
    }
    
    return right - 1;
}

 

upper bound는 찾아야 하는 값 a에 가장 근접하게 큰 수를 찾도록 돕는 알고리즘입니다.

이분탐색을 수행하면서 경계점을 찾아야 하는 경우 사용할 수 있습니다.

 

mid의 값을 left + (right - left) / 2로 업데이트한 후, value가 크거나 같다면 left = mid + 1 하여 upper 경계를 올립니다.

value가 크거나 같다는 말은 충분한 양을 획득했다는 의미이므로 높이를 올려서 잘라야 하는 양을 줄이는 것입니다.

(기기의 높이를 올리면 잘리는 길이가 줄어듬)

 

반면, 작다면 right = mid로 수정하여 value를 증가시키는 방법으로 이분탐색을 진행합니다.

left가 right보다 같거나 커지는 시점이 오면 루프를 종료하고, upper의 경계를 설정했으므로 right의 값에서 -1을 하여 리턴합니다.

 

-1을 하는 이유는 다음과 같습니다.

left 35

right 37

mid 36 

value >= m

-> left = mid + 1;

left <= right 이므로 종료하게 됩니다. 여기서 right는 찾아야 하는 target의 값보다 1이 크게 되므로(자연수)

이와 같은 로직을 작성할 수 있게 됩니다.

 

이상으로 백준 나무 자르기 자바 풀이를 마치도록 하겠습니다.

감사합니다.!!! 

 

 

 

 

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 스택 문제 히스토그램 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/1725

 

1725번: 히스토그램

첫 행에는 N (1 ≤ N ≤ 100,000) 이 주어진다. N은 히스토그램의 가로 칸의 수이다. 다음 N 행에 걸쳐 각 칸의 높이가 왼쪽에서부터 차례대로 주어진다. 각 칸의 높이는 1,000,000,000보다 작거나 같은

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

public class Main {

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

        n = parseInt(br.readLine());
        histogram = new int[n];

        for (int i = 0; i < n; i++) histogram[i] = parseInt(br.readLine());
        sb.append(getArea());
        System.out.println(sb);
    }

    static int getArea() { // 최대 20억을 넘지 않음
        Stack<Integer> stack = new Stack<>();
        int area = 0; // 20억을 넘지 않음

        for (int i = 0; i < n; i++) {

            // 스택이 비지 않고 인덱스의 값이 peek의 히스토그램 값보다 작거나 같다면
            while (!stack.isEmpty() && histogram[stack.peek()] >= histogram[i]) {
                int h = histogram[stack.pop()];

                // 스택이 비어 있다면 길이는 i, 스택이 있다면 현재 위치에서 stack의 인덱스
                int w = stack.isEmpty() ? i : i - 1 - stack.peek();

                area = Math.max(area, h * w);
            }
            stack.push(i);
        }

        // stack에 남아있는거 전부 제거하며 다시 업데이트
        while(!stack.isEmpty()) {
            int h = histogram[stack.pop()];

            int w = stack.isEmpty() ? n : n - 1 - stack.peek();
            area = Math.max(area, h * w);
        }

        return area;
    }
}

 

2. 풀이 중점 사항

 

해당 문제는 매우 어려웠던 문제로 하단의 블로그를 참고하였습니다.

stack에 값을 빼서, 면적을 구해야 하는 과정은 이해가 되었지만 도중에 높이가 0이 오는 경우에는 어떻게 해야 하는가?

라는 물음에 빠지면서, 많은 시간을 소비하였습니다.

 

// 스택이 비지 않고 인덱스의 값이 peek의 히스토그램 값보다 작거나 같다면
while (!stack.isEmpty() && histogram[stack.peek()] >= histogram[i]) {
    int h = histogram[stack.pop()];

    // 스택이 비어 있다면 길이는 i, 스택이 있다면 현재 위치에서 stack의 인덱스
    int w = stack.isEmpty() ? i : i - 1 - stack.peek();

    area = Math.max(area, h * w);
}
stack.push(i);

 

현재 있는 인덱스의 값보다 스택에 있는 높이가 높거나 같다면, 스택에서 빼면서 면적을 구하는 코드입니다.

위에 있는 코드는 다음과 같은데, 그림과 함께 설명하도록 하겠습니다.

 

 

예시는 7개의 입력과 4 2 1 2 0 2 1의 높이로 구성되어 있는 히스토그램입니다.

빨간색 화살표는 인덱스, 초록색 화살표는 스택의 peek입니다.

 

시작 인덱스 0은 stack.isEmpty()이므로 인덱스를 추가합니다.

 

 

스택에 있는 높이는 4인데, 인덱스 1에 있는 높이는 2입니다. 따라서 스택에 있는 0을 빼서 면적을 구해야 합니다.

 

 

stack이 비어 있으므로, 인덱스 길이만큼을 w로 설정합니다. 

Area = 4입니다.

스택에 인덱스 1을 추가합니다.

 

 

스택에 있는 peek는 1이고, 그것의 높이는 2입니다. 반면 포인터의 위치의 높이는 1이므로 

스택 1에 대한 면적을 구합니다.

 

 

스택이 비어있으므로, 인덱스 길이만큼 너비를 설정합니다. (스택의 너비를 인덱스 길이만큼 구하는 이유는 다음과 같이 연속된 같은 높이를 가지는 블록의 면적을 모두 고려하기 위함입니다.)

 

 

stack에 있는 인덱스 2의 높이보다 인덱스 3의 높이가 더 크므로 진행합니다.

 

 

같은 방법으로, 인덱스 4 이전에 쌓였던 스택을 모두 제거하며 면적을 구합니다.

 

 

여기서 중요한 부분은, 현재 포인터 위치 - 1 - 스택의. peek() 값만큼 너비를 선택해야 합니다.

그 이유는 stack.peek()는 경계를 설정하는 부분이고, 다음 스택에 값이 있으니 그 이전까지 면적을 구해야 합니다.

포인터의 길이는 항상 스택에 있는 값보다 +1 이상이 더 큽니다.

즉 point - 1은 너비의 가장 오른쪽의 끝점을 의미하고, stack.peek()는 시작하는 위치라고 생각하면,

(2, 3)의 넓이이므로 1이 도출되게 되는 것입니다. 이것을 한 번에 계산하기 위해 3 - 2 = point - 1 - stack.peek()가 도출됩니다.

 

이어서, 스택이 비어있으므로 point의 인덱스 값을 너비로 설정하여 면적을 구합니다.

 

 

 

제가 어려웠던 부분은 이처럼 0이 도중에 나오는 경우 어떻게 해결해야 하는가? 였습니다.

하지만 이전에 설정한 범위를 생각해 보면, while루프는 stack.peek()의 값이 히스토그램의 값보다 크거나 같을 때 

면적을 계산합니다. 즉, stack.peek()이 0이 된다면, 다시 0이 나오기 전까지 그 상태가 유지되는 것입니다.

(ex 0 2 1 3 4 --> 이렇게 유지될 경우 0보다 같거나 작은 수는 없으니 stack에 0은 그대로 유지)

 

따라서, 이처럼 0을 가진 h의 높이가 stack.peek()로 경계를 만들어주고 있는 것을 볼 수 있습니다.

 

 

 

마지막은 이처럼 stack에 0이 쌓여 있는 경우, 혹은 계속 상승하는 상태여서 stack에서 나오지 않는 경우가 있을 수 있습니다.

따라서, 스택에서 값을 빼며 면적을 다시 구합니다.

 

 

 

이를 통해 가장 큰 면적 4를 구할 수 있습니다.

 

이상으로 히스토그램 자바 풀이를 마치도록 하겠습니다.! 감사합니다.!

 

참조 블로그: https://st-lab.tistory.com/255

 

[백준] 6549번 : 히스토그램에서 가장 큰 직사각형 - JAVA [자바]

https://www.acmicpc.net/problem/6549 6549번: 히스토그램에서 가장 큰 직사각형 입력은 테스트 케이스 여러 개로 이루어져 있다. 각 테스트 케이스는 한 줄로 이루어져 있고, 직사각형의 수 n이 가장 처음

st-lab.tistory.com

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 문자열 문제 - 무한 문자열 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/12871

 

12871번: 무한 문자열

첫째 줄에 s, 둘째 줄에 t가 주어진다. 두 문자열 s와 t의 길이는 50보다 작거나 같은 자연수이고, 알파벳 소문자로만 이루어져 있다. 

www.acmicpc.net

 

1. 풀이 소스 

 

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        
        String str1 = br.readLine();
        String str2 = br.readLine();
        
        int gcd = getGCD(str1.length(), str2.length()); // 최대 공약수 찾기
        if (gcd == 1) gcd = str1.length() * str2.length(); // 1이라면 최소 공배수는 두 문자열 길이의 곱
        else gcd = str1.length() * (str2.length() / gcd); // 1이 아니라면, 문자열 길이 * 문자열 길이 / 최대 공약수

        str1 = repeat(str1, gcd); // 문자열 반복
        str2 = repeat(str2, gcd);

        if (str1.equals(str2)) sb.append(1);
        else sb.append(0);
        
        System.out.print(sb);
    }

    // 유클리드 호재법
    static int getGCD(int x, int y) {
        if (y == 0) return x;
        return getGCD(y, x % y);
    }

    // 최소 공배수 개수만큼 문자열 증가
    static String repeat(String str, int lcm) {
        String repeat = str;
        while (str.length() < lcm) {
            str += repeat;
        }
        return str;
    }
}

 

2. 풀이 중점 사항

 

최소 공배수만큼 문자열을 반복한 후, 두 문자열을 비교하는 방법을 사용하였습니다.

중점 사항은 최대 공약수를 구하는 알고리즘입니다. 유클리드 호재법을 활용하여 재귀로 문제를 해결할 수 있습니다.

 

static int getGCD(int x, int y) {
    if (y == 0) return x;
    return getGCD(y, x % y);
}

 

나머지 풀이는 주석으로 대체하였습니다 감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 문자열 문제 - 여우는 어떻게 울지? 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/9536

 

9536번: 여우는 어떻게 울지?

각 테스트케이스마다 여우의 울음소리를 한 줄씩, 녹음된 순서대로 출력한다. 여우의 울음소리가 녹음되어 있음이 보장된다. (알려진 것과는 달리, 여우는 모스 부호로 의사소통하지 않는다.)

www.acmicpc.net

 

1. 풀이 소스

 

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

public class Main {
    public static void main(String[] args) throws IOException {
        
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine());
        StringBuilder sb = new StringBuilder();
        
        for (int i = 0; i < T; i++) {
            HashMap<String, ArrayList<Integer>> map = new HashMap<>();
            String[] saying = br.readLine().split(" "); // 울음소리 저장

            for (int c = 0; c < saying.length; c++) {
                if (!map.containsKey(saying[c])) map.put(saying[c], new ArrayList<>(List.of(c))); //키 파악
                else map.get(saying[c]).add(c); // 키 있다면 list에 저장
            }

            String str;
            while((str = br.readLine()) != null && !str.equals("what does the fox say?")) {
                String[] strArr = str.split(" ");
                String sound = strArr[strArr.length - 1];
                List<Integer> list = map.get(sound);
                for (Integer num : list) saying[num] = ""; // 울음 소리가 저장된 인덱스 찾아서 "" 만들기
            }

            for (String say : saying) {
                if (say != null && !say.equals("")) sb.append(say).append(" "); // null이 아니고 ""가 아니라면 울음 소리 저장
            }
            sb.append("\n");
        }
        
        System.out.print(sb);
    }
}

 

2. 풀이 중점 사항

 

HashMap<String, ArrayList<Integer>> map = new HashMap<>();
String[] saying = br.readLine().split(" "); // 울음소리 저장

for (int c = 0; c < saying.length; c++) {
    if (!map.containsKey(saying[c])) map.put(saying[c], new ArrayList<>(List.of(c))); //키 파악
    else map.get(saying[c]).add(c); // 키 있다면 list에 저장
}

 

울음소리는 계속 반복되므로, HashMap에 울음소리와 울음소리의 인덱스를 저장할 ArrayList<>을 key - value로 설정하여 값을 저장하였습니다.

 

정해진 개수만큼 입력을 받는 것이 아닌, EOF 혹은 특정 문자열 입력을 받으면 종료하는 로직을 작성해야할 때가 있습니다.

String str;
while((str = br.readLine()) != null && !str.equals("what does the fox say?")) {
}

 

이 경우, str에 br.readLine() 값을 할당했을 때, 그 값이 null이 아니고, 필요로 하는 문자열이 아닌 경우에만 입력을 계속 받을 수 있도록 처리하였습니다.

 

이상으로 여우는 어떻게 울지? 풀이를 마치도록 하겠습니다. 감사합니다.!

+ Recent posts