没有用的话qaq :
Ummmm…图论的大部分知识本来早就有学过,只是一直没有写成博文来梳理,但既然上了qbxt DP图论就写一篇来总结下,主要是来听DP的,但…由于太菜的原因,DP听得天花乱坠QWQ
一,基础知识篇
1,强连通:如果有两个点u和v,如果u能到达v,v也能到达u,就称u和v是强连通的。
2,强连通分量:有向图极大的强连通子图。
3,强连通图:有向图中任意两个顶点都强连通的图称为强连通图。
4,缩点:将所有的强连通分量都缩成一个点,有向图缩点会形成一个DAG
比如上图中,1和4可以互相到达,我们称1和4强连通。 1,2,3,4构成了一个极大强连通子图(即不存在一个包含1,2,3,4但仍比1,2,3,4构成的强连通子图还大的图),1,2,3,4构成一个强连通分量。
二,找寻强连通分量的方法
1,Kosaraju算法 :基于两次DFS的有向图强连通子图算法。
算法的主要思想是建一个正着的图,再建一个反着的图,先dfs遍历正向图,记下完成遍历的顺序,记住是完成遍历的顺序而不是遍历的顺序,比如上图 遍历的顺序是1-2-4-6-3-5 完成遍历的顺序是 6-4-2-5-3-1,然后再用返图按完成遍历的顺序取最后一个完成访问的节点开始dfs,每dfs一遍证明有一个强连通分量。
一般求取强连通分量运用下一种方法较多,此种方法博主认为OI中应用较少
代码实现
inline void dfs_one(int st)
{
vis[st]=true;
for(int i=first[st];i;i=edge[i].ed)
if(!vis[edge[i].ed]) dfs_one(edge[i].ed);
d[++cnt]=edge[i].ed;
}
inline void dfs_two(int st)
{
vis[st]=true;
for(int i=firsr[st];i;i=edge[i].nxt)
if(!vis[edge[i].ed]) dfs(edge[i].ed);
}
inline void kosaraju()
{
for(int i=1;i<=n;++i)
if(!vis[i]) dfs_one(i);
memset(vis,false,sizeof(vis));
for(int i=n;i>=1;--i)
if(!vis[d[i]])
{
cs++;
dfs_two(d[i]);
}
}
2,tarjan算法:基于一课dfs树的有向图强连通分量算法。
首先什么是dfs树,就是从某个节点出发访问一次所有节点的过程中走过的边形成的结构称为dfs树,dfs树上的边称为树边。
图中绿色边构成的便是一棵dfs树,当然有人将dfs过程中的边分为四种类型
树边(绿色边):DFS时找到的还没有被搜索过的点形成的边
前向边(蓝色边):在搜索的时候遇到子树中的结点的时候形成的
反组边(黄色边):指向祖先结点的边
横叉边(红色边):在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先时形成的
但我觉得为了方便理解,在真正的算法中后三种类型的边的作用其实是相同的,所以我将它们统称为非树边,但非树边中分为两类一种是横叉边,一种是普通的非树边。那么如何利用dfs树找到图中的强联通分量呢?我们就用到了图灵奖得主,Tarjan先生发明的算法,其流程是
a.给每个点设置两个参数dfn和low,dfn[i]代表节点i被访问到的时间,low[i]代表从i点出发能追溯到的访问时间最早的点,其中树边的追溯具有传递性,这也构成了我们这个算法。
b.从节点st开始搜索,如果搜到没有访问过的节点,就代表搜到的是树边,先把这个点压入栈,并给搜到的节点盖个时间戳,在搜索的时候初始化dfn[i]=low[i]=++cnt;那么怎么追溯呢,回溯就起到了关键的作用。如果找到了没有被访问过的点即dfn[i]=0的点就访问这个点,并在回溯时候更新low,因为说过树边的追溯具有传递性,所以low[i]=min(low[i],low[edge[i].ed];如果找到了已经访问过的点且这个点不属于任何一个强连通分量(如果某个点已经属于某个强联通分量了你再去追溯会出错的!!这种边叫横叉边也是非树边),就是非树边,在追溯的时候仅追溯这条边的终点即low[i]=min(low[i],low[edge[i].ed]);试想一下这时候就找到了强联通分量...qwq
c.在回溯的过程中如果发现某个点的dfn值与low值相等,这说明这是这个强连通分量的原点(即这个强连通分量重第一个被搜到的点),此时这个点与它在栈中到栈顶的所有元素同属于一个强连通分量,因为它是这个强连通分量中最早被搜到的嘛所以肯定最深,而且这些点中一定不存在不属于这个强连通分量的点,试想一下,如果有的话它早就在回溯的是偶被别的强连通分量弹走了,手摸下然后意会。
正确性证明的话,博主不会什么证明只能说它一定是对的,可以手摸下,其实是yousiki没讲证明 ,还是我太菜。
看一下代码实现
inline void tarjan(int st)
{
dfn[st]=low[st]=++cnt;//盖时间戳
s[++top]=st;
for(int i=first[st];i;i=edge[i].nxt)
{
int e=edge[i].ed;
if(!dfn[e])//证明是树边
{
tarjan(e);
low[st]=min(low[st],low[e]);
}
else if(!css[e])
{
low[st]=min(low[st],low[e]);
}
}
if(dfn[st]==low[st])
{
cs++;
while(s[top]!=st)
{
css[s[top]]=cs; size[cs]++; top--;//size是这个强连通分量的大小
}
css[s[top]]=cs; size[cs]++; top--;//把st弹出来
}
}
洛谷P3387 缩点模板,只需要在普通缩点下统计一下每个强联通分量的点权和,然后重新建个图在DAG上dp一下就好
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#define maxn 10005
using namespace std;
struct node
{
int ed,nxt;
} ;
node edge[maxn*10];
int first[maxn],cnt,n,m;
int css[maxn],cs,s[maxn],top,sum[maxn],dfn[maxn],low[maxn],dfncnt;
int x[maxn*10],y[maxn*10],dp[maxn],ans,a[maxn];
inline void add_edge(int s,int e)
{
cnt++;
edge[cnt].ed=e;
edge[cnt].nxt=first[s];
first[s]=cnt;
return;
}
inline void tarjan(int k)
{
dfn[k]=low[k]=++dfncnt; s[++top]=k;
for(int i=first[k];i!=0;i=edge[i].nxt)
{
int e=edge[i].ed;
if(!dfn[e])
{
tarjan(e); low[k]=min(low[e],low[k]);
}
else if(!css[e])
{
low[k]=min(low[e],low[k]);
}
}
if(dfn[k]==low[k])
{
cs++;
while(s[top]!=k)
{
css[s[top]]=cs; sum[cs]+=a[s[top]]; top--;
}
css[s[top]]=cs; sum[cs]+=a[s[top]]; top--;
}
return;
}
inline int dfs(int k)
{
int maxsum=0;
if(dp[k]) return dp[k];
dp[k]=sum[k];
for(int i=first[k];i!=0;i=edge[i].nxt)
{
int e=edge[i].ed;
if(!dp[e]) dfs(e);
maxsum=max(maxsum,dp[e]);
}
dp[k]+=maxsum;
return 0;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x[i],&y[i]);
add_edge(x[i],y[i]);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i]) tarjan(i);
}
memset(first,0,sizeof(first));
for(int i=1;i<=n;i++)
{
edge[i].ed=0;
edge[i].nxt=0;
}
for(int i=1;i<=m;i++)
{
if(css[x[i]]!=css[y[i]]) add_edge(css[x[i]],css[y[i]]);
}
for(int i=1;i<=cs;i++)
{
if(!dp[i]) dfs(i);
ans=max(ans,dp[i]);
}
printf("%d",ans);
return 0;
}
写在最后 Yousiki Orz Orz