티스토리 뷰

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

 

15316번: 현수시티

여러분도 모두 알다시피, 경기과학고는 수원시 장안구 송죽동에 세워져 있다. 하지만, 사실 여러분이 아는 송죽동이 전부가 아니었으니... 여러분이 잠든 사이, 송죽동은 음침하고 위험한 도시 '

www.acmicpc.net

문제 요약 : 정점과 간선이 주어지고 쿼리가 주어지는데 i번째 간선을 연결했다가 끊었다가할 때 어떤 정점 u, v가 연결되어있는지 확인하는 문제이다.

먼저 u,v 가 연결되어있는지를 확인하는 작업은 union find 를 사용한다.

그런데 끊어야 하기 때문에 path compression 은 사용할 수 없고 rank compression만 사용해야한다.

그리고 특정 작업을 롤백하기 위해서 합쳐질 때의 정점과 정점의 rank 변화량을 스택에 저장해주면 된다.

#define N 200001
int p[N];
int r[N];
struct trip {
    int x, y, c;
};
stack<trip>st;

parent 와 rank, 그리고 스택을 선언했다.

find 함수에서path compression을 쓰면

int find(int x) {
    if (x == p[x]) return x;
    return p[x] = find(p[x]);
}

로 구현하지만 이렇게 되면 롤백이 불가능하므로

int find(int x) {
    if (x == p[x]) return x;
    return find(p[x]);
}

로 구현한다.

int merge(int x, int y) {
    x = find(x), y = find(y);
    if (x == y) return 0;
    if (r[x] < r[y]) swap(x, y);
    p[y] = x;
    if (r[x] == r[y]) {
        r[x]++;
        st.push({ x,y,1 });
    }
    else st.push({ x,y,0 });
    return 1;
}
void rollback(int cnt) {
    while (cnt--) {
        trip e = st.top(); st.pop();
        p[e.y] = e.y;
        if (e.c) r[e.x]--;
    }
}

x,y 를 merge할때 rank가 변했음을 같이 저장해주고 있다.
rollback은 스택에 있는 것을 꺼내서
원래대로 되돌려주는 작업만을 하면 된다.

 

그 다음 중요한 것은 어떤 데이터들을 어떻게 저장할 것인가가 필요하다.
우선 edge들이 들어오므로 edge를 배열로 저장해야 할 것이다.
그리고 정점이 연결되어있는지에 대한 쿼리를 처리해야하는데 이 때 쿼리를 위한 정점도 저장해두자.
그리고 어떤 간선이 사용되고 있는지, 그리고 그 간선on/off 쿼리를 위해 간선의 상태가 변할 때를 저장해줄 배열이 하나 필요하다.

pair<int, int>ans[N]; // 2번 쿼리에 대한 정점을 저장
pair<int, int>edge[N]; //edge 를 저장
int queryNum[N]; // Q개의 쿼리가 들어오는데 2번쿼리가 들어올 때마다 1씩 값을 증가시켜서 어떤 1번쿼리가 몇 번째의 2번 쿼리에 영향을 줄지 계산하기 위한 배열
int light[N]; //어떤 간선이 on / off 되었는지를 나타내는 배열
int before[N]; // 어떤 간선이 on 되는 시점을 저장하는 배열

위와 같이 선언해준다.
그 다음은 간선들이 i번째 2번쿼리부터 on이고 j번째 2번쿼리에 off되었을 때의 수명을 저장하기 위한 배열이 필요하다.

struct Life {
    int idx, s, e; // idx번째 간선은 2번째 쿼리 s ~ e 까지의 구간에 사용될 것이다.
};
vector<Life> lives;

1번 쿼리가 몇 번들어올지 모르므로 동적으로 추가할 수 있는 vector로 선언했다.

마지막으로 세그먼트트리가 필요하다.
2번쿼리만을 생각해보면 2번쿼리가 x개 들어온다면

2번쿼리 넘버링을 1 2 ... x 로 할 수 있을 것이고 이것을 구간으로 생각한다면
1번 쿼리가 2번의 특정 구간에 영향을 주는 것으로 생각할 수 있다.

 

이제 우리는 x를 구간으로 삼는 세그먼트트리를 만들면 된다.
세그먼트트리의 한 노드는 특정 구간을 담당하게 만들어줄 수 있다.
즉 이 구간이 위에 선언한 Life 의 s, e에 잘 매칭해주면 된다.
우리는 특정 구간(i번째 2번쿼리 ~ j번째 2번쿼리)을 담당하는 세그먼트트리의 노드에 해당할 간선들을 모두 저장해주면 해결할 수 있다.

그렇기 때문에 세그먼트 트리는

vector<int> seg[4 * N];

로 선언하고
update 와 query 함수를 작성해주자.

