11062. 카드 게임


문제

근우와 명우는 재미있는 카드 게임을 하고 있다. N개의 카드가 일렬로 놓여 있다. 각 카드에는 점수가 적혀있다. 근우부터 시작하여 번갈아가면서 턴이 진행되는데 한 턴에는 가장 왼쪽에 있는 카드나 가장 오른쪽에 있는 카드를 가져갈 수 있다. 카드가 더 이상 남아있지 않을 때까지 턴은 반복된다. 게임의 점수는 자신이 가져간 카드에 적힌 수의 합이다.

근우와 명우는 서로 자신의 점수를 가장 높이기 위해 최선의 전략으로 게임에 임한다. 놓여있는 카드의 개수 N과 카드가 놓여있는 상태가 주어졌을 때 근우가 얻는 점수를 구하는 프로그램을 작성하시오.

예를 들어 카드가 [4, 3, 1, 2]로 놓여있다고 하자. 근우는 처음에 4가 적힌 카드를 가져가고, 명우는 3이 적힌 카드를 가져간다. 그리고 근우는 2가 적힌 카드를 가져가고, 명우는 마지막으로 1이 적힌 카드를 가져간다. 이 때 근우와 명우는 최선의 전략으로 임했으며, 근우가 얻는 점수는 6이다.

입력

입력의 첫 줄에는 테스트케이스의 수 T가 주어진다.

각 테스트케이스 마다 첫 줄에는 카드의 개수 N(1 ≤ N ≤ 1,000)이 주어진다. 두 번째 줄에는 N개의 자연수가 공백으로 구분되어 주어지는데, i번째로 주어지는 수는 왼쪽에서 i번째에 놓인 카드에 적힌 수를 의미한다. 카드에 적혀있는 수는 1이상 10,000이하다.

출력

각 테스트케이스마다 근우와 명우가 최선의 전략으로 임할 때 근우가 얻게되는 점수를 줄로 구분하여 출력한다.

예제 입력 

2
4
1 2 5 2
9
1 1 1 1 2 2 2 2 2

예제 출력 

6
8


1. 접근


두 명이 일련의 규칙에 따라 게임을 진행한다.


단 둘다 최선의 전략을 알고 있다 가정할 때(최선의 전략이 뭔지는 아직 모른다),

선 턴인 주인공이 최대로 얻을 수 있는 점수는 몇 점일까?


계속되는 알고리즘 게임의 일종으로, 아무리 짱구를 굴려봐도 최선의 전략이 무엇일지는 감이 안오므로,

모든 경우를 계산해보는(하지만 빠른) 다이나믹 프로그래밍으로 풀어보자.



2. 풀이


dp[turn][남은 카드들 중 왼쪽 끝][남은 카드들 중 오른쪽 끝]으로 배열을 잡아보자.


턴은 번갈아 진행되며, 남은 카드들의 양 쪽 끝은 0 ~ n-1 까지다.

다이나믹 프로그래밍으로 한 번 기록된 dp배열에 접근하면 상수시간에 리턴되므로 배열 크기에 비례한 O(2*n*n)에 풀 수 있겠다.


이제 문제를 풀어줄 함수를 정의해보자.


func(int turn, int x, int y) = 턴이 turn이고, 왼쪽 끝은 x, 오른쪽 끝은 y 일 때 내가 얻을 수 있는 최대의 점수.


우리는 이제 기저사례로 x==y인 경우를 생각해 볼 수 있다.

남은 카드가 한 장이란 말로, 내 턴이였다면 점수를 취하고, 상대 턴이라면 "그냥 그랬구나" 하고 넘어가면 된다.


이제 왼쪽을 가져가든지, 오른쪽을 가져가든지 점수가 얼마나 될지 계산해봐야 한다.


내 턴이였다면 실제로 점수 계산을 위한 코드가 있어야 할테고,

상대 턴이였다면, 상대도 최선의 전략으로 플레이하기 때문에 양 쪽 플레이 중 나의 점수를 최소로 하는 플레이를 할 것이다.



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
 
#define my 0 
 
int dp[2][1001][1001];
int arr[1001];
int t, n, i;
 
int func(int turn, int x, int y) {
    if (x == y) {
        if (turn == my)
            return arr[x];
        else
            return 0;
    }
 
    int& ref = dp[turn][x][y];
    if (ref != -1)
        return ref;
 
    if (turn == my)
        ref = max(func(turn ^ 1, x + 1, y) + arr[x], func(turn ^ 1, x, y - 1+ arr[y]);
    else
        ref = min(func(turn ^ 1, x + 1, y), func(turn ^ 1, x, y - 1));
 
    return ref;
}
 
int main() {
    scanf("%d"&t);
    while (t--) {
        memset(dp, -1sizeof(dp));
 
        scanf("%d"&n);
 
        for (i = 0; i < n; ++i)
            scanf("%d"&arr[i]);
 
        printf("%d\n", func(my, 0, n - 1));
    }
 
    return 0;
}
cs


'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 10835 카드게임  (2) 2017.08.24
백준) 3032 승리(IVANA)  (0) 2017.08.23
백준) 9657 돌 게임 3  (0) 2017.08.18
백준) 9656 돌 게임 2  (0) 2017.08.17
백준) 9655 돌 게임  (0) 2017.08.17

9657. 돌 게임 3


문제

돌 게임은 두 명이서 즐기는 재밌는 게임이다.

