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/1744

 

1744번: 수 묶기

길이가 N인 수열이 주어졌을 때, 그 수열의 합을 구하려고 한다. 하지만, 그냥 그 수열의 합을 모두 더해서 구하는 것이 아니라, 수열의 두 수를 묶으려고 한다. 어떤 수를 묶으려고 할 때, 위치에

www.acmicpc.net

 

풀이

수열의 합의 최대값을 구하는 문제이다. 숫자 2개를 위치에 관계 없이 묶거나 묶지 않고 더할 수 있으며, 묶을 경우엔 두 수를 곱해서 더해야 한다.

 

그냥 더하는 것보다 두 수를 곱할수록 합은 더 커진다. 그럼 어떻게 곱해야 더 크게 만들 수 있을까? 숫자 하나만 더한다고 생각했을 때, 가장 큰 수를 만드는 방법은 가장 큰 두 수를 곱하는 것이다. 즉, greedy하게 내림차순으로 큰 두 수씩 묶으면 값을 가장 크게 만들 수 있다.

 

음수인 경우엔 어떨까?

음수를 곱하면 양수가 되므로 마찬가지로 곱하는 게 항상 이득이다. 따라서, greedy하게 오름차순으로 절대값이 큰 두 수씩 묶으면 값을 가장 크게 만들 수 있다. 

 

근데, 묶다가 마지막에 하나가 남는 경우가 생긴다. 양,음수 모두 홀수개일 때는 하나가 남는다.

음수의 경우 만약 0이 있다면 0과 곱하고, 0이 없다면 어쩔 수 없이 가장 작은 하나를 남겨둔다. 0을 곱하거나 양수와 곱하면 더 커질테니까.

양수는 남으면 그냥 더하는 수밖에 없다. 음수나 0이랑 곱하면 더 작이지기 때문이다.

 

즉, 0은 남는 음수에 곱하는 용도로 사용한다. 음수와 곱하면 음수를 더 크게 만들 수 있지만 음수는 음수끼리 곱하는 것이 더 이득이기 때문이다.

 

Greedy)

이 문제는 위와 같이 greedy 방식으로 풀면 해결할 수 있다. 근데 위 방법대로 하면 정답을 받지 못한다ㅋㅋ! 0과 음수는 예외처리 했지만 1은 고려해주지 않았기 때문이다.

 

1은 어떤 수를 곱해도 자기 자신이 된다. 하지만 더한다면? (자기자신+1)이 된다. 1은 0과 곱하면 0으로 더 작아지고, 음수와 곱해도 작아진다. 따라서 1은 항상 묶지 않고 따로 더해줘야 한다. 1은 모두 따로 빼서 더하고 나머지 양수에 위의 방법을 적용하면 정답을 받을 수 있다.

 

위 방법을 정리하면 다음과 같다.

1. 수열을 입력 받는다 - 양수,음수를 따로 저장해준다. 0이 있는지 체크하고, 1은 모두 빼서 정답에 미리 더해준다.

2. 양,음 수열을 각각 정렬한다.

3-1 양의 수열) 

  • 개수가 0이면 0을, 1개면 그 원소를 정답에 더한다.
  • 짝수개라면 크기순으로 2개씩 묶어서 곱한 것을 합해서 정답에 더한다.
  • 홀수개이면 가장 작은 수와 나머지 짝수개를 묶은 것을 합해서 정답에 더해준다.    

3-2 음의 수열) 양의 수열과 절대값에 대해 같은 방식으로 처리해준다

4) 음의 수열이 홀수개 && 0이 수열에 있다면, 더해줬던 가장 큰 음수를 정답에서 빼준다.  

 

코드

fun main(){
    var answer = 0

    val n = readLine()!!.toInt()
    val negatives = mutableListOf<Int>()
    val positives = mutableListOf<Int>()
    var hasZero = false
    repeat(n){ i ->
        val number = readLine()!!.toInt()
        when{
            number == 0 -> hasZero = true // 여러 개여도 상관없다
            number == 1 -> answer++	  // 바로 더해준다
            number > 1 -> positives.add(number)
            else -> negatives.add(number)
        }
    }

    val numOfNegative = negatives.size
    val numOfPositive = positives.size
    positives.sort()
    negatives.sortDescending()	// 같은 함수로 처리하기 위해 내림차순 정렬
    
    // 3)
    answer += handleSequence(positives, numOfPositive) + handleSequence(negatives, numOfNegative)
    
    // 4)
    if (numOfNegative % 2 != 0 && hasZero){	
        answer -= negatives[0]
    }

    println(answer)
}

fun handleSequence(sequence: MutableList<Int>, numOfSequence: Int): Int{
    return when{
        numOfSequence == 0 -> 0
        numOfSequence == 1 -> sequence[0]
        
        // 2개씩 묶어서 곱한 것을 모두 더한다.
        numOfSequence % 2 == 0 -> sequence.chunked(2).map{ it[0]*it[1] }.reduce{ sum, num -> sum + num }
        
        else -> sequence[0] +
                sequence.subList(1, numOfSequence)
                    .chunked(2)
                    .map{it[0]*it[1]}
                    .reduce{sum, num -> sum + num}
    }
}

 

