https://programmers.co.kr/learn/courses/30/lessons/81303

 

코딩테스트 연습 - 표 편집

8 2 ["D 2","C","U 3","C","D 4","C","U 2","Z","Z"] "OOOOXOOO" 8 2 ["D 2","C","U 3","C","D 4","C","U 2","Z","Z","U 1","C"] "OOXOXOOO"

programmers.co.kr

 

풀이1

처음에 스택을 활용한 커서 구현 방법배열에 삭제 여부를 표시하는 방식을 사용해서 정확도는 통과할 수 있었지만, 효율성 테스트에서 문제가 있었다. 두 가지 방법 모두 삭제된 행을 복구할 때 O(N)이 소요됨에 따라 명령어 * 전체 행 = 약 2000억 번의 연산이 필요하기 때문이다. 커서를 움직이는 숫자의 합은 100만 이하라고 주어졌기 때문에, 복구하는데 걸리는 시간을 줄여야 효율성 테스트를 통과할 수 있다. 그리고 연결 리스트를 사용하면 O(1)으로 해결할 수 있다.

 

연결 리스트

 

그림과 같이 연결리스트를 사용해서 삭제한 행을 스택에 저장하면, 삭제 과정이 연결리스트의 삽입과 같아진다.

그리고 항상 최근에 삭제된 행부터 복구하기 때문에, 삭제가 된 이후의 커서 이동이나 삭제는 신경쓰지 않고 이전 상태 그대로 복구할 수 있다. 단, 삭제하거나 복구할 때 양 끝의 행인지만 주의하면 된다!

 

풀이 2

삭제/복구 동작을 유심히 살펴보면 재밌는 사실을 발견할 수 있는데, 행을 삭제하더라도 cursor의 인덱스는 같다는 것이다.

 

ex)

0-1-2-3-4, cursor: 3, size: 5 

----삭제----

0-1-2-4, cursor: 3(4를 가리킴), size: 4, stack: 3

 

즉, cursor의 인덱스와 행의 개수만으로 차트를 표현할 수 있다. 단, 마지막 행을 삭제했을 때는 cursor -= 1을 해주어야 하는데, 이것도 실제 삭제 동작과 같다.

 

ex)

0-1-2-3-4, cursor: 4, size: 5

----삭제----

0-1-2-3, cursor: 4 - 1 = 3, size: 5, stack: 4

 

복구할 때는 행의 개수를 다시 증가시키고, 만약 현재 cursor의 인덱스가 복구하려는 행의 인덱스보다 크거나 같으면 cursor += 1을 해준다. 왜냐면 아래로 밀어야 하기 때문이다.

 

ex)

0-1-2-4, cursor: 3 (4를 가리킴), size: 4, stack: 3

----복구----

0-1-2-3-4, cursor: 3 + 1 = 4, size: 5

 

 

이런 풀이도 있다 정도로 알면 좋을 것 같습니다.

코드2를 참고해주세요!

 

코드1 (연결 리스트 사용)

import java.util.*

// 행 넘버, 위에 행, 아래 행, 삭제 여부
data class Row(val number: Int, var prev: Row? = null, var next: Row? = null, var state: Char = 'O')

class Solution {
    fun solution(n: Int, k: Int, cmd: Array<String>): String {
        val rowArray = Array<Row>(n) { i -> Row(i) }
        for (i in 0 until n-1) { // 앞으로 연결
            rowArray[i].next = rowArray[i+1]
        }
        for (i in 1 until n){    // 뒤로 연결
            rowArray[i].prev = rowArray[i-1]
        }

        var cursor = rowArray[k]
        val removeStack = Stack<Row>()

        cmd.forEach {
            when (it[0]) {
                'U' -> { 
                    val prevStep = it.split(" ")[1].toInt()
                    repeat(prevStep) {
                        cursor = cursor.prev!!
                    }
                }

                'D' -> {
                    val nextStep = it.split(" ")[1].toInt()
                    repeat(nextStep) {
                        cursor = cursor.next!!
                    }
                }

                'C' -> {
                    val removeRow = cursor
                    val prevRow = cursor.prev
                    val nextRow = cursor.next
                    removeStack.push(removeRow)
                    removeRow.state = 'X' // 삭제 표시

                    if (nextRow == null) { // 삭제될 행이 마지막 행인 경우
                        cursor = prevRow!!
                        prevRow.next = null
                    }else{
                        cursor = nextRow
                        nextRow.prev = prevRow

                        // 첫번째 행을 삭제하는 경우를 고려하지 않으면 NullPointerexception 발생
                        if (prevRow != null) prevRow.next = nextRow
                    }
                }

                else -> {
                    val removedRow = removeStack.pop()
                    val prevRow = removedRow.prev
                    val nextRow = removedRow.next
                    removedRow.state = 'O'

                    if (nextRow == null){ // 마지막 행이었던 경우
                        prevRow!!.next = removedRow
                    }else{
                        nextRow.prev = removedRow

                        if (prevRow != null) prevRow.next = removedRow
                    }
                }
            }
        }

        // 배열에 저장하지 않고 head, tail을 사용해서
        // 스택에 남아있는 행들을 다시 삽입한 후
        // head부터 순서대로 출력할 수도 있습니다.
        val answer = StringBuilder()
        rowArray.forEach {
            answer.append(it.state)
        }

        return answer.toString()
    }
}

 

