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

 

2632번: 피자판매

첫 번째 줄에는 손님이 구매하고자 하는 피자크기를 나타내는 2,000,000 이하의 자연수가 주어진다. 두 번째 줄에는 A, B 피자의 피자조각의 개수를 나타내 는 정수 m, n 이 차례로 주어진다 (3 ≤ m, n

www.acmicpc.net

안녕하세요, 이륙사입니다.

 

풀이

이번 문제는 특정 알고리즘을 사용해서 완전 탐색(brute force)을 하는 문제였습니다. 

 

저는 문제를 다음과 같이 해석했습니다. 

"배열A와 배열B의 숫자들을 더해서 특정 숫자를 만드는 경우의 수를 알고싶다. 단, 각 배열에서 숫자를 뽑을 땐 인덱스를 기준으로 연속해야 하며, 한쪽 배열의 숫자들만 사용할 수도 있다. 그리고 배열은 원형으로 되어있다."

 

즉, 각 원형 배열에서 길이 0 ~ (m or n) 으로 만들 수 있는 모든 연속 합을 각각 구하고, 그것을 A, B의 연속합 배열이라고 합시다. 각 연속 합 배열에서 숫자를 하나씩 골라서 더했을 때 원하는 숫자를 몇 개 만들 수 있는지 확인하면 정답을 구할 수 있습니다.

 

연속합 구하기

연속합을 구하는 과정은 다음과 같습니다.

길이1, 길이3일 때 연속 합

배열이 원형이기 때문에 인덱스를 넘어가더라도 시작하는 위치가 마지막 인덱스가 될 때까지 합을 구합니다.

ex) 길이 3 예시에서 빨간색 구간의 연속합: array[3] + array[4] + array[0] 

따라서 길이가 몇이든 항상 array의 전체 길이 만큼(위에선 5개) 연속합을 구하게 됩니다.

단, 한쪽 피자만 사용할 수도 있으며, 길이가 0이거나 전체인 경우에는 1개의 연속합이 나옵니다.

    var startSum = 0 // 첫 구간의 합
    for (sequentSize in 1 until pizza.size - 1) {
        startSum += pizza[sequentSize - 1] // 인덱스 0부터 연속 길이만큼의 피자 합

        var start = 0
        var end = sequentSize
        var sequentSum = startSum

        // 오른쪽으로 슬라이딩 하면서 길이 sequentSize에 해당하는 연속 합들을 구한다.
        // 마지막 인덱스가 첫번째 숫자가 될 때까지 진행한다.
        while(start <= pizza.lastIndex) {
            sumCount[sequentSum] += 1 // 해당 연속합이 몇 개 존재하는지 count
            sequentSum = sequentSum - pizza[start++] + pizza[end++] // 구간 오른쪽으로 이동
            if (end == pizza.size) {
                end = 0
            }
        }
    }
    sumCount[0] += 1 // 피자 0개 사용
    sumCount[startSum + pizza[pizza.lastIndex]] = 1 // 전체 합

 

누적 합 사용

위의 방법으로 연속 합을 구할 수 있습니다. 하지만 투포인터 형식을 사용해서 약간은 복잡해보입니다. 이때 누적 합 방식을 사용하면 좀 더 빠르고 편하게 구할 수 있습니다.

각 인덱스에서 시작해서 오른쪽으로 이동하면서 누적합을 구합니다. 그러면 각 인덱스를 시작으로 길이가 1 ~ n-1인 연속합을 모두 구할 수 있습니다.

for (i in 0 until pizza.size) { // 시작 위치
        var sum = 0

        // 항상 pizza-1개의 연속합을 찾을 수 있다(전체 합 제외)
        for (j in i until i + pizza.size - 1) {
            sum += pizza[j % pizza.size]
        }
    }

 

가지 수 계산

모든 연속합을 구했으니 이제 모두 확인해보면 답을 찾을 수 있습니다. 하지만 피자조각이 최대 1000개이기 때문에 전부 확인하면 시간초과가 발생합니다.

각 연속합을 구할 때 O(n^2), 두 연속합끼리 비교할 때 O(n^2) ==> 최종: O(n^4)

 

