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

 

2146번: 다리 만들기

여러 섬으로 이루어진 나라가 있다. 이 나라의 대통령은 섬을 잇는 다리를 만들겠다는 공약으로 인기몰이를 해 당선될 수 있었다. 하지만 막상 대통령에 취임하자, 다리를 놓는다는 것이 아깝다

www.acmicpc.net

풀이

바다와 육지로 이루어진 인접 행렬 형태의 그래프가 주어진다. 그러면 모든 섬들을 찾아서, 그 섬들을 잇는 다리 중 가장 짧은 다리를 찾는 문제이다.

 

1. 섬 분류)

섬을 찾기 위해, 붙어있는 육지들을 하나로 묶어서 라벨링을 할 수 있다. 육지 한 점을 시작으로 인접한 곳을 탐색해 나가면서 인접한 곳에는 모두 같은 번호를 붙인다. dfs나 bfs를 사용할 수 있다.

악필 지송..

 

2. 다리 연결)

방법 1) 모든 육지에서부터 한 번씩 경로를 탐색한다

 

이 부분이 까다로운데, 가장 쉽게 생각할 수 있는 방법은 모든 육지를 시작 지점으로 bfs를 수행하는 것이다. 너비 우선으로 탐색하기 때문에 시작 지점과 다른 섬의 육지에 처음 방문하게 됐을 때, 그 이동 거리가 그 지점에서 다른 섬으로 가는 최단 거리가 된다. 모든 지점에서 다른 섬으로 가는 최단 거리를 구하면, 그 중 최소값이 전체에서 가장 짧은 다리가 된다.

 

dfs를 사용하면 어떤 방향을 우선 탐색하느냐에 따라 아래처럼 돌아가는 경로를 먼저 탐색할 수 있다. 그래서 모든 점을 방문해야 하기 때문에 bfs를 사용하는 것이 좋다.

아래, 오른쪽, 위, 왼쪽 순으로 dfs 탐색했을 때

 

시간복잡도는 라벨링 O(n) + 경로 탐색 O(n^3) = O(n^3)이지만, n이 100이라서 괜찮다. 

 

 

방법 2) 육지를 모두 Queue에 넣고, 한 번만 bfs를 수행한다

 

사실 bfs를 사용하면 O(n^2)으로도 경로 탐색을 할 수 있다. 모든 육지를 Queue에 넣고 bfs를 한 번만 수행하는 것이다.

 

방문된 곳은 이동 거리어떤 섬에서부터 방문했는지를 표시한다. 그리고 다른 섬 or 다른 섬에서 방문한 바다를 만났을 때, (자신이 이동한 거리) + (다른 섬이 이동한 거리)가 최단 거리가 된다. 모든 지점이 돌아가면서 한 칸씩만 이동하기 때문이다.

방문 처리: 동그라미에서 만나게 된다
이동 거리: 2+1 = 3 

  

(1)방문 표시 + (2)이동 거리 + (3)어떤 섬에서 방문했는지 표시해야 하기 때문에 복잡해서 떠올리기가 어려웠던 것 같다.

나는 그림처럼 방문한 바다에는 시작 지점의 라벨을 저장해서 방문 표시와 어떤 섬에서 시작했는지를 한번에 나타냈는데, 좀 헷갈렸어서 방문 체크를 위한 배열을 따로 사용하는 것도 좋을 것 같다. 이동 거리를 저장하는 배열은 따로 선언했다. 

 

코드 (방법1)

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*
import kotlin.math.min

val dRow = arrayOf(-1, 1, 0, 0)
val dCol = arrayOf(0, 0, -1, 1)

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val n = readLine().toInt()
    val graph = Array(n) { // 그래프 입력 받아서 저장
        StringTokenizer(readLine()).let { st ->
            IntArray(n) { st.nextToken().toInt() }
        }
    }

    var label = 2 // 중복 방문 체크를 위해 2부터 시작
    for (i in 0 until n) {
        for (j in 0 until n) {
            if (graph[i][j] == 1) {
                graph[i][j] = label // 섬 분류
                labelingDfs(graph, i, j, label++)
            }
        }
    }

    var answer = Integer.MAX_VALUE
    for (i in 0 until n) {   // 모든 육지마다 최단 경로 탐색
        for (j in 0 until n) {
            if (graph[i][j] > 1) {
                val copiedGraph = copyGraph(graph) // bfs 수행할 때마다 그래프 복제해야 함
                answer = min(answer, bfs(copiedGraph, i, j))
            }
        }
    }
    print(answer)
}

