문제

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.

예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {1020, 10, 30, 20, 50} 이고, 길이는 4이다.

입력

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다.

둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)

출력

첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이를 출력한다.

둘째 줄에는 정답이 될 수 있는 가장 긴 증가하는 부분 수열을 출력한다.

내가 푼 풀이

이제까지 가장 긴 증가하는 부분수열(LIS)를 구하는 방법은 두가지가 있었다.

첫번째는 DP방법으로 해당 인덱스의 원소까지의 부분수열 길이를 최신화 하는 방법이였다.

이는 시간복잡도가 $O(N^2)$가 걸리므로 주어진 문제의 입력은 최대 100만개의 원소이므로 시간초과가 일어났다.

 

두번째 방법으론 이분탐색이 있다.

이분탐색을 통해 이문제를 해결했지만 몇가지 주의해야할 점이 있었다.

이전에 이분탐색으로 구하면 최장증가 부분수열의 "길이"만 구할 수 있었다.

이분탐색을 통해 배열에서 원소가 위치하는 자리를 구하고 해당위치에 넣은결과 길이는 정답이지만, 부분수열이 옳지 않게 구하게 되었다.

 

예로 수열 [10, 20, 10, 30, 15, 50]이 주어졌을 때, 이분탐색을 이용하여 구한다면

[10, 15, 30, 50]배열이 구해지고 이는 최장증가 부분수열이 아니다.

이유는 15는 50이전의 원소로 30보다 나중에 나오는 원소이므로 옳지 않다.

 

부분수열은 구할 수 없지만 길이는 알고있다.

이 점을 이용하여 이분탐색을 통해 부분수열을 구할 때, 해당 원소의 인덱스를 저장한다.

괄호안 숫자는 해당원소가 주어진 수열의위치를 의미한다.

 

첫번째 원소 10

첫번째 원소는 비교대상이 없으므로 첫번째에 넣는다.

result = [10(1)]

idx = [1]

 

두번째 원소 20

두번쨰 원소는 이분탐색을 이용하여 10뒤에 오는 숫자임을 알 수 있다.

result = [10(1), 20(2)]

idx = [1, 2]

 

세번째 원소 10

세번째 원소는 이분탐색 결과 첫번째 오는 숫자와 같음을 알 수 있다.

result = [10(3), 20(2)]

idx = [1, 2, 1]

 

네번째 원소 30

네번째 원소는 20뒤에오는 숫자임을 알 수 있다.

result = [10(3), 20(2), 30(4)]

idx = [1, 2, 1, 3]

 

다섯번째 원소 15

다섯번째 원소는 이분탐색 결과 10과 20사이에 오는 숫자임을 알 수 있다.

이분탐색 결과 부분수열은 아래와 같다.

result = [10(3), 15(5), 30(4)]

idx = [1, 2, 1, 3, 2]

-> 이분탐색으로 만들어진 부분수열은 이미 정답과는 다른 부분수열이 만들어졌다. 해당원소의 위치를 배열안에 저장한다.

 

여섯번째 원소 50

여섯번째 원소는 이분탐색결과 마지막에 오는 숫자임을 알 수 있다.

result = [10(3), 15(5), 30(4), 50(6)]

idx = [1, 2, 1, 3, 2, 4]

 

-> 결과

result = [10(3), 15(5), 30(4), 50(6)]

idx = [1, 2, 1, 3, 2, 4]

 

이전의 이분탐색을 이용하면 result 배열을 얻고 길이는 정답이지만 부분 수열은 옳지않은 수열이 된다.

이분탐색을 통해 부분수열의 값이 변경될때 해당 원소가 위치하는 인덱스값을 저장함으로써 길이를 차감하며 부분수열을 구할 수 있게된다.

길이가 4이므로 역순으로 4 3 2 1의 원소를 뽑으면 [50, 30, 20 ,10]이고 이배열의 역순은 정답이 된다.

 

이분탐색을 이용하는 방법은 알았지만, 부분수열을 뽑아내는 과정은 쉽게 떠올리지 못했다.

 

코드로 구현하면 다음과 같다.

import Foundation

// 입력받기
let N = Int(readLine()!)!
var A = readLine()!.split(separator: " ").map{Int(String($0))!}
A.insert(0, at: 0)
let INF = 1000000001
// 이분탐색 결과배열과 인덱스 배열
var result = Array(repeating: INF, count: N+1)
var idxArr = [Int]()
result[1] = A[1]