탁자 위에 돌 N개가 있다. 상근이와 창영이는 턴을 번갈아가면서 돌을 가져가며, 돌은 1개, 3개 또는 4개 가져갈 수 있다. 마지막 돌을 가져가는 사람이 게임을 이기게 된다.

두 사람이 완벽하게 게임을 했을 때, 이기는 사람을 구하는 프로그램을 작성하시오. 게임은 상근이가 먼저 시작한다.

입력

첫째 줄에 N이 주어진다. (1 ≤ N ≤ 1000)

출력

상근이가 게임을 이기면 SK를, 창영이가 게임을 이기면 CY을 출력한다.

예제 입력 

6

예제 출력 

SK



1-1) 풀이 1


돌 게임 1(http://js1jj2sk3.tistory.com/30)과 비슷한 문제다. 4개도 가져갈 수 있다는 조건만 추가해주면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <string.h>
using namespace std;
 
#define my 1
#define your 0
#define win true
#define lose false
 
int n;
int dp[2][1001];
 
int func(int turn, int remain) {
    if (remain == 0) {
        if (turn == my)
            return dp[turn][remain] = lose;
        if (turn == your)
            return dp[turn][remain] = win;
    }
 
    if (remain < 0) {
        if (turn == my)
            return 1;
        if (turn == your)
            return 0;
    }
 
    if (dp[turn][remain] != -1)
        return dp[turn][remain];
 
    if (turn == my)
        dp[my][remain] =
        func(turn ^ 1, remain - 1| func(turn ^ 1, remain - 3| func(turn ^ 1, remain - 4);
 
    if (turn == your)
        dp[your][remain] =
        func(turn ^ 1, remain - 1& func(turn ^ 1, remain - 3& func(turn ^ 1, remain - 4);
 
    return dp[turn][remain];
}
 
int main() {
    scanf("%d"&n);
    memset(dp, -1sizeof(dp));
 
    if (func(1, n) == win)
        puts("SK");
    else
        puts("CY");
 
    return 0;
}
cs



1-2) 풀이 2 


0개라면 이미졌다.


1개면 이긴다.


2개면 진다.


3개면 이긴다.


4개면 이긴다.


까지가 기본적인 전략이고, http://js1jj2sk3.tistory.com/30 에서 설명한 추가 전략대로면,


5개면 완벽한 선택으로 상대에게 1, 3, 4 상태를 만들어 주지 않기 위해 3개를 가져가야 한다.


6개면 동일하게 4개를 가져가야 한다.


7개면 1개를 가져가든(나머지 6) 3개를 가져가든(나머지 4) 4개를 가져가든(나머지 3) 상대의 필승이다.


7개일 때부터는 이미 가져갈 수 있는 세 가지 전략에 대해 결과를 이미 알고 있다.


따라서 0개 ~ 6개의 결과가 반복될 것을 알 수 있다. (패/승/패/승/승/승/승)

'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 3032 승리(IVANA)  (0) 2017.08.23
백준) 11062 카드게임(Card Game)  (1) 2017.08.23
백준) 9656 돌 게임 2  (0) 2017.08.17
백준) 9655 돌 게임  (0) 2017.08.17
백준) 11066 파일 합치기  (6) 2017.07.09

9656. 돌 게임 2


문제

돌 게임은 두 명이서 즐기는 재밌는 게임이다.

탁자 위에 돌 N개가 있다. 상근이와 창영이는 턴을 번갈아가면서 돌을 가져가며, 돌은 1개 또는 3개 가져갈 수 있다. 마지막 돌을 가져가는 사람이 게임을 지게 된다.

두 사람이 완벽하게 게임을 했을 때, 이기는 사람을 구하는 프로그램을 작성하시오. 게임은 상근이가 먼저 시작한다.

입력

첫째 줄에 N이 주어진다. (1 ≤ N ≤ 1000)

출력

상근이가 게임을 이기면 SK를, 창영이가 게임을 이기면 CY을 출력한다.

예제 입력 

4

예제 출력 

SK




돌 게임 1(http://js1jj2sk3.tistory.com/30)의 결과만 반대로 출력해주면 되는 문제.



'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 3032 승리(IVANA)  (0) 2017.08.23
백준) 11062 카드게임(Card Game)  (1) 2017.08.23
백준) 9657 돌 게임 3  (0) 2017.08.18
백준) 9655 돌 게임  (0) 2017.08.17
백준) 11066 파일 합치기  (6) 2017.07.09

9655. 돌 게임


문제

돌 게임은 두 명이서 즐기는 재밌는 게임이다.

탁자 위에 돌 N개가 있다. 상근이와 창영이는 턴을 번갈아가면서 돌을 가져가며, 돌은 1개 또는 3개 가져갈 수 있다. 마지막 돌을 가져가는 사람이 게임을 이기게 된다.

두 사람이 완벽하게 게임을 했을 때, 이기는 사람을 구하는 프로그램을 작성하시오. 게임은 상근이가 먼저 시작한다.

입력

첫째 줄에 N이 주어진다. (1 ≤ N ≤ 1000)

출력

상근이가 게임을 이기면 SK를, 창영이가 게임을 이기면 CY을 출력한다.

예제 입력 

5

예제 출력 

SK



1. 접근


내가 상근이라면 현재 남은 돌의 갯수를 보고 어떤 선택을 내리는게 완벽한 플레이일까?


1개 남았다면 당연히 1개 가져가면 이긴다.


