[CF1361E]James and the Chase

122 篇文章 0 订阅
98 篇文章 0 订阅
这篇博客讨论了一个关于强连通有向图的问题,其中‘好点’定义为从该点出发到图中其他任意点都存在且仅有一条简单路径。文章提出了通过随机化方法寻找‘好点’,并详细阐述了如何通过DFS遍历判断一个点是否为‘好点’,以及如何利用已知的好点信息推断其他点的属性。作者还提供了相应的C++代码实现。
摘要由CSDN通过智能技术生成

题目

传送门 to luogu

题目概要
给你一个 n n n 个点 m m m 条边的强连通有向图。规定 “ 好点 ” 满足,从这个点到其他任意一个点,都有且仅有一条简单路径。简单路径是不经过重复的点的路径。

请输出所有 “ 好点 ” 。特别地,如果好点数量严格小于 20 % ⋅ n 20\%\cdot n 20%n 的话,输出 − 1 -1 1 即可。

数据范围与提示
多组数据, ∑ n ≤ 1 0 5 ,    ∑ m ≤ 2 × 1 0 5 \sum n\le 10^5,\;\sum m\le 2\times 10^5 n105,m2×105

思路

这个 20 % 20\% 20% 很奇怪啊……它想让我们干什么呢?显然是 随机化。我们用随机化,可以很容易地找到 某一个好点

问题有二:怎么找好点(可以接受 O ( n ) \mathcal O(n) O(n) 每次)?知道一个好点有什么用?

先去解决第一个问题吧。既然我们只研究这一个点,不妨称之为 s s s ,那我们可以试着用一下 d p \tt dp dp 吧,用 f ( x ) f(x) f(x) 表示,是否存在 s → x s\rightarrow x sx 的唯一简单路径。那么 f ( x ) = 1 f(x)=1 f(x)=1 等价于恰有一个 y y y 使得 ⟨ y , x ⟩ \lang y,x\rang y,x 存在并且 f ( y ) = 1 f(y)=1 f(y)=1 。当然,由于我们的目标是 ∀ x ,    f ( x ) = 1 \forall x,\;f(x)=1 x,f(x)=1 ,我们也可以忘掉那个 f ( y ) = 1 f(y)=1 f(y)=1 的限制。

下文的一些名词,有必要解释一下。横叉边:两个端点在 d f s \rm dfs dfs 树上的 l c a lca lca 不是二者之一;返祖边:从 x x x 指向 x x x d f s \rm dfs dfs 树上的祖先;重孙边:反向后为返祖边。

所以,它看上去像个 d p \tt dp dp ,其实就是 d f s \rm dfs dfs 了嘛!一路 d f s \rm dfs dfs 下去,就已经存在了一条路径; 如果中途出现横叉边,哦豁, s s s 必定不是 “ 好点 ” 了;如果中途出现返祖边,不用管,因为走回去并不是简单路径;如果中途出现重孙边,哦豁,也完蛋了。

而强联通的条件已经保证了 n n n 个点都会出现在 d f s \rm dfs dfs 树中,所以无需更多判断。

好,总结一下: s s s 是 “ 好点 ” ,当且仅当以它为根的 d f s \rm dfs dfs 树没有横叉边与重孙边。明显 O ( n ) \mathcal O(n) O(n) 直接就可以判断了。那么,如果我们做 k k k 次,考虑到只有 80 % 80\% 80% 的概率选择到非 “ 好点 ” ,错误的概率只有 0. 8 k 0.8^k 0.8k 而已,取 k = 100 k=100 k=100 就绰绰有余。(错误是指,做完 k k k 次都没找到 “ 好点 ” ,但实际上其有 20 % 20\% 20% 的占比。)

现在要解决问题二了。我们有了一个好点。现在可以大胆猜一下结论。难道说 “ 好点 ” 就是能走到它的点?荒谬。感觉搞不动啊。怎么办呢?

咱还是回到刚才那个 d f s \rm dfs dfs 树的方法上。只对根有效,却需要求所有点的信息,这比较常见吧?考虑 换根。把根换到 x x x 的方法,可以直接构造一个:图是强联通的,所以 x x x 的子树内肯定有一条返祖边连向 x x x 的祖先 y y y 。我们就假设这条边是 d f s \rm dfs dfs 树边。那么,根据换根的基本姿势,此时的 y y y(以 x x x 为大根的视角)的子树,多数都是原先的 y y y(以 y y y 为大根的视角)的子树。唯一不同就是原先的 y y y 认为 x x x 也是它的子树。

下文用 g ( x ) g(x) g(x) 表示 x x x 是否为 “ 好点 ” ,是则为 1 1 1 ,否则为 0 0 0

我们想要直接用 y y y 的信息,但是要筛除 x x x 作为 y y y 的子树。这时候,找到一个 “好点” 的作用就显现了——因为我们求的是,以其为根的 d f s \rm dfs dfs 树是否有返祖边,而 x x x 作为 y y y 的子树 铁定没有 横叉边与重孙边(否则 s s s 就已经不是 “好点” 了)!所以全然没有影响!也就是说, g ( y ) g(y) g(y) 可以直接拷过来!