fun labelingDfs(graph: Array<IntArray>, row: Int, col: Int, label: Int) {
    for (i in 0 until 4) {
        val newRow = row + dRow[i]
        val newCol = col + dCol[i]

        if (newRow in graph.indices && newCol in graph.indices && graph[newRow][newCol] == 1) {
            graph[newRow][newCol] = label // 방문 체크 + 섬 라벨링
            labelingDfs(graph, newRow, newCol, label)
        }
    }
}

fun copyGraph(graph: Array<IntArray>): Array<IntArray>{
    val tempGraph = Array(graph.size){IntArray(graph.size)}
    for (i in graph.indices) {
        for (j in graph.indices){
            tempGraph[i][j] = graph[i][j]
        }
    }

    return tempGraph
}

fun bfs(graph: Array<IntArray>, startRow: Int, startCol: Int): Int {
    var distance = 0
    val label = graph[startRow][startCol]
    val queue: Queue<Pair<Int, Int>> = LinkedList()
    queue.offer(Pair(startRow, startCol))

    while (queue.isNotEmpty()) {
        val size = queue.size 

        // 다리를 한 번씩 모두 늘리면 distance 증가
        repeat(size) {
            val pair = queue.poll()
            val row = pair.first
            val col = pair.second

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

                if (newRow in graph.indices && newCol in graph.indices) {

                    // 방문하지 않았던 바다
                    if (graph[newRow][newCol] == 0) {
                        graph[newRow][newCol] = label
                        queue.offer(Pair(newRow, newCol))
                    }

                    // 바다도 아니고 같은 섬도 아니다 -> 다른 섬에 도착
                    else if (graph[newRow][newCol] != label) {
                        return distance // 경로 이동 횟수 반환
                    }
                }
            }
        }

        distance++
    }

    return Integer.MAX_VALUE // 섬 중앙에서 시작했을 때
}

 

코드 (방법2)

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

val dRow = arrayOf(-1, 1, 0, 0)
val dCol = arrayOf(0, 0, -1, 1)
val queue: Queue<Pair<Int, Int>> = LinkedList()

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val n = readLine().toInt()
    val graph = Array(n) { // 그래프 입력 받아서 저장
        StringTokenizer(readLine()).let { st ->
            IntArray(n) { st.nextToken().toInt() }
        }
    }

    var label = 2 // 중복 방문 체크를 위해 2부터 시작
    for (i in 0 until n) {
        for (j in 0 until n) {
            if (graph[i][j] == 1) {
                graph[i][j] = label // 섬 분류
                labelingDfs(graph, i, j, label++)
            }
        }
    }

    val answer = bfs(graph) // bfs로 최단 경로 탐색
    print(answer)
}

fun labelingDfs(graph: Array<IntArray>, row: Int, col: Int, label: Int) {
    var isEdge = false // 섬의 육지 중 테두리를 찾기 위한 플래그 

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

        if (newRow in graph.indices && newCol in graph.indices && graph[newRow][newCol] != label) {
            if (graph[newRow][newCol] == 0) { // 주변에 바다가 있다 -> 섬의 테두리이다
                isEdge = true
                continue
            }

            graph[newRow][newCol] = label
            labelingDfs(graph, newRow, newCol, label)
        }
    }

    if (isEdge) queue.add(Pair(row, col)) // 섬의 테두리에서만 bfs 시작
}

