[editorial] Google Code Jam Round 3 풀이 [업데이트 완료]

  • astein
    astein

    아무도 안올리셔서 제가 씁니다 :)

    A. How Big Are the Pockets?
    [문제 설명]
    Polygonovich 교수는 평면 상의 위치에서 랜덤하게 걸어다니는것을 즐긴다. 교수는 아침에 원점에서 북쪽을 바라보며 긴 여행을 떠난다. 교수는 직진하거나 왼쪽으로 90도 회전, 오른쪽으로 90도 회전만 하면서 여행을 즐긴다. 이 여행동안 한번 방문했던 점을 두번 다시 방문하지 않으며, 해가 지기 전에 시작했던 위치로 돌아온다. 따라서 교수가 지나간 경로는 연결하면 하나의 도형을 만들게 된다. (아래 그림 참조)
    a1.jpg
    교수가 한 바퀴 돌고 온 구간이 위의 그림과 같다고 하자. 당신은 이 구간이 convex가 아니라는 것을 깨달았다. 따라서 아래와 같이 "pocket" 을 채워서 convex로 만들려고 한다.
    a2.jpg
    아래의 둘 중 하나를 만족하면 "pocket" 이라고 부른다.
    1) 한 점을 기준으로 위-아래가 막혀있으면 이 점은 pocket이다.
    2) 한 점을 기준으로 왼쪽-오른쪽이 막혀있으면 이 점은 pocket이다.
    따라서 첫 번째 그림에서 x는 2)를, y는 1), 2)를, z)는 1)을 만족하므로 이 세 점은 pocket이고, w는 둘 다 만족하지 않으므로 pocket이 아니다.
    교수가 이동한 경로가 입력으로 주어질 때, "pocket"의 크기를 출력하는 프로그램을 작성하시오.
    Small Data Set) 각 좌표의 절대값은 100 이하
    Large Data Set) 각 좌표의 절대값은 3000 이하
    [spoiler="풀이 보러가기"][문제 풀이]
    Small Data Set의 경우에는 좌표의 절대값이 작기 때문에, 배열로 표현할 수 있습니다. 아래의 그림을 살펴보도록 하지요.
    a3.jpg
    위와 같이 좌표를 변형합니다. 즉, 모든 좌표를 2배해서 점선부분까지 배열에 채우는 것이지요. 이렇게 2배씩 해주는 이유는, 내부/외부의 점을 쉽게 체크하기 위해서 입니다. 이렇게 해서 내부/외부를 나눴다면 문제에서 정의한 pocket의 수를 찾으면 해결할 수 있습니다. 내부가 아닌 모든 점에 대하여 현재 점이 pocket인지 찾으면 됩니다. 케이스 하나에 대하여 최대 400 * 400 * 400 정도의 연산이면 답을 구할 수 있지요. 소스코드에 간단한 주석을 달아두었습니다 :D
    하지만 Large Data Set은 좌표의 절대값이 너무 크지요. 따라서 각 점에 대하여 직접 pocket을 구하는 위의 알고리즘으로는 무리가 있습니다. 따라서 현재 점이 pocket인지 아닌지를 빨리 계산하면 되지요. 임의의 어떤 점에서 그 점의 왼쪽에 polygon 내부점이 존재하는가? 오른쪽에 내부점이 존재하는가? 위쪽은? 아래쪽은? 이라는 4가지의 질문을 바로바로 대답할 수 있다면 pocket인지 아닌지를 알 수 있지요.
    만약 (x, a)라는 점이 내부점이고, (x, b)라는 점이 내부점이라면, a <= c <= b를 만족하는 모든 c에 대하여 (x, c)가 내부점이나 pocket이 된다는 점을 알 수 있습니다. 이는 90도 회전하여 y좌표에 대해서도 마찬가지이지요. 즉 모든 좌표에 대하여 현재 점이 내부점이냐? pocket이냐? 둘 다 아니냐?를 O(1)만에 계산할 수 있게 된 셈입니다.
    Large Data Set은 직접 테이블로 표현할 수 없습니다. 하지만 모든 점에 대하여 이 점이 내부점 or pocket인지를 구할 수 있고요. 입력받은 다각형이 있을 때 내부 넓이는 따로 구해야 합니다. 다각형의 내부넓이를 구하는 알고리즘은 검색하면 금방 나올테니 여기에다가 쓰진 않겠습니다 :)
    [/spoiler]
    [spoiler="소스코드 보러가기(Small)"]~~~ cpp

    #include
    #include
    #include
    #include
    #include
    #include
    using namespace std;
    int dx[4] = {-1, 0, 1, 0};
    int dy[4] = {0, 1, 0, -1};
    int table[420][420];
    void fill(int x, int y) {
    table[x][y] = 1;
    queue > Q;
    Q.push(make_pair(x, y));
    while (!Q.empty()) {
    pair now = Q.front(), next; Q.pop();

    for (int i = 0; i < 4; ++i) {
    next.first = now.first + dx[i];
    next.second = now.second + dy[i];
    if (next.first < 0 || next.first == 420) continue;
    if (next.second < 0 || next.second == 420) continue;
    if (table[next.first][next.second] == 0) {
    table[next.first][next.second] = 1;
    Q.push(next);
    }
    }
    }
    }
    int main() {
    int T;
    cin >> T;
    for (int cn = 1; cn <= T; ++cn) {
    printf("Case #%d: ", cn);
    int L, N, head = 0, x = 0, y = 0;
    vector > P;
    string S;
    cin >> L;
    P.push_back(make_pair(x, y));
    for (int i = 0; i < L; ++i) {
    cin >> S >> N;
    for (int j = 0; j < N; ++j) {
    for (int k = 0; k < S.size(); ++k) {
    if (S[k] == 'F') {
    x += dx[head];
    y += dy[head];
    P.push_back(make_pair(x, y));
    }
    if (S[k] == 'R') head = (head + 1) % 4;
    if (S[k] == 'L') head = (head + 3) % 4;
    }
    }
    }
    int ret = 0;
    for (int i = 0; i < P.size(); ++i) {
    P[i].first += 105;
    P[i].second += 105;
    }
    memset(table, 0, sizeof(table));
    for (int i = 0; i < P.size() - 1; ++i) {
    table[P[i].first * 2][P[i].second * 2] = 1;
    table[P[i].first + P[i + 1].first][P[i].second + P[i + 1].second] = 1;
    }
    fill(0, 0);
    for (int i = 1; i < 420; i += 2) {
    for (int j = 1; j < 420; j += 2)
    if (table[i][j] == 1) { // 바깥쪽인 경우
    int ct = 0;
    for (int k = j - 1; k >= 0; --k) if (table[i][k] == 0) { ct++; break; }
    for (int k = j + 1; k < 420; ++k) if (table[i][k] == 0) { ct++; break; }
    for (int k = i - 1; k >= 0; --k) if (table[k][j] == 0) { ct++; break; }
    for (int k = i + 1; k < 420; ++k) if (table[k][j] == 0) { ct++; break; }
    if (ct >= 3) ret++; // 상하좌우로 반직선을 그렸는데 세 점이상 만났다는 말은 pocket 이라는 뜻
    }
    }
    cout << ret << endl;
    }
    }

    [/spoiler]
    [spoiler="소스코드 보러가기(Large)"]~~~ cpp
    
    #include <cstdio>
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <string>
    #include <queue>
    using namespace std;
    int dx[4] = {-1, 0, 1, 0};
    int dy[4] = {0, 1, 0, -1};
    // 넓이 구하는 루틴 
    int getArea(vector <pair<int, int> > P) {
        long long ret = 0;
        for (int i = 0; i < P.size() - 1; ++i) {
            ret += P[i].first * P[i + 1].second - P[i].second * P[i + 1].first;
        }
        ret /= 2;
        return (ret > 0) ? ret : -ret;
    }
    int main() {
        int T;
        cin >> T;
        for (int cn = 1; cn <= T; ++cn) {
            printf("Case #%d: ", cn);
            int L, N, head = 0, x = 0, y = 0;
            string S;
            vector <pair<int, int> > P;
            cin >> L;
            P.push_back(make_pair(x, y));
            for (int i = 0; i < L; ++i) {
                cin >> S >> N;
                for (int j = 0; j < N; ++j) {
                    for (int k = 0; k < S.size(); ++k) {
                        if (S[k] == 'F') {
                            x += dx[head];
                            y += dy[head];
                            P.push_back(make_pair(x, y));
                        }
                        if (S[k] == 'R') head = (head + 1) % 4;
                        if (S[k] == 'L') head = (head + 3) % 4;
                    }
                }
            }
            int ret = -getArea(P); // 입력받은 도형의 내부점은 결과에서 빼줘야 하므로 초기값을 -(넓이)로 둡니다.
            int minx[6001], miny[6001], maxx[6001], maxy[6001];
            for (int i = 0; i <= 6000; ++i) {
                minx[i] = miny[i] = 99999;
                maxx[i] = maxy[i] = -99999;
            }
            for (int i = 0; i < P.size() - 1; ++i) { 
                if (P[i].first == P[i + 1].first) {
                    miny[3000 + min(P[i].second, P[i + 1].second)] <?= 3000 + P[i].first;
                    maxy[3000 + min(P[i].second, P[i + 1].second)] >?= 3000 + P[i].first;
                } else {
                    minx[3000 + min(P[i].first, P[i + 1].first)] <?= 3000 + P[i].second;
                    maxx[3000 + min(P[i].first, P[i + 1].first)] >?= 3000 + P[i].second;
                }
            }
            for (int i = 0; i <= 6000; ++i) {
                for (int j = 0; j <= 6000; ++j) {
                    int ct = 0;
                    if (miny[j] <= i) ct++;
                    if (i < maxy[j]) ct++;
                    if (minx[i] <= j) ct++;
                    if (j < maxx[i]) ct++;
                    if (ct >= 3) ret++; // 세 점 이상 만나면 pocket 혹은 내부점
                }
            }
            cout << ret << endl;
        }
    }
    
    ~~~[/spoiler]
    
    
    
      B. Portal
       [문제 설명]
      Portal은 1인칭 퍼즐 게임으로, 이 게임의 주인공은 벽에 두 개의 포탈을 만들어서, 한쪽 포탈에 들어가면 반대쪽 포탈로 나올 수 있다. 이 문제는 비슷한 아이디어에서 출발한다.
      (R, C) 크기의 격자판이 있다. 당신은 이 격자판 어딘가에 위치해 있고, 또 다른 위치에는 맛있는 케이크가 놓여져 있다. 당신은 매우 배가 고프기 때문에 최대한 빨리 이 케이크를 먹고 싶어한다. 당신은 현재 위치에서 동서남북 네 방향으로 이동할 수 있으며, 당신에게는 Portal Gun이 있어 포탈을 만들어서 이동할 수도 있다.
      당신을 Portal Gun을 이용하여 두 가지 포탈 (노란색, 파란색)을 만들 수 있으며, 무조건 직진하는 성질을 가지고 있다. 또한 케이크가 있더라도 그대로 투과하는 성질을 가지고 있다. 다만 Portal Gun은 동서남북 네 방향으로만 발사할 수 있다.
      노란색과 파란색 포탈을 만들었다면, 당신은 이 포탈을 통해 이동할 수 있다. 노란색 포탈로 들어갔다면 파란색 포탈로 나오게 되며, 그 반대의 경우도 성립한다. 당신은 Portal Gun을 사용함으로써 더 빨리 케이크를 먹을 수 있게 될 수 있을 것이다! 
       아래의 그림을 살펴보자. 
    <IMG alt=b1.jpg src="/files/attach/images/53443/797/048/b1.jpg" editor_component="image_link">
      회색 칸은 벽을 나타내고 흰색 칸은 아무것도 없는 빈 공간을 나타낸다. 또한 빨간색 점은 당신의 위치이다.
    <IMG alt=b2.jpg src="/files/attach/images/53443/797/048/b2.jpg" editor_component="image_link">
      오른쪽을 향해 파란색 포탈 건을 발사하면 위의 그림과 같이 파란색 포탈이 생긴다.
    <IMG alt=b3.jpg src="/files/attach/images/53443/797/048/b3.jpg" editor_component="image_link">
      아래쪽을 향해 노란색 포탈 건을 발사한 후의 상태이다.
    <IMG alt=b4.jpg src="/files/attach/images/53443/797/048/b4.jpg" editor_component="image_link">
       남쪽으로 한번 이동하였다.
    <IMG alt=b5.jpg src="/files/attach/images/53443/797/048/b5.jpg" editor_component="image_link">
      한번 더 남쪽으로 이동하였다. 아래쪽의 노란색 포탈을 통해 파란색 포탈로 순간이동하게 되었다.
    <IMG alt=b6.jpg src="/files/attach/images/53443/797/048/b6.jpg" editor_component="image_link">
      똑같은 색깔의 포탈은 최대 1개만 존재한다. 현재 위치에서 파란색 포탈을 왼쪽으로 쏘면 위의 그림과 같이 된다.
      포탈은 무조건 벽에만 만들 수 있으며, 포탈이 없는 벽으로는 이동할 수 없다. 포탈 건을 사용하는 것은 이동횟수로 세지 않는다고 가정한다. 케이크를 먹을 수 있는 최소 이동횟수를 구하시오.
      Small Data Set) 최대 Grid 크기는 8 x 8
      Large Data Set) 최대 Grid 크기는 15 x 15
    [spoiler="풀이 보러가기"][문제 풀이]
      다른 문제들이 수학적인 능력을 많이 요구했다면, B번 문제는 코딩실력을 요구했다고 볼 수 있겠습니다. 문제 자체는 상당히 Clear한 문제이고, 많이 알려진 유형이기 때문이지요. 너비-우선탐색(BFS)을 사용하여 이동하면서 모든 상태를 검사해보면 됩니다. Large Data Set이라고 해도 생각보다 많지 않기 때문이지요. 각 상태를 어떻게 표현하는지는 각자의 취향에 따라 달라지겠지만, 저는 아래와 같이 표현하였습니다.
    ~~~ cpp
    
      struct state {
          int x, y; // 현재 위치
          int bx[2], by[2]; // 포탈이 열린 위치. (문제에서는 벽에서만 포탈이 생긴다고 했지만, 사실 어떤 셀에서 한칸 이동하면 다른칸에서 나온다-고 봐도 같기 때문이지요.)
      }

    또한 너비우선탐색은 초기상태에서 시작하여 점점 깊이가 깊어지도록 하는 것이 핵심입니다. 이를 priority_queue로 두고 할 수도 있겠지만, deque를 사용하여 접근하였습니다. deque를 사용하면 현재 리스트에 있는 상태들의 깊이 차이가 최대 1이라는 것을 보장할 수 있습니다. 현재 위치에서 포탈건을 발사했으면 이동횟수는 증가하지 않으므로 deque에 push_front를, 한칸 이동한 경우는 push_back을 해주면 되기 때문이지요. 현재 레벨에서의 모든 상태를 검색한 다음에야 다음 레벨로 넘어가게 될 것이고, 이전 상태의 deque에 남아있지 않게 되니까요.
    또한 현재 상태를 이전에 지나간 적이 있는가? 를 map을 사용하여 check하였습니다. 6차원 배열을 잡아도 되지만 좀 귀찮아서요 [...] B번 문제는 문제풀이보다는 소스코드를 보는게 낫지 않을까...라고 생각하고 있습니다 :) 결국 구현의 문제니까요. [/spoiler]
    [spoiler="소스코드 보러가기"]~~~ cpp

    #include
    #include
    #include
    #include
    #include
    #include
    using namespace std;
    int n, m;
    vector a;
    struct state {
    int x, y, bx[2], by[2];
    state() : x(0), y(0) {
    for (int i = 0; i < 2; ++i) bx[i] = by[i] = 0;
    }
    state(int a, int b, int c, int d, int e, int f) : x(a), y(b) {
    bx[0] = c, by[0] = d;
    bx[1] = e, by[1] = e;
    }
    bool operator < (const state &a) const {
    if (x != a.x) return x < a.x;
    if (y != a.y) return y < a.y;
    for (int i = 0; i < 2; ++i) {
    if (bx[i] != a.bx[i]) return bx[i] < a.bx[i];
    if (by[i] != a.by[i]) return by[i] < a.by[i];
    }
    return false;
    };
    };
    int dx[4] = {-1, 0, 1, 0};
    int dy[4] = {0, -1, 0, 1};
    pair go(int x, int y, int dir) {
    while (true) {
    x += dx[dir];
    y += dy[dir];
    if (a[x][y] == '#') return make_pair(x - dx[dir], y - dy[dir]);
    }
    return make_pair(-1, -1);
    }
    int main() {
    int T;
    cin >> T;
    for (int cn = 1; cn <= T; ++cn) {
    cin >> n >> m;
    a.resize(n + 2);
    a[0] = string(m + 2, '#');
    for (int i = 1; i <= n; ++i) {
    cin >> a[i];
    a[i] = '#' + a[i] + '#';
    }
    a[n + 1] = string(m + 2, '#');
    n += 2, m += 2;
    int sx, sy;
    for (int i = 0; i < n; ++i)
    for (int j = 0; j < m; ++j)
    if (a[i][j] == 'O') {
    sx = i;
    sy = j;
    }
    deque deq;
    map table;
    table[state(sx, sy, 0, 0, 0, 0)] = 0;
    deq.push_back(state(sx, sy, 0, 0, 0, 0));
    int ret = -1;
    while (!deq.empty()) {
    state now = deq.front(), next;
    int dpt = table[now];
    if (a[now.x][now.y] == 'X') {
    ret = table[now];
    break;
    }
    deq.pop_front();
    // i 방향 에 노란색 포탈건을, j 방향으로 파란색 포탈건을 발사. i, j 값이 4라면 발사하지 않음.
    for (int i = 0; i < 5; ++i) {
    pair tmp1;
    if (i == 4) tmp1 = make_pair(now.bx[0], now.by[0]);
    else tmp1 = go(now.x, now.y, i);
    for (int j = 0; j < 5; ++j) {
    pair tmp2;
    if (j == 4) tmp2 = make_pair(now.bx[1], now.by[1]);
    else tmp2 = go(now.x, now.y, j);
    next = now;
    next.bx[0] = tmp1.first, next.by[0] = tmp1.second;
    next.bx[1] = tmp2.first, next.by[1] = tmp2.second;
    if (table.count(next) != 1) {
    table[next] = dpt;
    deq.push_front(next);
    }
    }
    }
    // i방향으로 이동
    for (int i = 0; i < 4; ++i) {
    next = now;
    next.x += dx[i], next.y += dy[i];
    if (a[next.x][next.y] != '#' && table.count(next) != 1) {
    table[next] = dpt + 1;
    deq.push_back(next);
    }
    }
    // 주변 벽에 포탈이 있으면 포탈로 진입 -> 다음 위치는 반대편 포탈이 있는 곳.
    for (int i = 0; i < 2; ++i) {
    if (now.bx[0] == now.x && now.by[0] == now.y) {
    next = now;
    next.x = now.bx[1 - i];
    next.y = now.by[1 - i];
    if (a[next.x][next.y] != '#' && table.count(next) != 1) {
    table[next] = dpt + 1;
    deq.push_back(next);
    }
    }
    }
    }
    printf("Case #%d: ", cn);
    if (ret == -1) printf("THE CAKE IS A LIE\n"); else printf("%d\n", ret);
    }
    }

    # 지정된 언어 [/spoiler]를 찾을 수 없습니다.
    C. No Cheating [문제 설명] 기말고사를 앞두고 있는 알고스팟 고등학교에서는 몇 명의 학생이 수시로 치팅을 시도한다는 정보를 입수했다. 그래서 이 학교의 교장인 JM은 이러한 부정행위를 방지하는 자리배치를 하려고 한다. 이 클래스룸은 M개의 행 N개의 열로 구성되어 있으며 각각의 칸에 책상들이 배치되어 있다. 다만 일부 책상은 파손되어 배치할 수 없게 되어 있다. 각 학생은 자기 자리로부터 왼쪽, 오른쪽, 왼쪽 앞, 오른쪽 앞 자리의 시험지를 볼 수 있다. 따라서 어떤 자리에 학생을 위치시켰다면 A,C,D,E 위치에는 배치할 수 없다. <IMG alt=c1.jpg src="/files/attach/images/53443/797/048/c1.jpg" editor_component="image_link"> 한 교실의 배치도가 주어져 있을 때, 이 교실에서 최대 몇 명의 학생이 시험을 볼 수 있는지 구하시오. 물론 모든 학생이 치팅할 수 없는 위치에 있어야 한다. Small Data Set) 최대 교실 크기는 10 x 10 Large Data Set) 최대 교실 크기는 80 x 80 [spoiler="풀이 보러가기"] [문제 풀이] Small은 다이나믹으로도 해결할 수 있습니다. 각 행의 상태를 2진수로 표현하여 모든 경우의 상태를 저장할 수 있거든요. 그렇게 하여 각 라인별로 어떤 상태일때 최대 몇 명의 학생을 배치할 수 있는가? 를 저장하면서 진행해 나가는 방식입니다. 두번째 줄에 1000100 이라는 상태의 최대값을 구한다고 가정해 봅시다. 1에다가 학생을 배치한다고 보면 됩니다. 이 경우에는 첫번째 줄의 1,2,4,5,6 칸에 학생이 배치되어 있으면 Cheating을 할 수 있게 되지요. 즉 1,2,4,5,6이 항상 비어있는 모든 경우의 최대값 + 2 라는 값이 되는 것이지요. (두번째 줄에 2명 배치했으니까요 :) Small이 다이나믹인 반면 Large는 매칭으로 해결할 수 있습니다. 이를 그래프로 나타내면 항상 Bipartite Graph가 나오게 되기 때문이지요. 각 Column별로 나눠서 생각을 해보면 알 수 있는데, 홀수번째 Column을 왼쪽에, 짝수번째 Column을 오른쪽에 배치하면 Bipartite Graph의 형태를 가지게 됨을 알 수 있습니다. 그러면 이제 이 문제는 König's Theorem 을 사용하면 (우리가 원하는 답) = (배치할 수 있는 모든 경우의 수) - (Bipartite Graph에서의 Maximum Matching Number) 가 됨을 알 수 있습니다. 저도 자세한 증명과정은 모르기 때문에 길게 쓰기 힘드네요 : ( 대신 이 문제와 비슷한 형태로 Matching을 사용할 수 있는 문제를 링크걸어 두겠습니다. (비슷한 문제(PKU 3398)) [/spoiler] [spoiler="소스코드 보러가기(Small)"]~~~ cpp #include <cstdio> #include <iostream> #include <vector> #include <string> using namespace std; int table[11][1024]; int count(int a) { // a에 1이 몇개 들어가는지 세는 루틴 int ret = 0; while (a) { ret += a & 1; a >>= 1; } return ret; } int main() { int T; cin >> T; for (int cn = 1; cn <= T; ++cn) { int n, m; cin >> n >> m; vector <string> a(n); for (int i = 0; i < n; ++i) cin >> a[i]; memset(table, -1, sizeof(table)); for (int i = 0; i < n; ++i) { for (int j = 0; j < (1 << m); ++j) { bool ispos = true; for (int k = 0; k < m; ++k) if ((j & (1 << k)) && a[i][k] == 'x') ispos = false; for (int k = 0; k < m - 1; ++k) if ((j & (1 << k)) && (j & (1 << (k + 1)))) ispos = false; if (!ispos) continue; int isposnum = 0; // 이전 레벨에서 1을 배치할 수 없는 장소를 체크합니다. for (int k = 0; k < m; ++k) { if (j & (1 << k)) { if (k != 0) isposnum |= (1 << (k - 1)); if (k != m - 1) isposnum |= (1 << (k + 1)); } } if (i == 0) { table[i][j] = count(j); } else { int ret = 0; for (int k = 0; k < (1 << m); ++k) { if (isposnum & k) continue; // Cheating 가능하면 패스 ret >?= table[i - 1][k]; // ret = max(ret, table[i - 1][k]) } table[i][j] = ret + count(j); } } } int ret = 0; for (int i = 0; i < (1 << m); ++i) ret >?= table[n - 1][i]; printf("Case #%d: %d\n", cn, ret); } }

    [/spoiler]
    [spoiler="소스코드 보러가기(Large)"]~~~ cpp

    #include
    #include
    #include
    #include
    using namespace std;
    /*********************************************** *

    • Maximum Matching - from Davidoff TeamNotebook *
    • ***********************************************/ #define MAXN 3200 #define MAXM 3200 int match1[MAXN]; int match2[MAXM]; char check[MAXM]; int N, M; vector > graph; bool extendMatch(int a) { for (int i = 0 ; i < graph[a].size() ; i++) { int b = graph[a][i]; if (check[b]) continue; if (match2[b] == -1) { match1[a] = b; match2[b] = a; return true; } } for (int i = 0 ; i < graph[a].size() ; i++) { int b = graph[a][i]; if (check[b]) continue; check[b] = 1; if (extendMatch(match2[b])) { match1[a] = b; match2[b] = a; return true; } } return false; } int maxMatch() { memset(match1, -1, N*sizeof(int)); memset(match2, -1, M*sizeof(int)); int cnt = 0; for (int i = 0 ; i < N ; i++) { memset(check, 0, M); if (extendMatch(i)) cnt++; } return cnt; } int main() { int T; cin >> T; for (int cn = 1; cn <= T; ++cn) { int n, m; cin >> n >> m; vector a(n); int ret = 0; for (int i = 0; i < n; ++i) { cin >> a[i]; for (int j = 0; j < m; ++j) ret += (a[i][j] == '.'); } N = n * ((m + 1) / 2); M = n * m - N; graph.assign(N, vector ()); // construct bipartite graph for (int i = 0; i < m; i += 2) { for (int j = 0; j < n; ++j) { for (int k = -1; k <= 1; k += 2) { for (int l = -1; l <= 1; ++l) { if (j + l < 0 || j + l == n) continue; if (i + k < 0 || i + k == m) continue; if (a[j][i] == '.' && a[j + l][i + k] == '.') { graph[(i / 2) * n + j].push_back( ((i + k) / 2) * n + (j + l)); } } } } } printf("Case #%d: %d\n", cn, ret-maxMatch()); } }
    # 지정된 언어 [/spoiler]를 찾을 수 없습니다.
    D. Endless Knight [문제 설명] 체스의 Knight는 L자 모양으로 점프할 수 있는 말이다. (r1, c1)에서 (r2, c2)로 점프할 수 있다는 뜻은 (r1 - r2)^2 + (c1 - c2)^2 = 5를 만족하는 것과 같은 의미를 가진다. 이 문제에서는 왼쪽 위 좌표(1, 1)에서 출발하여 (H, W)까지 이동하는 것이 목표이다. 또한 이 문제에서 적용되는 두 가지 제약조건이 있는데, 첫 번째 조건은 Knight는 반드시 x,y 좌표가 증가하는 방향으로만 이동하여야 한다. 즉 (1, 2)로 이동하거나 (2, 1)로 이동하여야 한다. [ (1, -2)나 (2, -1)은 잘못된 이동이다. ] 두 번째 조건은 Knight가 절대로 갈 수 없는 바위가 R개(R <= 10) 있다는 점이다. 당신은 (1, 1)에서 (H, W)까지 가는 경로의 수를 구하고 싶어한다. 다만 이 수는 매우 크기 때문에 10007로 나눈 나머지를 구하려고 한다. 참고로 10007은 소수(Prime Number)이다. Small Data Set) H, W <= 100 Large Data Set) H, W <= 100,000,000 [spoiler="풀이 보러가기"][문제 풀이] Small Data Set는 참가자가 제일 많이 풀어서 제출한만큼 동적계획법의 기본적인 개념을 이해하고 있다면 쉽게 풀 수 있습니다. > Table[i,j] = (i, j) 위치에 올 수 있는 경우의 수를 10007로 나눈 나머지. Table[1,1] = 1을 초기값으로 두고, 위의 테이블 정의에 따라 채워나가면 됩니다. Table[i, j] = (Table[i - 2][j - 1] + Table[i - 1][j - 2]) % 10007이 되지요. 만약 (i, j)값이 입력에서 주어진 바위가 있는 위치라면 Table[i, j] = 0으로 두면 됩니다. Large Data Set은 H,W 가 너무 커서 테이블을 그릴수가 없겠네요. 하지만 여기에서 R이 작다는 사실을 이용하여 포함-배제 원리를 사용합니다. 바위 A, B가 있을때 우리가 구하고자 하는 (A, B를 한번도 지나지 않는 경우의 수) = (모든 경우의 수) - (A를 반드시 지나는 경우의 수) - (B를 반드시 지나는 경우의 수) + (A와 B를 모두 지나는 경우의 수) 가 되니까요. 이는 여러개일때도 같은 방법으로 접근할 수가 있지요. 그러면 (1, 1)에서 임의의 점 (X, Y)까지 가는 경우의 수를 구해야 합니다. Move A : (1, 2)로 이동. Move B : (2, 1)로 이동이라고 한다면, (X, Y)가 결정되면 A로 몇번 B로 몇번 이동해야 도착할 수 있는지 알 수 있지요. 예를 들어 X = 5, Y = 6이라면 A로 2번 B로 1번 이동하면 갈 수 있다는 식으로요. 그러면 전체 3번 이동중에 A가 2개 이므로 Combination 개념으로 넘어가게 됩니다. 즉, 임의의 두 점을 지나기 위해서 몇번인지는 Combination을 구하는 것과 같다는 것을 알 수 있습니다. 이제 큰 Combination을 어떻게 구하는지의 문제로 넘어가게 됩니다. 여기에서 중요한 사실을 빠뜨릴 뻔 했군요. 우리는 10007로 나눈 나머지를 구하려고 합니다. 즉 nCk가 10007의 배수라면 나머지는 0이 되는 셈이지요. n! 의 10007의 계수가 k! * (n-k)! 의 10007의 계수보다 크다면 nCk가 10007의 배수가 되는 것입니다. 그러면 이제 10007의 배수가 아닐 경우의 nCk를 10007로 나눈 나머지만 구하면 됩니다. n! = 1 * 2 * 3 * ... * n [좀 더 풀어서 나타내면] = (1 * 2 * 3 * ... * 10006) * 10007 * (10008 * 10009 * 10010 * ...) * 20014 * (....) * 30021 * ... * n [우리는 10007로 나눈 나머지를 구하면 되므로, 10008 -> 1, 10009 -> 2, ..., 로 변형 가능합니다.] = (1 * 2 * 3 * ... * 10006) * 10007 * (1 * 2 * 3 * ...) * 20014 * (....) * 30021 * ... * n [게다가 10007의 계수는 분모/분자가 같으므로 10007은 나눠줘도 상관없지요.] = (1 * 2 * 3 * ... * 10006) * 1 * (1 * 2 * 3 * ...) * 2 * (....) * 3 * ... * n 직접 계산을 해 보면 1부터 10006까지의 곱을 10007로 나눈 나머지는 10006 (= -1) 임을 알 수 있습니다. (증명이 궁금하시다면 윌슨의 정리 를 참고하세요). 좀 더 정리하면 아래와 같이 되지요. = (1 * 2 * 3 * ... * (n % 10007)) * (1 * 2 * 3 * ... * (n / 10007)) * (-1) ^ [n / 10007 이 짝수인지 홀수인지] 이리하여 분자의 n!을 10007로 나눈 나머지를 좀 더 간단한 연산으로 수정하였습니다. 위의 값은 1 ~ 10007까지의 factorial을 pre-processing한다면 바로바로 알 수 있지요. 분자 부분도 같은 방법으로 구할 수 있습니다. 다만, 분자의 경우에는 나누어주어야 하므로 역원을 곱해주어야 하지요. B / A = B * A^(-1) 이니까요. 제 소스에는 역원을 진짜 단순...하게 구했습니다만, 좀 더 Smart 하게 구하는 방법은 인터넷에 친절하게 나와있습니다 ! 이상으로 에디토리얼을 마치겠습니다 ㅜㅜ 다음 대회는 9월 22일에나 있으니 많이 남았군요 ~[/spoiler] [spoiler="소스코드 보러가기(Small)"]~~~ cpp #include <cstdio> #include <iostream> #include <vector> using namespace std; int main() { int T; int H, W, R; cin >> T; for (int cn = 1; cn <= T; ++cn) { printf("Case #%d: ", cn); cin >> H >> W >> R; vector <vector <int> > table(H, vector <int> (W, 0)); table[0][0] = 1; for (int i = 0; i < R; ++i) { int x, y; cin >> x >> y; table[x - 1][y - 1] = -1; } for (int i = 1; i < H; ++i) { for (int j = 1; j < W; ++j) { if (table[i][j] == -1) continue; int r1 = 0, r2 = 0; if (i >= 2) r1 = table[i - 2][j - 1]; , if (j >= 2) r2 = table[i - 1][j - 2]; table[i][j] = (r1 + r2) % 10007; } } cout << table[H - 1][W - 1] << endl; } }

    [/spoiler]
    [spoiler="소스코드 보러가기(Large)"]

    #include <vector>
    #include <cstdio>
    #include <iostream>
    using namespace std;
    const int MOD = 10007;
    int inverse[MOD];
    int f[MOD];
    inline int P(int X) { return X / MOD; }
    void init() {
        // get the inverse -- it's very poor algorithm :(
        f[0] = 1;
        for (int i = 1; i < MOD; ++i) {
            f[i] = (f[i - 1] * i) % MOD;
            for (int j = 1; j < MOD; ++j) {
                if ((i * j) % MOD == 1) {
                    inverse[i] = j;
                    break;
                }
            }
        }
    }
    // N! = 1 * 2 * ... * N;
    // and 10006! == 1 (mod MOD) --> Wilson's Theorem...
    // so, N! = 1..10006 * 10007 * 10008..20013 * 20014 * ...
    //        = 1 * (1 * 10007) * 1 * (2 * 10007) * ... -> 10007은 분모,분자의 계수가 같으므로 고려할 필요가 없음
    //        = 1 * 2 * ... * (N / 10007) * (1 * 2 * .. * (N % 10007)) * (-1)^sign
    int fact(int N) {
        int ret = 1;
        if (N < MOD) return f[N];
        int sign = 1;
        if (N % (2 * MOD) >= MOD) sign = 10006;
        return ((fact(N / MOD) * fact(N % MOD)) % MOD * sign) % MOD;
    }
    int C(int N, int K) {
        if (P(N) > P(N - K) + P(K)) return 0; // 10007의 배수 체크
        // N! / (K! * (N-K)!) 을 10007로 나눈 나머지
        // = N! * (K! * (N-K)!)^-1 을 10007로 나눈 나머지
        // = N! * (K!)^-1 * ((N-K)!)^-1 을 10007로 나눈 나머지.
        int ret = 1;
        ret = (ret * fact(N)) % MOD;
        ret = (ret * inverse[fact(K)]) % MOD;
        ret = (ret * inverse[fact(N - K)]) % MOD;
        return ret;
    }
    int count(int x, int y) {
        if ((x + y) % 3 != 0) return 0;
        int p = (x + y) / 3;
        x -= p, y -= p;
        if (x < 0 || y < 0) return 0;
        return C(x + y, x);
    }
    int main() {
        int T;
        cin >> T;
        int n, m;
        init();
        for (int cn = 1; cn <= T; ++cn) {
            int k;
            cin >> n >> m >> k;
            vector <pair<int, int> > p(k + 1);
            for (int i = 0; i < k; ++i)
                cin >> p[i].first >> p[i].second;
            p[k] = make_pair(n, m);
            sort(p.begin(), p.end());
            int ret = 0;
            for (int bit = (1 << k); bit < (1 << (k + 1)); ++bit) {
                pair<int, int> before = make_pair(1, 1);
                int sign = -1;
                int now = 1;
                for (int j = 0; j <= k; ++j) {
                    if (bit & (1 << j)) {
                        now = now * count(p[j].first - before.first, p[j].second - before.second);
                        now %= MOD;
                        before = p[j];
                        sign *= -1;
                    }
                }
                ret += sign * now;
                ret %= MOD;
                ret += MOD;
                ret %= MOD;
            }
            printf("Case #%d: %d\n", cn, ret);
        }
    }
    

    [/spoiler]

    [이 글은 과거 홈페이지에서 이전된 글입니다. 원문보기]

    15년 전
