每个结点的结束时间和开始时间是dfs序,开始时间是此点第一次被遍历到时,结束时间为此点已经没法拓展,从栈中弹出,即已经遍历结束,不懂dfs序,可以看这个dfs序
2.深度优先遍历G的转置图GT ,选择遍历的起点时,按照结点的结束时间从大到小进行。遍历的过程中,
一边遍历,一边给结点做分类标记,每找到一个新的起点,分类标记值就加1。
如果此节点已经在反图中遍历过,就不再从它遍历,挑选下一个结束时间晚的
3. 第2步中产生的标记值相同的结点构成深度优先森林中的一棵树,也即一个强连通分量
至于证明。。。emmm,我没看懂,就自己想了一下
如果正图中 b->a,那么反图中为 a->b,而b的结束时间一定比a要晚,所以先遍历b,如果b->a,则说明在原图中a->b
#include
using namespace std;
int f[1000][1000];//存储正图
int rf[1000][1000];//存储反图
int vis[1000];
stack s;//用来存储节点离开时间
stack s1[1000];
int n;
//对原图dfs,找出每个节点的离开时间,用栈存储,直接pop,就不用再逆序
void dfs(int a){
vis[a]=1;
for(int i=1;i<=n;i++){
if(f[a][i]&&!vis[i]){
dfs(i);
}
}
s.push(a);
}
void rdfs(int a,int k){
vis[a]=1;
for(int i=1;i<=n;i++){
if(rf[a][i]&&!vis[i]){
rdfs(i,k);
}
}
s1[k].push(a);//对每个连通分量分支,记录下来
}
int main(){
int m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
f[x][y]=1;
rf[y][x]=1;
}
//对原图dfs
for(int i=1;i<=n;i++){
if(!vis[i])
dfs(1);
}
memset(vis,0,sizeof(vis));
int k=0;
//对反图dfs
while(!s.empty()){
if(!vis[s.top()])//节点按照节点离开时间从大到小遍历
rdfs(s.top(),++k);//计算连通分量个数
s.pop();
}
cout<
用邻接表写的代码(老师写的)
#include
#include
const int V = 1e4 + 7, E = 5e4 + 7;
int hd[V], to[E], fr[E], nt[E], pr[E], tl[E];
int n, m, id[V], vis[V], cnt[V], scc;
bool out[V];
void dfs(int u, int &clk) {//用引用,减少全局变量的使用
id[u] = 1;
for (int i = hd[u]; i; i = nt[i])
if (!id[to[i]]) dfs(to[i], clk);
vis[++clk] = u;//当一个节点是叶子节点时,记录结束时间且只记录结束时间
}
void rdfs(int v, int clk) {
id[v] = clk, ++cnt[clk];
for (int i = tl[v]; i; i = pr[i])
if (!id[fr[i]]) rdfs(fr[i], clk);
}
void Korasaju() {
for (int i = 1, clk = 0; i <= n; ++i) if (!id[i]) dfs(i, clk);//最晚结束时间必然是n
for (int i = 1; i <= n; ++i) id[i] = 0;
for (int i = n; i; --i)
if (!id[vis[i]]) rdfs(vis[i], ++scc);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
nt[i] = hd[u], pr[i] = tl[v], hd[u] = tl[v] = i;
to[i] = v, fr[i] = u;//同时存储正向与反向图
}
int ans = 0;
Korasaju();
//自此,产生了一个以强连通分量编号作为结点,构成的一个DAG图
for (int i = 1; i <= n; ++i)
for (int j = hd[i]; j; j = nt[j])
if (id[i] != id[to[j]]) out[id[i]] = true;
//如果度用++统计的话,需要考虑两个连通分量之间存在重复边(此题并不需要)
for (int i = 1; i <= scc; ++i)
if (!out[i]) {
if (ans) { puts("0"); return 0; }
ans = cnt[i];
}
printf("%d\n", ans);
return 0;
}
low数组和dfn数组,dfn是第一次搜到此点的时间戳,low值是此点所连接的点中最小dfn
栈用来存储搜索到的点
沿着起点一直搜索,如果搜到返祖边,则将此点的low值更改,low[u]=min(low[u],dfn[v])
至于为什么是dfn[v],不是low[v]?
low[v]并不是最终结果,不该使用,就算low[v]=dfn[v],也不使用他
当此点无法再搜下去,比较dfn与low值,相等则说明栈中从此点一直到栈顶为一个连通分量,将这些从栈中弹出,然后回溯,回溯时比较点的low值,再次更新low值,low[u]=min(low[u],low[v])
至于缩点只要将一个分量的染色就好了
受欢迎的牛
有用的定理
有向无环图中唯一出度为0的点,一定可以由任何点出发均可达(由于无环,所以从任何点出发往前走,必然终止于
一个出度为0的点)
因为强连通所有点都可互相到达,可以把强连通分量缩成一个点,如果有唯一的一个点的出度为0,那么这个分量的个数即为答案
如果不止一个出度为0,则无解。
#include
#include
#include
#include
using namespace std;
int n,m;
const int e=1e4+4;
//标记时间戳
int low[e],dfn[e],index;
//tarjan中的栈,和判断是否在栈中
stack s;
bool ins[e];
//标记所属分量,用于缩点
int col[e];
//计算有几个分量
int cnt;
//每个分量中的成员
vector ve[e];
//邻接表
struct edge{
int to,next;
};
edge f[5*e];
int head[e],id;
void add(int from,int to){
f[++id].next=head[from];
f[id].to=to;
head[from]=id;
}
//Tarjan
void Tarjan(int u){
dfn[u]=low[u]=++index;
s.push(u);
ins[u]=1;
for(int i=head[u];i;i=f[i].next){
int v=f[i].to;
if(!dfn[v])//没有深搜的点
Tarjan(v),low[u]=min(low[u],low[v]);//回溯阶段,无法继续探索,则从底往上逐级返回//low值
else if(ins[v])
low[u]=min(low[u],dfn[v]);//在栈中的点
}
//当此点全部探索完成,low值依然没有改变,说明为联通分量的根节点 ,可以出栈
if(dfn[u]==low[u]){
++cnt;
int t;
do{
t=s.top();
s.pop();
ins[u]=0;//标记出栈
ve[cnt].push_back(t);
col[t]=cnt;//缩点(染色标记)
}while(t!=u);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
add(a,b);
}
for(int i=1;i<=n;i++){
if(!dfn[i])
Tarjan(i);
}
int out[e]={0};//统计每个联通块的出度
for(int i=1;i<=n;i++){
for(int j=head[i];j;j=f[i].next){
int to=f[i].to;
if(col[i]!=col[to])//不是同一个连通块,则出度+1
++out[col[i]];
}
}
int sum=0,ans;
for(int i=1;i<=cnt;i++){
if(!out[i]) sum++,ans=ve[i].size();
}
if(sum==1)
cout<
对于tarjan中的栈的用处,有个博客写的特别详细
https://blog.csdn.net/qq_38234381/article/details/79981531
用数组模拟栈的写法(老师写的)
#include
#include
const int V = 1e4 + 7, E = 5e4 + 7;
//hd-head, fr-from, nt-next, pr-pre, tl-tail
int hd[V], to[E], nt[E], lbl[V], cnt[V], low[V], dfn[V], stk[V], tp, scc;
bool out[V];
void Tarjan(int u, int &clk){
low[u] = dfn[u] = ++clk;
stk[++tp] = u;
for (int i = hd[u]; i; i = nt[i])
if (not dfn[to[i]])
Tarjan(to[i], clk), low[u] = std::min(low[u], low[to[i]]);
else if (not lbl[to[i]])
low[u] = std::min(low[u], dfn[to[i]]);
if (low[u] == dfn[u])
for (++scc; stk[tp+1] != u; --tp)
lbl[stk[tp]] = scc, ++cnt[scc];
}
int main() {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
nt[i] = hd[u], hd[u] = i, to[i] = v;
}
for (int i = 1, clk = 0; i <= n; ++i)
if (not lbl[i]) Tarjan(i, clk);
for (int i = 1; i <= n; ++i)
for (int j = hd[i]; j; j = nt[j])
if (lbl[i] != lbl[to[j]]) out[lbl[i]] = true;
int ans = 0;
for (int i = 1; i <= scc; ++i)
if (not out[i]) {
if (ans) { puts("0"); return 0; }
ans = cnt[i];
}
printf("%d\n", ans);
return 0;
}
用pair存储每条边的两个端点,重点在于处理自环边和重复边,用sort处理,重新建图
#include
#include
#include
const int N = 5e5 + 7;
int hd[N], to[N], fr[N], nx[N], stk[N], tp, scc;
int low[N], dfn[N], id[N], sum[N], w[N], ans;
bool bar[N], inq[N], stop[N];
std::pair eg[N];
void Tarjan(int u, int &clk) {
low[u] = dfn[u] = ++clk;
stk[++tp] = u;
for (int i = hd[u], v; i; i = nx[i])
if (!dfn[v=to[i]])
Tarjan(v, clk), low[u] = std::min(low[u], low[v]);
else if (!id[v])
low[u] = std::min(low[u], dfn[v]);
if (low[u] == dfn[u])
for (++scc; stk[tp+1] != u; --tp)
id[stk[tp]] = scc, sum[scc] += w[stk[tp]], stop[scc] |= bar[stk[tp]];
}
int dis[N];
void spfa(int s) {
std::queue que;
que.push(s), ans = dis[s] = sum[s];
while (que.size()) {
int u = que.front(); que.pop();
inq[u] = false;
for (int i = hd[u]; i; i = nx[i]) {
int v = to[i], tot = dis[u] + sum[v];
if (dis[v] < tot) {
dis[v] = tot;
if (stop[v]) ans = std::max(ans, dis[v]);
if (not inq[v]) que.push(v);
}
}
}
}
int main() {
int n, m, s, p;
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
nx[i] = hd[u], hd[u] = i, to[i] = v, fr[i] = u;
}
for (int i = 1; i <= n; ++i) scanf("%d", w+i);
scanf("%d%d", &s, &p);
for (int i = 1, x; i <= p; ++i)
scanf("%d", &x), bar[x] = true;
for (int i = 1, clk = 0; i <= n; ++i) if (!dfn[i]) Tarjan(i, clk);
//缩点建新图
for (int i = 1; i <= m; ++i)
eg[i].first = id[fr[i]], eg[i].second = id[to[i]];
std::sort(eg+1, eg+m+1);
for (int i = 1; i <= scc; ++i) hd[i] = 0;
for (int i = 1; i <= m; ++i) {
int u = eg[i].first, v = eg[i].second;
int uu = eg[i-1].first, vv = eg[i-1].second;
if (u!=v && (u!=uu || v!=vv))//清除自环边和重复边
nx[i] = hd[u], hd[u] = i, to[i] = v;
}
//在新图上直接跑spfa
spfa(id[s]);
printf("%d\n", ans);
return 0;
}