fun bfs(graph: Array<IntArray>): Int {
    val lengthGraph = Array(graph.size) { IntArray(graph.size) } // 이동 거리 저장하는 배열

    while (queue.isNotEmpty()) {
        val pair = queue.poll()
        val row = pair.first
        val col = pair.second

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

            // 그래프 바깥이거나 해당 라벨에서 이미 방문했던 곳 or 같은 섬이면 건너뛴다 
            if (newRow !in graph.indices || newCol !in graph.indices || graph[newRow][newCol] == graph[row][col]) {
                continue
            }

            // 0이 아니다 -> 다른 라벨이 방문했던 곳 or 다른 라벨의 섬 -> 최단 경로 탐색 완료 
            if (graph[newRow][newCol] != 0) {
                return lengthGraph[row][col] + lengthGraph[newRow][newCol]
            }

            graph[newRow][newCol] = graph[row][col] // 시작점의 라벨로 설정
            lengthGraph[newRow][newCol] = lengthGraph[row][col] + 1 // 이동 거리
            queue.offer(Pair(newRow, newCol))
        }
    }

    return 0
}

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

 

7576번: 토마토

첫 줄에는 상자의 크기를 나타내는 두 정수 M,N이 주어진다. M은 상자의 가로 칸의 수, N은 상자의 세로 칸의 수를 나타낸다. 단, 2 ≤ M,N ≤ 1,000 이다. 둘째 줄부터는 하나의 상자에 저장된 토마토

www.acmicpc.net

풀이

인접 행렬 형태의 그래프에서 익은 토마토와 인접한, 익지 않은 토마토를 방문하는 그래프 탐색 문제이다.

 

인접한 네 방향을 우선으로 확인하기 때문에 BFS를 사용할 수 있다. 만약 DFS를 사용하면 아래 예시처럼 방문했던 노드를 다시 방문해야 하는 상황이 생기기 때문에 거의 O(N * M)의 시간 복잡도를 갖는다.

 

ex) 1과 0은 토마토의 상태, 2와 3은 (1 + 걸린 일수) 

DFS를 사용하면 노드를 다시 방문하게 된다

 

주의할 점은, 처음에 1로 주어진 토마토가 모두 Queue에 들어가는 시작점이 된다. 지금은 당연해 보이는데, 처음에 한 점에서만 시작한다고 생각해서 한참을 해맸다..ㅋㅋ

 

그리고 이전 토마토에서 인접한 곳을 방문하는 데 하루가 걸리므로, 그래프에 (이전 토마토의 그래프 값 + 1)을 저장하면 중복 방문 체크와 더불어 지난 일수도 알 수 있다. 

 

구현 방법

1. 그래프를 입력 받는다.

2. 익은 토마토(1)의 위치를 모두 큐에 넣고, 익지 않은 토마토의 개수도 세준다.

3. 그래프 값이 모두 1이라면 0을 출력하고 종료한다.

4. BFS로 모든 1에서부터 가능한 모든 0을 방문하면서, 그래프에 자신의 값에 1을 더한 값을 저장한다. 

5. 가능한 노드를 모두 탐색했는데 익지 않은 토마토가 남아있다면 -1을, 그렇지 않으면 그래프의 (최대값 -1)을 출력한다. (1에서부터 시작했으니까)

 

코드 1

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

var unripeCount = 0
val dRow = listOf(-1, 1, 0, 0)
val dCol = listOf(0, 0, -1, 1)

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val input = readLine().split(" ")
    val n = input[1].toInt()
    val m = input[0].toInt()
    val graph = Array(n){ IntArray(m) }
    val queue: Queue<Pair<Int, Int>> = LinkedList()

    for (i in 0 until n){
        val st = StringTokenizer(readLine())

        for (j in 0 until m){
            val tomato = st.nextToken().toInt()
            graph[i][j] = tomato

            when (tomato) {
                1 -> queue.add(Pair(i, j))
                0 -> unripeCount++  // 익지 않은 토마토
            }
        }
    }

    if (unripeCount == 0){  // 처음부터 모든 토마토가 익어있으면
        print(0)
        return
    }

    val day = bfsToRipe(graph, queue)
    if (unripeCount > 0) print(-1) // 모든 위치를 방문했는데 익지 않은 토마토가 남아있으면 -1
    else print(day)
}

