어휴.. 엄청 어렵네요..


이분 매칭일 것이라고 생각은 했는데


어떻게 적용해야할까?가 문제였습니다.


중복되는 강의를 제외하고 어떻게 연결을 하지.. 라는 엄청난 고민...


나는 바보였다!!!!!!!!!!!!...


중복된 과목끼리 선을 만들어서 N개의 과목 - 중복된 과목 을 하면 정답이 나오는 것을...


역발상인거 같아요!!


무언가 풀다가 막히면 항상 거꾸로 역발상하는 방법도 생각해보는 것도 좋을 것 같습니다.


중복을 피하는 것이 아닌 중복을 찾는다...

어려웠습니다...

후!


그걸 제외하고는 다른 이분매칭과 다르지 않습니다.

이분 매칭을 리마인드하기 좋은 문제였습니다..

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
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <string.h>
 
#define ll long long
#define mp make_pair
#define pii pair<intint>
#define vi vector<int>
#define vii vector<pair<intint> >
#define vll vector<ll>
 
#define MOD 86400
#define INF 0x7fffffff
#define MAX_SIZE (int)1e5
 
using namespace std;
//ios::sync_with_stdio(false); cin.tie(0);
 
int n, m;
 
vi edge[2001];
bool visit[2001];
 
int A[2001];
int B[2001];
 
vi vi_left;
 
bool f(int idx) {
    visit[idx] = 1;
 
    for (int i = 0; i < edge[idx].size(); i++) {
        int to = edge[idx][i];
 
        if (!B[to] || (!visit[B[to]] && f(B[to]))) {
            B[to] = idx;
            A[idx] = to;
 
            return 1;
        }
    }
 
    return 0;
}
 
int main() {
    ios::sync_with_stdio(false); cin.tie(0);
 
    cin >> n >> m;
 
    for (int i = 0; i < n; i++) {
        int a;
        char major;
 
        cin >> a >> major;
        
        if (major == 'c') {
            vi_left.push_back(a);
        }
    }
 
    for (int i = 0; i < m; i++) {
        int a, b;
        cin >> a >> b;
 
        edge[a].push_back(b);
        edge[b].push_back(a);
    }
 
    int ret = n;
 
    for (int i = 0; i < vi_left.size(); i++) {
        memset(visit, 0sizeof(visit));
 
        ret -= f(vi_left[i]);
    }
 
    cout << ret;
 
    return 0;
}
cs


'알고리즘 > 네트워크 플로우' 카테고리의 다른 글

[1671] 상어의 저녁식사  (0) 2017.07.02
[1017] 소수 쌍  (0) 2017.06.27
[6086] 최대유량  (1) 2017.06.03

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



이분매칭으로 문제를 해결했습니다.


문제해결방법

1. 인풋으로 들어온 상어의 능력치 별로 간선을 만들어줍니다.

a가 b를 먹을 수 있다면 a의 간선에 b를 넣어줍니다.

이 문제의 핵심은 a==b인 경우입니다.(세 개의 능력치가 같은 경우)

이게 왜 중요하냐면

a가 b를 먹었는데

밑에서 b가 a를 또 먹을 수 있기 때문에 이것을 방지해야합니다.


가장 간단한 방법으로(사실 다른 방법따위 모름) 인덱스가 a < b인 경우에만 먹을 수 있게 하면 됩니다.(물론 같은경우에만)

이렇게 되면 a와 b가 능력치가 같을경우 a를 처리할떄 b를 먹을 것이고 b를 처리할 때는 a > b 이므로 먹을 수 없습니다.


2. 간선을 만든 후 상어는 각각 두마리씩 먹을 수 있습니다. 그러므로 한번 처리할떄 두번씩 먹어줍니다 

제가 계속 실수 했던 부분이

visit을 초기화안하고 먹고 또먹었습니다.


이렇게

1
2
3
4
5
6
    for (int i = 0; i < n; i++) {
        memset(visit, 0sizeof(visit));
        f(i);
        memset(visit, 0sizeof(visit));
        f(i);
    }

cs


이렇게 멤셋을 두번해줘야합니다!!!
이유는 아래의 예시를 보면서 설명하겠습니다.

이 그림은 현재
1번이 1번과 2번을 매칭했을 때
2번이 1번을 뺏어서 1번이 3번과 매칭된 상황입니다.
2번이 1번을 뺏으면서 visit[1]은 방문한 상태가 되겠죠??
근데 visit초기화를 안하면
2번이  2번에 매칭을 시도하면서 1번으로 들어가서 1-2를 미뤄내고 4번에 연결되어야하는데(1과 4사이에 선이 있다고 가정)
이미 방문했기 때문에 그냥 넘어가버리게 되죠!@

