[AtCoder] ABC157 D – Friend Suggestions 解説(400点)
問題概要
N 人がいて、M 組が友達関係、K 組がブロック関係である。それぞれに対して以下を満たす「友達候補」が何人いるか答えよ。
- まだ友達関係ではない
- ブロック関係ではない
- 友達関係を辿ってたどり着ける
制約
- \(2 ≤ N ≤ 10^5\)
- \(0 ≤ M ≤ 10^5\)
- \(0 ≤ K ≤ 10^5\)
- 友達関係とブロック関係は重複しない
考え方
友達候補というのは、友達関係を辿って到達できる人物です。この性質はグラフの構造に非常によく似ています。
人間を頂点・友達関係を辺としたグラフとして考えると、「友達候補」は以下の条件を満たす必要があります。
- 間に辺がない
- パスがある(同じ連結成分内である)
- ブロック関係ではない
よって、「友達候補」の数は以下のように計算可能です。
友達候補の数 = 連結成分のサイズ-1 – 自身からの辺(友達関係)の数 – 連結成分内のブロック関係の数
1を引くのは、自身を「友達候補」から外すためです。
自身からの辺(友達関係)の数 は問題に与えられています。
連結成分ごとのサイズや、連結成分内のブロック関係の数は、深さ優先探索や Union-Find Tree などを用いて取得可能です。
解法(DFS)
- DFS を用いて、「自身がどの連結成分に含まれるか」「連結成分ごとのサイズ」をあらかじめ求める
- 「友達候補の数 = 連結成分のサイズ-1 – 自身からの辺(友達関係)の数 – 連結成分内のブロック関係の数」を各人間に対して計算する
C++での実装例
#include <bits/stdc++.h> #define LOOP(n) for (int _i = 0; _i < (n); _i++) #define REP(i, n) for (int i = 0; i < (n); ++i) #define RREP(i, n) for (int i = (n); i >= 0; --i) #define FOR(i, r, n) for (int i = (r); i < (n); ++i) #define ALL(obj) begin(obj), end(obj) using namespace std; struct Edge { int to; }; using Graph = vector<vector<Edge>>; int N, M, K; int id; // 深さ優先探索 vector<int> cc_id; // どの連結成分か vector<int> cc_num; // 連結成分のサイズ void dfs(const Graph &G, int v, int &cnt) { cc_id[v] = id; cnt++; for (auto e : G[v]) { if (cc_id[e.to] == -1) { dfs(G, e.to, cnt); } } } int main() { cin >> N >> M >> K; Graph G(N); // グラフとして考える vector<int> A(M), B(M); REP(i, M) { cin >> A.at(i) >> B.at(i); A[i]--, B[i]--; G[A[i]].push_back({B[i]}); G[B[i]].push_back({A[i]}); } vector<int> C(K), D(K); REP(i, K) { cin >> C.at(i) >> D.at(i); C[i]--, D[i]--; } // どの連結成分に属し、サイズがいくつなのかを求める cc_id.assign(N, -1); // 初期化 id = 0; REP(i, N) { if (cc_id[i] == -1) { int cnt = 0; dfs(G, i, cnt); cc_num.push_back(cnt); id++; } } vector<int> ans(N); REP(i, N) { ans[i] = cc_num[cc_id[i]] - 1 - (int)G[i].size(); } REP(i, K) { // 同じ連結成分にあるブロック関係を引く if (cc_id[C[i]] == cc_id[D[i]]) { ans[C[i]]--; ans[D[i]]--; } } REP(i, (int)ans.size()) { cout << ans[i]; if (i != (int)ans.size() - 1) cout << " "; } cout << endl; return 0; }
解法(Union-Find Tree)
- Union-Find Tree を用いて、1つの連結成分を一つの集合(木) としてまとめる
- 「友達候補の数 = 連結成分のサイズ-1 – 自身からの辺(友達関係)の数 – 連結成分内のブロック関係の数」を各人間に対して計算する
C++での実装例
Tree のサイズを求めたいので、Union-Find Tree の併合時の工夫として、サイズを利用したものを使うと良いです。
#include <bits/stdc++.h> #define LOOP(n) for (int _i = 0; _i < (n); _i++) #define REP(i, n) for (int i = 0; i < (n); ++i) #define RREP(i, n) for (int i = (n); i >= 0; --i) #define FOR(i, r, n) for (int i = (r); i < (n); ++i) #define ALL(obj) begin(obj), end(obj) using namespace std; int N, M, K; /* UnionFind:素集合系管理の構造体(union by size) isSame(x, y): x と y が同じ集合にいるか。 計算量はならし O(α(n)) unite(x, y): x と y を同じ集合にする。計算量はならし O(α(n)) treeSize(x): x を含む集合の要素数。 */ struct UnionFind { vector<int> size, parents; UnionFind() {} UnionFind(int n) { // make n trees. size.resize(n, 0); parents.resize(n, 0); for (int i = 0; i < n; i++) { makeTree(i); } } void makeTree(int x) { parents[x] = x; // the parent of x is x size[x] = 1; } bool isSame(int x, int y) { return findRoot(x) == findRoot(y); } bool unite(int x, int y) { x = findRoot(x); y = findRoot(y); if (x == y) return false; if (size[x] > size[y]) { parents[y] = x; size[x] += size[y]; } else { parents[x] = y; size[y] += size[x]; } return true; } int findRoot(int x) { if (x != parents[x]) { parents[x] = findRoot(parents[x]); } return parents[x]; } int treeSize(int x) { return size[findRoot(x)]; } }; int main() { cin >> N >> M >> K; UnionFind uf(N); // Union-Find Tree で考える vector<int> edges_num(N, 0); // 自身から出る辺の数 vector<int> A(M), B(M); REP(i, M) { cin >> A.at(i) >> B.at(i); A[i]--, B[i]--; edges_num[A[i]]++; edges_num[B[i]]++; uf.unite(A[i], B[i]); } vector<int> C(K), D(K); REP(i, K) { cin >> C.at(i) >> D.at(i); C[i]--, D[i]--; } vector<int> ans(N); REP(i, N) { ans[i] = uf.treeSize(i) - 1 - edges_num[i]; } REP(i, K) { // 同じ連結成分にあるブロック関係を引く if (uf.isSame(C[i], D[i])) { ans[C[i]]--; ans[D[i]]--; } } REP(i, (int)ans.size()) { cout << ans[i]; if (i != (int)ans.size() - 1) cout << " "; } cout << endl; return 0; }
ディスカッション
コメント一覧
まだ、コメントがありません