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

 

2156번: 포도주 시식

효주는 포도주 시식회에 갔다. 그 곳에 갔더니, 테이블 위에 다양한 포도주가 들어있는 포도주 잔이 일렬로 놓여 있었다. 효주는 포도주 시식을 하려고 하는데, 여기에는 다음과 같은 두 가지 규

www.acmicpc.net

 

풀이

1 <= n <= 10,000이라서 모두 탐색하면 시간 초과가 난다. 풀어보면 어떻게 과거의 모든 경우의 수를 예측해서 문제를 해결하지 싶은데, 이런 경우에는 다이나믹 프로그래밍을 사용할 때가 많았다.

 

나는 다이나믹 프로그래밍 문제를 다음과 같은 절차로 해결한다.

 

1. 반복문에서 사용할 점화식을 구성하기 위해 함수를 정의한다.

-> dp[i]: 포도주가 i번째 까지 있을 때, 조건을 만족하는 최대 포도주 양

2. 함수의 초기 값을 설정한다.

-> dp[0] = 0, dp[1] = wine[1], dp[2] = wine[1] + wine[2] (포도주 인덱스는 1부터 시작하도록 세팅)

3. 초기 값을 기반으로 모든 경우의 수를 커버할 수 있는 점화식을 구한다.

-> dp[i] =

{

 if (i번째 와인을 마시지 않는다) dp[i-1] 

 if (i번째, i-1번째 와인을 마시고, i-2는 마시지 않는다) wine[i] + wine[i-1] + dp[i-3] 

 if (i번째, i-2번째 와인을 마시고, i-1은 마시지 않는다) wine[i] + dp[i-2]

}

 

이렇게 반드시 조건을 만족시키게 점화식을 구성하고, 초기 값부터 점화식을 만족하도록 설정하면 모든 항이 조건을 만족하게 된다.

그리고 점화식을 구성하면서 i-3까지 인덱스를 봐야 하길래, 3번째 포도주부터 반복문을 돌리기 위해 포도주 인덱스를 1부터 시작했다. 

 

그리고 i를 3 ~ n까지 반복문을 돌려서 dp 값을 구하면, dp[n]이 답이 된다.

 

코드

import kotlin.math.max

fun main() {
    val n = readLine()!!.toInt()
    val wines = IntArray(n+2)
    val dp = IntArray(n+2)
    
    // 포도주 인덱스 1부터 시작
    repeat(n){ i ->
        wines[i+1] = readLine()!!.toInt()
    }

    // 초기값 설정
    dp[1] = wines[1]
    dp[2] = wines[1] + wines[2]
    for (i in 3..n){
        // 점화식
        val maxWithCurrentWine 
            = max(dp[i-3] + wines[i-1] + wines[i], dp[i-2] + wines[i]) // i번째 와인을 마시는 경우 중 최대 값 
        dp[i] 
            = max(dp[i-1], maxWithCurrentWine) // i번째 와인을 마시는 or 안마시는 경우 중 최대 값
    }

    println(dp[n])
}

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

 

2668번: 숫자고르기

세로 두 줄, 가로로 N개의 칸으로 이루어진 표가 있다. 첫째 줄의 각 칸에는 정수 1, 2, …, N이 차례대로 들어 있고 둘째 줄의 각 칸에는 1이상 N이하인 정수가 들어 있다. 첫째 줄에서 숫자를 적절

www.acmicpc.net

 

풀이 & 잡담

어떤 알고리즘을 써야할까 고민을 많이 했다. 정렬, 그리디, 백트래킹, ... 그래프 탐색? 예시를 보니까 (1번 - 3번)을 고르면 3번을 타고 들어가서 (3번 -1번)을 고르는 식으로, 왠지 그래프 탐색 아닐까 생각이 들었다. 하지만 풀이 방법이 확실하게 떠오르지 않았고 알고리즘 유형을 봤더니 dfs 문제였는데, 그걸로 어떻게 최대 개수로 같은 집합을 뽑게 만들라는 건지 여전히 감이 오지 않았다. 그래서 풀이 방법을 찾아봤는데, dfs를 통해 그래프 방문의 사이클을 찾아내는 문제였다.

 