void update(int node, int s, int e, int i, int j, int val) {
    if (e<i || j < s) return;
    if (i <= s && e <= j) {
        seg[node].push_back(val);
        return;
    }
    int m = s + e >> 1;
    update(node * 2, s, m, i, j, val);
    update(node * 2 + 1, m + 1, e, i, j, val);
}

void query(int node, int s, int e) {
    int count = 0;
    for (int i : seg[node]) count += merge(edge[i].first, edge[i].second);
    if (s == e) {
        if (find(ans[s].first) == find(ans[s].second)) cout << "YES\n";    
        else cout << "NO\n";
        rollback(count);
        return;
    }
    int m = s + e >> 1;
    query(node * 2, s, m);
    query(node * 2 + 1, m + 1, e);
    rollback(count);
}

update에서는 구간에 해당하는 간선 번호(val)를 모두 저장하게 해두었고
query에서는 노드의 하위구간에 영향을 미칠 친구들을 모두 merge한 후 rollback시켜주고 있다.

이제 메인을 작성해보자

int main() {
    ios_base::sync_with_stdio(false), cin.tie(0);
    int n, m; cin >> n >> m;
    for (int i = 1; i <= n; ++i) p[i] = i;
    for (int i = 1, u, v; i <= m; ++i) {
        cin >> edge[i].first >> edge[i].second;
    }
    for (int i = 1; i <= m; ++i) {
        cin >> light[i];
    }
    int Q; cin >> Q;
    for (int i = 1, cmd, u, v; i <= Q; ++i) {
        cin >> cmd;
        if (cmd == 1) {
            cin >> u;
        }
        else {
            cin >> u >> v;
        }
    }
}

유니온파인드를 위한 p를 초기화하고 입력을 받는 부분이다.
cmd == 1 이면 쿼리카운트는 증가시킬 필요가 없고
cmd==2라면 쿼리카운트는 증가시켜야 한다.
이를 추가해주자.

if (cmd == 1) {
    cin >> u;
    queryNum[i] = queryNum[i - 1];
}
else {
    cin >> u >> v;
    queryNum[i] = queryNum[i - 1] + 1;
}

그 다음은 cmd==1일 때 light[u] 가 on이라면 off시켜주고 off 라면 on시켜주면서 on된 시점을 저장해주면 된다.
그리고 cmd==2라면 해답을 구하기위한 정점들을 저장해주자.

    for (int i = 1, cmd, u, v; i <= Q; ++i) {
        cin >> cmd;
        if (cmd == 1) {
            cin >> u;
            queryNum[i] = queryNum[i - 1];
            if (light[u]) {
                light[u] = 0;
            }
            else {
                light[u] = 1;
                before[u] = i;
            }
        }
        else {
            cin >> u >> v;
            queryNum[i] = queryNum[i - 1] + 1;
            ans[queryNum[i]] = { u,v };
        }
    }

이제 queryNum이 저장된 형태를 살펴보면
1 1 1 2 2 2 2 3 3 3 3 과 같은 형태일 것이다.
굵게 표시된 부분은 2번쿼리가 실행되었을 때이므로 굵게 표시된 이후의 기본 숫자들은 모두 1번쿼리가 실행된 것이다.
이를 통해 만약 1번쿼리가 들어온 시점x의 queryNum[x] 가 2라고 생각해보면 이미 2번쿼리는 실행되고 난 이후일 것이다. 우리는 before 배열에 on되는 시점을 저장해두었으므로 queryNum[before[y]] 는 이미 실행된 쿼리이므로 1을 더해주면 된다. 즉 queryNum[before[y]]이 2였다면 3번 쿼리부터 영향을 주게 될 것이라는 뜻이다.
그럼 light[u]가 on일 때 1번쿼리가 들어왔다면
이제 끄는 시점이 된 것이고
시작지점은 queryNum[before[u]] + 1이 되는 것이고
끝 지점은 queryNum[i] 가 되는 것이다.
이 것이 Life를 이루게 될 것이다.

            if (light[u]) {
                light[u] = 0;
                int start = queryNum[before[u]] + 1;
                int end = queryNum[i];
                lives.push_back({ u, start, end });
            }

와 같이 될 것이다.
즉 현재 상태를 보면

    for (int i = 1, cmd, u, v; i <= Q; ++i) {
        cin >> cmd;
        if (cmd == 1) {
            cin >> u;
            queryNum[i] = queryNum[i - 1];
            if (light[u]) {
                light[u] = 0;
                int start = queryNum[before[u]] + 1;
                int end = queryNum[i];
                lives.push_back({ u, start, end });
            }
            else {
                light[u] = 1;
                before[u] = i;
            }
        }
        else {
            cin >> u >> v;
            queryNum[i] = queryNum[i - 1] + 1;
            ans[queryNum[i]] = { u,v };
        }
    }