3개 남았다면 당연히 3개 가져가면 이긴다.


2개 남았다면 1개 가져가고 깨끗하게 지는게 매너플레이다.


4개 남았다면 무슨 짓을 해도 진다.


분명 남은 갯수와 승패에는 연관이 있는 것 같지만 일단 모든 경우를 생각해 보기로 하자.



2-1) 풀이 1


나나 상대나 두 가지 초이스를 할 수 있으므로 이차원 배열 dp[턴][남은 돌]을 정의해보자.


이 배열은 내가 이기는지 지는지 상태를 저장한다.


현재 dp[my][N] 이라면, 나는 1개나 3개를 가져간다. 턴은 상대에게 넘어가고, 그 경우에 내가 이길 수 있는 경우가

하나라도 있다면 나는 그 선택을 완벽하게 할 것이다.


반대로 현재 dp[your][N] 이라면 상대는 1개나 3개를 가져갈 수 있고, 턴은 내것이 될 것이고, 그 경우에 상대가 이길 수 있는 경우가

하나라도 있다면 상대는 그 선택을 완벽하게 할 것이다.


같은 말로, 내가 모두 이기는 경우만 남는다면 나의 승리다. 하나라도 상대가 이기는 경우가 있다면 상대의 승리다.



따라서 우리는 재귀적으로 경우를 확장시켜 보기로 하자.


dp[my][N] 부터 시작해서 dp[my][0], dp[your][0] 까지 확장시킨다.


한 케이스는 두 케이스로 분화된다. (1개를 가져가거나 3개를 가져가거나)


남은 돌이 0개인 채로 턴을 받으면  그 사람의 패배인 것이다.



2-1) 풀이 1. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <string.h>
using namespace std;
 
#define my 1
#define your 0
#define win true
#define lose false
 
int n;
int dp[2][1001];
 
int func(int turn, int remain) {
    if (remain == 0) {
        if (turn == my)
            return dp[turn][remain] = lose;
        if (turn == your)
            return dp[turn][remain] = win;
    }
 
    if (remain < 0) {
        if (turn == my)
            return 1;
        if (turn == your)
            return 0;
    }
 
    if (dp[turn][remain] != -1)
        return dp[turn][remain];
 
    if (turn == my)
        dp[my][remain] =
        func(turn ^ 1, remain - 1| func(turn ^ 1, remain - 3);
 
    if (turn == your)
        dp[your][remain] =
        func(turn ^ 1, remain - 1& func(turn ^ 1, remain - 3);
 
    return dp[turn][remain];
}
 
int main() {
    scanf("%d"&n);
    memset(dp, -1sizeof(dp));
 
    if (func(1, n) == win)
        puts("SK");
    else
        puts("CY");
 
    return 0;
}
cs


분화시키다보면 remain이 음수가 되버리는 경우가 있다(21행). 그 경우엔 논리식(31, 35행) 에 영향을 주지않는 값을 리턴해주기로 하자.



2-2) 풀이 2


베스킨라벤스 31 게임을 생각해보면 비슷한 이유로 승패가 갈린다.


우리는 초딩때의 경험으로 30, 26, 22, ... , 2를 먹으면 이긴다는 필승법을 알고 있다.


30을 먹으면 상대는 31을 먹고 질 수 밖에 없다.

30을 먹으려면 26을 먹어야 한다. 상대가 몇개를 먹건 나는 30을 먹을 수 있다.

26을 먹으려면 22을 먹어야 한다.

...


이는 한 번에 3개까지 먹을 수 있다는 게임의 특징에서 비롯된다.



마찬가지로 상대가 이길 수 없게 할려면 2개나 4개를 남겨주면 이긴다.

2개를 남기려면 내 턴에 3개나 5개여야 한다.

4개를 남기려면 내 턴에 5개나 7개여야 한다.

...



따라서 승패는 짝수 홀수에 따라 갈린다.


2-2) 풀이 2. 코드


1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
using namespace std;
 
int n;
 
int main() {
    scanf("%d"&n);
 
    n % 2 == 1 ? puts("SK") : puts("CY");
 
    return 0;
}
cs



3. 후기


이미 기록된 dp배열에 접근할 땐 기존 값을 리턴해주기로 했다. (코드의 28줄)


헌데 이 조건을 함수의 맨 처음에 두었더니 이상한 값이 리턴되었다. 먼저 기저를 작성하고 나중에 기존값 여부를 확인하자.

'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 3032 승리(IVANA)  (0) 2017.08.23
백준) 11062 카드게임(Card Game)  (1) 2017.08.23
백준) 9657 돌 게임 3  (0) 2017.08.18
백준) 9656 돌 게임 2  (0) 2017.08.17
백준) 11066 파일 합치기  (6) 2017.07.09

1. 세그먼트-트리(구간 트리)란?


세그먼트-트리(구간 트리)란 말 그대로 세그먼트(구간)를 저장하는 트리 자료구조다.


이 자료구조의 기능은 임의의 포인트를 어떤 세그먼트가 포함하고 있는지 반환해줄 수 있다.


또한 다음과 같은 구조를 갖는다.



1) 완전 이진 트리 (마지막 레벨을 제외하고 모든 노드는 자식을 두 개 갖는다.)


2) 트리의 잎들은 기본 간격들에 대응된다. 따라서 가장 좌단의 잎은 가장 처음의 기본 간격에 해당한다.


3) 내부 노드들은 기본 간격을 합친 간격에 대응된다.