6개의 댓글이 있습니다.
  • astein
    astein

    많이 늦었지만. GCJ R3 풀이가 업데이트되었습니다. 관심 좀... (굽신굽신)


    15년 전 link
  • JongMan
    JongMan

    헉스 내가 쓰기로 해놓고 배째서 ㅠㅠㅠㅠ 스탱님 죄송 ㅠㅠㅠㅠ


    15년 전 link
  • dgsquare
    dgsquare

    잘 보고 갑니다~! R3부터는 1번부터 난이도가 ㄷㄷㄷ 하네요.


    15년 전 link
  • carlo
    carlo

    역시 누군가 해놓은 사람이 있군요 ㅎㅎ
    허접 포탈 풀이를 해보았습니다.
    1.미로를 찾는다.
    2.포탈로 거리를 줄인다.
    우선 미로찾는 알고리즘을 간단히 생각해 보았다.
    이름하여 프로브 알고리즘.
    전제:프로브는 자기가 지나온 길의 중요포인트(시작,끝,코너) 및 전체 코너의 수와 전체이동거리를 기억한다. 자기자신을 무한히 복사할 수 있다.(기억한거 포함) - (클래스로 구현하면 될듯)
    맵을 읽어들인 배열이 있다.
    미로찾기 알고리즘: 프로브알고리즘 by Carlo
    스텝1:시작점에 프로브를 만든다.
    스텝2:모든프로브는 새로운길이 2개이상일 경우 자기자신을 하나이상 복사하여 (어떠한방향으로)한칸 전진한다.
    스텝3:모든프로브의 생과사를 점검한다.
    1:충돌상황 점검: 생존의 우선순위 -- 적은전체거리>적은코너>컴퓨터저장상위랭크
    2:생존 프로브수: 0 이면 실패
    3:끝점에 도착한프로브? 있으면 성공
    스텝4:Go to 스텝2
    포탈사용 알고리즘: 포탈가속화알고리즘 by Carlo
    전제:생존프로브는 중요 포인트, 전체 거리, 전체 코너수를 기억하고 있다.
    : Sum=0, N=0
    스텝1:메모리에 기억하고있는 N,N+1번째 포인트 사의의 거리와 N,N+1번째 포인의 이동방향의 각각의 벽방향과의 거리의 합((N쪽벽과 N사이의거리)+ (N+1쪽벽과 N+1사이의 거리))과 비교하여 작은쪽으로 Sum에 더한다.
    스텝2:if N+1== 전체코너+ 2 끝
    스텝3:N++ Go to step1
    머그다지 효율성과 결과는 확인해보지 못했습니다. ;;
    http://univac.egloos.com/
    그럼, 남은 명절즐겁게 ...


    15년 전 link
  • Corea
    Corea

    C번 풀이에서,

    두번째 줄에 1000100 이라는 상태의 최대값을 구한다고 가정해 봅시다. 1에다가 학생을 배치한다고 보면 됩니다. 이 경우에는 첫번째 줄의 1,2,4,5,6 칸에 학생이 배치되어 있으면 Cheating을 할 수 있게 되지요. 즉 1,2,4,5,6이 항상 비어있는 모든 경우의 최대값 + 2 라는 값이 되는 것이지요. (두번째 줄에 2명 배치했으니까요 :)

    라고 되어있는데, 1,2,4,5,6이 아니라 2,4,6이 아닌가요~??


    15년 전 link
  • ibroker
    ibroker

    C번 문제는 bipartite graph에서 maximum independent set을 찾는 문제로 치환할 수 있겠군요.
    그래서 matching으로 되는듯?! 합니다.
    http://acm.pku.edu.cn/JudgeOnline/problem?id=3692
    이 문제도 풀어보세요~


    15년 전 link
  • 정회원 권한이 있어야 커멘트를 다실 수 있습니다. 정회원이 되시려면 온라인 저지에서 5문제 이상을 푸시고, 가입 후 7일 이상이 지나셔야 합니다. 현재 문제를 푸셨습니다.