fun bfsToRipe(graph: Array<IntArray>, queue: Queue<Pair<Int, Int>>): Int{
    var day = 0

    while(queue.isNotEmpty()){
        val pair = queue.poll() // 한 번 주변에게 영향을 준 토마토는 더 이상 쓰이지 않는다
        val row = pair.first
        val col = pair.second
        // 그래프에는 (영향을 받은 토마토 값 + 1)을 넣어서 날짜를 저장한다
        // 처음 토마토 날짜가 1부터 시작되므로, 마지막에 (최종 날짜 - 1)을 반환한다
        day = graph[row][col]

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

            // 그래프를 벗어나있거나, 익은 토마토이거나, 토마토가 들어있지 않은 곳이거나
            if (newRow !in graph.indices || newCol !in graph[0].indices || graph[newRow][newCol] != 0) continue

            unripeCount--
            graph[newRow][newCol] = graph[row][col] + 1 // 하루가 지났을 때 익지 않은 토마토가 영향을 받아서 익게된다
            queue.offer(Pair(newRow, newCol))
        }
    }

    return day - 1
}

 

코드 2

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

var unripeCount = 0
val dRow = listOf(-1, 1, 0, 0)
val dCol = listOf(0, 0, -1, 1)

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val input = readLine().split(" ")
    val n = input[1].toInt()
    val m = input[0].toInt()
    val graph = Array(n){ IntArray(m) }
    val queue: Queue<Pair<Int, Int>> = LinkedList()

    for (i in 0 until n){
        val st = StringTokenizer(readLine())

        for (j in 0 until m){
            val tomato = st.nextToken().toInt()
            graph[i][j] = tomato

            when (tomato) {
                1 -> queue.add(Pair(i, j))
                0 -> unripeCount++  // 익지 않은 토마토
            }
        }
    }

    if (unripeCount == 0){  // 처음부터 모든 토마토가 익어있으면
        print(0)
        return
    }

    val day = bfsToRipe(graph, queue)
    if (unripeCount > 0) print(-1) // 모든 위치를 방문했는데 익지 않은 토마토가 남아있으면 -1
    else print(day)
}

fun bfsToRipe(graph: Array<IntArray>, queue: Queue<Pair<Int, Int>>): Int{
    var day = 0

    while(queue.isNotEmpty()){
        day++
        var size = queue.size // 현재 큐에 있는 토마토의 주변을 전부 탐색할 때마다 하루가 지난다

        repeat(size){
            val pair = queue.poll() // 한 번 주변에게 영향을 준 토마토는 더 이상 쓰이지 않는다
            val row = pair.first
            val col = pair.second

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

                // 그래프를 벗어나있거나, 익은 토마토이거나, 토마토가 들어있지 않은 곳이거나
                if (newRow !in graph.indices || newCol !in graph[0].indices || graph[newRow][newCol] != 0) continue

                unripeCount--
                graph[newRow][newCol] = 1
                queue.offer(Pair(newRow, newCol))
            }
        }

    }

    // 마지막에 남아있는 토마토를 익히고 나서
    // 마지막 토마토 주변에 익지 않은 토마토가 있는지 한번 더 확인하기 때문에 1을 빼야한다
    return day-1 
}

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

 

9466번: 텀 프로젝트

이번 가을학기에 '문제 해결' 강의를 신청한 학생들은 텀 프로젝트를 수행해야 한다. 프로젝트 팀원 수에는 제한이 없다. 심지어 모든 학생들이 동일한 팀의 팀원인 경우와 같이 한 팀만 있을

www.acmicpc.net

 

풀이

문제를 이해하기 위해 예제를 그림으로 그려보았다.

 

어디서 시작해도 사이클이 생긴다

 

팀을 이루기 위해선 각 학생들이 선택한 학생들을 따라갔을 때, 사이클이 형성되어야 한다는 걸 알 수 있다. 그리고 사이클은 어디서 시작하든지 반드시 생긴다. 사이클이 생기지 않으려면 중간에 화살표가 끊기거나 화살표가 무한히 이어져야 하는데, 그럴려면 팀원을 선택하지 않은 학생이 있거나 학생 수가 무한히 많아야 하기 때문이다.

 

또한, 2 -> 1 -> 3 -> 3 처럼 중간에 다른 사이클이 생기는 학생이 팀에 속하지 않는 학생이 된다.

 

그래서 나는 각 학생이 선택한 학생을 배열에 저장한 후, dfs를 사용해서 사이클을 만드는 학생들의 수를 구했다. 그리고 전체 학생 수에서 빼주는 방식으로 답을 구했다.

 