어떻게 시간 복잡도를 줄일 수 있을까요?

우리가 찾고자 하는 목표가 정해져있다는 것에 주목해봅시다. 연속합A에서 특정 합 k를 골랐을 때, 연속합B에서 target - k가 있는지 확인만 하면 됩니다. 즉, A에서 숫자 골랐을 때 B에서는 그 숫자가 있는지 없는지, 있다면 몇 개가 있는지 바로 확인할 수 있다면 굳이 모든 조합을 비교하지 않아도 됩니다.

 

해싱

위의 방법대로 구현하기 위해서 Map(key: sum, value: count)을 사용할 수 있습니다. A의 연속합(sumA)을 구해서 모두 Map에 저장합니다. 그리고 B의 연속합(sumB)을 구해서 Map에 (target - sumB)이 있는지 즉, sumA + sumB = target을 만드는 sumA가 Map에 있는지 확인하면 O(n^2)의 시간으로 문제를 해결할 수 있습니다. 

// pizza A의 연속합을 구할 때마다 counArray에 sum count를 증가시킨다
// 숫자가 최대 1,000,000이기 때문에 map 대신 배열을 사용할 수 있음
sumCountArray[sequentSum] += 1


// pizza B의 연속합을 구할 때마다 target을 만드는 sumA가 있는지 확인
if (target - sum >= 0){
	count += sequentSumArray[target - sum] // 없으면 0, 있으면 1 이상이 저장되어 있음
}

연속합의 크기가 최대 1,000,000이기 때문에 Map 대신 배열을 사용했습니다.

 

투 포인터

원하는 숫자가 정해져있다는 것에서 힌트를 얻어 해싱 기법을 사용할 수 있었습니다. 마찬가지 이유로 투 포인터를 사용할 수 있습니다. 한쪽에서 연속합을 선택했을 때, 다른 쪽의 연속합이 정렬되어 있다면 크기에 따라 포인터를 이동시키면서 원하는 값을 찾을 수 있기 때문입니다.

val sumA = getSumArrayOf(pizzaA) // 연속합A
val sumB = getSumArrayOf(pizzaB) // 연속합B
sumA.sort() // 투 포인터를 사용하기 위해 정렬
sumB.sortDescending()

// 투포인터를 사용해서 정답을 찾는다
...

투포인터를 사용할 때 주의할 점이 있는데요. sumA + sumB = Target이 되는 두 포인터를 찾았을 때, 같은 연속합이 배열에 여러개 있을 수 있습니다. 따라서, 그때마다 중복된 값을 다 찾아서 곱한 값을 더해줘야 합니다.

while (indexA < arrayA.size && indexB < arrayB.size) {
        val sum = arrayA[indexA] + arrayB[indexB]

        if (sum < target) {
            indexA++
        } else if (sum > target) {
            indexB++
        } else { // 양쪽에서 중복 값을 다 찾아서 개수를 곱한다
            var countA = 0L
            var countB = 0L
            val prevA = arrayA[indexA]
            val prevB = arrayB[indexB]

            while (indexA < arrayA.size && arrayA[indexA] == prevA) {
                countA++
                indexA++
            }

            while (indexB < arrayB.size && arrayB[indexB] == prevB) {
                countB++
                indexB++
            }

            count += countA * countB
        }
    }

 

이분 탐색(upper_bound, lower_bound)

정렬된 데이터셋에서 원하는 값을 찾는 또 한가지 방법으로 이분 탐색이 있습니다. 여기서는 같은 연속합이 여러개 있을 수 있기 때문에 한쪽에서 연속합을 선택한 후, 반대편 연속합에서 원하는 값의 upper_bound와 lower_bound를 구해서 두 값을 빼주는 방식으로 target을 만드는 경우의 수를 구할 수 있습니다.

 