사이클은 반드시 형성된다. 아랫줄의 번호들이 1~n이기 때문이다. 같은 숫자가 2번 나오든, n까지 하나씩 나오든 반드시 사이클이 형성되므로, 사이클을 형성하는 번호들을 모두 찾으면 그것이 답이다.

 

사실 이 문제 이해하려고 다른 사람들 코드도 많이 보고, 생각을 많이 했더니 뭐가 이해가 안됐는지 기억이 안난다. 내가 뭘 모르는지 아는 것도 어려운 거였다.

 

쓰리디핏님의 블로그(https://3dpit.tistory.com/12)를 참고했는데, 접근 방법을 보며 금방 이해할 수 있었다. 그리고 사실 약간 충격을 받았다, 내가 여태껏 수동적으로 문제를 풀고 있었구나... 앞으론 이해가 잘 안될 때는 직접 그림도 그려보고, 예시를 적극적으로 활용하면서 문제를 이해해봐야겠다고 반성했다. 

 

사이클을 발견하는 문제란 건 알았는데, 어떻게 구현하는지도 문제였다. 가장 간결한 방법은 첫번째 줄에 있는 숫자들을 기준으로 1~n까지 하나씩 그래프 탐색을 하면서 (ex: 1을 시작으로 방문 -> 1은 3과 연결되어 있으므로 1을 방문 체크하고 3을 방문, 3을 방문 체크하고 연결되어 있는 1을 방문 ....) 사이클을 형성하는지 확인하는 것이다. 사이클은 반드시 형성되므로, 첫째줄에 있는 숫자들을 기준으로 방문 여부를 체크해서 중복 방문할 때 시작 번호에서 사이클이 형성되는지 확인하면 된다.

코드1이 이런 방식이다.

코드2는 위, 아래 줄의 방문한 번호들을 다 저장해가면서 푸는, 좀 더 직관에 가까운 풀이법이고,

코드3는 dfs는 스택으로도 풀 수 있을테니 시도해보다가, 단순하게 변수만 사용해도 구현이 가능하길래 변수로 스택을 사용한 것처럼 구현했다. 채점해보니 재귀보다 느렸다

 

이번 문제는 어렵게 느껴저서, 여러 코드를 읽어봤다. 복습에도, 문제 해결에 관한 사고력을 늘리는 데에도 도움이 많이 될 것 같아서 앞으로도 잘 안풀리는 문제들은 이렇게 공부하려고 한다.

 

코드1

fun main() {
    val n = readLine()!!.toInt()
    val secondLine = IntArray(n+1)
    val answerList = mutableListOf<Int>()

    repeat(n){ i ->
        secondLine[i+1] = readLine()!!.toInt()
    }

	// 첫째 쭐의 번호들을 대상으로 사이클을 형성하는 숫자인지 확인한다.
    for (i in 1..n){
        val visited = Array(n+1){ false }.apply{ this[i] = true }
        if(hasCycle(i, secondLine[i], visited, secondLine)){
            answerList.add(i)
        }
    }

    // 1부터 사이클을 확인했으므로 정렬하지 않아도 된다
    println(answerList.size)
    answerList.forEach{
        println(it)
    }
}

fun hasCycle(start: Int, current: Int, visited: Array<Boolean>, secondLine: IntArray): Boolean {
    // 현재 번호를 처음 방문하는 거라면 방문 처리를 하고 연결된 다음 번호를 방문한다
    if (visited[current].not()){
        visited[current] = true
        return hasCycle(start, secondLine[current], visited, secondLine)
    }

    // 중복 방문일 때, 탐색을 시작한 번호에서 사이클 시작된다 -> 사이클을 형성하는 번호(정답)이다
    // 중간에 사이클이 형성된다 -> 정답이 아니다
    return start == current
}

 

코드2

val answerList = mutableListOf<Int>()

fun main() {
    val n = readLine()!!.toInt()
    val secondLine = IntArray(n + 1)
    val isAnswer = Array(n + 1) { false }

    repeat(n) { i ->
        secondLine[i + 1] = readLine()!!.trim().toInt()
    }

    for (i in 1..n) {
        if (isAnswer[i]) continue

        val visitedFirstLine = mutableSetOf<Int>()
        val visitedSecondLine = mutableSetOf<Int>()
        checkCycle(i, secondLine, visitedFirstLine, visitedSecondLine, isAnswer)
    }

    println(answerList.size)
    answerList.sorted().forEach {
        println(it)
    }
}

fun checkCycle(
    index: Int,
    secondLine: IntArray,
    visitedFirstLine: MutableSet<Int>,
    visitedSecondLine: MutableSet<Int>,
    isAnswer: Array<Boolean>
){
    if (visitedFirstLine.contains(index)) {
        return
    }

    visitedFirstLine.add(index)
    visitedSecondLine.add(secondLine[index])
    if (visitedFirstLine == visitedSecondLine){
        visitedFirstLine.forEach { i ->
            isAnswer[i] = true
            answerList.add(i)
        }
        return
    }

    checkCycle(secondLine[index], secondLine, visitedFirstLine, visitedSecondLine, isAnswer)
}

 

코드3

fun main() {
    val n = readLine()!!.toInt()
    val secondLine = IntArray(n + 1)
    val answerList = mutableListOf<Int>()


    repeat(n) { i ->
        secondLine[i + 1] = readLine()!!.trim().toInt()
    }

    for (startIndex in 1..n) {
        val visited = Array(n + 1) { false }.apply { this[startIndex] = true }
        var currentIndex = secondLine[startIndex]

        while (true) {

            if (!visited[currentIndex]) {
                visited[currentIndex] = true
                currentIndex = secondLine[currentIndex]
                continue
            }
            
            if (startIndex == currentIndex) {
                answerList.add(startIndex)
            }
            break
        }
    }

    println(answerList.size)
    answerList.forEach {
        println(it)
    }
}

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

 

풀이

문제가 실버라서 얕봤다가 크게 다쳤다. 조건 자체를 이해하기 어려웠는데, 가장 인접한 두 공유기 사이의 거리를 최대로 하라는 말이 와닿지 않았다. 가장 인접한, 최대 거리??

그래서 마이구미님 블로그(https://mygumi.tistory.com/301)를 참고해서 이해할 수 있었다.

 

위 말을 맥락적으로 해석하면, 최대한 공평하면서도 간격이 크게 공유기를 설치하라는 것이다. 특정 두 집의 사이의 설치 간격이 너무 커지면, 그만큼 나머지 설치된 집들 사이의 간격은 짧아져서 가장 인접한 설치 거리가 짧아지기 때문이다.

 

즉, 거리가 가장 가까운 두 공유기 사이의 거리를 k라고 하면, 나머지 인접한 공유기들 사이의 모두 k보다 크거나 같아야 한다.

ex) k = 3,  2 <-> 5 <-> 12 <-> 15 <-> 19  - (공유기가 설치된 위치)

                         3        7           3           4          - (사이 거리) --> 모두 3보다 크거나 같다

 

이 문제는 이분 탐색의 응용인 파라메트릭 서치를 통해서 해결하는 문제로 알려져 있는데, 방법은 다음과 같다.

1. 가장 인접한 공유기 사이의 거리를 대상으로 이분탐색을 진행한한다.

2. 그 거리보다 크거나 같도록하는 기준을 만족시키면서 왼쪽 집부터 공유기를 설치하면서 갯수를 센다. 

3. 문제에서 주어진 공유기의 개수와 설치한 공유기의 개수를 비교해서, 같거나 더 많이 설치했다면 간격을 늘리고, 더 적게 설치했다면 간격을 줄인다.

4. left(lower) <= right(upper)일 때까지 1~3번을 반복하여 가장 인접한 설치 거리의 상한선을 찾는다.   

 

여기서 이해가 안갔던 건 왜 왼쪽 집부터 항상 공유기를 설치하느냐는 것이었다. 정확하게 증명하려면 약간의 수학이 필요한듯 한데, 자세한 건 https://www.acmicpc.net/board/view/50802를 참고해보자.

  

 

코드

fun main(){
    var n = 0
    var c = 0
    val listOfHouse = mutableListOf<Int>()

    readLine()!!.trim().split(" ").map{ it.toInt() }.let{
        n = it[0]
        c = it[1]
    }

    repeat(n){
        listOfHouse.add(readLine()!!.toInt())
    }
    listOfHouse.sort()	// 정렬: 이분탐색의 전제 조건

    var lower = 1	// 최소 간격
    var upper = listOfHouse.last() - listOfHouse[0]	// 최대 간격: 양 끝 집 사이의 거리
    var answer = upper

    while(lower <= upper){
        val mid = (lower + upper) / 2
        var count = 1			// 공유기 설치한 횟수
        var prevHouseIdx = 0	// 직전에 설치된 집

        // mid보다 인접 간격이 길거나 같도록 설치한다
        for(i in 1 until n){
            if (listOfHouse[i] - listOfHouse[prevHouseIdx] >= mid){
                count++
                prevHouseIdx = i
            }
        }
        
        // c == count -> 간격을 더 늘려도 모두 설치할 수 있는지 확인해본다
        // c < count -> 더 많이 설치됐다 -> 설치 간격을 늘려본다
        if (c <= count){
            lower = mid + 1
            answer = mid
        }else{			// 더 많은 공유기를 설치해야 한다 -> 간격을 줄인다
            upper = mid - 1
        }
    }

    println(answer)
}

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

 

11057번: 오르막 수

오르막 수는 수의 자리가 오름차순을 이루는 수를 말한다. 이때, 인접한 수가 같아도 오름차순으로 친다. 예를 들어, 2234와 3678, 11119는 오르막 수이지만, 2232, 3676, 91111은 오르막 수가 아니다. 수

www.acmicpc.net

 

풀이

백준 10844번 쉬운 계단 수와 거의 같은 문제다. (https://best-human-developer.tistory.com/14)

 

k-1 길이의 오르막 수에서 마지막 수보다 크거나 같은 수를 이어 붙이면 길이가 k인 오르막 수를 구할 수 있다.

ex) 123 -> 1233, 1234, ..., 1239

이렇게 수의 길이 k에 대해 직전 항으로 다음 항을 만들 수 있으므로 다이나믹 프로그래밍으로 문제를 해결할 수 있다.

 

길이 k와 마지막 자리 숫자 i로 점화식 dp를 정의하면,

dp[k][i]: 길이가 k이고, i로 끝나는 오르막 수

dp[k][i] = sum( dp[k-1][j] ), (0 <= j <= i) 

 

 

코드

fun main(){
    val n = readLine()!!.toInt()
    val dp = Array(n+1){IntArray(10)}
    
    // 길이가 1이고 i로 끝나는 오르막수
    for (i in 0 until 10){
    	dp[1][i] = 1
    }

    // 길이 2~n까지 n-1번 반복
    for (k in 2..n){
    
        for (i in 0..9){
        	for (j in 0..i){
            	dp[k][i] = (dp[k][i] + dp[k-1][j]) % 10_007
            }
        }

    }

    println(dp[n].toList().reduce{acc, num -> (acc + num) % 10_007})
}

+ Recent posts