개념은 매우 간단하다. 여기보면 이해하기 쉬운편(PS관점에서 trie에 관한 다른 인사이트도 얻을 수 있다)

단어가 끝나는 지점에 마킹해야함에 유의


글자단위 Trie

아래는 내가 구현해본 Trie코드이며, 이 문제의 답안이기도 하다.

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 <bits/stdc++.h>
using namespace std;
#define REP(i,n) for(int i=0;i<(int)(n);i++)
 
struct trie {
    struct node { map<char, node> child; bool end = false; };
    node root;
    node* insert(node* n, char c) {
        auto& m = n->child;
        if (m.count(c) == 0) m[c] = node();
        return &m[c];
    }
    void insert(string s) {
        node* n = &root;
        int m = s.length();
        for(int j=0;j<m;j++) n = insert(n, s[j]);
        n->end = true;
    }
 
    node* find(node* n, char c) {
        auto& m = n->child;
        if (m.count(c) == 0return NULL;
        return &m[c];
    }
 
    bool find(string s) {
        node* n = &root;
        int m = s.length();
        for (int j = 0; j < m; j++) {
            n = find(n, s[j]);
            if (n == NULLreturn false;
        }
        return n->end;
    }
};
 
int32_t main()
{
    int N, M; cin >> N >> M;
    trie t;
    REP(i, N) {
        string s; cin >> s;
        t.insert(s);
    }
    int ans = 0;
    REP(i, M) {
        string s; cin >> s;
        ans += t.find(s) ? 1 : 0;
    }
    cout << ans << '\n';
    return 0;
}
cs


단어단위 Trie

이건 응용인데 글자단위와 비슷한 개념으로 풀 수 있다. 이 문제 참조.

반응형

여기를 보면 기본 개념에 대해서 쉽게 익힐 수 있다. 본 글에서는 그럼에도 헷갈리는 점 위주로 써본다.

전체 문자열 텍스트T(길이 N)와, 찾으려는 패턴P(길이 M)가 있을때, 나이브하게 구현하면 $O(NM)$이 걸린다.

KMP를 사용하면 이를 $O(N+M)$으로 줄일 수 있다.


Pi함수(실패함수, 오버랩함수)

Pi함수는 위의 텍스트T와 패턴P중에서 패턴P에 대해서 미리 계산해놓는 함수이다. 실패함수라고도 하며, 문자열 매칭이 미스났을때 어디까지 back할지 정보를 제공한다.

pi[i]는 실패후에 앞부분에 이미 매칭한걸로 치는 오버랩구간을 의미하기 때문에 나는 오버랩함수라고 부른다.

인덱스01234567891011
텍스트ABCDABCDABEE
패턴ABCDABE

예를 들어 위와 같은 형태가 있을때, 6번 인덱스에서 불일치가 일어났는데 


인덱스01234567891011
텍스트ABCDABCDABEE
패턴ABCDABE

위와 같이 텍스트에서 불일치가 발생한 C왼쪽 부분에서 이미 매칭한걸로 치는 오버랩 구간은 최대한 길게 잡았을때 AB임을 알 수 있다. 

좀더 길게 DAB로 잡고 싶어도 패턴이 D로 시작하지 않는다. 아예 ABCDAB로 잡으려고 하면 이미 실패한 패턴이고 제자리걸음이라 택할수 없다.


인덱스01234567891011
텍스트ABCDABCDABEE
패턴ABCDABE

따라서 위와 같이, 텍스트의 C왼쪽 postfix AB와 패턴의 prefix AB를 위처럼 오버랩시키고 나면 인덱스 6부터 다시 비교를 시작하면 된다.

오버랩함수가 2일때 패턴을 보면 오른쪽으로 6-2=4칸 점프(?)한걸 볼 수 있는데,

이처럼 오버랩함수의 값이 오히려 작으면 작을수록 점프를 많이한다.

오버랩함수가 5이면 6-5=1이라서 한 칸만 오른쪽으로 이동한다.

오버랩이 많이 되면 오른쪽이동량은 적은대신 이미 많이 오버랩되어 있으므로 적게 추가로 매칭되면 매칭이 완성되는 장점이 있으므로 무조건 오버랩이 적게되서 오른쪽으로 점프를 많이 하는게 좋은건 아니다.


그래서 pi[i]는 주어진 문자열의 0~i 까지의 부분 문자열 중에서 최대 prefix == suffix길이로 세팅한다.

문자열 "ABAABAB"의 pi배열을 구해봅시다.

 i

부분 문자열 

 pi[i]

 0

A

 0

 1

AB 

 0

 2

ABA

 1

 3

ABAA

 1

 4

ABAAB

 2

 5

ABAABA

 3

 6

ABAABAB

 2

팰린드롬 개념이 아님에 주의!


문자열 "ABABABC"의 pi배열을 구해봅시다.

 i

부분 문자열 

 pi[i]

 0

A

 0

 1

AB 

 0

 2

ABA

 1

 3

ABAB

2

 4

ABABA

 3

 5

ABABAB

 4

 6

ABABABC

 0

prefix와 posfix가 오버랩되도 문제없음에 주의(위의 초록색 글자)


이해하기는 쉽지만, 효율적으로 $O(M)$에 pi함수를 구하는건 약간 어렵다.

그래도 맨위 링크를 통해 공부하고 아래와 같은 함수를 짤 수 있었다.

1
2
3
4
5
6
7
8
9
10
using vi = vector<int>;
vi getPi(string p) {
    int m = (int)p.length();
    vi pi(m);
    for (int j = 0, i = 1; i < m; i++) {
        while (j > 0 && p[i] != p[j]) j = pi[j - 1];
        if (p[i] == p[j]) pi[i] = ++j;
    }
    return pi;
}
cs

오버랩을 while로 여러번 적용하는 케이스가 발생하는 것은 아래를 참조하자.


불일치 발생

오버랩 함수 1차적용


오버랩 함수 2차 적용

오버랩함수 3차적용

"AABAAA"에 대해서 Pi를 구해보면 Pi = {0,1,0,1,2,2} 이렇게 나오는데 중간에 0이 섞이기도 하고 마지막 2가 찍힐때 내가 원하던 조금 복잡한 다이나믹이 나오니 필요하면 검토해보자.

Pi함수가 갖는 오버랩의 의미에 대해서는 백준 1305 광고 문제를 풀어보면 직접적인 도움이 된다.

KMP

Pi함수를 이해했다면 KMP는 이를 이용해서 어렵지 않게 아래와 같이 짤 수 있다.
(물론 스크래치부터 짜면 구현실수를 많이 포함되게 된다. 대략적인 흐름만 이해하고 복사해서 쓰자)
이 문제의 답안이기도 하다.
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
#include <bits/stdc++.h>
using namespace std;
 
using vi = vector<int>;
vi getPi(string p) {
    int m = (int)p.length();
    vi pi(m);
    for (int j = 0, i = 1; i < m; i++) {
        while (j > 0 && p[i] != p[j]) j = pi[j - 1];
        if (p[i] == p[j]) pi[i] = ++j;
    }
    return pi;
}
//p가 발견된 위치들을 리턴(0 base)
vi KMP(string t, string p) {
    int n = (int)t.length(), m = (int)p.length();
    vi pi = getPi(p);
    vi r;
    for (int i = 0, j = 0; i < n; i++) {
        while (j > 0 && t[i] != p[j]) j = pi[j - 1];
        if (t[i] == p[j]) {
            if (j == m - 1
                r.push_back(i - m + 1), 
                j = pi[j];  //j=0이 아님에 주의(오버랩고려)
            else j++;
        }
    }
    return r;
}
 
int32_t main()
{
    ios::sync_with_stdio(0); cin.tie(0);
 
    string t, p;
    getline(cin, t), getline(cin, p);
    vi v = KMP(t,p);
    cout << v.size() << '\n';
    for (auto a : v)cout << a + 1 << ' ';
    cout << '\n';
    return 0;
}
cs






반응형

'Programming > Algorithm' 카테고리의 다른 글

위상정렬(Topological Sort)  (0) 2020.04.23
트라이(Trie)  (0) 2020.04.22
기하  (0) 2020.04.19
포화이진트리(perfect binary tree), 완전이진트리  (0) 2020.04.19
트리DP  (0) 2020.04.15

 

CCW(Counter Clock-Wise)

이론적으로는 외적을 구하는것 같은데, 알고리즘 기하문제를 풀기위해 사용한다. 

 

용도1. 다각형 면적구하기

점3개를 주면 꼭지점 3개로 해석해 면적을 구해준다. (정확히는 평행사변형의 면적을 주는건데 삼각형의 면적이 더 유용해서 2로 나눠서 쓴다)

이걸 사용하면 도형의 외각선 좌표들이 순서대로 주어질때 아래처럼 삼각형으로 쪼개면 다각형의 면적을 구할 수 있다.

 

점 3개가 순서대로 반시계 방향을 이루면 +값을 가지고 시계방향을 이루면 -값을 가진다. (오른손 법칙을 떠올리면 좋다)따라서 최종결과에 절대값을 씌워야 할 수 있다.
 
재밌는 점은 아래와 같은 볼록다각형에 대해서도 자동으로 삭감이 이루어지면서 면적계산이 제대로 된다는 점이다.
 
 
내 소스는 다음과 같다. 이 문제의 답안이기도 하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <bits/stdc++.h>
#define int long long
using namespace std;
#define REP(i,n) for(int i=1;i<=(int)(n);i++)
 
struct point { int x, y; };
double CCW(point A, point B, point C) {
    return (B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y);
}
 
int32_t main()
{
    vector<point> v;
    int N, x, y; cin >> N; REP(i, N)cin >> x >> y, v.push_back({ x,y });
 
    double sum = 0;
    for (int i = 1; i < N-1; i++) {
        sum += CCW(v[0], v[i], v[i + 1]);
    }
    cout.precision(1);
    cout << fixed << (double)abs(sum)/2 <<'\n';
    return 0;
}
cs
 

용도2. 점 3개의 방향성 구하기

위에서 반시계 방향일때 CCW가 +값을 리턴한다고 했는데 +,0,-를 이용해서 방향성을 판단할 수 있다는 것. 아래 선분교차 판단 등에 쓰인다.

 

용도3. 선분교차 판단

여기에 설명 잘 나와있어서 보고 이해하면 될 것 같다.

주의할점은 CCW를 1,0,-1을 리턴하는 것으로 변경하지 않으면 long long을 사용해도 오버플로가 날 수 있다는 점이다(아래코드 보면 CCW간 곱셈이 들어가서..)

아래코드를 나중에 활용하자. 이 문제의 답안이기도 하다.

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
#include <bits/stdc++.h>
#define int long long
using namespace std;
#define REP(i,n) for(int i=1;i<=(int)(n);i++)
 
struct point { double x, y; };
double CCW(point A, point B, point C, bool sign_only=true) {
    double r = (B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y);
    if (sign_only == falsereturn r;
    if (r == 0)return 0;
    return r > 0 ? 1 : -1;
}
struct line { point s, e; };
//touch_ok가 false이면, 두 선분이 교차하지 않고 만나기만 하는 경우에는 false를 리턴
bool Intersect(line x, line y, bool touch_ok=false) {
    point a = x.s, b = x.e;
    point c = y.s, d = y.e;
    double ab = CCW(a, b, c) * CCW(a, b, d);
    double cd = CCW(c, d, a) * CCW(c, d, b);
    if (ab == 0 && cd == 0) { // 이건 두 선분이 평행한 경우
        pair<doubledouble> aa = { a.x, a.y }, bb = { b.x,b.y }, 
            cc = { c.x, c.y }, dd = { d.x,d.y };
        if (aa > bb)swap(aa, bb);
        if (cc > dd)swap(cc, dd);
        if(touch_ok) return cc <= bb && aa <= dd; // 0이면 점끼리 만나는 것
        return cc < bb && aa < dd; // a<d이면서 b,c가 교차하면 선분
    }
    if(touch_ok) return ab <= 0 && cd <= 0; // 0이면 두 선분이 한점에서 만나는 것
    return ab < 0 && cd < 0; // 이게 기본. 각선분에서 나머지 2개점 방향이 달라야 교차
}
 
int32_t main()
{
    ios::sync_with_stdio(0); cin.tie(0);
#ifndef ONLINE_JUDGE
    freopen("input.txt""rt", stdin);
#endif
 
    vector<line> l;
    double a, b, c, d; REP(i, 2)cin >> a >> b >> c >> d, l.push_back({ a,b,c,d });
    cout << (Intersect(l[0],l[1])?1:0<<'\n';
    return 0;
}
cs

 

반응형

'Programming > Algorithm' 카테고리의 다른 글

트라이(Trie)  (0) 2020.04.22
KMP( Knuth, Morris, Prett)  (0) 2020.04.20
포화이진트리(perfect binary tree), 완전이진트리  (0) 2020.04.19
트리DP  (0) 2020.04.15
세그멘트 트리  (0) 2020.04.13

이진으로 꽉차있는 다음과 같은 트리를 말한다.

leaf node에서 오른쪽에서 부터 몇개 비면 완전이진트리라고도 한다. (때로는 포화이진트리를 완전이진트리라고 부르기도 한다.)

배열로 표현하기 좋다.

1베이스로 볼때.. 자식 노드는 2n, 2n+1 인덱스에 위치함을 알 수 있다.
레벨별 인덱스 위치는 1레벨이 1이고 2레벨이 2이고, 3레벨이 4이고, 4레벨이 8인것을 보아 
n레벨의 시작인덱스 위치는 $2^n$임을 알 수 있다.

포화이진트리코드

아래는 내가 작성한 코드이며, 배열로 포화이진트리를 받아서 preorder로 방문해준다.
이 문제의 답에 쓰이기도 했다.
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
#include <bits/stdc++.h>
using namespace std;
using vi = vector<int>;
 
//포화 이진 트리 (0-base)
struct perfect_binary_tree {
    vi v;
    int n;
    perfect_binary_tree(int* arr, int arr_size) {
        v.assign(arr, arr + arr_size);
        n = arr_size;
    }
    perfect_binary_tree(vi& v) {
        this->= v;
        n = (int)v.size();
    }
    vi preorder() {
        vi ret;
        function<void(int)> dfs = [&](int ix) {
            ret.push_back(v[ix]);
            int l = (ix+1* 2-1;
            int r = (ix+1* 2 ;
            if (l < n-1) dfs(l);
            if (r < n-1) dfs(r);
        };
        dfs(0);
        return ret;
    }
};
 
int32_t main()
{
    vi v = { 1,2,3,4,5,6,7,8 };
    perfect_binary_tree t(v);
    vi p = t.preorder();
    for(int a:p)cout << a << ' ';
    return 0;
}
cs




반응형

'Programming > Algorithm' 카테고리의 다른 글

KMP( Knuth, Morris, Prett)  (0) 2020.04.20
기하  (0) 2020.04.19
트리DP  (0) 2020.04.15
세그멘트 트리  (0) 2020.04.13
최소신장트리(MST, Minimum Spanning Tree)  (0) 2020.04.12

트리의 속성

정점이 N이면 간선의 개수는 N-1개이다.

인접리스트를 트리로 만들때 root노드는 아무거로나 선택해도 답에 지장이 없(었다 지금까지는).

인접리스트를 트리로 전환하기

간선 가중치 없는 무방향 인접리스트를 방향그래프 형태의 트리로 전환해준다.

이렇게 했을때 이점은 parent방향으로 이동하는 간선을 끊어버리기 때문에 순서를 매겨서 루트부터 전 노드 방문하기가 편해진다.

아래 코드에서 treefy()를 재활용하면 된다.이 문제의 답안이기도 하다. 하나의 함수로 패키징하기 위해서 람다(lambda) 함수를 사용했고, 재귀를 가능하게 하기 위해서 std::function을 사용했다.

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
#include <bits/stdc++.h>
using namespace std;
#define REP(i,n) for(int i=0;i<(int)(n);i++)
using vi = vector<int>;
const int INF = (int)1e9, MAXN = 100001;
 
//무방향그래프를 인풋으로 받아, 특정 정점을 루트를 가지는 방향그래프(트리)로 만들어준다.
struct result { vector<vi> tree; int root; vi parent, level; };
result treefy(vector<vi>& adj_list, int root = -1bool base1 = false) {
    int max_v = (int)adj_list.size();
    vector<vi> tree(max_v);
    if (root == -1)
    {
        //find root(왼쪽이 parent인경우만 성립)
        map<intint> m;
        for (int i = (int)base1; i < max_v;i++)
            for (auto a : adj_list[i]) m[a]++;
        for (int i = (int)base1; i < max_v; i++
            if (m[i] == 0) { root = i; break; }
    }
    vi parent(max_v), level(max_v);
    function<void(intint,int)> dfs = [&](int node, int par,int lv) {
        parent[node]=par,level[node] = lv;
        for (auto adj : adj_list[node]) {
            if (adj == par) continue;  // 부모방향 간선제거
            tree[node].push_back(adj);
            dfs(adj, node, lv+1);
        }
    };
    dfs(root, -INF, 1);
 
    return { tree, root, parent, level };
}
 
int N, R, Q, U, V;
int main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> N >> R >> Q;
    vector<vi> adj_list(N + 1);
    REP(i, N - 1) {
        cin >> U >> V;
        adj_list[U].push_back(V);
        adj_list[V].push_back(U);
    }
    vector<vi> tree = treefy(adj_list, R).tree;
 
    vi dp(MAXN, INF);
    function<int(int)> cnt_sub = [&](int root) {
        int& res = dp[root];
        if (res < INF) return res;
        int sum = 1for (auto adj : tree[root]) sum += cnt_sub(adj);
        return res = sum;
    };
    cnt_sub(R);
 
    while (Q--cin >> U, cout << dp[U] << '\n';
    return 0;
}
cs

 

랜덤으로 트리 인풋만들기

디버깅을 하거나 문제를 출제하다보면, 랜덤한 트리를 만들 필요가 있는데, 임의로 간선을 선택하다보면 사이클이 생겨서 잘 안된다.

이럴때 MST구할때 쓰는 크루스칼 알고리즘을 사용하면 간단하게 사이클 없는 트리를 만들 수 있다.

아래는 내가 구현한 코드이다.

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
#include <bits/stdc++.h>
using namespace std;
#define REP(i,n) for(int i=0;i<(int)(n);i++)
#define REP1(i,n) for(int i=1;i<=(int)(n);i++)
using vi = vector<int>;
using vvi = vector<vi>;
struct union_find {
    vi parent;
    union_find(int n) {
        parent.resize(n + 1);
        REP1(i, n)parent[i] = i;
    }
    int find(int x) {
        int ox = x;
        while (parent[x] != x) x = parent[x];
        return parent[ox] = x;;
    }
 
    void uni(int x, int y) {
        int xp = find(x), yp = find(y);
        parent[yp] = xp;
    }
};
 
int main()
{
    int N = 7;
    cout << N << '\n';
    vvi adj_list(N + 1);
    union_find uf(N);
    int U, V; 
    REP(i, N - 1) {  // 트리는 반드시 N-1개의 간선을 가진다.
        A:
        U = rand() % N + 1;
        V = rand() % N + 1;
        if (uf.find(U) == uf.find(V)) goto A;  // 사이클을 가지면 채택하지 않는다.
        uf.uni(U, V);
        cout << U << ' ' << V << '\n';
        adj_list[U].push_back(V);
        adj_list[V].push_back(U);
    }
    return 0;
}
 
cs

돌리면 아래와 같은 결과를 출력한다.

1
2
3
4
5
6
7
7
7 2
7 6
4 3
6 1
4 7
1 5
cs

 

트리 모양을 로 확인해보면 다음과 같다.

 
 

 

 
 

주어진 그래프가 트리인지 판정하기

일단 트리는 undirected graph에서만 정의되기 때문에, undirected로 한정할 수 있고,

이 문제의 경우처럼, 유니온파인드로 판정하는게 비교적 쉬운 방법인 것 같다.

 

 

 

반응형

여기 참조


작성중

반응형

 

여기 참조함

 

아래처럼 그래프에서 사이클을 제거하고 트리형태로 만들면 신장트리라고 한다.

 

트리로 만들었기 때문에 간선의 수는 정점이N개 일때 정확히 N-1개가 된다.

트리일 경우 항상 간선은 N-1이 되는건 맞지만 위의 예시 처럼 반드시 직선이 되지는 않는다. 아래 MST예시 참조

모든 MST가 직선은 당연히 아니다.

 

최소신장트리는 신장트리중 간선 가중치합이 최소인 트리를 말한다.

간선가중치가 없을 때는 O(N-1)임이 명확하므로 Trivial 하다(이 문제 참조)

 

최소신장트리를 구하는데는 크루스칼알고리즘과 프림알고리즘이 있다.

 

둘다 음수가중치가 있어도 정상 동작가능하다.

 

크루스칼(Kruskal) 알고리즘(간선중심)

선행지식: 유니온 파인드(Union-Find)

 

간선중심이라 첫 간선이 최종답안에 포함되지 않을 수 있다.

> 근데 아닌듯. 소팅한다음에 최소 간선부터 시작인데 이건 무조건 포함하는듯하다.

가중치 오름차순으로 간선을 소팅한다음 union find로 사이클 체크하면서 사이클이 아닌경우 가중치합을 구해주면 된다.(무척간단)

중간과정에서는 정점별로 그룹이 생겨서 사이클도 판단하고 그렇지만, 유니온파인드 과정을 마치고 나면 모든 정점은 하나의 그룹에 포함되게 된다(이점 주의)

여기에 설명 잘되어 있고, 아래는 내 구현이다. 이 문제의 답안이기도 함

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
#include <bits/stdc++.h>
using namespace std;
struct node { int u, v, w; };
int MST_Kruskal(int max_v, vector<node>& v) {
    struct union_find {
        vector<int> parent;
        union_find(int n) {
            parent.resize(n + 1);
            for (int i = 0; i <= n; i++) parent[i] = i;
        }
        int find(int x) {
            int ox = x;
            while (parent[x] != x) x = parent[x];
            return parent[ox] = x;
        }
 
        void uni(int x, int y) {
            int xp = find(x), yp = find(y);
            parent[yp] = xp;
        }
    };
    sort(v.begin(), v.end(), [](node& a, node& b) {return a.w < b.w; });
    union_find uf(max_v);
    int ans = 0;
    for (auto a : v) {
        if (uf.find(a.u) == uf.find(a.v)) continue;
        uf.uni(a.u, a.v);
        ans += a.w;
    }
    return ans;
}
int V, E, A, B, C;
int32_t main()
{
    cin >> V >> E;
    vector<node> v;
    for(int i=0;i<E;i++cin >> A >> B >> C, v.push_back({ A,B,C });
    cout << MST_Kruskal(V, v) << endl;
    return 0;
}
 
cs

간결한 풀이를 위해 일반적인 인접리스트와 다르게 {u, v, w}자체를 백터로 전달했음에 주의(union-find특징상 이렇게 간단히 전달해도 잘 풀림)

 

위 코드의 경우 모든 정점이 사용되지 않아도 정상MST로 판정하는데 해당 부분을 수정한 코드는 아래와 같다.(base1도 추가)

struct node { int u, v, w; };
int MST_Kruskal(int max_v, vector<node>& v, bool base1=false) {
    if (base1) max_v++;
    struct union_find {
        vector<int> parent;
        int components;
        union_find(int n) : components(n) {
            parent.resize(n + 1);
            for (int i = 0; i <= n; i++) parent[i] = i;
        }
        int find(int x) {
            int ox = x;
            while (parent[x] != x) x = parent[x];
            return parent[ox] = x;
        }
 
        void uni(int x, int y) {
            int xp = find(x), yp = find(y);
            if (xp != yp) {
                parent[yp] = xp;
                components--;
            }
        }
    };

    sort(v.begin(), v.end(), [](node& a, node& b) {return a.w < b.w; });
    union_find uf(max_v);
    int ans = 0;
    for (auto a : v) {
        if (uf.find(a.u) == uf.find(a.v)) continue;
        uf.uni(a.u, a.v);
        ans += a.w;
    }
    
    if (uf.components != 1+base1) return -1;  // 모든 정점을 사용하지 않으면 -1리턴
    return ans;
}

 

크루스칼에는 소팅이 들어가기 때문에 보통 $O(E \times log(E))$로 계산하지만, 카운팅 소트가 가능한 경우는 $O(E)$로 계산되기도 하고,

전체가 다 소팅이 필요하다기 보다는 V-1개의 간선이 완성될때 까지만 필요하기 때문에, 힙소팅이 유리할수도 있는 측면이 있다.

관련하여 이런 논문도 발견

프림(Prim) 알고리즘(정점 중심)

선행지식: 너비 우선 탐색(Breadth-first search, BFS)다익스트라 알고리즘 Dijkstra (필수는 아님)

정점 중심이라 시작정점은 MST에 꼭 포함되게 된다.(모든 정점이 포함되므로)

프림은 여기보면 예제 보면 설명 잘되어 있고, 설명을 보면 Queue를 쓴다고 되어 있고 우선순위큐 이야기가 나오기 때문에, 다익스트라 변형으로 풀 수 있을 것 같은 느낌이 든다. 그런데 아래 예제를 보면..

 

다익스트라는 1번에서 거리상 유리한 2번을 방문한다음에 1번에서 2번째로 집어넣은 3번을 방문하게 된다. 즉 간선으로 보자면 [1,2]간선을 방문하고 [1,3]간선을 방문하게 되는데, 프림알고리즘 상에서는 [1,2]간선을 방문한다음 [2,3]간선과 [1,3]간선을 비교해서 [2,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
#include <bits/stdc++.h>
using namespace std;
struct node { int v, w; };
bool operator<(node l, node r) { return l.w > r.w; }
// 다익스트라와의 차이
int MST_Prim(int max_v, int start_v, vector<vector<node>>& adj_list) {
    vector<int> visit(max_v + 10);
    priority_queue<node> q; q.push({ start_v, 0 });
    int sum = 0;  // 1. 글로벌썸 처리
    while (q.size()) {
        node n = q.top(); q.pop();
        if (visit[n.v]) continue;  // 2. min_dist대신 visit처리
        visit[n.v] = 1;
        sum += n.w;
        for (auto adj : adj_list[n.v]) {
            q.push({ adj.v, adj.w });  // 3. visit처리를 여기서 하면 안됨
        }
    }
    return sum;
}
int V, E, A, B, C;
int32_t main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> V >> E;
    vector<vector<node>> adj_list(V + 1);
    for (int i = 0; i < E; i++)
        cin >> A >> B >> C,
        adj_list[A].push_back({ B,C }),
        adj_list[B].push_back({ A,C });
    cout << MST_Prim(V, 1, adj_list) << endl;
    return 0;
}
cs

 

크루스칼 vs 프림

일반적으로는 간선이 sparse 할수록 크루스칼이 유리하고 dense할수록 프림이 유리한걸로 알려져 있다.

 

크루스칼의 시간 복잡도는 정렬하는데 $O(E \times log(E))$정도 걸리고, 유니온파인드 하는데 $O(E)$정도 걸리므로, 전체적으로 $O(E \times log(E))$정도로 보면 될 것 같다.

프림의 복잡도는 다익스트라와 비슷하게 $O((V+E)\times log(V))$이다. (우선순위큐를 쓴 경우이며, 피보나치힙을 쓴경우는 좀 더 내려간다)

Dense한 그래프의 경우는 $E=V^2$정도이므로 크루스칼은 $O(V^2 log(V^2))$, 프림은 $O(V^2 log(V))$정도로 보면 될거 같다.

Sparse(V > E)한 그래프의 경우는 크루스칼은 $O(E \times log(E))$, 프림은 $O(V \times log(V))$가 되므로 크루스칼이 유리하다.

 

반응형

'Programming > Algorithm' 카테고리의 다른 글

트리DP  (0) 2020.04.15
세그멘트 트리  (0) 2020.04.13
유니온 파인드(Union-Find), 서로소 집합(Disjoint Set)  (0) 2020.04.10
그래프 최장거리, 최대이득, 최다비용등  (0) 2020.04.09
비트연산  (0) 2020.04.02

직관적이지 않은 동작이 있는데, 아래 예제를 통해 파악해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{    //찾으려는 값이 있는 경우,
    //lower_bound는 가장 왼쪽 위치를
    //upper_bound는 가장 오른쪽 보다 한칸 오른쪽 위치를 리턴(주의)
    int arr[] = { 1,2,3,4,5,5,5,6 };
    auto l = lower_bound(arr, arr + 85);
    auto r = upper_bound(arr, arr + 85);
    printf("l:%d, r:%d, *l:%d, *r:%d\n", l - arr, r - arr, *l, *r);
}
{    //찾으려는 값이 없는 경우,
    //lower_bound는 한칸 오른쪽 위치를,
    //upper_bound도 마찬가지로 한칸 오른쪽 위치를 리턴(이경우 두 값이 같음)
    int arr[] = { 1,2,3,4,6 };
    auto l = lower_bound(arr, arr + 55);
    auto r = upper_bound(arr, arr + 55);
    printf("l:%d, r:%d, *l:%d, *r:%d\n", l - arr, r - arr, *l, *r);
}
cs

출력:

1
2
l:4, r:7, *l:5, *r:6
l:4, r:4, *l:6, *r:6  
cs

 

찾으려는 값이 왼쪽에 더 가까워도, 한칸 오른쪽 위치를 리턴함에 주의하자.

1
2
3
4
vector<int> v = { 1,5,10,15,20 };
auto l = lower_bound(v.begin(), v.end(), 11);
auto r = upper_bound(v.begin(), v.end(), 11);
printf("l:%d, r:%d, *l:%d, *r:%d\n", l - v.begin(), r - v.begin(), *l, *r);
cs

출력: (15보다는 10에 가까운 11을 찾았음에도, 15의 인덱스 위치를 리턴함에 주의)

1
l:3, r:3*l:15*r:15
cs

 

 

 

반응형

'Programming' 카테고리의 다른 글

window에서 vscode로 원격 linux에 대한 ssh 개발환경 설정하기  (0) 2024.07.20
yaml  (0) 2024.03.02
디자인패턴  (0) 2023.08.17
라즈베리파이 초기 세팅  (0) 2023.01.20


ls -l 하면 파일의 퍼미션정보를 보여주는데, username, groupname도 순서대로 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
[wi.kim@wikim-osx ~]$ ls -l
total 326224
drwxrwxrwx@ 10 wi.kim  DNCO\Domain Users       320  4 26  2018 AgentInstaller_Mac64
drwxr-xr-x   2 wi.kim  DNCO\Domain Users        64  5 19  2017 AnacondaProjects
drwxr-xr-x   9 wi.kim  DNCO\Domain Users       288  4 18  2019 AndroidStudioProjects
drwx------   5 wi.kim  DNCO\Domain Users       160  1 29  2019 Applications
drwxr-xr-x   8 wi.kim  DNCO\Domain Users       256  2 21 14:53 CLionProjects
drwx------+ 21 wi.kim  DNCO\Domain Users       672  1 28 14:25 Desktop
drwx------+  5 wi.kim  DNCO\Domain Users       160  4  9 14:17 Documents
drwx------+  6 wi.kim  DNCO\Domain Users       192  4  9 17:16 Downloads
-rw-r--r--@  1 wi.kim  DNCO\Domain Users    145634 10 20  2016 KakaoTalk_Photo_2016-10-20-11-40-50_1.jpeg
-rw-r--r--@  1 wi.kim  DNCO\Domain Users    174827 10 28  2016 KakaoTalk_Photo_2016-10-28-16-36-25.jpeg
cs




id wi.kim 처럼 하면 다음처럼 기본적인 uid, gid와 더불어서 속한 모든 그룹에 대한 정보를 준다.

uid=513(wi.kim) gid=500(dev) groups=500(dev), 510(graphics)


특정 파일에 대한 user, group ownership 바꾸기

chmod wi.kim:mygroup filename 이렇게 하면 된다.



질문.

그룹퍼미션은 어떤경우에 쓰이는가

위의 rwxrwxrwx중에서 중간3개 그룹퍼미션 따라갈 것 같네..

프로그램을 실행할 경우 권한은 어떻게 되는가

파이선으로 간단히 테스트 해봤을때는 실행한 사람의 권한을 따라가는 것 같다.



반응형

'Programming > Linux' 카테고리의 다른 글

tmux and byobu  (0) 2020.09.21
vim  (0) 2020.09.20
NTP설정  (0) 2020.03.30
supervisor  (0) 2020.03.18
ssh 자동로그인(ssh-keygen)  (0) 2020.03.10

문제를 풀다보니까, 유니온-파인드 개념이 어려운게 아니라, 어려운 그리디 문제가 많은것 같다(형식만 유니온-파인드인)

 

일단 개념은 여기여기, 여기, 여기여기가 배우기 좋다.

교집합이 없는 서로소 집합(Disjoint Set)이라고도 불린다.

union연산과 find연산 단 2개만을 지원하는 자료구조로 disjoint한 집함들을 표현하는 자료구조.

따라서 도로 분할하는건 굉장히 힘들지만 보통 합치는 방향만 필요할때 유용.

이게 표현하려는 집합은 어떤 두 집합 사이에도 교집합 원소가 하나도 없고, 모든 집합의 합집합은 전체집합이라는 뜻.

따라서 파티션과 같다. 기본적으로 set마다 하나의 트리를 이루므로 forest(숲)의 형태를 띈다. (숲 = 트리의 집합)

 

 

근데 굳이 이걸 왜쓰는지가 중요한데,

트리구조 여러개를 다루면서 동적으로 변하는 구조에 대해서는 기존 STL만 가지고 핸들링하기 한계가 있는것 같음

근데 다른 자료구조에 비해 실무적인 연관성이 상당히 떨어져 보이기는 한다.

문제를 위해 꼬으고 꼬은 것처럼 보인달까? 

 

아무튼 disjoint set 마다 트리를 하나씩 가지게 하자는 개념인데..

 

초기화

최초 한 번만 수행되며, N개의 서로 독립적인 트리를 만들어 주는 단계 (모든 노드가 root 노드)

 

Find

Finx(x)는 노드 x의 부모중 루트를 반환하는 연산을 한다. 

disjoint set에서 임의의 두 노드들이 같은 집합에 속해있는지 확인하는 방법은 Find()로 나오는 값이 같은지 보는것.

Find연산의 시간복잡도는 트리의 높이 h에 비례 하므로 경로 압축과정을 통해 넓고 얕게 트리를 유지하는 코드가 들어가게 된다.

 

 

 --> 

이렇게..

 

Union

초기화랑 find만 있으면 당연히 의미가 없고, 같은 set에 포함된 노드들을 합치는 연산이 필요하다. 그게 union이고, 

두개의 트리를 합치는 작업이 들어가는데, 합쳐진 트리의 높이를 낮게 유지하기 위해 작은 트리의 부모를 큰 트리의 root로 하는 작업을 해주게 된다. 이걸 랭크압축이라고 하며, 여기를 참조하자.

 

시간복잡도

union연산의 복잡도도 find연산이 지배함.

find연산은 노드개수 N개, 연산수 M개일때 O(Mlog*N)인데 log*N이 워낙 작은 값이라 O(M)으로 봐도 좋다고 한다.

즉 거의 상수시간에 find, union연산이 되는것

 

 

재활용코드

아래는 내가 처음 짜본 union-find 코드이다. 이 문제의 답안이기도 하다.

경로압축과 랭크압축중 경로압축만 간단하게 되어 있다.(경로압축 안할경우 TLE발생)

코드 작성 자체는 매우 심플하고 구현하기 쉬웠다.

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
#include <bits/stdc++.h>
using namespace std;
#define REP(i,n) for(int i=0;i<(int)(n);i++)
#define REP1(i,n) for(int i=1;i<=(int)(n);i++)
int n, m, op, x, y;
int find(int x, vector<int>&parent){
    int ox = x;
    while(parent[x]!=x) x=parent[x];
    parent[ox]=x;  // 여기, 경로압축
    return x;
}
 
void uni(int x, int y, vector<int>&parent){
    int xp = find(x,parent), yp = find(y, parent);
    //임의로 y를 x에 붙여보자.
    parent[yp]=xp;
}
 
int32_t main()
{
    scanf("%d%d"&n,&m);
    vector<int> parent(n+1);REP1(i,n)parent[i]=i;
    REP(i,m){
        scanf("%d%d%d"&op,&x,&y);
        if(op==0){  //union
            uni(x,y,parent);
        }else{  //find
            printf("%s\n", find(x, parent)==find(y, parent)?"YES":"NO");
        }
    }
    return 0;
}
cs

 

실제 재활용하기에는 아래코드가 더 좋은것 같다(캡슐화되어 있고, 좀더 효율적인 경로압축, 랭크압축 되어 있음)

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
#include <bits/stdc++.h>
using namespace std;
#define REP(i,n) for(int i=0;i<(int)(n);i++)
#define REP1(i,n) for(int i=1;i<=(int)(n);i++)
const int INF = (int)1e9;
 
struct union_find{
    vector<int> p,r;
    union_find(int n){
        p.resize(n+1), r.resize(n+1);
        for (int i=0; i<=n; i++) {
            p[i] = i;
        }
    }
    int find(int x){
        if(p[x] == x) return x;
        else return p[x] = find(p[x]);
    }
 
    void uni(int a, int b){
        a = find(a);
        b = find(b);
        if(r[a] < r[b]) p[b] = a;
        else p[a] = b;
        if(r[a] == r[b]) r[a]++;
    }
};
 
int n, m, op, x, y;
int32_t main()
{
#ifndef ONLINE_JUDGE
    freopen("input.txt""rt", stdin);
#endif
 
    scanf("%d%d"&n,&m);
    union_find uf(n);
    REP(i,m){
        scanf("%d%d%d"&op,&x,&y);
        if(op==0) uf.uni(x,y);
        else printf("%s\n", uf.find(x)==uf.find(y)?"YES":"NO");
    }
    return 0;
}
 
cs

 

아래는 uni()함수에서 합쳐진 집합의 크기를 리턴하도록 개선한 버전(내꺼 초기버전 베이스로 만들었다)

이 문제의 답안이기도 하다. 이버전이 랭크압축한거 보다 오히려 왼쪽으로 정렬되는 효과가 있어서 일부문제에서는 더 좋았다.

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 <bits/stdc++.h>
using namespace std;
#define REP(i,n) for(int i=0;i<(int)(n);i++)
#define REP1(i,n) for(int i=1;i<=(int)(n);i++)
 
struct union_find {
    vector<int> parent, cnt;
    union_find(int n) {
        parent.resize(n + 1), cnt.resize(n + 1);
        REP1(i, n)parent[i] = i, cnt[i] = 1;
    }
    int find(int x) {
        if (parent[x] == x) return x;
        else return parent[x] = find(parent[x]);
    }
 
    int uni(int x, int y) {
        int xp = find(x), yp = find(y);
        //임의로 y를 x에 붙여보자.
        if (xp != yp) {
            cnt[xp] += cnt[yp];
            parent[yp] = xp;
        }
        return cnt[xp];  // 합친 집합의 개수를 리턴
    }
};
 
int T, F, ix;
string s1, s2;
int32_t main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> T; while (T--) {
        cin >> F;
        map<stringint> m; ix = 0;
        union_find uf(2 * F);
        while (F--) {
            cin >> s1 >> s2;
            if (m.count(s1) == 0)m[s1] = ++ix;
            if (m.count(s2) == 0)m[s2] = ++ix;
            cout << uf.uni(m[s1], m[s2]) << '\n';
        }
    }
    return 0;
}
 
cs

 

 

 

 

반응형

'Programming > Algorithm' 카테고리의 다른 글

세그멘트 트리  (0) 2020.04.13
최소신장트리(MST, Minimum Spanning Tree)  (0) 2020.04.12
그래프 최장거리, 최대이득, 최다비용등  (0) 2020.04.09
비트연산  (0) 2020.04.02
사이클 검출(Cycle Detection)  (0) 2020.03.30

+ Recent posts