코드2 (cursor랑 size로 차트 표현)

import java.util.*

class Solution {
    fun solution(n: Int, k: Int, cmd: Array<String>): String {
        var cursor = k
        var size = n
        val removeStack = Stack<Int>()

        cmd.forEach {
            when (it[0]) {
                'U' -> {
                    val step = it.split(" ")[1].toInt()
                    cursor -= step
                }
                'D' -> {
                    val step = it.split(" ")[1].toInt()
                    cursor += step
                }
                'C' -> {
                    removeStack.push(cursor)
                    size -= 1
                    if (cursor == size) cursor--
                }
                else -> {
                    val comebackRow = removeStack.pop()
                    size += 1
                    if (comebackRow <= cursor) cursor++
                }
            }
        }

        val answer = StringBuilder()
        for (i in 0 until size) answer.append('O') // 살아있는 행 먼저 표시
        
        while (removeStack.isNotEmpty()) {
            val removedRow = removeStack.pop()     // 연결리스트의 삽입과 같다
            answer.insert(removedRow, 'X')
        }

        return answer.toString()
    }
}

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

 

1074번: Z

한수는 크기가 2N × 2N인 2차원 배열을 Z모양으로 탐색하려고 한다. 예를 들어, 2×2배열을 왼쪽 위칸, 오른쪽 위칸, 왼쪽 아래칸, 오른쪽 아래칸 순서대로 방문하면 Z모양이다. N > 1인 경우, 배열을

www.acmicpc.net

 

풀이

같은 유형의 작은 문제들로 계속 쪼개어지는 것으로 보아, 분할 정복 문제라는 것을 알 수 있었다. 그치만 시간 제한이 0.5초라서 주의해야 할 부분이 있었다.

 

행렬 표현

먼저 탐색에 앞서 행렬은 시작점과 각 변의 길이를 통해 나타낼 수 있다.

행렬 표현

 

1. 시간 제한을 고려하지 않는 방법

  1. (0, 0)과 전체 크기의 행렬을 대상으로 탐색을 시작한다.
  2. 지금 탐색중인 행렬의 길이를 확인한다.
    1. 2보다 길거나 같으면, 길이를 2로 나눠서 Z모양에 따라 다시 재귀적으로 탐색한다.
    2. 길이가 1이고,
      1. 목표 지점이라면, 몇 번째로 방문했는지 저장하고 탐색을 종료한다.
      2. 목표 지점이 아니라면,해당 점의 위치를 확인하고 방문 순서(count)를 +1 증가시키고 재귀 탐색을 종료한다.
  3. 목표점의 방문 순서를 출력한다.

이렇게 하면 항상 정답을 찾을 수 있다. 하지만 코드를 제출해보면 시간 초과가 발생한다.

 

📌시간 복잡도)

- (2^n * 2^n) 행렬의 길이가 1(한 점)이 될 때까지 2로 나누는 횟수 = log(2^n) = n

- 모든 점의 개수 = 2^n * 2^n

연산 횟수: (2^n * 2^n) * n = 2^15 * 2^ 15 * 15 = 약 160억

시간 복잡도: O(n*4^n)

 

따라서, 연산 횟수를 줄여야 한다. 그리고 아래의 그림과 같이 찾는 점이 존재하는 구간만 재귀적으로 탐색하면 시간 복잡도를 확 줄일 수 있다. 나머지 부분은 방문 순서만 업데이트 해준다.

 

2. 시간 복잡도를 개선한 방법

필요한 곳만 탐색

 

  1. (2^n * 2^n) 행렬을 Z모양의 순서대로 탐색한다.
  2. 지금 탐색중인 범위가 내가 찾으려고 하는 최종 범위(점)인지 확인한다.
    1. 찾던 점이라면, 몇 번째로 방문했는지 저장한다.
    2. 아니라면, Z방향 순서대로
      1. 목표지점을 포함하지 않는 곳은 방문 순서(visitCount)만 길이 * 길이만큼 증가시키고 탐색을 종료한다
      2. 목표지점을 포함하는 곳은 길이를 절반으로 나눠서 탐색을 계속한다.
  3. 목표점의 방문 순서를 출력한다.

📌줄어든 시간 복잡도)

- 필요한 점을 찾기 위해 구간을 나누는 횟수 = log(2^n) = n,