但是要注意,父子关系改变可能导致边的分类变化。原本是 x x x 子树内连向 x x x 的祖先的,如果不是我们假定为 d f s \rm dfs dfs 树的那条边(也就是 y y y 为端点的那条边),那么就变成横叉边或重孙边了, g ( x ) = 0 g(x)=0 g(x)=0 就毫无疑问了!

你担心: x x x 的祖先连到 x x x 子树内的点,由重孙边变化成了返祖边,怎么消除影响?还是那句话, g ( y ) g(y) g(y) 是正确的,正确性由 s s s 为 “ 好点 ” 保证了!

所以啊,只判断这俩就足够了。毕竟 x x x 的子树由两部分构成:原本就是 x x x 的子树(以 s s s 为根时);以 y y y 为根时 y y y 的子树去掉 x x x 的子树。所以横叉边要么在某个内部,要么有跨越;重孙边只可能在 y y y 那一边。而跨越就是 x x x 子树内的某个点 (不是我们的假定边)向 x x x 的祖先连了边;内部就是 g ( y ) g(y) g(y) ,毕竟 s s s 为根保证了 x x x 子树内没有危险。

至此,整道题就结束了!

代码

一年之后,我做到了原题。我不太确定我原来是怎么想的,但是我将新的代码附在这里。

如果代码与上面的内容有出入,以代码中的注释为准

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <random>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long llong;
inline int readint(){
	int a = 0, c = getchar(), f = 1;
	for(; !isdigit(c); c=getchar())
		if(c == '-') f = -f;
	for(; isdigit(c); c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}

const int MAXN = 100005;
struct Edge{
	int to, nxt;
	Edge() = default;
	Edge(int _t,int _n):to(_t),nxt(_n){}
};
Edge e[MAXN<<1];
int head[MAXN], cntEdge;
void addEdge(int a,int b){
	e[cntEdge] = Edge(b,head[a]);
	head[a] = cntEdge ++;
}

const int INF = 0x3fffffff;
struct PII{
	int first, second; ///< first is less than second
	PII() = default;
	PII(int _f,int _s):first(_f),second(_s){}
	void clear(){ first = second = INF; }
	inline void add(int x){
		if(x < first) second = first, first = x;
		else if(x < second) second = x;
	}
	PII operator & (const PII &t) const {
		PII c = *this; c.add(t.first);
		c.add(t.second); return c;
	}
};
bool insta[MAXN]; int item[MAXN];
int dfn[MAXN], dfsClock; PII low[MAXN];
bool dfs(int x){
	dfn[x] = ++ dfsClock; low[x].clear();
	item[dfn[x]] = x; insta[x] = true;
	for(int i=head[x]; ~i; i=e[i].nxt)
		if(!dfn[e[i].to]){
			if(!dfs(e[i].to)) return false;
			low[x] = (low[x]&low[e[i].to]);
		}
		else if(!insta[e[i].to])
			return false; // to brother or grandson
		else low[x].add(dfn[e[i].to]);
	insta[x] = false; return true;
}

bool dp[MAXN];
void solve(int x){
	if(low[x].second >= dfn[x])
		dp[x] = dp[item[low[x].first]];
	else dp[x] = false; // clear
	for(int i=head[x]; ~i; i=e[i].nxt)
		if(dfn[e[i].to] > dfn[x])
			solve(e[i].to);
}

mt19937 rnd;
int main(){
	rnd.seed(5201314);
	for(int T=readint(); T; --T){
		int n = readint(), m = readint();
		memset(head+1,-1,n<<2), cntEdge = 0;
		for(int a,b; m; --m){
			a = readint(), b = readint();
			if(a == b) continue;
			addEdge(a,b); // directed
		}
		uniform_int_distribution<int> xyx(1,n);
		int rt; // any "good" node
		for(bool ok=0; !ok; ){
			dfsClock = 0; // before every dfs
			memset(dfn+1,0,n<<2);
			memset(insta+1,false,n);
			ok = dfs(rt = xyx(rnd));
		}
		dp[rt] = true;
		for(int i=head[rt]; ~i; i=e[i].nxt)
			solve(e[i].to); // all its children
		rep(i,1,n) if(dp[i]) printf("%d ",i);
		putchar('\n');
	}
	return 0;
}

/*

take x as root, build dfs-tree

x is "interesting" iff edges not on tree is to return ancester

since the graph is strong-connected, in every subtree there's a edge to x

if we change root, we'll need to use that edge

and it's obvious that
	if there're 2 or more edges, this son is not "interesting";
	if there's only 1 edge, this son is "interesting".

bonus: if there're 2 or more edges from subtree to outside, it's not "good".

otherwise there's exactly 1 edge. we'll just need to make sure that's "good" node.

CASE CLOSED.

*/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值