리뷰

  • 처음에 그리디 문제인지 몰랐다. 그리디 문제는 딱 보고 눈치챌 수 있을 거라고 생각했는데, 앞으로 열심히 풀어야겠다..ㅋㅋ
  • 1도 예외처리를 해줘야 했다니. 숫자는 어렵다.

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

 

1707번: 이분 그래프

입력은 여러 개의 테스트 케이스로 구성되어 있는데, 첫째 줄에 테스트 케이스의 개수 K가 주어진다. 각 테스트 케이스의 첫째 줄에는 그래프의 정점의 개수 V와 간선의 개수 E가 빈 칸을 사이에

www.acmicpc.net

 

풀이

그래프의 모든 노드를 인접한 노드와 같은 집합에 속하지 않도록 하면서, 두 집합으로 분할할 수 있는지 판별하는 문제이다.

 

먼저 노드를 두 집합으로 분할해야 하므로, 모든 노드를 방문하면서 어떤 집합에 속할 것인지를 정해준다. 분할하는 것을 쉽게 색칠한다고 표현하자. DFS나 BFS를 사용해서 노드를 한번씩만 방문하면서 인접 노드를 현재 노드와 다른 색으로 색칠하면 된다. 나는 colors라는 배열을 만들어서 모든 노드를 0과1로 나누었다. 

 

얼핏 단순하게 "인접 노드를 모두 다른 색으로 칠하면 무조건 이분 그래프로 만들 수 있는 거 아닌가?"라고 생각할 수도 있지만, 인접노드가 이미 이전에 방문되었고, 현재 노드와 같은 색으로 칠해진 경우가 발생할 수 있다.

  

분할이 끝나면 그래프에서 모든 노드를 돌면서 자신과 같은 색인 인접노드가 있는지 확인해주면 이분 그래프인지 아닌지 판별할 수 있다.

 

참고로 연결 그래프라면 DFS나 BFS를 사용해서 노드를 한번씩만 방문하면서 이분 그래프 여부를 판별할 수 있으며, 연결 그래프가 아니라면 위에 언급한 대로 노드마다 인접 노드를 검사해주면 된다.

 

이를 좀 더 구체적으로 적으면 다음과 같다.  

1. DFSorBFS를 사용해서 모든 노드를 한번씩 방문한다.

2. 노드마다 인접 노드와 다른 색으로 칠해준다. 

3. 노드마다 인접 노드를 확인해서 같은 색으로 칠한 노드가 있으면 "NO", 없으면 "YES"를 출력한다.

      

 

코드

fun main(){
    for(i in 0 until readLine()!!.toInt()){
        var numberOfNode = 0
        var numberOfEdge = 0
        readLine()!!.split(" ").let{
            numberOfNode = it[0].toInt()
            numberOfEdge = it[1].toInt()
        }

        val visit = Array(numberOfNode){false}
        val colors = Array(numberOfNode){-1}
        val graph = Array(numberOfNode){mutableListOf<Int>()}
        // 인접리스트로 그래프 저장
        for(i in 0 until numberOfEdge){
            readLine()!!.split(" ").map{it.toInt()-1}.let{
                graph[it[0]].add(it[1])
                graph[it[1]].add(it[0])
            }
        }

	// DFS로 모든 노드 방문
        (0 until numberOfNode).forEach { node ->
            if (!visit[node]){
                visit[node] = true
                colors[node] = 0	// 첫 노드를 0으로 색칠
                dfs(graph, node, visit, colors)
            }
        }

	// 모든 노드마다 인접 노드가 무슨 색인지 확인한다
        if (isBipartite(graph, colors)){
            println("YES")
            continue
        }
        println("NO")
    }
}

fun isBipartite(graph: Array<MutableList<Int>>, colors: Array<Int>): Boolean{
    graph.indices.forEach { node ->
        graph[node].forEach { adjacent ->
            if (colors[node] == colors[adjacent]){	// 현재 노드와 인접노드의 색이 같으면 false
                return false
            }
        }
    }

    return true
}

fun dfs(graph: Array<MutableList<Int>>, node: Int, visit: Array<Boolean>, colors: Array<Int>){
    graph[node].forEach { toNode ->
        if (!visit[toNode]){
            visit[toNode] = true
            colors[toNode] = (colors[node]+1) % 2	// 인접노드를 현재 노드와 다른 색(0,1)으로 칠한다
            dfs(graph, toNode, visit, colors)
        }
    }
}

 

리뷰

이분그래프에 대해 알고 있고, DFS와 BFS를 쓸 줄 아는지 묻는 문제였던 것 같다.

+ Recent posts