따라서 두 잎을 자식으로 하는 내부 노드는, 두 잎에 해당하는 기본 간격을 합친 간격에 대응된다.



위와 같은 규칙으로 우리는 다음 기능들을 유추해 낼 수 있다.


1) n개의 간격이 있다면, 최대 4n + 1 개의 기본 간격이 있을 수 있다. 기본 간격은 잎에 대응된다.

그래서 이진트리의 잎이 4n + 1개 이므로, 높이는 O(log n)이다. 따라서 사용하는 공간은 O(n * log n)이다.


2) 루트 노드부터 1번, 왼쪽 자식은 2번, 오른쪽 자식은 3번, ... 식으로 번호를 매긴다면 다음이 성립한다.

부모 노드가 k번 이라면 왼쪽 자식은 2 * k 번, 오른쪽 자식은 2 * k + 1 번이다.

또한 n개의 기본 간격이 있다면, 총 노드 수는 2 * n - 1 개다.





2. 세그먼트-트리의 활용분야


세그먼트-트리의 강점은 높이가 log n 이라는 특징에서 비롯된다.


두 가지 질의를 하는 문제가 있다고 하자. (질의는 N번 주어진다.)


1) [from, to] 범위의 정보를 모두 구해라.

2) 기본 간격 A의 정보(a)를 b로 변경해라.


단순히 1번 질의를 O(N)으로 수행할 수도 있다.(1번 질의를 받을 때 마다 매번 구한다)

그러면 총 시간 복잡도는 O(N * N)이 될 것이다.


미리 [from, to]를 구하고 저장해둔다면 1번 질의를 상수시간에 해낼 수 있다.

하지만 2번 질의가 주어지는 순간, 미리 구해둔 정보 중 A와 관련된 모든 정보를 수정해야 한다.

따라서 최악의 경우 시간 복잡도는 동일하다.



하지만 세그먼트 트리는 위와 같은 질의를 높이에 비례해서 해낼 수 있다.


만약 1번 질의로 [1, 6]가 주어졌다고 하자.

루트는 현재 [1, 8]을 알고 있지만, 질의와 관계업는 정보도 갖고 있다. 따라서 왼쪽과 오른쪽 자식에게 세부 정보를 알아내야 한다.


왼쪽 자식(2번 노드)은 [1, 4]를 안다. 이는 질의의 일부분이다.


오른쪽 자식(3번 노드)은 [5, 8]을 안다. 하지만 질의와 겹치지 않는 부분이 있다. 다시 왼쪽, 오른쪽 자식에게 세부 정보를 알아내자.


3번 노드의 왼쪽 자식(6번 노드)은 [5, 6]을 안다. 이는 질의의 일부분이다.

3번 노드의 오른쪽 자식(7번 노드)은 [7, 8]을 안다. 이는 질의와 상관없는 정보다.


따라서 2번 노드와 6번 노드를 합치면 질의에 답할 수 있다.



2번 질의 또한 위와 유사한 과정을 통해 부모 노드들을 갱신시켜가면서 수행된다.


1번 질의와 2번 질의 모두 루트부터 시작된 트리 탐색으로 높이에 비례한 수행시간을 가진다.


따라서 세그먼트-트리의 위와 같은 질의에 대한 수행시간은 O(log n)이다.




3. 문제에 적용


정보 탐색과 갱신은 루트부터 시작된다.


탐색은 노드의 번호와 기타 정보를 통해 아래방향으로 진행된다.


하지만 갱신은 갱신 노드에 도달한 이후 윗방향으로 이루어진다. 따라서 재귀적인 코드가 직관적이다.



3-1) 백준 : 2042 구간 합 구하기 https://www.acmicpc.net/problem/2042


코드 : 


3-2) 백준 : 10868 최소값 https://www.acmicpc.net/problem/10868


코드 : 


1944. 복제 로봇 


문제

세준이는 어느 날 획기적인 로봇을 한 개 개발하였다. 그 로봇은 복제 장치를 이용하면 자기 자신을 똑같은 로봇으로 원하는 개수만큼 복제시킬 수 있다. 세준이는 어느 날 이 로봇을 테스트하기 위하여 어떤 미로에 이 로봇을 풀어 놓았다. 이 로봇의 임무는 미로에 흩어진 열쇠들을 모두 찾는 것이다. 그리고 열쇠가 있는 곳들과 로봇이 출발하는 위치에 로봇이 복제할 수 있는 장치를 장착해 두었다.

N*N의 정사각형 미로(1≤N≤50)와 M개의 흩어진 열쇠(1≤M≤250)의 위치, 그리고 이 로봇의 시작 위치가 주어져 있을 때, 모든 열쇠를 찾으면서 로봇이 움직이는 횟수의 합을 최소로 하는 프로그램을 작성하여라. 로봇은 상하좌우 네 방향으로 움직이며, 로봇이 열쇠가 있는 위치에 도달했을 때 열쇠를 찾은 것으로 한다. (복제된 로봇이어도 상관없다) 하나의 칸에 동시에 여러 개의 로봇이 위치할 수 있으며, 로봇이 한 번 지나간 자리라도 다른 로봇 또는 자기 자신이 다시 지나갈 수 있다. 복제에는 시간이 들지 않으며, 로봇이 움직이는 횟수의 합은 분열된 로봇 각각이 움직인 횟수의 총 합을 말한다. 복제된 로봇이 열쇠를 모두 찾은 후 같은 위치로 모일 필요는 없다.

