[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;
}
ディスカッション
コメント一覧
まだ、コメントがありません