이게 핵심입니당!!

3. 마지막으로 B배열이 -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
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
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <string.h>
#include <queue>
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
#include <stack>
 
#define ll long long
#define INF 987654321
#define MOD 1000000
#define MAX_SIZE 1000
 
using namespace std;
 
int B[MAX_SIZE];
bool visit[MAX_SIZE];
vector<int> edge[MAX_SIZE];
 
int sharkSize[MAX_SIZE];
int sharkIntel[MAX_SIZE];
int sharkSpeed[MAX_SIZE];
 
int n;
 
int eat(int a, int b) {
    int ret = 0;
    if (sharkSize[a] < sharkSize[b]) return 0;
    else if(sharkSize[a] == sharkSize[b]) ret++;
    if (sharkIntel[a] < sharkIntel[b]) return 0;
    else if (sharkIntel[a] == sharkIntel[b]) ret++;
    if (sharkSpeed[a] < sharkSpeed[b]) return 0;
    else if (sharkSpeed[a] == sharkSpeed[b]) ret++;
 
    if (ret == 3return 2;
    return 1;
}
 
bool f(int from) {
    visit[from] = 1;
 
    for (int i = 0; i < edge[from].size(); i++) {
        int to = edge[from][i];
        if (B[to] == -1 || !visit[B[to]] && f(B[to])) {
            B[to] = from;
            return 1;
        }
    }
 
    return 0;
}
 
 
int main() {
    scanf("%d"&n);
 
    for (int i = 0; i < n; i++) {
        scanf("%d %d %d"&sharkSize[i], &sharkIntel[i], &sharkSpeed[i]);
    }
 
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            int eatFlag = eat(i, j);
 
            if (eatFlag == 1) edge[i].push_back(j); //먹을 수 있는경우
            else if (eatFlag == 2 && i < j) edge[i].push_back(j); // 능력치가 같은 경우
        }
    }
 
    memset(B, -1sizeof(B));
    
    for (int i = 0; i < n; i++) {
        memset(visit, 0sizeof(visit));
        f(i);
        memset(visit, 0sizeof(visit));
        f(i);
    }
 
    int ret = 0;
    for (int i = 0; i < n; i++if (B[i] == -1) ret++;
    printf("%d", ret);
 
    return 0;
}
cs





'알고리즘 > 네트워크 플로우' 카테고리의 다른 글

[12843] 복수전공  (0) 2017.08.24
[1017] 소수 쌍  (0) 2017.06.27
[6086] 최대유량  (1) 2017.06.03

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



처음엔 이분 매칭 문제인 것을 모르고..

엄청나게 틀렸네요... 시간초과 + 구현실수....


알고나서도 한참을 해맸는데요!

첫번째 이유는 첫번째 수와 매칭되는 수를 출력한다는 점이었어요..


소수 쌍을 구하는 것이기 때문에

홀수 + 홀수 = 짝수(2를 제외하고는 무조건 소수가 안됨)

짝수 + 짝수 = 짝수(무조건 안됨)

홀수 + 짝수 = 홀수(가능성 있음)

입니다!


그렇기 때문에 그룹을 홀수와 짝수로 나누어야하는데요!


첫번쨰 수가 홀수일수 도있고 짝수일 수도 있기 때문에 어떻게 진행해야할지 난해했습니다.

두번째 소수를 구하는 방법입니다. 풀고나니깐 뭐.. 에라토스테네스의 체를 써도 되고 그냥 나눠서 확인해도

입력받는 수가 1000까지기 때문에 최대 1999(두 수를 합쳐야하므로)밖에 안되서 오래걸리진 않습니다.


마지막으로 출력입니다. 입력이 정렬되서 들어온다는 말은 없으므로 출력할 때 정렬을 해야합니다.

저같은 경우는 우선순위 큐에 넣어서 그냥 큐가 빌때까지 출력했습니다.

-1이 출력되는 조건은 큐가 비어있다면 -1을 출력했습니다.


문제 해결 전략은

1. 첫번째 수와 다른 그룹의 수와 매칭을 시킨다.

2. 1에서 매칭된 수가 소수라면 1에서 매칭된 쌍을 제외하고 이분매칭 알고리즘을 이용해서 나머지를 매칭합니다.

3. 만약 전부 매칭해서 매칭 된 수가 입력 받은 N의 절반이 된다면 전부 매칭된 것이므로 우선순위 큐에다가 처음에 첫번째 수와 매칭한 수를 푸시해줍니다.

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
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
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <string.h>
#include <queue>
#include <iostream>
#include <algorithm>
#include <vector>
 