구현 방식

(1) 모든 학생을 한번씩 시작점으로 탐색해서, 자기 자신으로 돌아오는지 확인

가장 간단해 보이는 건 모든 학생을 시작 지점으로 탐색해서 자기 자신으로 돌아오는지 확인하는 방법인데, 이렇게 하면 시간 초과가 발생한다. 탐색에 걸리는 시간은 O(n), 모든 학생마다 탐색하면 O(n^2) = 100억이 되기 때문이다.

 

그래서 그래프 탐색에서 시간을 줄이는 방법인, 한번 확인한 학생은 다시 확인하지 않도록 해야 문제를 해결할 수 있다. 

한번 방문했던 학생이 다시 나오면, 이전의 탐색 과정이 그대로 등장한다는 점을 이용한다.

(ex) 1 -> 2  -> 3 -> 1 을 탐색하고 나서, 4 -> 1이 나온다면 1 -> 2 -> 3 -> 1 탐색을 다시 반복하게 된다.)

 

(2) 중복 체크 자료구조 2개 사용 

확인한 학생은 다시 방문하지 않도록 해야 하는데, 문제는 사이클을 이루는 학생이 다시 나타났을 때 방금 탐색하다가 확인한 건지, 이전 탐색에서 확인한 건지 알 수가 없다는 것이다.

 

그래서 방문 체크하는 배열을 2개 사용하였다. 

- finished: 사이클을 형성 여부까지 확인했는지 체크

- visited: 한 번이라도 방문 했는지 체크

 

알고리즘) dfs (재귀 or 스택)

  1. 처음 방문하는 학생일 경우 visited에 체크하고 그 학생이 선택한 학생을 방문한다.
  2. 다시 방문하는 학생일 경우,
    1. finished가 체크된 학생 -> 이전의 탐색 과정이 그대로 나오므로 탐색을 종료한다.
    2. 그렇지 않다면, 사이클을 형성하는 학생이므로, 그 학생의 다음 학생부터 자기 자신으로 되돌아올 때까지 반복문을 돌리면서 사이클을 형성하는 학생 카운트를 증가시킨다.
  3. 함수나 스택을 빠져나가면서 사이클까지 모두 확인했음을 finished에 체크한다.

 

(3) 방문 확인 배열 + 탐색을 시작할 때마다 새로운 중복 체크 자료구조를 생성해서 사용

새 탐색을 시작할 때마다 추가 자료구조 사용

탐색 과정에서 이전에 사이클 여부까지 확인했는지를 필터링할 수는 있지만, 새로운 자료구조를 생성할 때마다 O(n)의 시간복잡도가 더 필요해서 결국 O(n^2)으로 시간 초과가 발생한다.

 

나는 여기서 많이 해맸었다..ㅋㅋ

 

코드1 (방식 2)

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

lateinit var graph: IntArray
lateinit var visited: BooleanArray
lateinit var finished: BooleanArray
var count = 0

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val t = readLine().toInt()
    val answer = StringBuilder()

    repeat(t){
        val n = readLine().toInt()
        val st = StringTokenizer(readLine())
        count = 0
        graph = IntArray(n+1)
        visited = BooleanArray(n+1)
        finished = BooleanArray(n+1)

        for (i in 1..n){
            graph[i] = st.nextToken().toInt()
        }

        for (i in 1..n){
            if (visited[i].not()) dfs(i)
        }

        answer.append(n - count).appendLine()
    }

    print(answer)
}

fun dfs(student: Int){
    // 처음 방문한 학생
    if (visited[student].not()) {
        visited[student] = true // 방문 체크
        dfs(graph[student])     // 다음 학생 방문
    }
    else{
        if (finished[student]) return // 사이클 형성 여부까지 이미 확인한 학생
        else{
            var nextStudent = graph[student] // 자신부터 시작하면 while문이 안돌아간다

            while (nextStudent != student){
                count++ // 사이클을 형성하는 학생 수 증가
                nextStudent = graph[nextStudent]
            }
            count++ // 자기 자신도 센다 ex) 2 -> 1 -> 3 -> 2: 반복문에서 1,3을 세고 마지막에 2를 센다

            return
        }
    }

    finished[student] = true // 사이클 형성 여부까지 확인했음
}

 