일 것이다.
그런데 잘 생각해보면 끝까지 켜져있는 경우가 있을 수 있다. 이것은 구간 끝까지 살아서 영향을 주는 간선이 될 것이므로 이 것을 처리해 주자.

for (int i = 1; i <= m; ++i) if (light[i]) lives.push_back({ i, queryNum[before[i]] + 1, queryNum[Q] });

그럼 마지막으로 모아둔 모든 life를 이용해서 세그먼트트리를 업데이트 해주자.

for (Life& l : lives) update(1, 1, queryNum[Q], l.s, l.e, l.idx);

세그먼트트리노드는 1부터 시작하며 1~queryNum[Q] 의 구간을 다루고 있으며 해당 라이프의 구간 l.s, l.e와 간선의 번호 l.idx를 업데이트 해주면 된다.
마지막엔 query를 한번만 실행해주면 된다.

query(1, 1, queryNum[Q]);

이유는 세그먼트트리는 이진트리로 모든 노드를 탐색하면되고 이 때 순서가 왼쪽이 번호가 작은 구간이 들어있기 때문에 리프노드에 도달했을 때 2번쿼리의 답을 차례대로 출력해줄 것이다.

코드최종

#include <iostream>
#include <vector>
#include <map>
#include <stack>
using namespace std;
#define N 200001
int p[N];
int r[N];
struct trip {
    int x, y, c;
};
stack<trip>st;
int find(int x) {
    if (x == p[x]) return x;
    return find(p[x]);
}
int merge(int x, int y) {
    x = find(x), y = find(y);
    if (x == y) return 0;
    if (r[x] < r[y]) swap(x, y);
    p[y] = x;
    if (r[x] == r[y]) {
        r[x]++;
        st.push({ x,y,1 });
    }
    else st.push({ x,y,0 });
    return 1;
}
void rollback(int cnt) {
    while (cnt--) {
        trip e = st.top(); st.pop();
        p[e.y] = e.y;
        if (e.c) r[e.x]--;
    }
}
pair<int, int>ans[N];
pair<int, int>edge[N];
int queryNum[N];
int light[N];
int before[N];
struct Life {
    int idx, s, e;
};
vector<Life> lives;
vector<int> seg[4 * N];
void update(int node, int s, int e, int i, int j, int val) {
    if (e<i || j < s) return;
    if (i <= s && e <= j) {
        seg[node].push_back(val);
        return;
    }
    int m = s + e >> 1;
    update(node * 2, s, m, i, j, val);
    update(node * 2 + 1, m + 1, e, i, j, val);
}

void query(int node, int s, int e) {
    int count = 0;
    for (int i : seg[node]) count += merge(edge[i].first, edge[i].second);
    if (s == e) {
        if (find(ans[s].first) == find(ans[s].second)) cout << "YES\n";    
        else cout << "NO\n";
        rollback(count);
        return;
    }
    int m = s + e >> 1;
    query(node * 2, s, m);
    query(node * 2 + 1, m + 1, e);
    rollback(count);
}
int main() {
    ios_base::sync_with_stdio(false), cin.tie(0);
    int n, m; cin >> n >> m;
    for (int i = 1; i <= n; ++i) p[i] = i;
    for (int i = 1; i <= m; ++i) {
        cin >> edge[i].first >> edge[i].second;
    }
    for (int i = 1; i <= m; ++i) {
        cin >> light[i];
    }
    int Q; cin >> Q;
     for (int i = 1, cmd, u, v; i <= Q; ++i) {
        cin >> cmd;
        if (cmd == 1) {
            cin >> u;
            queryNum[i] = queryNum[i - 1];
            if (light[u]) {
                light[u] = 0;
                int start = queryNum[before[u]] + 1;
                int end = queryNum[i];
                lives.push_back({ u, start, end });
            }
            else {
                light[u] = 1;
                before[u] = i;
            }
        }
        else {
            cin >> u >> v;
            queryNum[i] = queryNum[i - 1] + 1;
            ans[queryNum[i]] = { u,v };
        }
    }
    for (int i = 1; i <= m; ++i) if (light[i]) lives.push_back({ i, queryNum[before[i]] + 1, queryNum[Q] });
    for (Life& l : lives) update(1, 1, queryNum[Q], l.s, l.e, l.idx);
    query(1, 1, queryNum[Q]);
}

 

'Algorithm' 카테고리의 다른 글

[LeetCode] Set Matrix Zeroes 풀이  (0) 2021.08.14
[Dynamic Programming] DP 동적계획  (0) 2021.08.13
[Graph] SCC(Strongly Connected Components) 강한 결합 요소(1)  (0) 2021.08.12
포스팅 시작  (0) 2021.08.12
해시  (0) 2021.05.30
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
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
글 보관함