- 나눌 때마다 방문 순서 업데이트 하는 연산 횟수 3번

연산 횟수: 3 * n = 3 * 15 = 45

시간 복잡도: O(n)

 

코드

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

var answer = 0
var numbering = 0

// dudwls901님 풀이 참고 버전

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val input = readLine().split(" ").map{ it.toInt() }
    val (n, r, c) = input

    visitGraph(1 shl(n), 0, 0, r, c)
    print(answer)
}

fun visitGraph(size: Int, row: Int, col: Int, targetRow: Int, targetCol: Int) {

    // 탐색중인 Z구간에 목표지점이 없으면
    // 탐색하지 않고 방문 순서만 update하고 탐색을 종료한다
    if (targetRow !in row until row + size || targetCol !in col until col + size) {
        numbering += size * size
        return
    }

    // 목표지점을 찾았으면 방문 순서를 저장하고 리턴한다
    if (row == targetRow && col == targetCol) {
        answer = numbering
        return
    }

    val halfSize = size / 2

    visitGraph(halfSize, row, col, targetRow, targetCol) // 왼쪽 위
    visitGraph(halfSize, row, col + halfSize, targetRow, targetCol) // 오른쪽 위
    visitGraph(halfSize, row + halfSize, col, targetRow, targetCol) // 왼쪽 아래
    visitGraph(halfSize, row + halfSize, col + halfSize, targetRow, targetCol) // 오른쪽 아래
}

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

 

1783번: 병든 나이트

첫째 줄에 체스판의 세로 길이 N와 가로 길이 M이 주어진다. N과 M은 2,000,000,000보다 작거나 같은 자연수이다.

www.acmicpc.net

 

풀이

문제가 이해하기 어려웠어서 지문을 조금 풀어 써보면 다음과 같다.

 

"이동 방식을 모두 한 번씩 사용해서 움직일 수 있도록 체스판이 주어졌다면, 최소한 모든 방법을 1번 씩은 사용해야 한다. 만약 모든 방식을 사용할 수 없다면, 4번 이상 움직일 수 있더라도 3번 이하로 움직인다.

방법 하나를 사용할 때마다 한 번 움직인 것이며, 시작 지점도 방문한 것으로 간주한다. 위의 조건을 만족하는 최대 방문 횟수를 구하여라."

 

이동 방식을 모두 사용할 수 있는 경우와 없는 경우는 어떻게 나눌 수 있을까?

 

1. 모두 사용할 수는 없는 경우

1) 세로 길이 == 1

이동 방식을 살펴보면 가장 왼쪽 아래에서 시작해서 오른쪽으로만 이동하며, 위나 아래로 함께 움직여야 한다.

따라서 세로 길이가 1이면 가로가 아무리 길어도 움직일 수 없다.

=> 최대 방문 횟수 = 0

 

2) 세로 길이 == 2

 

위 아래로 두 칸씩 올라가는 방법이 있기 때문에,세로 길이가 2이면 위나 아래로 두 칸 이동하는 방법은 사용할 수 없다. => 최대 방문 횟수 = min(4, (세로 길이 + 1) / 2)

 

3) 세로 >= 3 && 가로 < 7

M &lt; 7

세로가 3이상이더라도 모든 방법을 사용할 수 있는 건 아니다. 모든 방법을 한 번씩 사용하면 항상 가로 방향으로 6번을 움직이기 때문이다. 따라서, 가로 길이가 7 미만이면 세로가 아무리 길어도 모든 방법을 사용할 수 없다. 세로가 3이상일 때 가장 많이 움직일 수 있는 방법은 오른쪽으로 한 칸씩만 이동하는 것이기 때문에

=> 최대 방문 횟수 = min(4, 가로 길이)

 

2. 모두 사용할 수 있는 경우 - 세로 >= 3 && 가로 >= 7

N &gt;= 3 &amp;&amp; M &gt;= 7

이제 적절히 순서를 조합해서 모든 방법을 다 사용할 수 있다. 모든 방법을 한 번씩 다 사용하고 나면, 오른쪽으로 가장 많이 방문할 수 있도록 움직여서 최대 방문 횟수를 구할 수 있다.

=> 최대 방문 횟수 = 5 + (가로 길이 - 7) 

 

 

일반 구현 문제였지만 지문이 이해하기 어렵고 분기문이 많아서 쉽지 않았던 것 같네요!

 