코드2 (방식2와 개념은 같지만 추가 방문 체크용 자료 구조로 Map을 사용했습니다)

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

lateinit var graph: IntArray
lateinit var visited: BooleanArray
lateinit var finished: BooleanArray

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val t = readLine().toInt()
    val answer = StringBuilder()

    repeat(t){
        val n = readLine().toInt()
        val st = StringTokenizer(readLine())
        var count = 0
        graph = IntArray(n+1)
        visited = BooleanArray(n+1)
        finished = BooleanArray(n+1)

        for (i in 1..n){
            graph[i] = st.nextToken().toInt()
        }

        for (i in 1..n){
            if (!visited[i]){
                count += dfs(i, mutableListOf(), HashMap<Int, Int>())
                // updateGroup(hasGroup, students, startIndex)
            }
        }

        answer.append(n - count).appendLine()
    }

    print(answer)
}

// Map을 추가 방문 체크 자료구조로 사용했습니다
fun dfs(student: Int, studentList: MutableList<Int>, indexMap: MutableMap<Int, Int>): Int {
    if (finished[student]) return 0 // 사이클 형성 여부가 이미 확인된 학생
    if (visited[student]) {         // 사이클이 형성됨
        return studentList.size - indexMap[student]!!
    }

    visited[student] = true
    indexMap[student] = studentList.size
    studentList.add(student)
    val count = dfs(graph[student], studentList, indexMap)

    finished[student] = true
    return count
}

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

 

1406번: 에디터

첫째 줄에는 초기에 편집기에 입력되어 있는 문자열이 주어진다. 이 문자열은 길이가 N이고, 영어 소문자로만 이루어져 있으며, 길이는 100,000을 넘지 않는다. 둘째 줄에는 입력할 명령어의 개수

www.acmicpc.net

 

풀이

자료구조에 관한 문제이다. 문자열을 어딘가에 저장해야 하는데, 커서의 개념이 존재하고 그걸 움직일 수도 있다. 커서에 방향이 있으므로 데이터에 순서가 있고, 추가/삭제가 쉬운 자료구조를 사용해야 한다. 따라서, 배열은 사용할 수 없고, 추가/삭제가 빠르고 데이터에 순서가 있는 연결 리스트를 생각해볼 수 있다. 

 

커서는 어떻게 구현할 수 있을까?

 

방법1) 연결 리스트

연결리스트를 응용하면 커서의 개념을 구현할 수 있다. 연결 리스트를 2개를 사용해서 커서의 왼쪽 리스트와 오른쪽 리스트의 개념으로 사용하면, 왼쪽 리스트의 끝과 오른쪽 리스트의 시작 지점 사이에 자연스럽게 커서가 생긴다. 그러면 왼쪽 리스트의 끝에 데이터를 추가/삭제할 수 있다.

ex) (왼쪽 리스트: a-b-c-d-x) - 커서 - (오른쪽 리스트: y) 

 

방법2) 스택

연결 리스트와 마찬가지로 왼쪽 스택과 오른쪽을 스택을 사용해서 구현할 수 있다. 양 스택의 꼭대기에 있는 데이터를 움직여서 커서를 움직이고, 왼쪽 스택의 pop/push로 커서 왼쪽에 데이터를 추가/삭제할 수 있다.

연결리스트와 차이점은 실제 문자열의 순서가 스택에 저장된 순서와 조금 다르다는 것이다. 문자열을 왼쪽에서 오른쪽으로 읽을 때, 왼쪽 스택의 바닥에서 꼭대기, 오른쪽 스택의 꼭대기에서 바닥 순서로 읽어야 한다.

 

방법3) listIterator

다른 사람들의 코드를 읽어보다가 문제에서 요구한 것과 완벽하게 동일한 자료구조가 자바에 존재한다는 걸 알게됐다. listIterator라는 녀석이다. Iterator 인터페이스를 상속한 것으로 Collection의 List에 사용할 수 있는데, 컬렉션의 요소에 접근, 추가, 삭제 등을 지원한다. Iterator는 정방향으로만 순회가 가능하지만 listIterator는 역방향으로도 순회가 가능하고 커서도 존재한다ㅋㅋ. 주의할 점은, 원래의 컬렉션 객체를 참조하기 때문에 Iterator에서 데이터를 추가/삭제하면 원래 객체에도 그대로 반영된다.

