-
중급반 7주차 풀이 - DSU, MSTALOHA - 2022/ALOHA - 중급반 (2022 1학기) 2022. 5. 25. 15:34
1. [1717] 집합의 표현 (Gold IV)
더보기Union-Find 구현 문제입니다. 특별한 것도 없습니다.
int par[1000020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } void Main(){ int n, q; cin >> n >> q; for (int i = 0; i <= n; i++){ par[i] = i; } while (q--){ int typ; cin >> typ; if (typ == 0){ int a, b; cin >> a >> b; uni(a, b); } if (typ == 1){ int a, b; cin >> a >> b; cout << ( fnd(a) == fnd(b) ? "YES" : "NO" ) << endl; } } }
2. [1976] 여행 가자 (Gold IV)
더보기연결된 도시들끼리 모두 Union을 해봅시다.
그럼 같은 집합에 속한 도시들끼리는 서로 여행을 갈 수 있고, 그렇지 않으면 서로 여행을 갈 수 없게 됩니다.
그러니 여행 계획에서 다른 집합으로 넘어가야 하는 일이 발생하는지 체크해주면 됩니다.
int par[220]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } int arr[1020]; void Main(){ int n, m; cin >> n >> m; for (int i = 1; i <= n; i++){ par[i] = i; } for (int i = 1; i <= n; i++){ for (int j = 1; j <= n; j++){ int x; cin >> x; if (x){ uni(i, j); } } } for (int i = 1; i <= m; i++){ cin >> arr[i]; } for (int i = 1; i < m; i++){ if ( fnd(arr[i]) != fnd(arr[i+1]) ){ cout << "NO"; return; } } cout << "YES"; return; }
3. [18116] 로봇 조립 (Gold IV)
더보기이번에는 집합의 크기를 추가적으로 저장해야 합니다.
초기 상태에서는 모든 집합의 크기가 1이고, 두 다른 집합을 합치면 새로운 집합의 크기는 두 집합 크기의 합이 되므로 이를 그대로 구현해주면 됩니다.
const int N = 1000000; int par[1000020], siz[1000020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ int pa = fnd(a), pb = fnd(b); if (pa == pb){ return; } par[pa] = pb; siz[pb] += siz[pa]; } void Main(){ for (int i = 1; i <= N; i++){ par[i] = i; siz[i] = 1; } int q; cin >> q; while (q--){ char typ; cin >> typ; if (typ == 'I'){ int a, b; cin >> a >> b; uni(a, b); } if (typ == 'Q'){ int a; cin >> a; cout << siz[fnd(a)] << endl; } } }
4. [1197] 최소 스패닝 트리 (Gold IV)
더보기MST 연습 문제입니다. Kruskal로 풀어주면 됩니다.
int par[10020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } void Main(){ int n, m; cin >> n >> m; for (int i = 1; i <= n; i++){ par[i] = i; } priority_queue< pi3, vector<pi3>, greater<pi3> > pq; while (m--){ int v, w, x; cin >> v >> w >> x; pq.push({ x, {v, w} }); } int ans = 0; while (!pq.empty()){ int x = pq.top().fr; int v = pq.top().sc.fr, w = pq.top().sc.sc; pq.pop(); if (fnd(v) == fnd(w)){ continue; } uni(v, w); ans += x; } cout << ans; }
더보기MST 연습 문제입니다. Prim으로 풀어주면 됩니다.
bool chk[10020]; vector<pi2> adj[10020]; void Main(){ int n, m; cin >> n >> m; while (m--){ int v, w, x; cin >> v >> w >> x; adj[v].push_back({w, x}); adj[w].push_back({v, x}); } priority_queue< pi2, vector<pi2>, greater<pi2> > pq; chk[1] = 1; for (pi2 p : adj[1]){ pq.push({p.sc, p.fr}); } int ans = 0; while (!pq.empty()){ int d = pq.top().fr, v = pq.top().sc; pq.pop(); if (chk[v]){ continue; } chk[v] = 1; ans += d; for (pi2 p : adj[v]){ pq.push({p.sc, p.fr}); } } cout << ans; }
5. [1647] 도시 분할 계획 (Gold IV)
더보기MST를 구하되, 두 마을로 나눌 수 있으니 \( N-2 \)개의 간선만 사용하면 됩니다.
int par[100020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } void Main(){ int n, m; cin >> n >> m; for (int i = 1; i <= n; i++){ par[i] = i; } priority_queue< pi3, vector<pi3>, greater<pi3> > pq; while (m--){ int v, w, x; cin >> v >> w >> x; pq.push({ x, {v, w} }); } int cnt = n-2, ans = 0; while (cnt){ int x = pq.top().fr; int v = pq.top().sc.fr, w = pq.top().sc.sc; pq.pop(); if (fnd(v) == fnd(w)){ continue; } uni(v, w); ans += x; cnt -= 1; } cout << ans; }
6. [14621] 나만 안되는 연애 (Gold III)
더보기MST를 구하면 되지만, '남초' - '여초'를 연결하는 간선들만 남겨야 합니다.
또한, MST가 정말 모든 정점을 포함하고 있는지도 확인해줘야 합니다.
그러니 그렇게 해주면 됩니다.
이 문제에서 '경로'가 실제 두 정점간의 경로와는 관련없음에 주의하세요.
int par[1020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } bool chk[1020]; void Main(){ int n, m; cin >> n >> m; for (int i = 1; i <= n; i++){ char ch; cin >> ch; chk[i] = ch=='M'; } for (int i = 1; i <= n; i++){ par[i] = i; } priority_queue< pi3, vector<pi3>, greater<pi3> > pq; while (m--){ int v, w, x; cin >> v >> w >> x; if (chk[v] == chk[w]){ continue; } pq.push({ x, {v, w} }); //cout << v << ' ' << w << " / " << x << endl; } int cnt = n-1, ans = 0; while (!pq.empty() && cnt){ int x = pq.top().fr; int v = pq.top().sc.fr, w = pq.top().sc.sc; pq.pop(); if (fnd(v) == fnd(w)){ continue; } uni(v, w); ans += x; cnt -= 1; } cout << (cnt ? -1 : ans); }
7. [2887] 행성 터널 (Gold I)
더보기간선의 개수가 \( O(N^2) \)이 되는, 무서운 문제입니다.
하지만 MST에 포함될 수 있는 후보는 "한 축을 기준으로 정렬한 뒤, 인접한 도시들을 잇는 간선"들밖에 되지 않습니다.
만약 그렇지 않다고 합시다. 즉, 어느 축으로도 정렬해도 인접하지 않지만 MST에 "포함되어야만 하는" 간선 \( v \leftrightarrow w \)가 존재한다고 합시다.
정의에 의해, 어떤 축으로 정렬했을 때 \( v \)와 \( w \) 사이에 \( x \)라는 정점이 존재해야 합니다.
그럼, \( v \leftrightarrow x \leftrightarrow w \)의 두 간선을 사용할 수 있게 되고, \( v \le x \le w \)일 때 |v-x| + |x-w| = |v-w| \)가 되니, \( v \leftrightarrow w \) 대신 \( v \leftrightarrow x \leftrightarrow w \)의 두 간선을 써줄 수 있게 됩니다.
즉, \( v \leftrightarrow w \)가 항상 MST에 들어가야 한다는 가정에 위배됩니다.
한 축을 기준으로 정렬한 뒤, 인접한 도시들을 잇는 간선은 축 하나당 \( O(N) \)개니, 이제 약 \( 3N \)개의 간선으로 MST를 구해주면 됩니다.
#define x fr.fr #define y sc.fr #define z sc.sc #define idx fr.sc int par[100020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } pl4 pos[100020]; inline ll dis(pl4 p1, pl4 p2){ return min({ abs(p1.x-p2.x), abs(p1.y-p2.y), abs(p1.z-p2.z) }); } void Main(){ int n; cin >> n; for (int i = 1; i <= n; i++){ cin >> pos[i].x >> pos[i].y >> pos[i].z; pos[i].idx = i; } for (int i = 1; i <= n; i++){ par[i] = i; } priority_queue< pl3, vector<pl3>, greater<pl3> > pq; sort(pos+1, pos+n+1, [](pl4 a, pl4 b){ return a.x < b.x; }); for (int i = 1; i < n; i++){ pq.push({ dis(pos[i], pos[i+1]), {pos[i].idx, pos[i+1].idx} }); } sort(pos+1, pos+n+1, [](pl4 a, pl4 b){ return a.y < b.y; }); for (int i = 1; i < n; i++){ pq.push({ dis(pos[i], pos[i+1]), {pos[i].idx, pos[i+1].idx} }); } sort(pos+1, pos+n+1, [](pl4 a, pl4 b){ return a.z < b.z; }); for (int i = 1; i < n; i++){ pq.push({ dis(pos[i], pos[i+1]), {pos[i].idx, pos[i+1].idx} }); } int cnt = n-1, ans = 0; while (!pq.empty() && cnt){ int d = pq.top().fr; int v = pq.top().sc.fr, w = pq.top().sc.sc; pq.pop(); if (fnd(v) == fnd(w)){ continue; } uni(v, w); ans += d; cnt -= 1; } cout << (cnt ? -1 : ans); }
8. [20040] 사이클 게임 (Gold IV)
더보기MST 구하던대로 해주면 됩니다.
간선을 하나씩 입력받으면서 잇다가 이미 두 정점이 같은 집합에 속해있을 때 답을 출력하고 종료해주면 됩니다.
만약 위 경우가 끝까지 나오지 않는다면 0을 출력하고 종료해야 함에 주의하세요.
int par[500020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } void Main(){ int n, m; cin >> n >> m; for (int i = 0; i < n; i++){ par[i] = i; } for (int ans = 1; ans <= m; ans++){ int v, w; cin >> v >> w; if (fnd(v) == fnd(w)){ cout << ans; return; } uni(v, w); } cout << 0; }
9. [14595] 동방 프로젝트 (Large) (Gold II)
더보기Union 연산을 구간에 한 번에 넣는 문제입니다.
이는 집합의 루트를 맨 오른쪽에 놓는 방식으로 빠르게 해결할 수 있으며, 이 이유는
집합의 루트를 찾으면 그 바로 오른쪽 정점은 항상 다른 집합에 속하기 때문입니다.
int par[1000020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } void Main(){ int n; cin >> n; for (int i = 1; i <= n; i++){ par[i] = i; } int q; cin >> q; while (q--){ int st, ed; cin >> st >> ed; int ptr = fnd(st); while (ptr < ed){ uni(ptr, ptr+1); ptr = fnd(ptr+1); } } int ans = 0; for (int i = 1; i <= n; i++){ ans += i==fnd(i); } cout << ans; }
10. [17132] 두더지가 정보섬에 올라온 이유 (Platinum V)
더보기가중치가 큰 간선부터 이으면서 양쪽 집합에 생기는 새로운 경로의 수 (= 두 집합의 크기의 곱)만큼 답에 더해주면 됩니다.
즉, 간선을 이으면서 새로 생기는 모든 경로는 정확히 그 가중치와 동일한 행복도를 지니게 됩니다.
이유는 간단한데, 지금까지 이은 모든 간선은 이 간선보다 가중치가 크거나 같기 때문입니다.
ll ans = 0; int par[100020], siz[100020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b, ll x){ int pa = fnd(a), pb = fnd(b); ans += x*siz[pa]*siz[pb]; par[pa] = pb; siz[pb] += siz[pa]; } void Main(){ int n; cin >> n; for (int i = 1; i <= n; i++){ par[i] = i; siz[i] = 1; } priority_queue<pi3> pq; for (int i = 1; i < n; i++){ int v, w, x; cin >> v >> w >> x; pq.push({ x, {v, w} }); } while (!pq.empty()){ int x = pq.top().fr; int v = pq.top().sc.fr, w = pq.top().sc.sc; pq.pop(); uni(v, w, x); } cout << ans; }
11. [13306] 트리 (Platinum IV)
더보기쿼리를 주어진 순서대로 처리하기에는 너무 어려운 문제입니다.
하지만 이를 역순으로 처리한다면?
트리의 최종 상태인 모든 정점이 다 나눠져 있는 상태에서 시작해서
0번 쿼리로 두 정점을 잇고, 1번 쿼리로 두 정점이 같은 트리 (= 집합)에 속했는지 확인하는 문제가 됩니다.
익숙하지 않나요? 위 문제는 "1. [1717] 집합의 표현"과 정확히 동일한 문제이기 때문입니다!
정확히는, 두 정점을 이을 때 (나, 나의 부모)를 잘 찾아서 잇는다는 점이 다르긴 합니다.
근데 이건 그냥 각 정점별로 부모를 저장하면 간단하게 처리됩니다.
+ 쿼리를 역순으로 처리하니까 출력도 역순으로 해야 함에 주의하세요.
+ Q는 최대 20만이지만, 입력되는 쿼리의 개수는 최대 40만임에 주의하세요.
int par[200020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } int ptr[200020]; pi3 qry[400020]; void Main(){ int n, q; cin >> n >> q; q += n-1; for (int i = 1; i <= n; i++){ par[i] = i; } for (int i = 2; i <= n; i++){ cin >> ptr[i]; } for (int i = 1; i <= q; i++){ int typ; cin >> typ; if (typ == 0){ int v; cin >> v; qry[i] = { 0, {v, ptr[v]} }; } if (typ == 1){ int v, w; cin >> v >> w; qry[i] = { 1, {v, w} }; } } stack<bool> ans; for (int i = q; i >= 1; i--){ int typ = qry[i].fr; int v = qry[i].sc.fr, w = qry[i].sc.sc; if (typ == 0){ uni(v, w); } if (typ == 1){ ans.push( fnd(v) == fnd(w) ); } } while (!ans.empty()){ bool res = ans.top(); ans.pop(); cout << (res ? "YES" : "NO") << endl; } }
12. [17490] 일감호에 다리 놓기 (Gold III)
더보기공사 중이 아니라 서로 건너갈 수 있는 강의동을 하나의 집합으로 묶으면,
자명히 하나의 강의동 집합에서 와우도까지 가는데 필요한 돌의 개수는 \( \min S_{i} \)가 됩니다.
그러니 이 문제도 결국 Union-Find를 하면서 각 집합별로 필요한 돌의 개수의 최솟값을 같이 관리해주면 됩니다.
\( M \le 1 \)이라서 돌 없이도 이동할 수 있는 경우를 예외처리해야 함에 주의하세요.
int par[1000020], cnt[1000020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ int pa = fnd(a), pb = fnd(b); par[pa] = pb; cnt[pb] = min(cnt[pb], cnt[pa]); } bool chk[1000020]; void Main(){ int n, m; ll k; cin >> n >> m >> k; if (m <= 1){ cout << "YES"; return; } for (int i = 1; i <= n; i++){ par[i] = i; cin >> cnt[i]; } for (int i = 1; i <= m; i++){ int v, w; cin >> v >> w; if ((v+1)%n == w%n){ chk[v] = 1; } if ((w+1)%n == v%n){ chk[w] = 1; } } for (int i = 1; i <= n; i++){ if (!chk[i]){ if (i != n){ uni(i, i+1); } else{ uni(n, 1); } } } ll ans = 0; for (int i = 1; i <= n; i++){ if (fnd(i) != i){ continue; } ans += cnt[i]; } cout << (ans <= k ? "YES" : "NO"); }
13. [1774] 우주신과의 교감 (Gold III)
더보기이미 연결된 ( = 필수적으로 연결해야 하는) 간선들이 있는 MST입니다.
풀이는 간단한데, 어차피 연결할 거 그냥 시작하기 전에 연결해주고 시작해주면 됩니다.
간선의 가중치 = 두 정점의 거리 이고, 실수오차는 최대한 피하는 걸 추천하는 사람으로서
최종 답인 ans에 더할 때까지는 가중치를 두 정점의 거리의 제곱으로 저장했습니다.
int par[1020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } inline ll dis(pl2 p1, pl2 p2){ ll y = p1.fr-p2.fr, x = p1.sc-p2.sc; return y*y + x*x; } pl2 pos[1020]; void Main(){ int n, m; cin >> n >> m; for (int i = 1; i <= n; i++){ par[i] = i; } for (int i = 1; i <= n; i++){ cin >> pos[i].fr >> pos[i].sc; } priority_queue< pl3, vector<pl3>, greater<pl3> > pq; for (int i = 1; i <= n; i++){ for (int j = i+1; j <= n; j++){ pq.push({ dis(pos[i], pos[j]), {i, j} }); } } while (m--){ int v, w; cin >> v >> w; uni(v, w); } ld ans = 0; while (!pq.empty()){ ll x = pq.top().fr; int v = pq.top().sc.fr, w = pq.top().sc.sc; pq.pop(); if (fnd(v) == fnd(w)){ continue; } uni(v, w); ans += sqrt((ld)x); } cout << ans; }
14. [1045] 도로 (Gold I)
더보기Spanning Tree긴 한데, 우선 순위를 최대화하라고 합니다.
그런데 "앞에 있는 간선의 우선순위가 크면" 뒤의 가중치와 상관없이 더 높은 우선순위를 가지기 때문에, 결국 우선순위가 높은 간선부터 차례대로 보면 됩니다.
즉, 우선순위로 간선 재정렬하고 아무 일 없다는 듯이 MST 돌리면 됩니다.
+ 도로를 정확히 M개 가져야 하므로, 두 정점이 같은 집합에 속할 때에도 총 M-(N-1)회는 연결시켜줘야 합니다.
int par[60]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } int ans[60]; void Main(){ priority_queue< pi2, vector<pi2>, greater<pi2> > pq; int n, m; cin >> n >> m; m -= n-1; for (int i = 0; i < n; i++){ par[i] = i; } for (int i = 0; i < n; i++){ for (int j = 0; j < n; j++){ char ch; cin >> ch; if (ch == 'Y' && i < j){ pq.push({i, j}); } } } int cnt = n-1; while (!pq.empty()){ int v = pq.top().fr, w = pq.top().sc; pq.pop(); if (fnd(v) == fnd(w)){ if (m){ ans[v] += 1; ans[w] += 1; m -= 1; } } else{ uni(v, w); ans[v] += 1; ans[w] += 1; cnt -= 1; } } if (cnt || m){ cout << -1; return; } for (int i = 0; i < n; i++){ cout << ans[i] << ' '; } }
15. [23034] 조별과제 멈춰! (Platinum V)
더보기MST를 구한 뒤 x와 y를 잇는 경로 중 가중치가 가장 큰 간선을 제거하면 됩니다.
x - y를 미리 연결시키고 MST를 돌린다고 생각하면 편합니다.
이렇게만 해도 되는 이유가, 위 과정 때문에 MST에 생기는 변화는 x - y를 잇는 경로의 변화밖에 없기 때문입니다.
그렇지 않은 간선이 있다면, 애초부터 MST를 만들 때에 그 간선을 먼저 볼 것이기 때문입니다.
int par[1020]; int fnd(int a){ if (par[a] == a){ return a; } return par[a] = fnd(par[a]); } void uni(int a, int b){ par[fnd(a)] = fnd(b); } vector<pi2> adj[1020]; ll dis[1020][1020]; void dfs(int st, int now, int pre, ll mx){ dis[st][now] = mx; for (pi2 p : adj[now]){ int nxt = p.fr; ll dst = p.sc; if (nxt == pre){ continue; } dfs(st, nxt, now, max(mx, dst)); } } void Main(){ priority_queue< pi3, vector<pi3>, greater<pi3> > pq; int n, m; cin >> n >> m; for (int i = 1; i <= n; i++){ par[i] = i; } while (m--){ int v, w, x; cin >> v >> w >> x; pq.push({ x, {v, w} }); } ll ans = 0; while (!pq.empty()){ int x = pq.top().fr; int v = pq.top().sc.fr, w = pq.top().sc.sc; pq.pop(); if (fnd(v) == fnd(w)){ continue; } uni(v, w); ans += x; adj[v].push_back({w, x}); adj[w].push_back({v, x}); } for (int i = 1; i <= n; i++){ dfs(i, i, -1, 0); } int q; cin >> q; while (q--){ int v, w; cin >> v >> w; cout << ans - dis[v][w] << endl; } }
'ALOHA - 2022 > ALOHA - 중급반 (2022 1학기)' 카테고리의 다른 글
중급반 8주차 풀이 - 세그먼트 트리 (0) 2022.05.31 중급반 6주차 풀이 - 최단경로 (BFS, 다익스트라) (0) 2022.05.18 중급반 5주차 풀이 - DFS, BFS, 백트래킹 (0) 2022.05.12 중급반 4주차 풀이 - 냅색, 구간 DP (0) 2022.05.04 중급반 3주차 풀이 - 이진 탐색, 파라메트릭 서치, 두 포인터 (1) 2022.05.04