생각 과정

  1. 각 피자에서 1조각 이상을 선택해서 특정 크기의 피자를 판매하려고 할 때, 가능한 경우의 수를 구하는 문제이다.
  2. 피자조각을 선택할 땐 항상 연속해서 잘라야 하고, 한 쪽 피자만 사용할 수도 있다.
  3. 우선 두 피자의 모든 연속합 필요하다.
  4. 연속합을 모두 비교하면 시간 초과가 난다.
  5. 구하려고 하는 합이 정해져있다 -> 각각의 연속합을 구해서 반대편에서 원하는 값이 있는지 확인하는 방식을 떠올린다. 각각의 연속합을 구하는 과정은 시간 내에 해결할 수 있다. 

 

코드1 (해싱)

import java.io.BufferedReader
import java.io.InputStreamReader

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val target = readLine().toInt()
    val (m, n) = readLine().split(" ").map(String::toInt)
    val pizzaA = IntArray(m) { readLine().toInt() }
    val pizzaB = IntArray(n) { readLine().toInt() }

    val sequentSumArray = getSumArrayOf(pizzaA)
    print(getCount(sequentSumArray, pizzaB, target))
}

// 1연속 조각 ~ n연속 조각의 합 개수 count (해싱)
fun getSumArrayOf(pizza: IntArray): IntArray {
    val sumCountArray = IntArray(1_000_001)
    var totalSum = 0 // 전체 합을 담을 변수

    for (i in 0 until pizza.size) {
        totalSum += pizza[i]
        var sum = 0

        for (j in i until i + pizza.size - 1) { // 전체 합은 제외
            sum += pizza[j % pizza.size] // 누적합, 원형 배열
            sumCountArray[sum]++
        }
    }

    sumCountArray[0] = 1 // 길이 0
    sumCountArray[totalSum] = 1 // 전체 합
    return sumCountArray
}

// pizzaB의 연속합을 구해서 pizzaA의 연속합에 target을 만드는 값이 있는지 확인
fun getCount(sequentSumArray: IntArray, pizzaB: IntArray, target: Int): Long {
    var count = 0L
    var totalSum = 0

    for (i in 0 until pizzaB.size) {
        totalSum += pizzaB[i]
        var sum = 0

        for (j in i until i + pizzaB.size - 1) {
            sum += pizzaB[j % pizzaB.size]

            // target을 만드는 sumA가 있는지 확인
            if (target - sum >= 0){
                count += sequentSumArray[target - sum]
            }
        }
    }

    count += sequentSumArray[target] // pizzaA만 사용하는 경우
    if (target - totalSum >= 0){     // pizzaB 전체 합
        count += sequentSumArray[target - totalSum]
    } 
    return count
}

 

코드2 (투 포인터)

import java.io.BufferedReader
import java.io.InputStreamReader

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val target = readLine().toInt()
    val (m, n) = readLine().split(" ").map(String::toInt)
    val pizzaA = IntArray(m) { readLine().toInt() }
    val pizzaB = IntArray(n) { readLine().toInt() }

    val sumA = getSumArrayOf(pizzaA)
    val sumB = getSumArrayOf(pizzaB)
    sumA.sort()
    sumB.sortDescending()

    print(getCount(sumA, sumB, target))
}

// 1연속 조각 ~ n연속 조각의 합을 담은 배열 반환
fun getSumArrayOf(pizza: IntArray): IntArray {
    val size = pizza.size
    val sumArray = IntArray(1 + size * (size-1) + 1)
    var sumIndex = 0
    var totalSum = 0 // 전체 합을 담을 변수

    // 연속합을 모두 저장
    for (i in 0 until pizza.size) {
        totalSum += pizza[i]
        var sum = 0

        for (j in i until i + pizza.size - 1) { // 전체 합은 제외
            sum += pizza[j % pizza.size]
            sumArray[sumIndex++] = sum // 누적합, 원형 배열
        }
    }

    sumArray[sumIndex++] = 0 // 길이 0
    sumArray[sumIndex] = totalSum // 전체 합
    return sumArray
}

