一种 DAG 上可达性判定问题的解决方案
本文发表于 https://www.cnblogs.com/zifanoi/p/19318662(洛谷上不一定保持同步)。
1. 问题简述
给定一个有向无环图
其中
(容易用 bitset 做到
1.1 任意图
去除 DAG 的限制,即给定的图为任意有向图。此时进行一次缩点,即可转化为 DAG 问题,本质上是一样的。
1.2 树
限制给定的图是树的形态,则可以构造 dfn 序,容易做到
2. 定义
2.1 有向无环图(DAG)
设
我们称
即不存在从某个点出发又回到自身的有向路径。
2.2 搜索树(有向根树 / arborescence)
称
-
- 在有向图
(V,E_S) 中,r\in V 为根,并且对于任意顶点v\in V\setminus\{r\} ,存在且唯一一条由r 出发、沿着E_S 的有向路径到达v 。
(等价地,每个v\ne r 在E_S 中有且只有一条入边,且(V,E_S) 无有向环。)
我们有时把这样的结构也称为以
2.3 树剖(暂时没有用到)
先将有向树转为无向以便树剖定义:
树剖通常定义在无向图上。为此对
令
-
覆盖:
\bigcup_{i\in I}\chi(i)=V 。 -
边被包含:对任意无向边
\{u,v\}\in E_S^{\mathrm{u}} ,存在i\in I 使得\{u,v\}\subseteq\chi(i) 。 -
连通性/子树性质:对任意顶点
v\in V ,集合\{\,i\in I\mid v\in\chi(i)\,\} 在树T 中构成一个连通子树。
树剖的宽度定义为
2.4 DFS 序
设给定搜索树(有向根树)
其中
记
定义一个排列(序列)
满足
定义顶点的 dfn 值为
于是
3. 解决方案
本文探讨一种类似树剖的做法,构造某种 DFS 序将问题简化。
3.1 整体思路
构造
查询时通过二分判断是否存在一个区间包含
3.2 具体做法
设有向无环图(DAG)
并令
定义从
对路径
在有限的 DAG 上
对
查询直接二分然后判一下。
记
3.3 参考代码
#include<bits/stdc++.h>
#define ll long long
#define mxn 200003
#define md 1000000007
#define pb push_back
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define rept(i,a,b) for(int i=(a);i<(b);++i)
#define drep(i,a,b) for(int i=(a);i>=(b);--i)
using namespace std;
inline int read(){
int x=0;bool flag=true;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')flag=false;ch=getchar();}
while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return flag?x:-x;
}
struct node{
int l,r;
}st[mxn*200];
int n,m,q,tot,c[mxn],rt[mxn],dfn[mxn],a[205][205];
vector<int>g[mxn],e[mxn];
vector<node>s[mxn];
bool v[mxn];
ll sz[mxn];
void dfs(int x){
dfn[x]=++tot,v[x]=1;
sort(g[x].begin(),g[x].end(),[](int x,int y){
return sz[x]>sz[y];
});
for(int i:g[x])if(!v[i]){
e[x].pb(i);
dfs(i);
}
rt[x]=tot;
}
inline void add(int x,int y){
g[x].pb(y),c[y]++;
}
void build(){
n=read(),m=read();
for(int i=0,x,y;i<m;++i){
x=read(),y=read();
add(x,y);
}
}
signed main(){
build();
rep(i,1,n){
if(!c[i])g[0].pb(i);
sz[i]=g[i].size();
}
drep(i,n,1){
for(int j:g[i])sz[i]+=sz[j];
}
tot=-1;
dfs(0);
tot=0;
ll mx=0,sum=0;
drep(i,n,1){
st[tot=1]={dfn[i],rt[i]};
for(int j:g[i])for(node k:s[j])st[++tot]=k;
sort(st+1,st+tot+1,[](node x,node y){
return x.l==y.l?x.r>y.r:x.l<y.l;
});
rep(j,1,tot){
if(s[i].empty())s[i].pb(st[j]);
else if(st[j].l>s[i][s[i].size()-1].r+1)s[i].pb(st[j]);
else s[i][s[i].size()-1].r=max(s[i][s[i].size()-1].r,st[j].r);
}
mx=max(mx,(ll)s[i].size());
sum+=s[i].size();
}
cerr<<mx<<" "<<(double)sum/n<<'\n';
q=read();
int x,y;
while(q--){
x=read(),y=dfn[read()];
int l=0,r=s[x].size()-1;
while(l<r){
int mid=(l+r)>>1;
if(s[x][mid].l<=y)r=mid;
else l=mid+1;
}
puts(s[x][l].r>=y&&s[x][l].l<=y?"Yes":"No");
}
return 0;
}
4. 对于各类图的复杂度分析
咕咕咕
4.1 一种随机图:
考虑通过以下方式建图:
void build(){
mt19937 mt((unsigned ll)new char);
n=1e4,m=2e5;
rep(i,1,m){
int x=mt()%n+1,y=mt()%n+1;
while(x==y)x=mt()%n+1,y=mt()%n+1;
if(x>y)swap(x,y);
add(x,y);
}
}
输出数据:203 115.135。
取 m=1e5:353 175.38。
取 n=1e5,m=2e5:71 6.5275。
运行时间较短。
4.2 在随机树上随机撒边:
考虑通过以下方式建图:
void build(){
mt19937 mt((unsigned ll)new char);
n=1e5,m=2e5;
rept(i,1,n){
add(mt()%i+1,i+1);
}
rep(i,1,m-n+1){
int x=mt()%n+1,y=mt()%n+1;
while(x==y)x=mt()%n+1,y=mt()%n+1;
if(x>y)swap(x,y);
add(x,y);
}
}
输出数据:11342 20.3504。
运行时间较短。
4.3 在菊花图/链上随机撒边:
考虑通过以下两种方式建图:
void build(){
mt19937 mt((unsigned ll)new char);
n=1e5,m=2e5;
rept(i,1,n){
add(1,i+1);
}
rep(i,1,m-n+1){
int x=mt()%n+1,y=mt()%n+1;
while(x==y)x=mt()%n+1,y=mt()%n+1;
if(x>y)swap(x,y);
add(x,y);
}
}
void build(){
mt19937 mt((unsigned ll)new char);
n=1e5,m=2e5;
rept(i,1,n){
add(i,i+1);
}
rep(i,1,m-n+1){
int x=mt()%n+1,y=mt()%n+1;
while(x==y)x=mt()%n+1,y=mt()%n+1;
if(x>y)swap(x,y);
add(x,y);
}
}
输出数据分别为:9 1.74156,11 5.24551,运行时间较短。
5. Hack
忘了,咕咕咕。
6. 拓展
该算法将可达点转化为若干区间,容易处理一些更复杂的问题。
6.1 可达点权值之和
处理出可达点区间,容易通过处理前缀和快速查询。
用一些类似根号分治的东西可以将权值动态化。