2022牛客暑假多校J.Serval and Essay题解
题意:给定一张有向图,起初全部点都是白色,你可以选择任意一个点染成黑色,然后经过若干次迭代,每次迭代,对于每个点来说,若当前点的所有来点都是黑色,那么这个点也染成黑色。现在问,找到一个初始点,使得最后的黑色点数最多,输出点数。
知识点:启发式合并
首先需要知道一个结论,我们设 \(S_{x}\) 表示假设我们刚开始染点 \(x\) 最后得到的黑点集合。那么对于 \(\forall u, v \, (u \ne v)\),要么 \(S_{u} \subseteq S_{v}\) 要么 \(S_{u} \cap S_{v} = \empty\)
我尝试下用好理解的方式来解释这点
首先当 \(v \in S_u\) 时,由于染 \(u\) 可以染到 \(v\),所以染 \(v\) 得到的点集 \(S_v\) 一定是 \(S_u\) 的子集。
那么当 \(v \notin S_u\) 时
我们假设 \(|S_u| \ge |S_v|\) \(S_u \cap S_v = \empty\) 时,满足 \(S_v\) 不是 \(S_u\) 的子集,且两个集合为有序集,即集合可以表示为一个有序数组 \(\{ a_1, a_2, ..., a_{|S_u|}\}\) \(\{ b_1, b_2, ... ,b_{|S_v|}\}\)
既然交集不是空集,那么我们一定可以找到一个最小的 \(k\) 使得 \(b_k \notin S_u\) 于是一定有 \(\{ b_1, b_2, ... b_{k - 1}\} \subseteq S_u\) ,那么根据递推的性质,\(b_k\) 也一定在 \(S_u\) 中出现,矛盾了。刚刚是 \(k \ne 1\) 的情况下,如果是 \(k=1\) 的时候,说明二者无交集。若有交集,则说明存在一个点 \(b_{k'}\) 是能被染成黑色的,那么能到达这个点的所有点都是黑色,也就是说一定满足 \(b_1 \in S_u\),矛盾。
所以上述结论得证,那么知道这个结论之后,我们就想着求出所有点的 \(S\) 集合,然后如果有包含关系,则连边,最后一定得到一个森林,找到森林中最大的连通块,其大小就是答案了。
考虑实现上述所说,我们考虑用两个集合 \(from\) 和 \(to\) 维护每个点的出点和来点
当一个点的 \(from\) 大小只有1时,我们考虑合并两点,按照出点大小启发式合并,将出点少的点合并到多的那个点。如此一直合并,由于使用了启发式合并,每个点最多只会被合并log次,然后set的删和增都是log,总复杂度 \(O(n(log_2n)^2)\)所以这个方法可行
具体实现看代码
不知道是不是牛客机子原因,赛后补题的时候卡我常数了。
#include <bits/stdc++.h>
#define endl '\n'
#define ls u << 1
#define rs u << 1 | 1
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL,LL> PLL;
const int INF = 0x3f3f3f3f, N = 2e5 + 10;
const int MOD = 1e9 + 7;
const double eps = 1e-6;
const double PI = acos(-1);
inline int lowbit(int x) {return x & (-x);}
set<int> from[N], to[N];
int par[N], sz[N];
queue<PII> q;
int Find(int x) {
return x == par[x] ? x : par[x] = Find(par[x]);
}
void Merge(int x, int y) {
x = Find(x), y = Find(y);
if (x == y) return ;
if (to[x].size() > to[y].size()) {
//使用启发式合并
swap(x, y);
swap(from[x], from[y]);
}
par[x] = y;
sz[y] += sz[x];
for (int now : to[x]) {
int v = Find(now);
if (v == y) continue;
from[v].erase(x);
from[v].insert(y);
to[y].insert(v);
if (from[v].size() == 1) q.push({v, *from[v].begin()});
}
to[y].erase(x);
}
inline void solve() {
int n; cin >> n;
for (int i = 1; i <= n; i ++ ) {
from[i].clear(), to[i].clear();
par[i] = i;
sz[i] = 1;
}
for (int i = 1; i <= n; i ++ ) {
int k; cin >> k;
while (k -- ) {
int x; cin >> x;
from[i].insert(x);
to[x].insert(i);
}
}
for (int i = 1; i <= n; i ++ ) if (from[i].size() == 1) q.push({i, *from[i].begin()});
while (!q.empty()) {
auto t = q.front(); q.pop();
Merge(t.first, t.second);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, sz[Find(i)]);
cout << res << endl;
}
signed main() {
#ifdef DEBUG
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
auto now = clock();
#endif
ios::sync_with_stdio(false), cin.tie(nullptr);
cout << fixed << setprecision(2);
int T; cin >> T;
for (int i = 1; i <= T; i ++ ) {
cout << "Case #" << i << ": ";
solve();
}
#ifdef DEBUG
cout << "============================" << endl;
cout << "Program run for " << (clock() - now) / (double)CLOCKS_PER_SEC * 1000 << " ms." << endl;
#endif
return 0;
}