// 투포인터
fun getCount(arrayA: IntArray, arrayB: IntArray, target: Int): Long {
    var count = 0L
    var indexA = 0
    var indexB = 0

    while (indexA < arrayA.size && indexB < arrayB.size) {
        val sum = arrayA[indexA] + arrayB[indexB]

        if (sum < target) {
            indexA++
        } else if (sum > target) {
            indexB++
        } else {
            var countA = 0L
            var countB = 0L
            val prevA = arrayA[indexA]
            val prevB = arrayB[indexB]

            while (indexA < arrayA.size && arrayA[indexA] == prevA) {
                countA++
                indexA++
            }

            while (indexB < arrayB.size && arrayB[indexB] == prevB) {
                countB++
                indexB++
            }

            count += countA * countB
        }
    }

    return count
}

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

 

1261번: 알고스팟

첫째 줄에 미로의 크기를 나타내는 가로 크기 M, 세로 크기 N (1 ≤ N, M ≤ 100)이 주어진다. 다음 N개의 줄에는 미로의 상태를 나타내는 숫자 0과 1이 주어진다. 0은 빈 방을 의미하고, 1은 벽을 의미

www.acmicpc.net

안녕하세요, 이륙사입니다.

 

풀이1

이번 문제는 BFS를 활용하는 그래프 탐색 문제였습니다.

 

기본적인 BFS와는 문제와는 다른 점이 있는데요. 특정 위치에 최단거리로 가는 것이 아닌, 최소로 벽을 부수면서 간다는 것입니다. BFS의 특징은 가까운 순서대로 한번만 방문하면서 모든 위치를 방문할 수 있다는 것입니다. 하지만 이 문제에서 BFS를 그냥 사용하면, 이동 방향의 우선순위에 따라 벽을 더 많이 부수는 경로가 특정 위치에 먼저 도착할 수 있습니다.  

 

이런 문제를 해결하기 위해 특정 위치에 도착할 때마다 그때까지 부셨던 벽의 개수를 기록합니다. 그리고 새로운 경로에서 방문을 시도할 때, 갱신을 시도하는 값이 작을 때만 재방문을 허용합니다. 그러면 언젠가는 모두 최소 값으로 갱신되기 때문에 정답을 찾을 수 있습니다. 미로 크기가 100 * 100이라서 몇 번의 재탐색이 일어나라도 충분히 시간 안에 해결할 수 있습니다.

// 경로를 지나면서 벽을 몇 개 부셨는지 기록,
// 큰 값으로 초기화해서 처음 방문할 때는 항상 방문하면서 갱신하게 한다
val brokenCountAt = Array(maze.size) {
        IntArray(maze[0].size) { 10_000 }
    }
    
    ....

// 벽을 부셔야하는 상황에서 기존에 기록된 경로 값이 지금의 경로 값이 값보다 크다면 재탐색한다
            if (maze[nextRow][nextCol] == '1' && brokenCountAt[nextRow][nextCol] > brokenCountAt[row][col] + 1) {
                brokenCountAt[nextRow][nextCol] = brokenCountAt[row][col] + 1
                q.add(Pair(nextRow, nextCol))
            }
            // 벽을 부시지 않아도 되지만, 기존 경로 값이 지금 경로 값보다 크면 재탐색
            else if (maze[nextRow][nextCol] == '0' && brokenCountAt[nextRow][nextCol] > brokenCountAt[row][col]) {
                brokenCountAt[nextRow][nextCol] = brokenCountAt[row][col]
                q.add(Pair(nextRow, nextCol))
            }

 

참고로 DFS를 사용하면 한 방향으로 먼저 끝까지 탐색하고 돌아오는데, 다 탐색하고 왔더니 최소 경로가 아니라서 모두 재탐색을 해야하는 경우가 발생할 수 있기 때문에 BFS를 사용하는 것이 좋습니다.

 

풀이2

생각해보니 다익스트라 알고리즘이랑 아주 비슷한 것 같습니다. 사실 다익스트라랑 똑같습니다!ㅋㅋ 0과 1의 가중치를 갖고 있는 그래프 탐색 문제로 생각할 수 있습니다.

 

