【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\)。找到一个点能到达所有的点(也可能找不到),那么所有一类点和二类点都一定在这个集合中。
如果没有一类点,那么这个集合中所有点都是二类点。
如果有一类点,那么可能会猜 除了这些一类点外,集合中别的点都是二类点。这个是错误的,有以下反例:

3

我们注意到这张图缩点后就是一个点,其中 \(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\)

  1. 如果 \(x\) 子树中不能删除的返祖边大于等于 \(2\) 条,那么 \(x\) 一定不是二类点。
  2. 如果 \(x\) 子树中不能删除的返祖边等于 \(1\) 条,假设指向祖先 \(y\),那么 \(x\) 是二类点的充要条件是 \(y\)一类点或二类点
  3. 如果 \(x\) 子树中不能删除的返祖边等于 \(0\) 条,那么 \(x\) 为二类点的充要条件是 存在一条返祖边指向一个是一类点或二类点的祖先 \(y\)

证明一下:
1:显然的。
2:如果 \(y\) 为一类点,那么只需要把其余返祖边删掉即可。如果 \(y\) 为二类点,等等,\(y\) 可能是二类点吗?我们考虑不能删掉的边满足什么要求:有一个一类点通过这条返祖边指向了另一个一类点。那么不能删掉的边指向的一定是一类点了。因此不会有 \(y\) 是二类点的情况。
3: 如果存在一个点 \(y\) 满足 \(y\) 是一类点,那么把其它返祖边删掉即可。如果不存在,假设存在一条边指向的 \(y\) 是二类点,那么我们害怕下面这种情况:

4

此时 \(y\) 是通过保留一条在 \(x\) 子树里的返祖边使得自己成为一类点的。但是因为 \(x\) 只能保留一条返祖边因此会把 \(y\) 使用的边删掉,导致 \(y\) 此时不能作为一类点了。
那么我们保留所有二类点祖先中深度最小的那条边,设此时的祖先为 \(p\),那么 \(p\) 一定不是通过保留 \(x\) 子树中的边使自己成为一类点的,因此把 \(x\) 子树中其余所有返祖边删掉即可。

由此也可以从上到下依次确定每个二类点。

三.如何找到一个一类点

这个可以说是最难的部分。
考虑一棵非树边只有返祖边的 \(dfs\) 树有什么特点。
从叶子往上缩,叶子的入度总为 \(1\)(删掉自环)。
考虑一个缩叶子的过程:每次缩两个点直到只剩下一个点。
我们的想法是每次将入度为 \(1\) 的点 \(u\) 拿出来,设唯一指向它的点为 \(v\),那么将 \(u\) 缩到 \(v\) 里,同时需要将出边合并(删掉自环)。
最后剩下的不只一个点那么就没有一类点。否则剩下的点就是一个一类点。

来证明一下这个过程为什么是对的:
只需要证明 一个存在一类点的局面,缩一个点之后仍然存在一类点 即可。
假设存在一个一类点为 \(s\),考虑以 \(s\) 为根的一棵 \(dfs\) 树。
那么缩的点有两种情况:

  1. \(u \ne s\)。此时 \(v\) 一定是 \(u\) 在这棵 \(dfs\) 树上的父亲。那么将 \(u\) 缩到 \(v\)\(s\) 仍然是一类点。
  2. \(u = s\)。因为 \(s\) 没有父亲,那么一定是子树中的一个点 \(v\) 通过返祖边连上来的并且只有这一条指向 \(s\) 的返祖边。

5

考虑这时候 \(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;
}
posted @ 2026-09-10 15:04  Czl2007  阅读(5)  评论(0)    收藏  举报