ABOUT ME

한양대학교 알고리즘 동아리 ALOHA 소속의 hibye1217가 쓰는 동아리 탐방기 ALOHA (한양대학교) / SHARC (선린인터넷고등학교)

Today
Yesterday
Total
  • 중급반 7주차 풀이 - DSU, MST
    ALOHA - 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를 구하되, 두 마을로 나눌 수 있으니 N2개의 간선만 사용하면 됩니다.

    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(N2)이 되는, 무서운 문제입니다.

    하지만 MST에 포함될 수 있는 후보는 "한 축을 기준으로 정렬한 뒤, 인접한 도시들을 잇는 간선"들밖에 되지 않습니다.

     

    만약 그렇지 않다고 합시다. 즉, 어느 축으로도 정렬해도 인접하지 않지만 MST에 "포함되어야만 하는" 간선 vw가 존재한다고 합시다.

    정의에 의해, 어떤 축으로 정렬했을 때 vw 사이에 x라는 정점이 존재해야 합니다.

    그럼, vxw의 두 간선을 사용할 수 있게 되고, vxw일 때 |v-x| + |x-w| = |v-w| \)가 되니, vw 대신 vxw의 두 간선을 써줄 수 있게 됩니다.

    즉, vw가 항상 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)

    더보기

    공사 중이 아니라 서로 건너갈 수 있는 강의동을 하나의 집합으로 묶으면,

    자명히 하나의 강의동 집합에서 와우도까지 가는데 필요한 돌의 개수는 minSi가 됩니다.

    그러니 이 문제도 결국 Union-Find를 하면서 각 집합별로 필요한 돌의 개수의 최솟값을 같이 관리해주면 됩니다.

     

    M1이라서 돌 없이도 이동할 수 있는 경우를 예외처리해야 함에 주의하세요.

    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;
    	}
    }
Designed by Tistory.