// 이분탐색
func binarySearch(num: Int) {
    var left = 1
    var right = result.count-1
    
    while left <= right {
        let mid = (right + left) / 2
        
        if result[mid] < num {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    // 해당위치에 넣고, 인덱스배열에 저장
    result[left] = num
    idxArr.append(left)
}

for i in 1...N {
    binarySearch(num: A[i])
}

var answer = [Int]()
var count = 0

// 부분수열의 길이 구하기
for i in 1...N {
    if result[i] != INF {
        count += 1
    } else {
        break
    }
}
// 부분수열 구하기
for i in (0..<idxArr.count).reversed() {
    if idxArr[i] == count {
        count -= 1
        answer.append(A[i+1])
    }
}
// 출력
print(answer.count)
print(answer.map{String($0)}.reversed().joined(separator: " "))

 

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-17404 RGB거리 2 Swift  (1) 2024.07.16
BOJ-2631 줄세우기 Swift  (0) 2024.06.17
BOJ-1450 냅색문제 Swift  (0) 2024.06.06
BOJ-1644 소수의 연속합 Swift  (0) 2024.06.06
BOJ-11657 타임머신 Swift  (1) 2024.06.04

문제

RGB거리에는 집이 N개 있다. 거리는 선분으로 나타낼 수 있고, 1번 집부터 N번 집이 순서대로 있다.

집은 빨강, 초록, 파랑 중 하나의 색으로 칠해야 한다. 각각의 집을 빨강, 초록, 파랑으로 칠하는 비용이 주어졌을 때, 아래 규칙을 만족하면서 모든 집을 칠하는 비용의 최솟값을 구해보자.

  • 1번 집의 색은 2번, N번 집의 색과 같지 않아야 한다.
  • N번 집의 색은 N-1번, 1번 집의 색과 같지 않아야 한다.
  • i(2 ≤ i ≤ N-1)번 집의 색은 i-1, i+1번 집의 색과 같지 않아야 한다.

입력

첫째 줄에 집의 수 N(2 ≤ N ≤ 1,000)이 주어진다. 둘째 줄부터 N개의 줄에는 각 집을 빨강, 초록, 파랑으로 칠하는 비용이 1번 집부터 한 줄에 하나씩 주어진다. 집을 칠하는 비용은 1,000보다 작거나 같은 자연수이다.

출력

첫째 줄에 모든 집을 칠하는 비용의 최솟값을 출력한다.

 

접근방법: DP

해당문제를 풀기전 규칙을 먼저 이해해야한다. 규칙은 단순하다.

직선 위 N개의 집이 주어졌을 때, 서로 인접한 집과의 색이 같지 않고, 첫번째와 마지막의 집의 색이 같으면 안된다.

직선을 원으로 만들어 첫번째와 마지막 집을 인접시키면 모든 집은 인접한 집과 다른 색을 갖는다는 점과 같지만

이 문제에선 이 방식은 사용하지않지만 규칙을 이해하는데 쉬웠다.

 

첫번째 집부터 마지막집까지 색칠하는 과정을 거치는데 첫번째와 마지막집의 색이 같으면 안되므로 첫번째집의 색을 알고 있어야한다.

이를 DP방식을 이용하여 풀려면 이전까지 색칠한 비용을 알고 이용하여 그다음 집까지 색칠비용을 구한다.

 

인접한 집과 색이 같으면 안되므로 

첫번째 집이 빨강이면 두번째 집은 파랑과 초록이 가능하다

두번째 집이 파랑이라면 세번째 집은 빨강과 초록이 가능하다.

세번째 집이 초록이라면 네번째 집은 빨강과 파랑이 가능하다.

 

주어진 비용을 2차원 배열로 저장하면 빨강: 0, 초록: 1, 파랑: 2 인덱스를 갖는 배열이 된다.

위 방식을 사용하면 dp배열을 아래와 같이 정의할 수 있다.

dp[n][0]: n번째 집을 빨강으로 색칠한 최소비용

dp[n][1]: n번째 집을 초록으로 색칠한 최소비용

dp[n][2]: n번째 집을 파랑으로 색칠한 최소비용

dp[n][0] = arr[n][0] + min(dp[n-1][1], dp[n-1][2]) (arr: 색칠비용이 담긴 2차원배열)

 

이렇게 마지막 집까지 모두 색칠했을 때, 마지막 집과 첫번째 집의 색이 다른 경우의 비용을 갱신하여 값을 구한다.

 

코드로 구현하면 다음과 같다.

import Foundation

// 입력받기
let N = Int(readLine()!)!
var arr = [[0]]
let INF = 100000000
var ans = INF
for _ in 1...N {
    arr.append(readLine()!.split(separator: " ").map{Int(String($0))!})
}

// 첫번째 집이 어떤색을 칠하는지? 가 기준이 된다.
// 집이 나열된 선분을 순환구조로 나타낸다면, 인접한 집과의 색은 같으면 안된다.
// 첫번째 집을 세가지 색 모두 칠해보고 색칠비용의 최솟값을 구한다.
for i in 0...2 {
    // dp[i][j]: i번째집을 j색으로 칠했을 때의 최소비용
    var dp = Array(repeating: Array(repeating: 0, count: 3), count: N+1)
    // 첫번째 집과 마지막집의 색이 같으면 안되므로, 우리는 첫번째 집의 색을 기억하고 있어야한다.
    // 따라서 첫번째 집의 색 이외의 경우의수는 임의의 최댓값을 넣는다.
    for j in 0..<3 {
        if i == j {
            dp[1][j] = arr[1][j]
        } else {
            dp[1][j] = INF
        }
    }
    
    // 규칙에따라 현재집은 이전의 집과 다른색을 칠해야한다.
    // n번째 집까지 색칠한 최소비용: n-1번째까지 칠한 최소비용 + n번째 집의 색칠비용
    for j in 2...N {
        dp[j][0] = arr[j][0] + min(dp[j-1][1], dp[j-1][2])
        dp[j][1] = arr[j][1] + min(dp[j-1][0], dp[j-1][2])
        dp[j][2] = arr[j][2] + min(dp[j-1][0], dp[j-1][1])
    }
    // 첫번째 집과 마지막번째 집의 색이 같지 않다면, 최소비용을 갱신한다.
    for k in 0..<3 {
        if i != k {
            ans = min(ans, dp[N][k])
        }
    }
}
print(ans)

 

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-14003 가장 긴 증가하는 부분 수열 5 Swift  (0) 2024.08.16
BOJ-2631 줄세우기 Swift  (0) 2024.06.17
BOJ-1450 냅색문제 Swift  (0) 2024.06.06
BOJ-1644 소수의 연속합 Swift  (0) 2024.06.06
BOJ-11657 타임머신 Swift  (1) 2024.06.04

문제

KOI 어린이집에는 N명의 아이들이 있다. 오늘은 소풍을 가는 날이다. 선생님은 1번부터 N번까지 번호가 적혀있는 번호표를 아이들의 가슴에 붙여주었다. 선생님은 아이들을 효과적으로 보호하기 위해 목적지까지 번호순서대로 일렬로 서서 걸어가도록 하였다. 이동 도중에 보니 아이들의 번호순서가 바뀌었다. 그래서 선생님은 다시 번호 순서대로 줄을 세우기 위해서 아이들의 위치를 옮기려고 한다. 그리고 아이들이 혼란스러워하지 않도록 하기 위해 위치를 옮기는 아이들의 수를 최소로 하려고 한다.

예를 들어, 7명의 아이들이 다음과 같은 순서대로 줄을 서 있다고 하자.

3 7 5 2 6 1 4

아이들을 순서대로 줄을 세우기 위해, 먼저 4번 아이를 7번 아이의 뒤로 옮겨보자. 그러면 다음과 같은 순서가 된다.

3 7 4 5 2 6 1

이제, 7번 아이를 맨 뒤로 옮긴다.

3 4 5 2 6 1 7

다음 1번 아이를 맨 앞으로 옮긴다.

1 3 4 5 2 6 7

마지막으로 2번 아이를 1번 아이의 뒤로 옮기면 번호 순서대로 배치된다.

1 2 3 4 5 6 7

위의 방법으로 모두 4명의 아이를 옮겨 번호 순서대로 줄을 세운다. 위의 예에서 3명의 아이만을 옮겨서는 순서대로 배치할 수가 없다. 따라서, 4명을 옮기는 것이 가장 적은 수의 아이를 옮기는 것이다.

N명의 아이들이 임의의 순서로 줄을 서 있을 때, 번호 순서대로 배치하기 위해 옮겨지는 아이의 최소 수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에는 아이들의 수 N이 주어진다. 둘째 줄부터는 1부터 N까지의 숫자가 한 줄에 하나씩 주어진다. N은 2 이상 200 이하의 정수이다.

출력

첫째 줄에는 번호 순서대로 줄을 세우는데 옮겨지는 아이들의 최소 수를 출력한다.

내가 푼 풀이

접근방법: DP

아이들의 위치를 옮기는 수를 최소로 갖게하는 기준을 만드려고 한다.

 

아이들 7명이 있을때,

만약 아이들의 번호가 내림차순으로 서있다면, 어떠한 방법을 사용하더라도 최소로 옮기는 횟수는 6번이다.

반대로 아이들이 오름차순으로 서있다면 옮기지 않아도 된다.

아이들의 번호가 증가하는 순서대로 서 있다면 최소한으로 옮길 수 있을꺼라 생각했다.

 

그러면 증가하는 부분수열을 구할 수 있다면, 해당 부분수열은 옮기지 않아도 증가하는 순서대로 서있게 된다.

해당 부분수열을 제외한 다른 수는 증가하는 부분수열의 기준에 맞춰서 옮기게 되면 최소한으로 옮길 수 있게 된다.

 

코드로 구현하면 다음과 같다.

import Foundation

// 입력받기
let N = Int(readLine()!)!
var dp = Array(repeating: 0, count: N+1)
var arr = [Int]()
arr.append(0)
for _ in 0..<N {
    arr.append(Int(readLine()!)!)
}

// 증가하는 부분수열의 길이 구하기
// 증가하는 부분수열이 존재하면 해당 수열을 기준으로 아이들을 옮기는게 최소방법이다.
for i in 1...N {
    for j in 0..<i {
        if arr[i] > arr[j] {
            dp[i] = max(dp[i], dp[j] + 1)
        }
    }
}
print(N - dp.max()!)

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-14003 가장 긴 증가하는 부분 수열 5 Swift  (0) 2024.08.16
BOJ-17404 RGB거리 2 Swift  (1) 2024.07.16
BOJ-1450 냅색문제 Swift  (0) 2024.06.06
BOJ-1644 소수의 연속합 Swift  (0) 2024.06.06
BOJ-11657 타임머신 Swift  (1) 2024.06.04

문제

세준이는 N개의 물건을 가지고 있고, 최대 C만큼의 무게를 넣을 수 있는 가방을 하나 가지고 있다.

N개의 물건을 가방에 넣는 방법의 수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 N과 C가 주어진다. N은 30보다 작거나 같은 자연수, C는 109보다 작거나 같은 음이 아닌 정수이다. 둘째 줄에 물건의 무게가 주어진다. 무게도 109보다 작거나 같은 자연수이다.

출력

첫째 줄에 가방에 넣는 방법의 수를 출력한다.

내가 푼 풀이

접근방법 1: 브루트포스(조합) (시간초과)

N = 30이니까 밑져야본전식으로 가능한 모든 물건 합산의 경우의수를 구해봤지만 시간초과가 났다.

 

접근방법 2: Meet in the Middle / 이분탐색 / 조합

이 문제에서 새롭게 알게된 알고리즘이 있는데 바로 Meet in the Middle이다.

이 알고리즘은 브루트포스를 이용하기에 경우의수가 매우 많은경우 분할하여 연산횟수를 줄이는 기법이다.

접근방법 1번대로 모든 경우의수를 구한다면 $2^{30}$ 만큼 연산이 필요하고 이는 1,073,741,824번으로 시간초과가 발생한다.

이를 절반으로 나누어 경우의수를 구하면

$2^{15}  = 32,786$ 으로 연산횟수를 대폭 감소시킬 수 있다.

두개의 배열로 분할하여 경우의수를 갖는 부분집합을 구한다.

이때, 무게 C보다 큰 수는 불가능한 경우므로 부분집합에 넣지 않는다.

 

1. 주어진 무게의 배열을 절반으로 나눈다.

2. 절반으로 나눈 배열의 부분집합을 구한다.(배열의 원소를 더해서 나올 수 있는 모든 무게의 경우의수)

3. 두개의 부분집합 배열이 생성된다.

 

위 과정을 통해 오른쪽부분집합과 왼쪽부분집합이 구해졌다.

양쪽의 원소를 한개씩 가져와 더한 값이 C보다 작거나 같다면, 이는 가능한 경우의수가 된다.

 

따라서 (C - 오른쪽부분집합의 원소)보다 작거나 같은값은 모두 가능한 경우의 수가 된다.

갯수를 구하기 위해 이분탐색을 활용한다.

부분집합은 중복된 값을 가질 수 있다. 이는 가능한 모든 무게의 경우의수이기 때문이다.

이분탐색을 이용하여 중복된값을 갖는 배열의 가장 뒤에 위치한 인덱스를 구하면 해당 원소보다 작거나 같은 원소의 갯수를 구할 수 있다.

 

코드로 구현하면 다음과 같다.

import Foundation

// 입력받기
let input = readLine()!.split(separator: " ").map{Int(String($0))!}
let N = input[0], C = input[1]
var arr = readLine()!.split(separator: " ").map{Int(String($0))!}
var left = [Int]()
var right = [Int]()
var leftSums = [Int]()
var rightSums = [Int]()
var answer = 0
let mid = arr.count / 2

// 주어진 물건의 배열을 절반으로 나눈다.
for i in 0..<mid {
    left.append(arr[i])
}
for i in mid..<arr.count {
    right.append(arr[i])
}

// 조합
// 조합을 이용해 배열내 모든원소의 합 부분집합을 구한다.
func combi(targetArr: [Int], targetNum: Int, sum: Int, cnt: Int, index: Int, right: Bool) {
    if targetNum == cnt {
        if right {
            rightSums.append(sum)
        } else {
            leftSums.append(sum)
        }
        return
    }
    for i in index..<targetArr.count {
        let total = sum + targetArr[i]
        // 무게 C보다 큰수는 버린다. (불가능한 경우의수 이므로)
        if total <= C {
            combi(targetArr: targetArr, targetNum: targetNum, sum: total, cnt: cnt+1, index: i+1, right: right)
        }
    }
}

// 반으로 나눈 배열의 각 부분집합을 조합을 이용해 구한다.
// 오른쪽 배열
for i in 0...right.count {
    combi(targetArr: right, targetNum: i, sum: 0, cnt: 0, index: 0, right: true)
}
// 왼쪽 배열
for i in 0...left.count {
    combi(targetArr: left, targetNum: i, sum: 0, cnt: 0, index: 0, right: false)
}

// 이분탐색을 위해 정렬
leftSums.sort{$0 < $1}
rightSums.sort{$0 < $1}

// 이분탐색
// 부분집합의 합이 무게 C보다 작아야한다.
// 이는 오른쪽 부분집합 원소 하나를 꺼낸 값이 (C - 왼쪽부분집합의 값) 보다 작거나 같다면 가능한 경우의 수다.
// 이분탐색을 이용해 가능한 경우의수중 가장 큰수의 인덱스를 구한다.
// 여기서 인덱스는 곧 해당 원소보다 작거나 같은 원소의 갯수를 의미하므로 해당 인덱스를 더해준다.
// 중복된 값이 존재하는 이분탐색은 가장 뒤쪽의 인덱스를 구하는방법을 사용하자.
for rNum in rightSums {
    let sub = C - rNum
    var lidx = 0
    var ridx = leftSums.count - 1
    while lidx <= ridx {
        let mid = (lidx + ridx) / 2
        if leftSums[mid] <= sub {
            lidx = mid + 1
        }else {
            ridx = mid - 1
        }
    }
    answer += ridx+1
}

print(answer)

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-17404 RGB거리 2 Swift  (1) 2024.07.16
BOJ-2631 줄세우기 Swift  (0) 2024.06.17
BOJ-1644 소수의 연속합 Swift  (0) 2024.06.06
BOJ-11657 타임머신 Swift  (1) 2024.06.04
BOJ-9370 미확인 도착지 Swift  (1) 2024.06.03

문제

하나 이상의 연속된 소수의 합으로 나타낼 수 있는 자연수들이 있다. 몇 가지 자연수의 예를 들어 보면 다음과 같다.

  • 3 : 3 (한 가지)
  • 41 : 2+3+5+7+11+13 = 11+13+17 = 41 (세 가지)
  • 53 : 5+7+11+13+17 = 53 (두 가지)

하지만 연속된 소수의 합으로 나타낼 수 없는 자연수들도 있는데, 20이 그 예이다. 7+13을 계산하면 20이 되기는 하나 7과 13이 연속이 아니기에 적합한 표현이 아니다. 또한 한 소수는 반드시 한 번만 덧셈에 사용될 수 있기 때문에, 3+5+5+7과 같은 표현도 적합하지 않다.

자연수가 주어졌을 때, 이 자연수를 연속된 소수의 합으로 나타낼 수 있는 경우의 수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 자연수 N이 주어진다. (1 ≤ N ≤ 4,000,000)

출력

첫째 줄에 자연수 N을 연속된 소수의 합으로 나타낼 수 있는 경우의 수를 출력한다.

내가 푼 풀이

접근방법: 투포인터, 에라토스테네스의 체

주어지는 N의 최댓값이 400만이고 시간제한은 2초이므로, 에라토스테네스의 체를 이용하여 소수를 구할 수 있다.

에라토스테네스의 체를 이용한 소수구하기 알고리즘은 많은 메모리가 필요하지만 시간복잡도가 O(NloglogN) 이므로 4000000횟수까지 연산이 된다.

 

에라토스네테스의 체

1. 2부터 구하고자 하는 범위의 최댓값까지 자연수를 나열한다.

2. 나열된 자연수중 걸러지지않은 가장 작은 자연수를 구한다.

3. 자신을 제외한 자신의 배수를 모두 거른다.

4. 더이상 반복할 수 없을때까지 반복

 

소수를 걸러내기 위해 2부터 N까지 반복한다면, 시간초과가 뜰것이다.

여기서 시간복잡도를 개선하기위해,

자연수 N은 2부터 N의 제곱근까지 모든 자연수와 나누어 떨어지지 않는다면, 이는 소수임을 이용한다.

 

N이 400만이면 N의 제곱근인 2000까지 배수를 모두 걸러내면 된다.

2에서 400만 사이의 소수를 배열에 담는다.

 

투포인터

왼쪽인덱스부터 출발하는 투포인터 알고리즘을 사용한다.

left: 0, rigt:0 으로 시작하여, left부터 right까지의 모든 원소의 합(sum)을 주어진 N과 비교한다.

N과 sum이 같다면: 경우의수 + 1, left+1, sum -= arr[left]

N보다 sum이 크다면: left -= 1 sum -= arr[left]

N보다 sum이 작다면: right += 1, sum += arr[right]

(만약 right가 마지막원소를 넘긴다면 left를 증가시켜도 sum이 증가되지않으므로 탈출)

 

코드로 구현하면 다음과 같다.

import Foundation

// 입력받기
let N = Int(readLine()!)!
var num = 4000000
var arr = Array(repeating: true, count: num+1)
var primeNums = [Int]()
let sqrtNum = Int(sqrt(Double(num)))
arr[1] = false
// 에라토스테네스의 체를 활용하여 소수 구하기
for i in 2...sqrtNum {
    if arr[i] == true {
        var j = 2
        while i * j <= num {
            arr[i * j] = false
            j += 1
        }
    }
}
// 구했던 소수를 배열에 저장
for i in 2...num {
    if arr[i] {
        primeNums.append(i)
    }
}

var lidx = 0
var ridx = 0
var cnt = 0
var sum = primeNums[lidx]

// 투포인터 알고리즘
// 주어진 소수배열에서 왼쪽인덱스부터 오른쪽 인덱스까지의 합을 N과 비교한다.
// N이 된다면 cnt + 1 , N보다 작다면 오른쪽인덱스를 증가, N보다 크다면 왼쪽인덱스를 증가
// N이 작아서 오른쪽 인덱스를 늘려서 배열의 마지막인덱스를 초과한다면, 범위를 벗어난경우이고 왼쪽 인덱스를 올려도 합계 자체는 오르지 않기때문에 탈출
while lidx < primeNums.count && lidx <= ridx {
    if primeNums[lidx] > N { break }
    if sum == N {
        cnt += 1
        sum -= primeNums[lidx]
        lidx += 1
    } else if sum > N {
        sum -= primeNums[lidx]
        lidx += 1
    } else if sum < N {
        ridx += 1
        if ridx > primeNums.count - 1 {
            break
        }
        sum += primeNums[ridx]
    }
}

print(cnt)

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-2631 줄세우기 Swift  (0) 2024.06.17
BOJ-1450 냅색문제 Swift  (0) 2024.06.06
BOJ-11657 타임머신 Swift  (1) 2024.06.04
BOJ-9370 미확인 도착지 Swift  (1) 2024.06.03
BOJ-1707 이분 그래프 Swift  (0) 2024.06.03

문제

N개의 도시가 있다. 그리고 한 도시에서 출발하여 다른 도시에 도착하는 버스가 M개 있다. 각 버스는 A, B, C로 나타낼 수 있는데, A는 시작도시, B는 도착도시, C는 버스를 타고 이동하는데 걸리는 시간이다. 시간 C가 양수가 아닌 경우가 있다. C = 0인 경우는 순간 이동을 하는 경우, C < 0인 경우는 타임머신으로 시간을 되돌아가는 경우이다.

1번 도시에서 출발해서 나머지 도시로 가는 가장 빠른 시간을 구하는 프로그램을 작성하시오.

입력

첫째 줄에 도시의 개수 N (1 ≤ N ≤ 500), 버스 노선의 개수 M (1 ≤ M ≤ 6,000)이 주어진다. 둘째 줄부터 M개의 줄에는 버스 노선의 정보 A, B, C (1 ≤ A, B ≤ N, -10,000 ≤ C ≤ 10,000)가 주어진다. 

출력

만약 1번 도시에서 출발해 어떤 도시로 가는 과정에서 시간을 무한히 오래 전으로 되돌릴 수 있다면 첫째 줄에 -1을 출력한다. 그렇지 않다면 N-1개 줄에 걸쳐 각 줄에 1번 도시에서 출발해 2번 도시, 3번 도시, ..., N번 도시로 가는 가장 빠른 시간을 순서대로 출력한다. 만약 해당 도시로 가는 경로가 없다면 대신 -1을 출력한다.

내가 푼 풀이

접근방법 1. 플로이드-워셜 알고리즘 (시간초과)

모든 점에서의 최단거리를 구하고 출력하려 했지만, 시간초과가 났다.

 

접근방법: 2 벨만포드 알고리즘

다익스트라를 사용하기엔 음의 가중치도 존재하기에 벨만포드 알고리즘을 사용하였다.

음의 사이클을 판단하고 출력하는 과정에서 애를 먹었지만, 이번 기회에 벨만포드 알고리즘을 알게 되었다.

 

벨만포드 알고리즘은 N개의 노드가 주어진 그래프에서

N-1번 반복하여 다익스트라 알고리즘과 같이 인접한 점과의 거리를 이용해 최단거리를 갱신한다.

하지만 음의 가중치가 순환구조를 갖는 음의 사이클이 존재할 수 있다.

이는 반복하면 반복할수록 최단거리의 값이 작아진다.

 

N개의 노드가 주어졌을때, 음의 사이클이 존재하지 않다면 그래프의 최단거리 경로는 최대 N-1개의 간선을 지나갈 수 있다.

따라서 N-1번 반복을 하여 최단거리값을 갱신하지만 N번째에 최단거리가 갱신된다면, 이는 음의 사이클이 존재한다고 판단할 수 있다.

이 특징을 이용하여 음의 사이클이 존재한다면 -1을 출력하고, 존재하지않는다면 각 지점마다의 최단거리를 출력한다. (갈 수 없다면 -1)

 

코드로 구현하면 다음과 같다.

import Foundation

// 입력받기
let input = readLine()!.split(separator: " ").map{Int(String($0))!}
let N = input[0], M = input[1]
var graph = [Int: [(loc: Int, cost: Int)]]()
let INF: Int = 20000000000

// 그래프 입력받기
for _ in 0..<M {
    let input = readLine()!.split(separator: " ").map{Int(String($0))!}
    if graph[input[0]] == nil {
        graph[input[0]] = [(loc: input[1], cost: input[2])]
    } else {
        graph[input[0]]?.append((loc: input[1], cost: input[2]))
    }
}
// 벨만포드알고리즘 결과를 저장
var answer = bellmanFord(graph: graph, start: 1, N: N)

// 음의 사이클이 존재하는 경우 -1
// 존재하지 않는경우 각 지점의 최단거리 출력
if answer.0 {
    print(-1)
} else {
    for i in 2...N {
        if answer.1[i] == INF {
            print(-1)
        } else {
            print(answer.1[i])
        }
    }
}

// 벨만포드 알고리즘
// 다익스트라처럼 인접한 지점과의 거리로 최단거리를 갱신한다.
// 갱신 횟수는 N-1회 하여 갱신한다.(N: 노드의 갯수)
// 벨만포드 알고리즘은 음의 가중치를 가질 수 있고, 음의 사이클이 존재할 수 있다.
// 최단거리의 간선 경로 횟수는 최대 N-1개만 가질 수 있으므로 N-1회 반복하지만 N번째에도 최단거리가 갱신이 된다면, 이는 음의 사이클이 존재한다고 판단할 수 있다.
func bellmanFord( graph: [Int: [(loc: Int, cost: Int)]], start: Int, N: Int) -> (Bool, [Int]) {
    var minDistance = Array(repeating: INF, count: N+1)
    minDistance[start] = 0
    
    for i in 0...N {
        for node in 1...N {
            if graph[node] == nil { continue }
            for k in graph[node]! {
                if minDistance[node] != INF && minDistance[k.loc] > minDistance[node] + k.cost {
                    minDistance[k.loc] = minDistance[node] + k.cost
                    // N번째에도 최단거리가 갱신이 된다면 음의 사이클이 존재한다고 판단
                    if i == N {
                        return (true, minDistance)
                    }
                }
            }
        }
    }
    return (false, minDistance)
}

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-1450 냅색문제 Swift  (0) 2024.06.06
BOJ-1644 소수의 연속합 Swift  (0) 2024.06.06
BOJ-9370 미확인 도착지 Swift  (1) 2024.06.03
BOJ-1707 이분 그래프 Swift  (0) 2024.06.03
BOJ-7579 앱 Swift  (0) 2024.05.30

문제

(취익)B100 요원, 요란한 옷차림을 한 서커스 예술가 한 쌍이 한 도시의 거리들을 이동하고 있다. 너의 임무는 그들이 어디로 가고 있는지 알아내는 것이다. 우리가 알아낸 것은 그들이 s지점에서 출발했다는 것, 그리고 목적지 후보들 중 하나가 그들의 목적지라는 것이다. 그들이 급한 상황이기 때문에 목적지까지 우회하지 않고 최단거리로 갈 것이라 확신한다. 이상이다. (취익)

어휴! (요란한 옷차림을 했을지도 모를) 듀오가 어디에도 보이지 않는다. 다행히도 당신은 후각이 개만큼 뛰어나다. 이 후각으로 그들이 g와 h 교차로 사이에 있는 도로를 지나갔다는 것을 알아냈다.

이 듀오는 대체 어디로 가고 있는 것일까?

예제 입력의 두 번째 케이스를 시각화한 것이다. 이 듀오는 회색 원에서 두 검은 원 중 하나로 가고 있고 점선으로 표시된 도로에서 냄새를 맡았다. 따라서 그들은 6으로 향하고 있다.

입력

첫 번째 줄에는 테스트 케이스의 T(1 ≤ T ≤ 100)가 주어진다. 각 테스트 케이스마다

  • 첫 번째 줄에 3개의 정수 n, m, t (2 ≤ n ≤ 2 000, 1 ≤ m ≤ 50 000 and 1 ≤ t ≤ 100)가 주어진다. 각각 교차로, 도로, 목적지 후보의 개수이다.
  • 두 번째 줄에 3개의 정수 s, g, h (1 ≤ s, g, h ≤ n)가 주어진다. s는 예술가들의 출발지이고, g, h는 문제 설명에 나와 있다. (g ≠ h)
  • 그 다음 m개의 각 줄마다 3개의 정수 a, b, d (1 ≤ a < b ≤ n and 1 ≤ d ≤ 1 000)가 주어진다. a와 b 사이에 길이 d의 양방향 도로가 있다는 뜻이다.
  • 그 다음 t개의 각 줄마다 정수 x가 주어지는데, t개의 목적지 후보들을 의미한다. 이 t개의 지점들은 서로 다른 위치이며 모두 s와 같지 않다.

교차로 사이에는 도로가 많아봐야 1개이다. m개의 줄 중에서 g와 h 사이의 도로를 나타낸 것이 존재한다. 또한 이 도로는 목적지 후보들 중 적어도 1개로 향하는 최단 경로의 일부이다.

출력

테스트 케이스마다

  • 입력에서 주어진 목적지 후보들 중 불가능한 경우들을 제외한 목적지들을 공백으로 분리시킨 오름차순의 정수들로 출력한다.

내가 푼 풀이

접근방법: 다익스트라

다익스트라로 시작점으로 부터 최단거리를 구한다.

시작점 s에서 목적지 후보들까지의 최단거리를 구하고, 이 최단거리가 g-h 경로를 포함한 최단거리라면 가능한 목적지가 된다.

이 문제는 이해하기도 어려웠는데..

위 사진은 예제코드 두번째의 그래프이다.

목적지 후보 5,6 중 6번만 가능한데 이유는 다음과 같다.

g: 3, h: 1 , 출발지점은 2번이다.

2번-> 5번의 최단거리는 2 - 5:  5 이지만 g-h경로를 지나가는 최단경로가 아니므로 불가능한 목적지가 된다.

2번 ->6번의 최단거리는 2 - 1 - 3 - 6: 6으로 g-h경로를 지나가는 최단경로가 된다. 따라서 6번은 가능한 목적지가 된다.

 

알고리즘 접근은 다음과 같이 했다.

다익스트라를 이용하여 최단거리를 총 3번 구한다.

1. 시작점부터 모든 지점과의 최단거리

2. g지점부터 모든 지점과의 최단거리

3. h지점부터 모든 지점과의 최단거리

 

g -h경로를 통하는 최단거리를 구하려면, g또는 h까지 최단거리를 구해야한다.

주의) g또는 h까지 최단거리가 둘다 같을 수 있기 때문에 2가지 경우의 수를 구해야 한다.

(시작점: s, 도착점: m)

1. s -> g -> h -> m

2. s -> h -> g -> m

 

위 두가지 경우중 하나가 시작점부터 목적지까지의 최단거리와 같다면, g-h경로를 지나간 최단거리가 되므로 가능한 목적지가 된다.

(minS: 시작점에서의 최단거리, minG: g지점에서의 최단거리, minH: h지점에서의 최단거리)

minS[m] = minS[g] + (g-h거리) + minH[m]

minS[m] = minS[h] + (h-g거리) + minG[m]

 

코드로 구현하면 다음과 같다.

import Foundation

// 테스트케이스 수
let T = Int(readLine()!)!

for i in 0..<T {
    // 입력받기
    let input1 = readLine()!.split(separator: " ").map{Int(String($0))!}
    let n = input1[0], m = input1[1], t = input1[2]
    let input2 = readLine()!.split(separator: " ").map{Int(String($0))!}
    let s = input2[0], g = input2[1], h = input2[2]
    var graph = [Int: [(loc: Int, cost: Int)]]()
    var destination = [Int]()
    var answer = [Int]()
    var middleCost = 0
    
    // graph 저장
    for _ in 0..<m {
        let load = readLine()!.split(separator: " ").map{Int(String($0))!}
        // g-h거리 저장
        if (load[0] == g || load[1] == g) && (load[0] == h || load[1] == h) {
            middleCost = load[2]
        }
        if graph[load[0]] == nil {
            graph[load[0]] = [(loc: load[1], cost: load[2])]
        } else {
            graph[load[0]]?.append((loc: load[1], cost: load[2]))
        }
        if graph[load[1]] == nil {
            graph[load[1]] = [(loc: load[0], cost: load[2])]
        } else {
            graph[load[1]]?.append((loc: load[0], cost: load[2]))
        }
    }
    // 목적지 후보 저장
    for _ in 0..<t {
        destination.append(Int(readLine()!)!)
    }
    
    // minS: s지점에서 모든 점까지 최단거리
    // minG: g지점에서 모든 점까지 최단거리
    // minH: h지점에서 모든 점까지 최단거리
    var minS = dijkstra(s: s, n: n, graph: graph)
    var minG = dijkstra(s: g, n: n, graph: graph)
    var minH = dijkstra(s: h, n: n, graph: graph)
    
    // s: 시작점, m: 목적지
    // 두 경우의수 (s - g - h - m)의 최단거리 , (s - h - g - m)의 최단거리 가 (s - m)최단거리와 같다면 가능한 목적지
    for m in destination {
        let sumG = minS[g] + middleCost + minH[m]
        let sumH = minS[h] + middleCost + minG[m]
        
        if sumG == minS[m] || sumH == minS[m] {
            answer.append(m)
        }
    }
    // 오름차순으로 출력
    answer.sort{$0 < $1}
    print(answer.map{String($0)}.joined(separator: " "))
}

// 다익스트라
func dijkstra(s: Int, n: Int, graph: [Int: [(loc: Int, cost: Int)]]) -> [Int] {
    var needVisit = [(loc: s, cost: 0)]
    var idx = 0
    var min = Array(repeating: 2000000000, count: n+1)
    min[s] = 0
    
    while idx < needVisit.count {
        let node = needVisit[idx]
        idx += 1
        if graph[node.loc] == nil { continue }
        for i in graph[node.loc]! {
            let dist = i.cost
            if node.cost + dist < min[i.loc] {
                min[i.loc] = node.cost + dist
                needVisit.append((loc: i.loc, cost: min[i.loc]))
            }
        }
    }
    return min
}

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-1644 소수의 연속합 Swift  (0) 2024.06.06
BOJ-11657 타임머신 Swift  (1) 2024.06.04
BOJ-1707 이분 그래프 Swift  (0) 2024.06.03
BOJ-7579 앱 Swift  (0) 2024.05.30
BOJ-2011 암호코드 Swift  (0) 2024.05.19

문제

그래프의 정점의 집합을 둘로 분할하여, 각 집합에 속한 정점끼리는 서로 인접하지 않도록 분할할 수 있을 때, 그러한 그래프를 특별히 이분 그래프 (Bipartite Graph) 라 부른다.

그래프가 입력으로 주어졌을 때, 이 그래프가 이분 그래프인지 아닌지 판별하는 프로그램을 작성하시오.

입력

입력은 여러 개의 테스트 케이스로 구성되어 있는데, 첫째 줄에 테스트 케이스의 개수 K가 주어진다. 각 테스트 케이스의 첫째 줄에는 그래프의 정점의 개수 V와 간선의 개수 E가 빈 칸을 사이에 두고 순서대로 주어진다. 각 정점에는 1부터 V까지 차례로 번호가 붙어 있다. 이어서 둘째 줄부터 E개의 줄에 걸쳐 간선에 대한 정보가 주어지는데, 각 줄에 인접한 두 정점의 번호 u, v (u ≠ v)가 빈 칸을 사이에 두고 주어진다. 

출력

K개의 줄에 걸쳐 입력으로 주어진 그래프가 이분 그래프이면 YES, 아니면 NO를 순서대로 출력한다.

내가 푼 풀이

접근방법: BFS

이 문제는 이분그래프의 성질을 이해하고 있다면 더 쉽게 풀 수 있습니다.

저는 이분그래프의 성질을 알고 나서 풀었습니다..

 

이분그래프의 성질은 인접한 점끼리는 서로 다른 집합이어야합니다.

집합1: [1,3] , 집합2: [2,4] 으로 위 그래프는 이분그래프입니다.

위 그래프는 2번과 인접한 점 [1, 3, 4]이 있습니다.

두개의 집합으로 분할했을 때, [1,3,4], [2]로 나눌 수 있지만 3번과 4번이 서로 인접한 관계이므로 이분그래프가 될 수 없습니다.

 

위 성질을 이용해 BFS탐색을 통해 현재지점과 인접한 지점을 다른 집합으로 분리한다.

탐색을 진행하며 현재노드와 인접한 노드가 같은 집합으로 속한다면, 해당 그래프는 이분그래프가 아니라고 판단한다.

 

예제그래프)

 

