【NOI2024 D2T3】树形图 题解
比较 \(adhoc\),大众分应该是 \(20 \sim 30pts\)。
有些分是不难的。整体思路比较难想但是如果想到就比较顺。
题意
P10790 [NOI2024] 树形图
给定一个 \(n\) 个点 \(m\) 条边的简单有向图 \(G\),顶点从 \(1\) 到 \(n\) 编号。其中简单有向图的定义为不存在重边与自环的有向图。
定义顶点 \(r\) 是有向图 \(G\) 的根当且仅当对于 \(1\leq k\leq n\),顶点 \(r\) 到顶点 \(k\) 存在恰好一条有向简单路径,其中简单路径的定义为不经过重复点的路径。
定义每个点的种类如下:
- 若顶点 \(r\) 是图 \(G\) 的根,则称顶点 \(r\) 为图 \(G\) 的一类点。
- 若顶点 \(r\) 不是图 \(G\) 的一类点,且存在一种删边的方案,使得图 \(G\) 在删去若干条边后得到的图 \(G'\) 满足:所有图 \(G\) 中的一类点都是 \(G'\) 的根,且顶点 \(r\) 也是图 \(G'\) 的根,则称顶点 \(r\) 为图 \(G\) 的二类点。
- 若顶点 \(r\) 不满足上述条件,则称顶点 \(r\) 为图 \(G\) 的三类点。
根据上述定义,图 \(G\) 的每个点都恰好属于一类点,二类点,三类点之一。你需要判断点 \(1\sim n\) 分别属于这三个种类中的哪一种。
【数据范围】
对于所有测试数据保证:\(1\leq t\leq 10\),\(2\leq n\leq 10^5\),\(1\leq m\leq 2\times 10^5\),且图 \(G\) 不存在重边与自环。
| 测试点编号 | \(t\leq\) | \(n\leq\) | \(m\leq\) | 特殊性质 |
|---|---|---|---|---|
| \(1\) | \(3\) | \(10\) | \(20\) | 无 |
| \(2\) | \(10\) | \(10^3\) | \(2000\) | A |
| \(3,4\) | \(10\) | \(10^3\) | \(2000\) | B |
| \(5,6\) | \(10\) | \(10^3\) | \(2000\) | 无 |
| \(7\) | \(10\) | \(10^5\) | \(2\times 10^5\) | A |
| \(8,9\) | \(10\) | \(10^5\) | \(2\times 10^5\) | BC |
| \(10\sim 13\) | \(10\) | \(10^5\) | \(2\times 10^5\) | B |
| \(14,15\) | \(10\) | \(10^5\) | \(2\times 10^5\) | C |
| \(16\sim 20\) | \(10\) | \(10^5\) | \(2\times 10^5\) | 无 |
- 特殊性质 A:保证不存在一类点。
- 特殊性质 B:保证不存在二类点。
- 特殊性质 C:保证编号为 \(1\) 的点为图 \(G\) 的一类点。
分析:
根据题意,来考虑怎么判断一类点:
假设我们要 \(check\) 点 \(x\),那么以 \(x\) 为根建出一棵 \(dfs\) 树,那么所有点都在这棵 \(dfs\) 树上,并且非树边只能是返祖边。只要满足非树边不存在 叶向边 和 横叉边 那么 \(x\) 就是一类点。
那么二类点呢?
首先有一个观察,二类点一定能到达所有的点。那么我们将强连通分量缩成一个点,原图变成了一张 \(DAG\)。找到一个点能到达所有的点(也可能找不到),那么所有一类点和二类点都一定在这个集合中。
如果没有一类点,那么这个集合中所有点都是二类点。
如果有一类点,那么可能会猜 除了这些一类点外,集合中别的点都是二类点。这个是错误的,有以下反例:

我们注意到这张图缩点后就是一个点,其中 \(1,2,3\) 为一类点,但是 \(4\) 却不是二类点。
不知道怎么判断二类点?
我们先来看看部分分。
测试点 \(1\) 直接指数暴力即可。
对于特殊性质 \(A\),那么只要求一遍 \(trajan\) 就能找到所有二类点。可以通过测试点 \(2,7\)。
对于特殊性质 \(B\),枚举每个点 \(check\) 是否为 \(1\) 类点,复杂度 \(O(n^2)\)。可以通过测试点 \(3,4\)。
这样就有了 \(25pts\)。
接下来是正解,特殊性质也在提示我们思考的方向。
一.如何根据一个一类点求出所有一类点
设这个一类点为 \(s\),那么以 \(s\) 为根求出一棵 \(dfs\) 树。
考虑对于树上的一个 \(x\):
如果子树中没有 返祖边,那么 \(x\) 无法到达父亲一定不是一类点。
如果子树中的 返祖边 超过 \(2\) 条,那么 \(x\) 一定不是一类点。因为 \(x\) 到 它的父亲有至少两条路径。
如果子树中的 返祖边 只有 \(1\) 条,设这条返祖边连向的祖先为 \(y\),那么 \(x\) 是一类点的充要条件是 \(y\) 为一类点。
证明:
首先 \(x\) 到子树中的点只有一条路径。 \(x\) 到子树外的点必须经过 \(y\),若 \(y\) 是一类点的话那么 \(x\) 到子树外的所有点都只有一条路径,那么此时 \(x\) 是一类点。
如果 \(x\) 是一类点,那么因为 \(x\) 到子树外的点都要经过 \(y\) 因此 \(y\) 到 \(x\) 子树外的点只有一条路径,又因为 \(y\) 到 \(x\) 子树里的点都要经过 \(x\),因此肯定也只有一条路径。所以此时 \(y\) 是一类点。证毕。
因此我们可以从根开始往下确定所有一类点。
二.如何根据一类点求出所有二类点
现在我们已经确定了所有一类点。考虑一类点对边的限制:发现每个除了根之外的一类点都确定了一条返祖边不能删除。也就是除了这些返祖边和树边,其余返祖边我们都可以任意删除。
那么考虑一个点 \(x\) 是否为二类点:我们首先希望将 \(x\) 子树内的返祖边删的只剩下一条。这里的返祖边默认越过 \(x\)。
- 如果 \(x\) 子树中不能删除的返祖边大于等于 \(2\) 条,那么 \(x\) 一定不是二类点。
- 如果 \(x\) 子树中不能删除的返祖边等于 \(1\) 条,假设指向祖先 \(y\),那么 \(x\) 是二类点的充要条件是 \(y\) 为一类点或二类点。
- 如果 \(x\) 子树中不能删除的返祖边等于 \(0\) 条,那么 \(x\) 为二类点的充要条件是 存在一条返祖边指向一个是一类点或二类点的祖先 \(y\)。
证明一下:
1:显然的。
2:如果 \(y\) 为一类点,那么只需要把其余返祖边删掉即可。如果 \(y\) 为二类点,等等,\(y\) 可能是二类点吗?我们考虑不能删掉的边满足什么要求:有一个一类点通过这条返祖边指向了另一个一类点。那么不能删掉的边指向的一定是一类点了。因此不会有 \(y\) 是二类点的情况。
3: 如果存在一个点 \(y\) 满足 \(y\) 是一类点,那么把其它返祖边删掉即可。如果不存在,假设存在一条边指向的 \(y\) 是二类点,那么我们害怕下面这种情况:

此时 \(y\) 是通过保留一条在 \(x\) 子树里的返祖边使得自己成为一类点的。但是因为 \(x\) 只能保留一条返祖边因此会把 \(y\) 使用的边删掉,导致 \(y\) 此时不能作为一类点了。
那么我们保留所有二类点祖先中深度最小的那条边,设此时的祖先为 \(p\),那么 \(p\) 一定不是通过保留 \(x\) 子树中的边使自己成为一类点的,因此把 \(x\) 子树中其余所有返祖边删掉即可。
由此也可以从上到下依次确定每个二类点。
三.如何找到一个一类点
这个可以说是最难的部分。
考虑一棵非树边只有返祖边的 \(dfs\) 树有什么特点。
从叶子往上缩,叶子的入度总为 \(1\)(删掉自环)。
考虑一个缩叶子的过程:每次缩两个点直到只剩下一个点。
我们的想法是每次将入度为 \(1\) 的点 \(u\) 拿出来,设唯一指向它的点为 \(v\),那么将 \(u\) 缩到 \(v\) 里,同时需要将出边合并(删掉自环)。
最后剩下的不只一个点那么就没有一类点。否则剩下的点就是一个一类点。
来证明一下这个过程为什么是对的:
只需要证明 一个存在一类点的局面,缩一个点之后仍然存在一类点 即可。
假设存在一个一类点为 \(s\),考虑以 \(s\) 为根的一棵 \(dfs\) 树。
那么缩的点有两种情况:
- \(u \ne s\)。此时 \(v\) 一定是 \(u\) 在这棵 \(dfs\) 树上的父亲。那么将 \(u\) 缩到 \(v\) 后 \(s\) 仍然是一类点。
- \(u = s\)。因为 \(s\) 没有父亲,那么一定是子树中的一个点 \(v\) 通过返祖边连上来的并且只有这一条指向 \(s\) 的返祖边。

考虑这时候 \(s_1\) 一定能作为一个一类点:子树内跨过 \(s_1\) 的返祖边只有一条并且连向了一个一类点。
那么就证明了只要最开始有一个一类点,每次缩边后一定仍然存在一类点。
具体怎么实现呢?
不考虑去掉自环,那么只需要将 \(u\) 的 出边和 \(v\) 的出边启发式合并。
考虑怎么将 \(v\) 的入度减去 \(u\) 的出边中指向 \(v\) 的边数:发现我们需要枚举 \(u\) 的出边集合,但是复杂度可能无法接受的。
其实发现也可以枚举 \(v\) 的入边集合。因此我们同时维护一个点的出边和入边集合,启发式合并就用两个集合的大小之和加权即可。这样复杂度也是对的。
总复杂度 \(O(m \log n)\)。
CODE:
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 1e5 + 10;
const int M = 2e5 + 10;
int testid, TT, n, m, ans[N];
int bel[N], in[N], tot;
int dep[N], low[N], dfn[N], len, L[N], R[N], Stk[N], dfc, Top;
bool ins[N], bok[M];
int u[M], v[M], mn[N], cnt[N], sum[N];
vector< int > E[N], G[N], to[N];
struct BIT {
int c[N];
inline int lowbit(int x) {return x & -x;}
inline void add(int x, int y) {for(; x < N; x += lowbit(x)) c[x] += y;}
inline int ask(int x) {int res = 0; for(; x; x -= lowbit(x)) res += c[x]; return res;}
inline void clr() {memset(c, 0, sizeof c);}
} T;
vector< int > In[N], Out[N];
void tarjan(int x) {
low[x] = dfn[x] = ++ len; Stk[++ Top] = x; ins[x] = 1;
for(auto v : E[x]) {
if(!dfn[v]) {
tarjan(v);
low[x] = min(low[x], low[v]);
}
else if(ins[v]) low[x] = min(low[x], dfn[v]);
}
if(dfn[x] == low[x]) {
int y; tot ++;
do {
y = Stk[Top --];
bel[y] = tot; ins[y] = 0;
} while(y != x);
}
}
int bin[N], sz[N];
int Find(int x) {return x == bin[x] ? x : bin[x] = Find(bin[x]);}
int Find() { // 找到一个一类点 每次缩入度为 1 的点
for(int i = 1; i <= n; i ++ ) {
bin[i] = i, in[i] = 0; sz[i] = 0;
vector< int > A, B;
swap(A, In[i]); swap(B, Out[i]);
}
for(int i = 1; i <= m; i ++ ) {
in[v[i]] ++; In[v[i]].pb(u[i]); Out[u[i]].pb(v[i]);
sz[u[i]] ++; sz[v[i]] ++;
}
queue< int > q;
for(int i = 1; i <= n; i ++ ) {
if(in[i] == 1) q.push(i);
}
while(!q.empty()) {
int u = q.front(); q.pop(); in[u] = 0;
for(auto v : In[u]) {
if(Find(v) != Find(u)) { // 合并
// 要么枚举 out[u], 要么枚举in[v] 才能判断到底有几个 u -> v, 所以枚举 out + in 更小的
if(sz[Find(u)] > sz[Find(v)]) // 枚举 in[v]
for(auto k : In[Find(v)]) in[Find(v)] -= (Find(k) == Find(u));
else
for(auto k : Out[Find(u)]) in[Find(v)] -= (Find(k) == Find(v));
sz[Find(v)] += sz[Find(u)];
if(Out[Find(v)].size() < Out[Find(u)].size()) swap(Out[Find(v)], Out[Find(u)]);
for(auto k : Out[Find(u)]) Out[Find(v)].pb(k);
bin[Find(u)] = Find(v);
if(in[Find(v)] == 1) q.push(Find(v));
}
}
}
int ct = 0, rt = -1;
for(int i = 1; i <= n; i ++ )
if(Find(i) == i) ct ++, rt = i;
return ct > 1 ? -1 : rt;
}
inline int Min(int x, int y) {return dep[x] < dep[y] ? x : y;}
void dfs(int x) {
L[x] = dfn[x] = ++ dfc;
for(auto v : E[x])
if(dep[v] == 0) G[x].pb(v), dep[v] = dep[x] + 1, dfs(v);
R[x] = dfc;
}
void dfs1(int x) {for(auto v : G[x]) dfs1(v), cnt[x] += cnt[v], mn[x] = Min(mn[x], mn[v]);}
void dfs1_(int x, int fa) {
if(ans[x] != 1 && cnt[x] == 1 && ans[mn[x]] == 1) ans[x] = 1;
sum[x] = sum[fa] + (ans[x] == 1);
for(auto v : G[x]) dfs1_(v, x);
}
void dfs2(int x) {for(auto v : G[x]) dfs2(v), cnt[x] += cnt[v], mn[x] = Min(mn[x], mn[v]);}
void dfs2_(int x) {
if(ans[x] == -1) {
if(cnt[x] == 1) ans[x] = (ans[mn[x]] == -1 ? -1 : 2);
else if(cnt[x] == 0) ans[x] = (T.ask(R[x]) - T.ask(L[x] - 1) > 0 ? 2 : -1);
}
if(ans[x] == 1 || ans[x] == 2) {
for(auto v : to[x]) T.add(dfn[v], 1);
}
for(auto v : G[x]) dfs2_(v);
}
void solve() {
scanf("%d%d", &n, &m); tot = dfc = len = 0;
for(int i = 1; i <= n; i ++ ) E[i].clear(), G[i].clear(), to[i].clear();
for(int i = 1; i <= n; i ++ ) ans[i] = -1, mn[i] = n + 1, sum[i] = dep[i] = cnt[i] = in[i] = dfn[i] = 0;
for(int i = 1; i <= m; i ++ ) {
scanf("%d%d", &u[i], &v[i]);
E[u[i]].pb(v[i]);
bok[i] = 0;
}
for(int i = 1; i <= n; i ++ )
if(!dfn[i]) tarjan(i); // 将强连通分量缩出来
for(int i = 1; i <= m; i ++ )
if(bel[u[i]] != bel[v[i]]) in[bel[v[i]]] ++;
int S = -1, ct = 0;
for(int i = 1; i <= tot; i ++ ) {
if(in[i] == 0) {S = i; ct ++;}
}
if(ct > 1) for(int i = 1; i <= n; i ++ ) ans[i] = 3;
else {
int s = Find(); // 第一步,找到一个一类点
if(s == -1) { // 没有一类点
for(int i = 1; i <= n; i ++ )
ans[i] = (bel[i] == S ? 2 : 3);
}
else {
dep[s] = 1; dep[n + 1] = n + 1;
dfs(s); // 第二步以 s 求出一棵生成树
for(int i = 1; i <= m; i ++ )
if(dep[u[i]] > dep[v[i]]) cnt[u[i]] ++, cnt[v[i]] --, mn[u[i]] = Min(mn[u[i]], v[i]); // 返祖边
dfs1(s); // 确定所有一类点
ans[s] = 1;
dfs1_(s, 0);
for(int i = 1; i <= m; i ++ )
if(dep[u[i]] > dep[v[i]] && sum[u[i]] - sum[v[i]] > 0) bok[i] = 1; // 不能被删掉
for(int i = 1; i <= n; i ++ ) cnt[i] = 0, mn[i] = n + 1;
for(int i = 1; i <= m; i ++ )
if(dep[u[i]] > dep[v[i]]) {
if(bok[i]) cnt[u[i]] ++, cnt[v[i]] --, mn[u[i]] = Min(mn[u[i]], v[i]);
else to[v[i]].pb(u[i]);
}
T.clr();
dfs2(s); // 确定所有二类点 求出每个点被多少限制边覆盖以及是谁
dfs2_(s); // 从上到下确定二类点
for(int i = 1; i <= n; i ++ )
if(ans[i] == -1) ans[i] = 3;
}
}
for(int i = 1; i <= n; i ++ ) printf("%d", ans[i]);
puts("");
}
int main() {
scanf("%d", &testid);
scanf("%d", &TT);
while(TT -- ) solve();
return 0;
}

浙公网安备 33010602011771号