풀이1에서 문제가 됐던 건 무작정 탐색하면 최소로 벽을 부수는(최소 누적 가중치) 경로가 나오지 않는다는 거였습니다.이제 그 이유를 알 수 있는데요, 이동할 때 가중치를 고려하지 않았기 때문입니다. 문제에서 찾고자 하는 건 최소 가중치를 갖는 경로입니다. 따라서, 움직일 때마다 가중치가 작은 방향을 우선으로 탐색하면 특정 위치를 최소 누적 가중치를 갖고 최초로 방문할 수 있습니다. 

 

이러한 방식으로 구현하기 위해 2가지 방법을 사용할 수 있습니다. 하나는 우선순위 큐를 사용하는 것이고, 다른 하나는 양방향 큐를 사용하는 것입니다. 양방향 큐를 사용하는 경우 0을 탐색할 때는 큐의 앞쪽에, 1을 탐색할 때는 큐의 뒤쪽에 넣으면서 진행합니다. 그러면 항상 누적 가중치가 작은 경로를 전부 탐색한 후에, 그 다음 누적 가중치가 큰 경로들을 탐색하기 때문에 모든 위치를 최소 경로로 한번만 지날 수 있습니다. 

val brokenCountAt = Array<IntArray>(rowSize) { // 벽을 부순 횟수를 기록 + 방문 여부 표시 역할
        IntArray(colSize) { -1 }
    }
    
    ....

if (brokenCountAt[nextRow][nextCol] != -1) continue // 한번 방문한 곳은 재방문X

if (maze[nextRow][nextCol] == 0) {
    brokenCountAt[nextRow][nextCol] = brokenCountAt[row][col]
    deque.addFirst(Pair(nextRow, nextCol))
} else {
    brokenCountAt[nextRow][nextCol] = brokenCountAt[row][col] + 1
    deque.addLast(Pair(nextRow, nextCol))
}

 

여기서도, 가중치에 따라 이동 방향의 우선순위를 결정하기 때문에 DFS 기반으로 탐색을 하기엔 어렵습니다.  

 

생각 과정

  1. 특정 위치로 가는 그래프 탐색 문젠데, 최단거리가 아니라 최소 가중치 문제이다
  2. 노드를 한번씩만 방문하면 이동방향에 따라 최소 가중치 경로를 찾지 못할 수도 있다
  3. 최소 가중치 경로를 찾을수 있도록 조건에 따라 재탐색을 허용한다.
  4. 아니면 처음부터 최소 가중치로 탐색하면서 한번씩만 방문할 순 없을까 고민해본다.
  5. 다익스트라를 알고 있다면 다익스트라나 우선순위 큐를 떠올릴 수 있다.

 

코드1 (BFS사용, 조건에 따라 재탐색 허용)

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val st = StringTokenizer(readLine())
    val colSize = st.nextToken().toInt()
    val rowSize = st.nextToken().toInt()
    val maze = Array(rowSize) {
        readLine().toCharArray()
    }

    print(getMinCount(maze))
}

fun getMinCount(maze: Array<CharArray>): Int {
    val dRow = intArrayOf(-1, 1, 0, 0)
    val dCol = intArrayOf(-0, 0, -1, 1)
    
    // 경로를 지나면서 벽을 몇 개 부셨는지 기록,
    // 큰 값으로 초기화해서 처음 방문할 때는 항상 방문하면서 갱신하게 한다
    val brokenCountAt = Array(maze.size) {
        IntArray(maze[0].size) { 10_000 }
    }
    val q: Queue<Pair<Int, Int>> = LinkedList()
    q.add(Pair(0, 0))
    brokenCountAt[0][0] = 0

    while (q.isNotEmpty()) {
        val (row, col) = q.poll()

        for (i in 0 until 4) {
            val nextRow = row + dRow[i]
            val nextCol = col + dCol[i]

            if (nextRow !in maze.indices || nextCol !in maze[0].indices) continue

            // 벽을 부셔야하는 상황에서 기존에 기록된 경로 값이 지금의 경로 값이 값보다 크다면 재탐색한다
            if (maze[nextRow][nextCol] == '1' && brokenCountAt[nextRow][nextCol] > brokenCountAt[row][col] + 1) {
                brokenCountAt[nextRow][nextCol] = brokenCountAt[row][col] + 1
                q.add(Pair(nextRow, nextCol))
            }
            // 벽을 부시지 않아도 되지만, 기존 경로 값이 지금 경로 값보다 크면 재탐색
            else if (maze[nextRow][nextCol] == '0' && brokenCountAt[nextRow][nextCol] > brokenCountAt[row][col]) {
                brokenCountAt[nextRow][nextCol] = brokenCountAt[row][col]
                q.add(Pair(nextRow, nextCol))
            }
        }
    }

    return brokenCountAt[maze.lastIndex][maze[0].lastIndex]
}

 