코드

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

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val input = readLine().split(" ")
    val rowLength = input[0].toInt()
    val colLength = input[1].toInt()

    when{
        // 모든 조건을 한번씩 사용할 수 없을 땐
        // 3번 이하로만 움직여서 방문 횟수를 4번 이하로 만든다.
        rowLength == 1 -> print(1)

        rowLength == 2 -> {
            val visitCount = (colLength + 1) / 2
            print( if(4 < visitCount) 4 else visitCount )
        }

        // 모든 조건을 한번 씩 써서 움직이면 가로로 6칸 이동하기 때문에
        // 가로 길이는 7이 기준이 된다.
        colLength < 7 -> print( if(4 < colLength) 4 else colLength)

        // 모든 조건을 한번씩 사용해서 움직일 수 있으므로
        // (한번씩 사용해서 방문한 횟수: 5) + (남아있는 가로 칸에서 가장 많이 움직일 수 있는 횟수)
        else -> print(5 + colLength - 7)
    }
}

 

참고

https://lipcoder.tistory.com/94

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

 

1517번: 버블 소트

첫째 줄에 N(1 ≤ N ≤ 500,000)이 주어진다. 다음 줄에는 N개의 정수로 A[1], A[2], …, A[N]이 주어진다. 각각의 A[i]는 0 ≤ |A[i]| ≤ 1,000,000,000의 범위에 들어있다.

www.acmicpc.net

 

풀이

크게 2가지의 풀이 법이 있다. 그 중 좀 더 쉬운 방법이 분할 정복법인데, 그것도 아이디어를 떠올리기가 쉽지 않은 것 같다. 그래서 다른 분들의 풀이와 코드를 많이 참고했다.

 

먼저, 수가 최대 50만개라서 버블 소트를 쓰면 시간 초과가 난다.

 

각 숫자가 몇 번 swap 되는지를 그림으로 알아보면 다음과 같다.

각 숫자의 교점의 합이 총 swap의 합이 된다. 그리고 각 숫자의 교점의 개수는 자신보다 오른쪽에 있는 숫자들 중 더 작은 수의 개수가 된다. 예를 들어, 위의 6은 1, 3, 5 => 3번 swap 한다.

 

그러면 어떻게 O(n^2)보다 빠르게 각 숫자들의 오른쪽을 탐색할 수 있을까? Merge Sort를 하면 merge sort의 시간 복잡도인 O(n * log^n) 안에 가능하다.

 

merge sort

 

위의 그림은 merge sort를 하는 과정에서 가장 작은 0을 정렬한 다음 상황이다.

 

이제 왼쪽에서 가장 작은 2와 오른쪽에서 가장 작은 1 중 더 작은 값을 정렬해야 한다. 오른쪽 값이 더 작기 때문에 1을 정렬한다. 그리고 그 과정에서 1이 2,4,6,8과 교차되는 것을 알 수 있다. 즉, 1은 2, 4, 6, 8과 swap이 되고, swap += (왼쪽 배열 개수 - 왼쪽 인덱스)와 같은 방식으로 swap 횟수를 계산할 수 있다. 이러한 과정은 오른쪽 최소 값이 왼쪽 최소값보다 작을 때마다 적용된다.

 

참고로 swap 횟수는 최악의 경우 대략 50만 + ... + 1 => 50만 * 50만이 될 수 있기 때문에 Long형을 사용해야 한다!

 

코드

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

var count = 0L
lateinit var tempArray: IntArray

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val n = readLine().toInt()
    val st = StringTokenizer(readLine())
    val numbers = IntArray(n) { i ->
        st.nextToken().toInt()
    }

    tempArray = IntArray(n)
    mergeSort(numbers, 0, n-1)

    print(count)
}

fun mergeSort(numbers: IntArray, start: Int, end: Int) {
    if (start == end) {
        return
    }

    val mid = (start + end) / 2
    mergeSort(numbers, start, mid)
    mergeSort(numbers, mid + 1, end)
    merge(numbers, start, end) // 정렬된 양쪽 배열을 합치면서 다시 정렬한다
}

fun merge(numbers: IntArray, start: Int, end: Int) {
    val mid = (start + end) / 2
    var left = start
    var right = mid + 1
    var tempIndex = start

    while(left <= mid && right <= end) {
        // 꼭 등호는 왼쪽이 더 작거나 같은 조건으로 붙여야 한다.
        // 그렇지 않으면 왼쪽과 오른쪽 값이 같을 때 1을 더 계산하게 된다.
        if (numbers[left] <= numbers[right]){
            // 왼쪽 값이 더 작거나 같다 -> 오른쪽 배열에 자신보다 큰 값이 없다
            tempArray[tempIndex++] = numbers[left++]
        }else{
            tempArray[tempIndex++] = numbers[right++]
            count += mid - left + 1
        }
    }

    // 왼쪽이나 오른쪽 중 남아있는 배열의 숫자 정렬
    while (left <= mid) {
        tempArray[tempIndex++] = numbers[left++]
    }
    while (right <= end) {
        tempArray[tempIndex++] = numbers[right++]
    }

    // 임시 배열에 정렬한 값을 원래 배열에 반영
    for (i in start..end) {
        numbers[i] = tempArray[i]
    }
}

+ Recent posts