#define ll long long
#define MAX_SIZE 50
#define INF 987654321
#define MOD 1000000
using namespace std;
 
int arrA[MAX_SIZE];
int arrB[MAX_SIZE];
int pick;
int idxA, idxB;
 
int matchA[MAX_SIZE];
int matchB[MAX_SIZE];
bool visit[MAX_SIZE];
 
bool isPrime(int a) {
    if (a == 1return 0;
    else if (a != 2 && a % 2 == 0return 0;
    
    for (int i = 3; i * i <= a; i += 2) {
        if (a % i == 0return 0;
    }
 
    return 1;
}
 
bool f(int idx) {
    if (visit[idx]) return 0;
    visit[idx] = 1;
 
    for (int i = 0; i < idxB; i++) {
        if (i == pick) continue;
 
        if (isPrime(arrA[idx] + arrB[i]) && (matchB[i] == -1 || !visit[matchB[i]] && f(matchB[i]))) {
            matchB[i] = idx;
            matchA[idx] = i;
 
            return 1;
        }
    }
 
    return 0;
}
 
int main() {
    int arr[MAX_SIZE];
    int n;
    scanf("%d"&n);
 
    for (int i = 0; i < n; i++scanf("%d"&arr[i]);
    
    for (int i = 0; i < n; i++) {
        if (arr[0] % 2 == arr[i] % 2) arrA[idxA++= arr[i];
        else arrB[idxB++= arr[i];
    }
 
    priority_queue<int> pq;
 
    for (int i = 0; i < idxB; i++) {
        if (isPrime(arrA[0+ arrB[i])) {
            pick = i;
            
            int cnt = 1;
            memset(matchA, -1sizeof(matchA));
            memset(matchB, -1sizeof(matchB));
 
            for (int j = 1; j < idxA; j++) {
                memset(visit, 0sizeof(visit));
                if (f(j)) cnt++;
            }
 
            if (cnt == n / 2) pq.push(-arrB[i]);
        }
    }
 
    if (pq.empty()) puts("-1");
    else {
        while (!pq.empty()) {
            printf("%d "-pq.top()), pq.pop();
        }
    }
 
    return 0;
}
cs


'알고리즘 > 네트워크 플로우' 카테고리의 다른 글

[12843] 복수전공  (0) 2017.08.24
[1671] 상어의 저녁식사  (0) 2017.07.02
[6086] 최대유량  (1) 2017.06.03

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


네트워크 플로우에서 최대 유량 문제입니다.

저도 처음 공부하기 때문에 다른 블로그를 참조하면서 공부했습니다.


필요한 변수 선언입니다.


1
2
3
4
5
    int n; //input으로 받는 edge 수
    vector<int> adj[MAX_SIZE]; // 인접
    int capacity[MAX_SIZE][MAX_SIZE] = { 0, }; // 용량
    int flow[MAX_SIZE][MAX_SIZE] = { 0, }; // 흐르는 양
    int prev[MAX_SIZE]; // 경로를 저장(현재 정점의 이전 정점이 무엇이었는지)
cs


입력 부분입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//입력
    scanf("%d"&n);
    getchar();
 
    for (int i = 0; i < n; i++) {
        char s, e;
        int c;
 
        scanf("%c %c %d"&s, &e, &c);
        getchar();
 
        s = ctoi(s), e = ctoi(e); //입력 받은 정점을 숫자로 변환
 
        //같은 간선이 여러개 들어오면 용량이 그만큼 늘어나면 됨
        capacity[s][e] += c;
 
        //정,역방향 간선 추가
        adj[s].push_back(e);
        adj[e].push_back(s);
    }
cs


15번째 줄의 += 부분이 잘이해가 안가실 수 있습니다.

capacity배열은 0으로 전부 초기화 되어있습니다.

입력에서 만약 A -> B 로 가는 edge를 여러개로 준다면

A->B로 가는 유량은 edge의 총 용량만큼이기 때문에 +=로 해주는 것입니다.


19번째에서 역방향 간선까지 추가하는 것이 포드 풀커슨 알고리즘의 핵심인 것 같습니다..(잘은 이해안감..ㅠㅠ)


처리부분입니다.

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
    //process
    int total_flow = 0;
    const int start = ctoi('A'), end = ctoi('Z');
 
    while (1) {
        for (int i = 0; i < MAX_SIZE; i++) prev[i] = -1;
 
        queue<int> q;
        q.push(start);
 
        while (!q.empty())
        {
            int curr = q.front();
            q.pop();
 
            for (int i = 0; i < adj[curr].size(); i++) {
                int next = adj[curr][i];
 
                if (capacity[curr][next] - flow[curr][next] > 0 && prev[next] == -1) {
                    q.push(next);
                    prev[next] = curr;
                    if (next == endbreak;
                }
            }
        }
 
        if (prev[end== -1break//Z까지 가는 길이 없으면..
 
        //A에서 Z로 가는 경로 중에서 가장 작은 용량을 선택    
        int min_flow = INF;
        for (int i = end; i != start; i = prev[i])
            min_flow = min(min_flow, capacity[prev[i]][i] - flow[prev[i]][i]);
 
        //A에서 Z로 가는 경로에 물을 흘림
        //지나간 경로의 반대 경로에는 -값의 물을 흘림
        for (int i = end; i != start; i = prev[i]) {
            flow[prev[i]][i] += min_flow;
            flow[i][prev[i]] -= min_flow;
        }
        
        total_flow += min_flow;
    }
cs

start, end는 시작과 끝점 입니다. 이 문제에서는 'A'와 'Z'가 되겠습니다.

bfs를 통해서 A->Z로 가는 경로를 찾습니다.

경로는 prev배열에 현재 정점 바로 전의 정점을 저장을 해서 경로를 알 수 있습니다.


예를 들어 A->B->C 라면 prev[A] = -1, prev[B] = A, prev[C] = B 입니다.

최종 도착지점은 항상 'Z'이므로 Z부터 역으로 추적하면 A까지 도달할 수 있습니다.


16번째 줄이 굉장히 중요합니다.

1
2
3
4
5
6
7
8
9
for (int i = 0; i < adj[curr].size(); i++) {
    int next = adj[curr][i];
 
    if (capacity[curr][next] - flow[curr][next] > 0 && prev[next] == -1) {
        q.push(next);
        prev[next] = curr;
        if (next == endbreak;
    }
}
cs


curr(현재 정점)에서 연결되어 있는 부분을 탐색합니다.


curr->next의 edge에 용량이 꽉 차지 않았다면 그리고 아직 방문하지 않았다면

if (capacity[curr][next] - flow[curr][next] > 0 && prev[next] == -1)

next 정점을 q에 넣고 prev[다음 정점] = 현재 정점 을 넣어줍니다.

만약 다음 정점이 'Z'(end)라면 루프를 빠져나옵니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        if (prev[end== -1break//Z까지 가는 길이 없으면..
 
        //A에서 Z로 가는 경로 중에서 가장 작은 용량을 선택    
        int min_flow = INF;
        for (int i = end; i != start; i = prev[i])
            min_flow = min(min_flow, capacity[prev[i]][i] - flow[prev[i]][i]);
 
        //A에서 Z로 가는 경로에 물을 흘림
        //지나간 경로의 반대 경로에는 -값의 물을 흘림
        for (int i = end; i != start; i = prev[i]) {
            flow[prev[i]][i] += min_flow;
            flow[i][prev[i]] -= min_flow;
        }
        
        total_flow += min_flow;
cs


주석을 달아 놓은 것처럼

1번째 줄은 정점 'Z'(end)의 이전 정점이 -1라는 것은 bfs에서 Z로 도달하지 못했음을 의미합니다.

그렇기 때문에 더이상 경로가 없음을 의미해서 break;합니다.


그게 아니라면 4번째 줄

bfs로 찾은 경로 구간 중에서 가장 용량이 작은 구간을 선택합니다.

유량을 가장 작은 용량으로 맞춰야만 그 경로로 흘릴 수 있기 때문입니다.

만약 이전 bfs에서 유량이 있을 수 있으므로 최소 용량은 capacity[이전 정점][현재 정점] - flow[이전 정점][현재 정점]으로 구해야 합니다.


그리고


10번째 줄에서 A->Z까지의 경로에 위에서 찾은 min_flow(가장 작은 유량)을 더합니다.

여기서 중요한 부분은 12번째 줄입니다. 경로의 반대 방향으로 -유량을 더해줍니다.

위의 input부분에서 역방향 간선까지 함께 만든 이유입니다.


capacity[curr][next] - flow[curr][next] > 0

아까 이 부분에서 0보다 크면 큐에다가 next를 푸시했는데요.

만약 flow[curr][next]가 -유량이라면 +가 되기 때문에 항상 흐를 수 있습니다.


예시를 보면서 이해하도록 하겠습니다.

이 예시는

http://blog.naver.com/PostView.nhn?blogId=kks227&logNo=220804885235

를 참고 했으며 제가 설명하는 것 보다 훨씬 상세하게 설명해주셨습니다. 저도 이 블로그를 보고 이해했습니다.


지저분해도 .. 이해해주세요!


bfs로



이 경로까지는 찾으셨을텐데요..


이 유량을 다합치면 9가 되죠.

이게 최대일 것 같지만 그렇지 않습니다.


A->E->F->D->B->G->Z의 경로로 1만큼 흘린다면 Z에 최대 10까지 도달할 수 있습니다.


저는 여기서 D->B구간이 이해가 안갔는데..

이 부분이 -유량을 흘린 이유입니다.

B->D구간의 유량은 3이 흐르고 있습니다.

D->B구간은 애초에 경로가 없습니다. 어떻게 흐를까요?


사실.. 진짜로 흐르는 것이 아니라 B->D구간을 상쇄시키는 느낌??적인 느낌입니다.

D->B구간으로 1을 흘려보내면 B->D구간에서 원래 3이 흐르고 있었으니.. 2가 되겠죠!


그리고 그 B->D에서 상쇄시킨 그 1을  B->G 구간으로 보내는 것입니다!!!!!....

느낌만 이해했습니다.. 완전 이해는 아닌..

어떻게 이런거 까지 생각해냈는지는 잘 모르겠네요...ㅠㅠㅠ

제가 참고하라고 올려드린 블로그..

http://blog.naver.com/PostView.nhn?blogId=kks227&logNo=220804885235

를 참조해주세요!!!!!! 정말 잘 설명해주셨습니다..


아래는 풀 코드입니다.


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
/*
input)
11
A B 3
A C 3
A D 4
B E 2
C B 10
C E 1
D F 5
E D 1
E F 1
E Z 2
F Z 5
output)
7
*/
 
#include <cstdio>
#include <iostream>
#include <vector>
#include <queue>
 
#define MAX_SIZE 52
#define INF 0x7fffffff
 
using namespace std;
 
int ctoi(char c) {
    if (c <= 'Z'return c - 'A';
    return c - 'a' + 26;
}
 
int main() {
    int n;
    vector<int> adj[MAX_SIZE]; // 인접
    int capacity[MAX_SIZE][MAX_SIZE] = { 0, }; // 용량
    int flow[MAX_SIZE][MAX_SIZE] = { 0, }; // 흐르는 양
    int prev[MAX_SIZE]; // 경로를 저장(현재 정점의 이전 정점이 무엇이었는지)
 
    //입력
    scanf("%d"&n);
    getchar();
 
    for (int i = 0; i < n; i++) {
        char s, e;
        int c;
 
        scanf("%c %c %d"&s, &e, &c);
        getchar();
 
        s = ctoi(s), e = ctoi(e); //입력 받은 정점을 숫자로 변환
 
        //같은 간선이 여러개 들어오면 용량이 그만큼 늘어나면 됨
        capacity[s][e] += c;
 
        //정,역방향 간선 추가
        adj[s].push_back(e);
        adj[e].push_back(s);
    }
 
    //process
    int total_flow = 0;
    const int start = ctoi('A'), end = ctoi('Z');
 
    while (1) {
        for (int i = 0; i < MAX_SIZE; i++) prev[i] = -1;
 
        queue<int> q;
        q.push(start);
 
        while (!q.empty())
        {
            int curr = q.front();
            q.pop();
 
            if (curr == endbreak;
 
            for (int i = 0; i < adj[curr].size(); i++) {
                int next = adj[curr][i];
 
                if (capacity[curr][next] - flow[curr][next] > 0 && prev[next] == -1) {
                    q.push(next);
                    prev[next] = curr;
                }
            }
        }
 
        if (prev[end== -1break//Z까지 가는 길이 없으면..
 
        //A에서 Z로 가는 경로 중에서 가장 작은 용량을 선택    
        int min_flow = INF;
        for (int i = end; i != start; i = prev[i])
            min_flow = min(min_flow, capacity[prev[i]][i] - flow[prev[i]][i]);
 
        //A에서 Z로 가는 경로에 물을 흘림
        //지나간 경로의 반대 경로에는 -값의 물을 흘림
        for (int i = end; i != start; i = prev[i]) {
            flow[prev[i]][i] += min_flow;
            flow[i][prev[i]] -= min_flow;
        }
        
        total_flow += min_flow;
    }
 
    printf("%d\n", total_flow);
 
    return 0;
}
cs


'알고리즘 > 네트워크 플로우' 카테고리의 다른 글

[12843] 복수전공  (0) 2017.08.24
[1671] 상어의 저녁식사  (0) 2017.07.02
[1017] 소수 쌍  (0) 2017.06.27

+ Recent posts