코드2 (낮은 가중치 우선 탐색)

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*

// anima94님 풀이 참고 
fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val (colSize, rowSize) = readLine().split(" ").map(String::toInt)
    val maze = Array<IntArray>(rowSize) {
        val string = readLine()
        IntArray(colSize) { i ->
            string[i] - '0'
        }
    }
    
    // 벽을 부순 횟수를 기록 + 방문 여부 표시 역할
    val brokenCountAt = Array<IntArray>(rowSize) {
        IntArray(colSize) { -1 }
    }
    val deque = ArrayDeque<Pair<Int, Int>>()
    val dRow = intArrayOf(-1, 1, 0, 0)
    val dCol = intArrayOf(0, 0, -1, 1)

    brokenCountAt[0][0] = 0
    deque.add(Pair(0, 0))

    while (true) {
        val (row, col) = deque.pollFirst()

        if (row == rowSize - 1 && col == colSize - 1) {
            print(brokenCountAt[row][col])
            return
        }

        for (i in 0 until 4) {
            val nextRow = row + dRow[i]
            val nextCol = col + dCol[i]

            if (nextRow !in 0 until rowSize || nextCol !in 0 until colSize) continue
            if (brokenCountAt[nextRow][nextCol] != -1) continue // 한번 방문한 곳은 재방문X

            if (maze[nextRow][nextCol] == 0) {
                brokenCountAt[nextRow][nextCol] = brokenCountAt[row][col]
                deque.addFirst(Pair(nextRow, nextCol))
            } else {
                brokenCountAt[nextRow][nextCol] = brokenCountAt[row][col] + 1
                deque.addLast(Pair(nextRow, nextCol))
            }
        }
    }
}

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

 

2661번: 좋은수열

첫 번째 줄에 1, 2, 3으로만 이루어져 있는 길이가 N인 좋은 수열들 중에서 가장 작은 수를 나타내는 수열만 출력한다. 수열을 이루는 1, 2, 3들 사이에는 빈칸을 두지 않는다.

www.acmicpc.net

안녕하세요, 이륙사입니다.

 

풀이

이번 문제는 백트래킹을 활용한 완전 탐색(brute force) 문제였습니다.

 

우선, 좋은 수열이란 무엇이고 나쁜 수열과 어떻게 구분지을 수 있을까요?

좋은 수열이란, 가장 끝자리부터 1자리, 2자리, 계속해서 인접 숫자들을 확인했을 때 같은 숫자가 없는 것을 말합니다.

 

그리고 이 과정을 통해 한 자리씩 추가해가면서 전체 수열을 만든다는 아이디어와 각 자리수를 만들 때마다 좋은/나쁜 수열을 판별해야겠다는 생각을 자연스럽게 떠올릴 수 있습니다. 어떤 수가 좋은 수열이라면, 그것의 부분 수열들도 모두 좋은 수열이어야 합니다. 

 

이 아이디어가 중요한 이유는 n자리를 만들고나서 좋은 수열인지 판별을 하면 3^80가지를 확인하기 때문입니다. 하지만 뒤에 자리수를 추가할 때마다 좋은 수열인지 확인한다면, 미리 불필요한 탐색의 가지수를 줄일 수 있기 때문에 훨씬 빠른 시간 안에 탐색이 가능합니다. 그리고 이처럼 불필요한 탐색을 사전에 줄이는 것이 백트래킹으로 전부 확인하는 방법의 핵심입니다.

백트래킹

 