입력

첫째 줄에 미로의 크기 N(1<=N<=50)과 열쇠의 개수 M(1<=M<=250) 이 공백을 사이에 두고 주어진다. 그리고 둘째 줄부터 N+1째 줄까지 미로의 정보가 주어진다. 미로는 1과 0, 그리고 S와 K로 주어진다. 1은 미로의 벽을 의미하고, 0은 지나다닐 수 있는 길, S는 로봇이 출발하는 위치, K는 열쇠의 위치가 주어진다. S는 1개, K는 M개가 주어진다. S와 K에서만 복제를 할 수 있음에 유의한다.

출력

첫째 줄에 모든 로봇이 움직인 횟수의 총 합을 출력한다. 모든 열쇠를 찾는 것이 불가능한 경우 횟수 대신 첫째 줄에 -1을 출력하면 된다.

예제 입력 

5 2
11111
1S001
10001
1K1K1
11111

예제 출력 

6



1. 접근


S에서 시작해서 K로 가야한다. K에 도착하면 원하는 수만큼 자신을 복사할 수 있으며, 최종적으로 모든 K에 도착해야한다.

이 때 가장 적은 경로의 합을 구하는 문제다.


S와 K들 사이의 경로를 그래프처럼 그려보자.


예시 입력에는 2개의 K개 있으며 (K1, K2) S에서 K1으로 가는 비용은 2, K2는 4이다.


먼저 K1으로 가기로 해보고, 도착 한뒤 자아 분열을 통해 K2로 가는 비용은 4이다.


따라서 우리는 S와 K들 사이의 최소거리를 비용으로 하는 간선들로 이루어진 그래프로 문제를 이해할 수 있다.


그러면 문제에서 원하는 해답은 그래프의 어떤 모습이겠는가?


그래프를 최소신장트리화시켜 비용의 합을 원하는 문제임을 알 수 있다.



2. 풀이


최소거리를 먼저 구해서 간선화시키는 방법이 필요할 것이다. 이에 BFS를 차용한다.


매 S, K들 마다 BFS를 돌려서 간선들을 만들고 저장한다.


BFS시에 모든 K를 방문하지 못한다면 길이 없는 것이므로 -1을 출력한다.


그 후엔 디스조인트-셋을 활용한 크루스칼 알고리즘을 사용하면 된다.


키의 번호는 정해져 있지 않으므로, 각 좌표의 키 번호를 지정하고 저장할 배열이 필요할 것이다.


이후 저장된 간선들에 대해 정렬하고 크루스칼 알고리즘을 돌리자.


참고 : http://js1jj2sk3.tistory.com/22



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <stdio.h>
#include <string.h>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
 
struct cd {
    int x, y;
};
cd start;
vector<cd> key_list;
 
struct edge {
    int weight, from, to;
};
bool cmp(const edge& e1, const edge& e2) {
    return e1.weight < e2.weight;
}
vector<edge> E;
 