참고) http://www.tcpschool.com/java/java_collectionFramework_iterator 여기에 자세하게 설명되어 있다.

 

 

코드1 (연결 리스트)

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

fun main() = with(BufferedReader(InputStreamReader(System.`in`))){
    // 커서 왼쪽에 문자들 추가
    val leftList = LinkedList<String>().apply{ addAll(readLine().map{ it.toString() }) }
    val rightList = LinkedList <String>()
    val n = readLine().toInt()

    repeat(n){
        val cmd = readLine()

        when(cmd){
            "L" -> {
                if (leftList.isNotEmpty()){ // 커서 왼쪽에 문자가 있을 때
                    rightList.addFirst(leftList.pollLast())
                }
            }
            "D" -> {
                if (rightList.isNotEmpty()){ // 커서 오른쪽에 문자가 있을 때
                    leftList.addLast(rightList.pollFirst())
                }
            }
            "B" -> {
                if (leftList.isNotEmpty()){
                    leftList.removeLast() // 커서 왼쪽에 있는 문자 삭제
                }
            }
            else -> {
                leftList.add(cmd[2].toString()) // 커서 왼쪽에 문자 추가
            }
        }
    }

    val answer = StringBuilder()
    leftList.forEach { answer.append(it) } // 연결리스트는 순서대로 문자를 읽을 수 있다.
    rightList.forEach{ answer.append(it) }
    print(answer)
}

 

코드2 (스택: 리스트 사용)

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

fun main() = with(BufferedReader(InputStreamReader(System.`in`))){
    val leftStack = readLine().map{ it.toString() }.toMutableList()
    val rightStack = mutableListOf<String>() // 리스트로 스택 구현
    val n = readLine().toInt()

    repeat(n){
        val cmd = readLine()

        when(cmd){
            "L" -> {
                if (leftStack.isNotEmpty()){
                    rightStack.add(leftStack.removeAt(leftStack.lastIndex))
                }
            }
            "D" -> {
                if (rightStack.isNotEmpty()){
                    leftStack.add(rightStack.removeAt(rightStack.lastIndex))
                }
            }
            "B" -> {
                if (leftStack.isNotEmpty()){
                    leftStack.removeAt(leftStack.lastIndex)
                }
            }
            else -> {
                leftStack.add(cmd[2].toString())
            }
        }
    }

    val answer = StringBuilder()
    leftStack.forEach { answer.append(it) }
    for (i in rightStack.lastIndex downTo 0){ // 오른쪽 스택은 top부터 문자를 읽어야 한다
        answer.append(rightStack[i])
    }
    print(answer)
}

 

코드3 (listIterator)

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

fun main() = with(BufferedReader(InputStreamReader(System.`in`))){
    val str = readLine()
    val list = LinkedList<String>()
    str.forEach { 
        list.add(it.toString())
    }
    val iter = list.listIterator(list.size)

    val n = readLine().toInt()
    repeat(n){
        val cmd = readLine()

        when(cmd){
            "L" -> {
                if (iter.hasPrevious()){ // 커서 왼쪽에 데이터가 있으면
                    iter.previous()      // 그걸 리턴하고 커서를 반환할 데이터의 왼쪽으로 움직인다.
                }
            }
            "D" -> {
                if (iter.hasNext()){ // 커서 오른쪽에 데이터가 있으면
                    iter.next()      // 그걸 리턴하고 커서를 반환할 데이터의 오른쪽으로 움직인다.
                }
            }
            "B" -> {
                if (iter.hasPrevious()){
                    iter.previous()
                    iter.remove()   // 가장 최근에 리턴한 데이터를 삭제한다 -> 실제 리스트에도 반영된다
                }
            }
            else -> {
                iter.add(cmd[2].toString()) // 커서 위치에 데이터 추가 -> 리스트에 반영
            }
        }
    }

    val answer = StringBuilder()
    list.forEach {
        answer.append(it)
    }
    print(answer)
}

+ Recent posts