생각 과정

  1. 좋은 수열과 나쁜 수열이란 무엇이고 그 둘을 어떻게 구별할지 생각한다.
  2. 그 과정에서 뒤에 한자리씩 추가하면서 전체 가지수를 확인하고, 숫자를 추가할 때마다 수열을 판별한다는 생각을 떠올린다.

 

코드

import java.io.BufferedReader
import java.io.InputStreamReader
import kotlin.system.exitProcess

var n = 0

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    n = readLine().toInt()
    findMinGood("")
}

fun findMinGood(number: String) {
    if (number.length == n){
        print(number)
        exitProcess(0)
    }

    for (i in 1..3){
        if (isGood(number + i)){
            findMinGood(number + i)
        }
    }
}

fun isGood(number: String): Boolean {
    val length = number.length
    for (i in 1..length / 2) {
        val left = number.substring(length - 2 * i , length - i)
        val right = number.substring(length - i ,length)
        if (left == right) return false
    }

    return true
}

/* 아래와 같이 비교할 수도 있습니다
fun isGood(number: String): Boolean {
    val length = number.length


    for (i in 1..length / 2) { // 비교 길이
        var isGood = false

        for (j in 0 until i) { // 자리수
            // 다른 숫자가 있다면
            if (number[(length - 1) - j] != number[(length - 1) - j - i]) {
                isGood = true
            }
        }

        if (isGood.not()) return false
    }

    return true
}

*/

 

'Problem Solving > 백준' 카테고리의 다른 글

백준 2632번: 피자 판매 (Kotlin)  (0) 2022.01.21
백준 1261번: 알고스팟 (Kotlin)  (0) 2022.01.20
백준 3108번: 로고 (Kotlin)  (0) 2022.01.18
백준 1525번: 퍼즐 (Kotlin)  (0) 2022.01.17
백준 2186번: 문자판 (Kotlin)  (0) 2022.01.16

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

 

3108번: 로고

로고는 주로 교육용에 쓰이는 프로그래밍 언어이다. 로고의 가장 큰 특징은 거북이 로봇인데, 사용자는 이 거북이 로봇을 움직이는 명령을 입력해 화면에 도형을 그릴 수 있다. 거북이는 위치와

www.acmicpc.net

안녕하세요, 이륙사입니다.

 

풀이

이번 문제는 Union-Find 혹은 BFS, DFS를 사용해서 해결할 수 있는 문제였습니다. 그중에서 이번엔 BFS를 사용해서 포스팅해보려고 합니다.

 

먼저, 예시들을 통해 PU의 개수는 직사각형의 수와 관련이 있다는 걸 알 수 있습니다. 그리고 만약 한점 이상에서 만나는 직사각형들이 있다면 그 직사각형들은 연필을 떼지 않고 한번에 그릴 수 있습니다. 즉, 몇 번의 한붓 그리기로 모든 직사각형들을 그릴 수 있는지 묻고 있으며, 직사각형 집합의 개수를 구하는 문제로도 생각할 수 있습니다. (참고로 직사각형만 그릴 수 있기 때문에, 2, 3번 명령어는 고려하지 않아도 됩니다.)

 

따라서 집합의 수를 구하기 위해 Union-Find를 떠올릴 수 있으며, 겹치는 직사각형의 점들은 모두 연결되어 있기 때문에 인접한 곳을 모두 탐색하는 BFS, DFS로도 해결할 수 있게 되는 겁니다! 탐색 함수를 호출한 횟수가 집합의 개수가 됩니다. 

for (y in 0 until MAX_PLUS_ONE) {
        for (x in 0 until MAX_PLUS_ONE) {
            if (graph[y][x] == 1 && visited[y][x].not()) { // 직사각형이 있고, 아직 그룹지어지지 않았다면
                answer += 1
                bfs(x, y)
            }
        }
    }

 

 

좌표 변환

단순 그래프 탐색 문제가 되었으므로, 열심히 구현해서 정답을 제출합니다. 그리고 틀립니다. 왜냐면 아래와 같이 겹치지 않는데도 인접하는 상황이 발생하기 때문입니다.

 