- 현재노드 1번 - 인접노드 2번

결과: [1], [2]

- 현재노드 2번 - 인접노드 1번, 3번, 4번

1번은 이미 다른집합으로 분리되었다.

결과: [1,3,4], [2]

- 현재노드 3번 - 인접노드 2번, 4번

현재노드인 3번과 인접노드 4번이 이전 탐색을 통해 같은 집합으로 저장되어 있었다.

결과 -> 이분그래프가 아님.

 

코드로 구현하면 다음과 같다

import Foundation

// 테스트케이스 수
let K = Int(readLine()!)!

for i in 0..<K {
    // 입력받기
    let input = readLine()!.split(separator: " ").map{Int(String($0))!}
    let V = input[0], E = input[1]
    var graph = [Int: [Int]]()
    for j in 0..<E {
        let input = readLine()!.split(separator: " ").map{Int(String($0))!}
        if graph[input[0]] == nil {
            graph[input[0]] = [input[1]]
        } else {
            graph[input[0]]?.append(input[1])
        }
        if graph[input[1]] == nil {
            graph[input[1]] = [input[0]]
        } else {
            graph[input[1]]?.append(input[0])
        }
    }
    
    var visited = Array(repeating: 0, count: V+1)
    var isBinaryGraph = true
    
    // BFS
    // 그래프 정점을 이분했을때 두개의 집합으로 나누자 1번집합과 -1번집합
    // visited 0: 방문하지않음, 1: 1번집합에 속함, -1: -1번집합에 속함
    // BFS탐색을 통해 인접한 접점은 현재 집합과 다른 집합에 속해야한다.
    // 만약 현재 노드의 접점과 인접한 접점이 같은 집합에 속한다면, 이분그래프로 나타낼 수 없다.
    // 간선 연결없는 접점이 존재할 수 있으므로 모든 접점을 BFS탐색 진행한다.
    for i in 1...V {
        if visited[i] != 0 { continue }
        var needVisit = [i]
        visited[i] = 1
        var idx = 0
        while idx < needVisit.count {
            let node = needVisit[idx]
            let color = visited[node] == 1 ? -1 : 1
            if graph[node] == nil { break }
            for j in graph[node]! {
                if visited[j] != 0 {
                    if visited[node] == visited[j] {
                        isBinaryGraph = false
                        break
                    }
                } else {
                    visited[j] = color
                    needVisit.append(j)
                }
            }
            if !isBinaryGraph { break }
            idx += 1
        }
    }
    if isBinaryGraph {
        print("YES")
    } else {
        print("NO")
    }
}

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-11657 타임머신 Swift  (1) 2024.06.04
BOJ-9370 미확인 도착지 Swift  (1) 2024.06.03
BOJ-7579 앱 Swift  (0) 2024.05.30
BOJ-2011 암호코드 Swift  (0) 2024.05.19
BOJ-2302 극장 좌석 Swift  (0) 2024.05.16

+ Recent posts