题目
题目概要
给你一个
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
∑n≤105,∑m≤2×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 s→x 的唯一简单路径。那么 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.
*/