이럴 때 좌표 값을 2배 늘려주면, 거리가 1이던 점들의 거리도 2가 돼서 문제를 해결할 수 있습니다! 또한, 좌표값은 양수이어야 하므로 먼저 +500 증가시킨 후에 좌표 값을 2배 늘려줍니다.

repeat(n) {
        val st = StringTokenizer(readLine())
        drawRect(2 * (st.nextToken().toInt() + 500), // 좌표를 오른쪽으로 이동 후 2배 증가
            2 * (st.nextToken().toInt() + 500),
            2 * (st.nextToken().toInt() + 500),
            2 * (st.nextToken().toInt() + 500)
        )
    }

 

디버깅을 통해서나 아니면 처음부터 이런 상황을 떠올릴 수 있으면 정말 좋겠지만, 문제 경험이 풍부하지 않고선 쉽지 않을 것 같습니다ㅋㅋ

 

 

예외 처리

정말 마지막으로, 처음에 (0, 0)에서 연필을 내린 상태로 시작한다는 점도 주의해주셔야 합니다. (0, 0)과 이어진 사각형들은 처음에 연필을 올리지 않고 그릴 수 있기 때문입니다. 

// 처음에 (1000,1000)에 연필을 내린 상태에서 시작
if (graph[1000][1000] == 1) print(answer - 1)
else print(answer)

 

생각 과정

  1. 예시를 통해 사각형 집합의 개수를 구하는 문제라는 것을 확인한다.
  2. 그래프 안에서 집합의 개수를 구하는 방법을 떠올린다 -> Union-Find, DFS, BFS

 

코드

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*

const val MAX_PLUS_ONE = 2001

val graph = Array(MAX_PLUS_ONE){
    IntArray(MAX_PLUS_ONE)
}
val visited = Array(MAX_PLUS_ONE) {
    BooleanArray(MAX_PLUS_ONE)
}
val dX = arrayOf(-1, 1, 0, 0)
val dY = arrayOf(0, 0, -1, 1)

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val n = readLine().toInt()
    var puCount = 0

    repeat(n) {
        val st = StringTokenizer(readLine())
        // 좌표를 오른쪽으로 이동 후 2배 증가
        val x1 = 2 * (st.nextToken().toInt() + 500); val y1 = 2  * (st.nextToken().toInt() + 500)
        val x2 = 2 * (st.nextToken().toInt() + 500); val y2 = 2 * (st.nextToken().toInt() + 500)

        drawRectangle(x1, y1, x2, y2)
    }

    for (y in 0 until MAX_PLUS_ONE){
        for (x in 0 until MAX_PLUS_ONE) {
            // 직사각형이 있고, 아직 그룹지어지지 않았다면
            if (graph[y][x] == 1 && visited[y][x].not()) {
                bfs(x, y)
                puCount++
            }
        }
    }

    // 연필은 (1000,1000)에 내린 상태에서 시작한다
    if (graph[1000][1000] == 1) print(puCount - 1)
    else print(puCount)
}

// 직사각형 그리기
fun drawRectangle(x1: Int, y1: Int, x2: Int, y2: Int) {
    for (i in y1..y2) {
        graph[i][x1] = 1
        graph[i][x2] = 1
    }

    for (i in x1 + 1 until x2) { // 변끼리 겹치는 점 제외
        graph[y1][i] = 1
        graph[y2][i] = 1
    }
}

fun bfs(startX: Int, startY: Int) {
    val q: Queue<Pair<Int, Int>> = LinkedList()
    q.add(Pair(startX, startY))
    visited[startY][startX] = true

    while (q.isNotEmpty()) {
        val (x, y) = q.poll()

        for (i in 0 until 4) {
            val nextX = x + dX[i]
            val nextY = y + dY[i]

            if (nextX !in 0 until MAX_SIZE || nextY !in 0 until MAX_SIZE) continue

            if (graph[nextY][nextX] == 1 && visited[nextY][nextX].not()) {
                visited[nextY][nextX] = true
                q.add(Pair(nextX, nextY))
            }
        }
    }
}

+ Recent posts