int n, m, i, j, cx, cy, nx, ny, cnt, sum;
int key_cnt, key_num[51][51];
int dir[4][2= { {-1,0},{1,0},{0,-1},{0,1} };
int dist[51][51];
bool visit[51][51];
char map[51][51];
 
int parent[251];
int find(int x) {
    if (x == parent[x])
        return x;
    else
        return parent[x] = find(parent[x]);
}
void unite(int x, int y) {
    x = find(x);
    y = find(y);
    parent[x] = y;
}
 
int bfs(int x, int y, int mom) {
    memset(visit, 0sizeof(visit));
    memset(dist, 0sizeof(dist));
    key_cnt = 1;
 
    queue<cd> q;
    q.push({ x,y });
    visit[x][y] = true;
 
    while (!q.empty()) {
        cx = q.front().x;
        cy = q.front().y;
        q.pop();
 
        for (i = 0; i < 4++i) {
            nx = cx + dir[i][0];
            ny = cy + dir[i][1];
 
            if (nx < 0 || nx >= n || ny < 0 || ny >= n)
                continue;
            if (visit[nx][ny] || map[nx][ny] == '1')
                continue;
 
            dist[nx][ny] = dist[cx][cy] + 1;
            visit[nx][ny] = true;
 
            if (map[nx][ny] == 'K') {
                key_cnt++;
                E.push_back({ dist[nx][ny], mom, key_num[nx][ny] });
            }
 
            if (key_cnt == key_list.size())
                return key_cnt;
 
            q.push({ nx,ny });
        }
    }
    return key_cnt;
}
 
int main() {
    scanf("%d %d"&n, &m);
    for (i = 0; i < n; ++i) {
        scanf("%s", map[i]);
        for (j = 0; j < n; ++j) {
            if (map[i][j] == 'S') {
                start = { i,j };
                map[i][j] = 'K';
            }
            if (map[i][j] == 'K') {
                key_list.push_back({ i,j });
                key_num[i][j] = ++key_cnt;
            }
        }
    }
 
    for (auto key : key_list) {
        if (bfs(key.x, key.y, key_num[key.x][key.y]) != key_list.size()) {
            puts("-1");
            return 0;
        }
    }
 
    sort(E.begin(), E.end(), cmp);
 
    for (i = 0; i <= key_list.size(); ++i)
        parent[i] = i;
 
    i = 0;
    while (cnt != key_list.size() - 1) {
        cx = find(E[i].from);
        cy = find(E[i].to);
 
        if (cx != cy) {
            unite(cx, cy);
            sum += E[i].weight;
            cnt++;
        }
        i++;
    }
    printf("%d", sum);
    return 0;
}
cs


2887. 행성 터널


문제

때는 2040년, 이민혁은 우주에 자신만의 왕국을 만들었다. 왕국은 N개의 행성으로 이루어져 있다. 민혁이는 이 행성을 효율적으로 지배하기 위해서 행성을 연결하는 터널을 만드려고 한다.

행성은 3차원 좌표위의 한 점으로 생각하면 된다. 

두 행성 A(xA, yA, zA)와 B(xB, yB, zB)를 터널로 연결할 때 드는 비용은 min(|xA-xB|, |yA-yB|, |zA-zB|)이다.

민혁이는 터널을 총 N-1개 건설해서 모든 행성이 서로 연결되게 하려고 한다. 이 때, 모든 행성을 터널로 연결하는데 필요한 최소 비용을 구하는 프로그램을 작성하시오.

입력

첫째 줄에 행성의 개수 N이 주어진다. (1 ≤ N ≤ 100,000) 다음 N개 줄에는 각 행성의 x, y, z좌표가 주어진다. 좌표는 -109보다 크거나 같고, 109보다 작거나 같은 정수이다. 한 위치에 행성이 두 개 이상 있는 경우는 없다. 

출력

첫째 줄에 모든 행성을 터널로 연결하는데 필요한 최소 비용을 출력한다.

예제 입력 

5
11 -15 -15
14 -5 -15
-1 -1 -5
10 -4 -1
19 -4 19

예제 출력 

4


1. 접근


모든 터널을 연결하는데 필요한 최소비용을 구하는 최소신장트리 문제이다.


크루스칼 알고리즘을 사용하자.



2. 풀이


간선을 고르는게 문제다.


A터널과 B터널의 연결은 x, y, z 좌표 중에 차이가 가장 작은 축으로 이루어진다.


단순히 N^2 으로 모든 터널을 만들어 본 다음에 크루스칼을 돌려볼 수 있지만, 시간제한이 문제다.



그러면 다음으로 떠오르는 생각은 정렬을 통해 N * log n 으로 해결할 방법을 찾아보는 것이다.


정렬이후 i, i - 1 행성의 차이로 터널을 만들어 보자. 그러면 총 3 * (N - 1) 개의 터널이 생길 것이다.


이 후 크루스칼을 돌리면 된다.



왜 이 방법이 최소신장트리의 모든 간선을 놓치지 않는 걸까?


단순히 x 축만 사용한다고 생각해보자. ( x = 0, 3, 4, 8, 13, 14 ) 


정렬 이후에 이웃 행성들 끼리만 터널을 만든다면 바로 0~14 까지 일렬로 쭈욱 터널이 생긴다.

바로 최소신장트리를 만들 수 있다. 이 간선들 외에 다른 최소신장 트리를 만드는 방법은 없다.


따라서 축을 세 개 쓴다고 해도 이는 변하지 않는다.

다만 쓸데 없는 간선이 2 * (N - 1)개 추가될 뿐이다.


정답인 N - 1 개의 간선은 놓쳐지지 않고 포함된다.


참고 : http://js1jj2sk3.tistory.com/22



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include <vector>
using namespace std;
 
struct planet {
    int x, y, z, num;
};
bool cmpx(const planet& p1, const planet& p2) {
    return p1.x > p2.x;
}
bool cmpy(const planet& p1, const planet& p2) {
    return p1.y > p2.y;
}
bool cmpz(const planet& p1, const planet& p2) {
    return p1.z > p2.z;
}
planet pl[100000];
 
struct edge {
    int weight, from, to;
};
bool operator<(const edge& e1, const edge& e2) {
    return e1.weight > e2.weight;
}
vector<edge> ed;
 
 
int parent[100000];
 
int find(int x) {
    if (x == parent[x])
        return x;
    else
        return parent[x] = find(parent[x]);
}
void unite(int x, int y) {
    x = find(x);
    y = find(y);
 
    parent[x] = y;
}
 
int n, i;
 
int main() {
    scanf("%d"&n);
    for (i = 0; i < n; ++i) {
        scanf("%d %d %d"&pl[i].x, &pl[i].y, &pl[i].z);
        pl[i].num = i;
        parent[i] = i;
    }
 
    sort(pl, pl + n, cmpx);
    for (i = 0; i < n - 1++i)
        ed.push_back({ abs(pl[i].x - pl[i + 1].x), pl[i].num, pl[i + 1].num });
 
    sort(pl, pl + n, cmpy);
    for (i = 0; i < n - 1++i)
        ed.push_back({ abs(pl[i].y - pl[i + 1].y), pl[i].num, pl[i + 1].num });
 
    sort(pl, pl + n, cmpz);
    for (i = 0; i < n - 1++i)
        ed.push_back({ abs(pl[i].z - pl[i + 1].z), pl[i].num, pl[i + 1].num });
 
    sort(ed.begin(), ed.end());
 
    int ans = 0;
    int cnt = 0;
    for (int i = ed.size() - 1; cnt != n - 1;--i) {
        if (find(ed[i].from) != find(ed[i].to)) {
            cnt++;
            ans += ed[i].weight;
 
            unite(ed[i].from, ed[i].to);
        }
        if (cnt == n - 1)
            break;
    }
    printf("%d", ans);
 
    return 0;
}
cs


9372. 행성 터널


문제

상근이는 겨울방학을 맞아 N개국을 여행하면서 자아를 찾기로 마음먹었다. 

하지만 상근이는 새로운 비행기를 무서워하기 때문에, 최대한 적은 종류의 비행기를 타고 국가들을 이동하려고 한다.

이번 방학 동안의 비행 스케줄이 주어졌을 때, 상근이가 가장 적은 종류의 비행기를 타고 모든 도시들을 여행할 수 있도록 도와주자.

상근이가 한 국가에서 다른 국가로 이동할 때 다른 국가를 거쳐 가도(심지어 이미 방문한 국가라도) 된다.

입력

첫 번째 줄에는 테스트 케이스의 수 T(T ≤ 100)가 주어지고,

각 테스트 케이스마다 다음과 같은 정보가 주어진다.

  • 첫 번째 줄에는 국가의 수 N(2 ≤ N ≤ 1 000)과 비행기의 종류 M(1 ≤ M ≤ 10 000) 가 주어진다.
  • 이후 M개의 줄에 a와 b 쌍들이 입력된다. (a와 b를 왕복하는 비행기가 있다는 것을 의미한다)
  • 주어지는 비행 스케줄은 항상 연결 그래프(Connected Graph)를 이룬다.

출력

테스트 케이스마다 한 줄을 출력한다.

  • 상근이가 모든 도시를 여행하기 위해 타야 하는 비행기 종류의 최소 개수를 출력한다.

예제 입력 

2
3 3
1 2
2 3
1 3
5 4
2 1
2 3
4 3
4 5

예제 출력 

2
4



1. 접근


번역이 좀 애매하게 되어있지만, 가장 적은 종류의 비행기란 결국 최소신장트리를 구하라는 뜻과 일맥상통한다.


원문에는 가장 기장을 적게 알게 되면서 모든 도시를 방문하는 기장 수를 출력하라고 되어있다.


비행기를 처음 타면 기장과 안면을 트게 되고, 다시 타도 알던 사이이므로 상관없다. 즉 간선을 가장 적게 포함하는 트리를 만들어야 한다.

 


2. 풀이


간선의 비용을 모두 같다고 해보자. 그러면 우리가 만들어야 하는 최소신장트리다.


허나 우리는 이미 최소신장트리의 간선 수를 알고 있다. n - 1 이므로 바로 출력해주자.


참고 : http://js1jj2sk3.tistory.com/22



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
using namespace std;
 
int t, v, e, a, b, i;
 
int main() {
    scanf("%d"&t);
    while (t--) {
        scanf("%d %d"&v, &e);
        for (i = 0; i < e; ++i) {
            scanf("%d %d"&a, &b);
        }
        printf("%d\n", v - 1);
    }
    return 0;
}
cs


1647. 도시 분할 계획


문제

동물원에서 막 탈출한 원숭이 한 마리가 세상구경을 하고 있다. 그러다가 평화로운 마을에 가게 되었는데, 그곳에서는 알 수 없는 일이 벌어지고 있었다.

마을은 N개의 집과 그 집들을 연결하는 M개의 길로 이루어져 있다. 길은 어느 방향으로든지 다닐 수 있는 편리한 길이다. 그리고 각 길마다 길을 유지하는데 드는 유지비가 있다.

마을의 이장은 마을을 두 개의 분리된 마을로 분할할 계획을 가지고 있다. 마을이 너무 커서 혼자서는 관리할 수 없기 때문이다. 마을을 분할할 때는 각 분리된 마을 안에 집들이 서로 연결되도록 분할해야 한다. 각 분리된 마을 안에 있는 임의의 두 집 사이에 경로가 항상 존재해야 한다는 뜻이다.

그렇게 마을의 이장은 계획을 세우다가 마을 안에 길이 너무 많다는 생각을 하게 되었다. 일단 분리된 두 마을 사이에 있는 길들은 필요가 없으므로 없앨 수 있다. 그리고 각 분리된 마을 안에서도 임의의 두 집 사이에 경로가 항상 존재하게 하면서 길을 더 없앨 수 있다. 마을의 이장은 위 조건을 만족하도록 길들을 모두 없애고 나머지 길의 유지비의 합을 최소로 하고 싶다. 이것을 구하는 프로그램을 작성하시오.

입력

첫째 줄에 집의 개수N, 길의 개수M이 주어진다. N은 2이상 100,000이하인 정수이고, M은 1이상 1,000,000이하인 정수이다. 그 다음 줄부터 M줄에 걸쳐 길의 정보가 A B C 세 개의 정수로 주어지는데 A번 집과 B번 집을 연결하는 길의 유지비가 C라는 뜻이다.

출력

첫째 줄에 없애고 남은 길 유지비의 합의 최소값을 출력한다.

예제 입력 

7 12
1 2 3
1 3 2
3 2 1
2 5 2
3 4 4
7 3 6
5 1 5
1 6 2
6 4 1
6 5 3
4 5 3
6 7 4

예제 출력 

8



1. 접근


마을 안에 길이 너무 많아서 최소신장트리로 만들려고 한다.


단 마을을 둘로 나누고자 한다. 즉, 트리를 두개로 나누려고 하는 것이다.


최소신장트리이니 일단 크루스칼 알고리즘을 쓰기로 한다.



2. 풀이


일단 마을을 최소신장트리로 만들어보자.


그러면 길은 N - 1개일 것이다. 이 트리에서 간선을 하나 제거해서 한 집을 똑 떼어놓아 보자.


그러면 기존의 트리는 N - 2개의 길로 이루어졌고, N - 1개를 포함하는 최소신장트리이다.


당연히 떨어진 집은 자체로도 마을이고 최소신장트리이다.


그러면 제거할 간선의 기준은 어떻게 되겠는가? 트리 중 가장 큰 간선이다.


이 간선은 또한 크루스칼 알고리즘의 가장 마지막에 합류한 간선이다.


따라서 길을 N - 2개 까지만 크루스칼 알고리즘을 진행해도 같은 결과를 얻을 수 있다.



마을을 두개로 나누는 방법은 여러가지이지만, 간선의 수 (N-2) 는 동일하다.


따라서 가장 큰 간선을 제거하는 위의 방법이 최소임은 보장된다.


참고 : http://js1jj2sk3.tistory.com/22



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <algorithm>
using namespace std;
 
struct edge {
    int weight, from, to;
};
bool operator<(const edge& e1, const edge& e2) {
    return e1.weight < e2.weight;
}
edge E[1000001];
 
int v, e, a, b, c, i, cnt, sum;
int parent[100001];
 
int find(int x) {
    if (x == parent[x])
        return x;
    else
        return parent[x] = find(parent[x]);
}
 
void unite(int x, int y) {
    x = parent[x];
    y = parent[y];
    parent[x] = y;
}
 
int main() {
    scanf("%d %d"&v, &e);
    for (i = 0; i < e; ++i) {
        scanf("%d %d %d"&a, &b, &c);
        E[i] = { c,a,b };
    }
    for (i = 1; i <= v; ++i)
        parent[i] = i;
    sort(E, E + e);
 
    i = 0;
    while (cnt != v - 2) {
        a = E[i].from;
        b = E[i].to;
        if (find(a) != find(b)) {
            unite(a, b);
            cnt++;
            sum += E[i].weight;
        }
        i++;
    }
    printf("%d", sum);
    return 0;
}
cs


1922. 네트워크 연결


문제

도현이는 컴퓨터와 컴퓨터를 모두 연결하는 네트워크를 구축하려 한다. 하지만 아쉽게도 허브가 있지 않아 컴퓨터와 컴퓨터를 직접 연결하여야 한다. 그런데 모두가 자료를 공유하기 위해서는 모든 컴퓨터가 연결이 되어 있어야 한다. (a와 b가 연결이 되어 있다는 말은 a에서 b로의 경로가 존재한다는 것을 의미한다. a에서 b를 연결하는 선이 있고, b와 c를 연결하는 선이 있으면 a와 c는 연결이 되어 있다.)

그런데 이왕이면 컴퓨터를 연결하는 비용을 최소로 하여야 컴퓨터를 연결하는 비용 외에 다른 곳에 돈을 더 쓸 수 있을 것이다. 이제 각 컴퓨터를 연결하는데 필요한 비용이 주어졌을 때 모든 컴퓨터를 연결하는데 필요한 최소비용을 출력하라. 모든 컴퓨터를 연결할 수 없는 경우는 없다.

입력

첫째 줄에 컴퓨터의 수(1<=N<=1000)가 주어진다.

둘째 줄에는 연결할 수 있는 선의 수(1<=M<=100,000)가 주어진다.

셋째 줄부터 M+2번째 줄까지 총 M개의 줄에 각 컴퓨터를 연결하는데 드는 비용이 주어진다. 이 비용의 정보는 세 개의 정수로 주어지는데, 만약에 a b c 가 주어져 있다고 하면 a컴퓨터와 b컴퓨터를 연결하는데 비용이 c만큼 든다는 것을 의미한다.

출력

모든 컴퓨터를 연결하는데 필요한 최소비용을 첫째 줄에 출력한다.

예제 입력 

6
9
1 2 5
1 3 4
2 3 2
2 4 7
3 4 6
3 5 11
4 5 3
4 6 8
5 6 8

예제 출력 

23



1. 접근


최소신장트리를 구해야 한다.


http://js1jj2sk3.tistory.com/23와 동일하다.


크루스칼 알고리즘을 사용하자.



2. 풀이


사이클의 유무를 확인하는데 디스조인트-셋을 사용한다.


참고 : http://js1jj2sk3.tistory.com/22



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <algorithm>
using namespace std;
 
struct edge {
    int weight, from, to;
};
bool operator<(const edge& e1, const edge& e2) {
    return e1.weight < e2.weight;
}
edge E[100001];
 
int v, e, a, b, c, i, cnt, sum;
int parent[1001];
 
int find(int x) {
    if (x == parent[x])
        return x;
    else
        return parent[x] = find(parent[x]);
}
 
void unite(int x, int y) {
    x = parent[x];
    y = parent[y];
    parent[x] = y;
}
 
int main() {
    scanf("%d %d"&v, &e);
    for (i = 0; i < e; ++i) {
        scanf("%d %d %d"&a, &b, &c);
        E[i] = { c,a,b };
    }
    for (i = 1; i <= v; ++i)
        parent[i] = i;
    sort(E, E + e);
 
    i = 0;
    while (cnt != v - 1) {
        a = E[i].from;
        b = E[i].to;
        if (find(a) != find(b)) {
            unite(a, b);
            cnt++;
            sum += E[i].weight;
        }
        i++;
    }
    printf("%d", sum);
    return 0;
}
cs


+ Recent posts