P4652 [CEOI2017] One-Way Streets 题解
前置知识
-
边双连通分量
-
缩点
题意
给定一张 \(n\) 个点 \(m\) 条边的无向图,现在想要把这张图定向。
有 \(p\) 个限制条件,每个条件形如 \((x_i,y_i)\),表示在新的有向图当中,\(x_i\) 要能够沿着一些边走到 \(y_i\)。
现在请你求出,每条边的方向是否能够唯一确定。同时请给出这些能够唯一确定的边的方向。
数据保证有解。
-
若第 \(i\) 条边必须得要是 \(a_i\) 指向 \(b_i\) 的,那么这个字符串的第 \(i\) 个字符应当为
R; -
若第 \(i\) 条边必须得要是 \(b_i\) 指向 \(a_i\) 的,那么这个字符串的第 \(i\) 个字符应当为
L; -
否则,若第 \(i\) 条边的方向无法唯一确定,那么这个字符串的第 \(i\) 个字符应当为
B。
整体思路
我们能确定一条边的方向,当且仅当至少一组 $ x_i $ 到 $ y_i $
必须经过这条边。那实际上也就是这条边是一条“割边”,也就是说删去这条边,左右两端的联通块之间没有任何边相连。
所以我们要去找割边。
但换句话讲,也就是找到一个个边双连通分量,每个边双连通分量内部的边都是不定的,因为这个联通分量内部的点之间互相可以到达且路径不止一条,就算删去了某条边,也依然可以从另一条路径到达。
(不会边双连通分量的同学请左转,P8436 【模板】边双连通分量)
但找到每个边双连通分量后该如何确定中间的“割边”的方向呢?
我们可以暴力去遍历每一个限制条件,但这样会超时。
那我们再想一想会发现,每个边双连通分量内部的边都不确定方向,就不用考虑了(反正输出B就完了)。我们只用考虑剩下的“割边”。
所以我们可以把每个边双连通分量用缩点缩起来,这样点与点之间的边就是原图上的“割边”了。
(不会缩点的同学请右转,P3387 【模板】缩点)
我们知道一棵树的每条边都是“割边”,因为任意删去一条边都会导致这棵树不再联通,所以我们缩完点后的图就可以近似的看成一棵树,只不过它有可能不联通,所以准确来说,是一个森林(很多棵树)。
而树上问题就要比图上问题方便得多了,因为两点之间的路径是唯一的。
不过话说回来,我们要快速确定新图中每条边的方向。
可以想到用树上差分。但可能跟“树上点差分”和“树上边差分”这样的“用来求和的差分”有所不同,我们只用统计每条边的方向。
不妨假设当这个点的差分值为正整数时,这个点通向他父亲的边是向根节点的。
当这个点的差分值为负整数时,这个点通向他父亲的边是向叶子节点的。
而当这个点的差分值为零时,这个点通向他父亲的边就是不定向的。
那我们只用知道这个点的差分值是正还是负。
所以当限制条件为 $ x_i $ 和 $ y_i $ 时,设数组 $ cha $
为差分数组,我们就将 $ cha[x_i] $ 加一,将 $ cha[y_i] $ 减一。
差分加减完成后别忘了从根节点开始递归,统计差分值。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=200005,M=400005;
int n,m,q,head[M],head2[M],cnt2,cnt=1;//用链式前向星存图
//cnt初始化为1的好处:编号为i^1的边就是编号i的边的反向边
int dfn[N],low[N],idx;//求边双连通分量的数组
int col[N],tmp;//每个点属于的边双连通分量的编号,以及边双连通分量的个数
int cha[N],d[N];//差分数组,新图上节点的深度
bool b[M];//判断是否为割边
struct tree//新图
{
int v,nxt;
}e[M];
struct node//原图
{
int v,nxt,u;
}a[M];
void add(int u,int v)//原图链式前向星存图
{
a[++cnt].v=v;
a[cnt].u=u;//这里存一下边的起点方便后面用
a[cnt].nxt=head[u];
head[u]=cnt;
}
void add2(int u,int v)//新图链式前向星存图
{
e[++cnt2].v=v;
e[cnt2].nxt=head2[u];
head2[u]=cnt2;
}
int read()//快读
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
//这里求边双连通分量的方法是把所有割边“删去”,再搜一遍剩下的图
void tarjan(int u)//求割边
{
dfn[u]=low[u]=++idx;
for(int i=head[u];i!=0;i=a[i].nxt)
{
int v=a[i].v;
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]) b[i]=b[i^1]=1;//标记割边
}
else low[u]=min(low[u],dfn[v]);
}
}
void dfs1(int u,int num)//当前搜索到第几个点以及当前边双连通分量的编号
{
col[u]=num;//编号
for(int i=head[u];i!=0;i=a[i].nxt)
{
if(!col[a[i].v]&&b[i]==0) dfs1(a[i].v,num);//如果这条边不是割边就可以访问
}
}
void dfs(int u,int fa)//统计差分数组
{
d[u]=d[fa]+1;//更新深度
for(int i=head2[u];i!=0;i=e[i].nxt)
{
int v=e[i].v;
if(v!=fa)
{
dfs(v,u);
cha[u]+=cha[v];//统计差分数组
}
}
}
int main()
{
n=read(),m=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
add(u,v),add(v,u);//加边
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);//求割边,(原图不一定联通)
for(int i=1;i<=n;i++) if(!col[i]) tmp++,dfs1(i,tmp);//求边双连通分量
for(int i=2;i<=cnt;i++)
{
if(col[a[i].u]!=col[a[i].v]) add2(col[a[i].u],col[a[i].v]);//缩点+加新边
}
q=read();
while(q--)
{
int u=read(),v=read();
cha[col[u]]++,cha[col[v]]--;//差分
}
for(int i=1;i<=tmp;i++)if(!d[i])dfs(i,0);//统计差分数组,(新图也不一定联通)
for(int i=2;i<=cnt;i+=2)//两个两个地加,过滤掉反向边
{
if(col[a[i].u]==col[a[i].v]) //如果是在同一个边双连通分量里
{
printf("B");//那么边的方向就不确定
continue;
}
if(d[col[a[i].u]]>d[col[a[i].v]])//如果这条边的起点所在的边双连通分量更深,那么这条边的差分值就在起点
{
if(cha[col[a[i].u]]>0) printf("R");//说明这条边是指向根节点,也就指向这条边的终点
else if(cha[col[a[i].u]]==0) printf("B");//如果没有一个限制条件经过这条边,就输出B
else printf("L");//反之
}
else
{
if(cha[col[a[i].v]]>0) printf("L");//说明这条边是指向根节点,也就指向这条边的起点(起点深度更小)
else if(cha[col[a[i].v]]==0) printf("B");//同上
else printf("R");//反之 (这里不要写成L了)
}
}
return 0;
}
闲话
(我居然有朝一日能抢到最优解!!!)(2023.10.4)


浙公网安备 33010602011771号