Tarjan算法其实就是一种带技巧的DFS,比普通的dfs多了两个标记:dfn和low,实质上还是一种DFS。
网上有很多的tarjan算法详解,这里就不重复造轮子了。刚学tarjan算法的时候觉得很神奇,花了很长时间去消化,但是不去用慢慢就又忘了,所以这里贴出一些tarjan算法的练习题去巩固。
题目链接
题意是给你一张有向图,信息会沿着有向图的路径传播,
1.问要把初始信息给几个点才能传遍网络
2.问添加几条有向边可以让整张图强连通
题解:强连通缩点之后图变成DAG,入度为0的点的数量为第一问答案,入度为0的点和出度为0的点的数量的最大值为第二问答案。
AC代码:
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 233;
vector g[maxn];
struct node{
int u,v,nxt;
}e[maxn*maxn];
int cnt;
int head[maxn];
void add(int u,int v){
e[cnt].u = u;
e[cnt].v = v;
e[cnt].nxt = head[u];
head[u] = cnt++;
}
int in[maxn],out[maxn];
int n;
void init(){
cin>>n;
cnt = 0;
memset(head,-1,sizeof head);
int u = 1,v;
while(u <= n){
while(~scanf("%d",&v)){
if(v == 0) break;
add(u,v);
}
u++;
}
}
int id[maxn];
int dfn[maxn],low[maxn];
int index;
int scc;
stack s;
int inq[maxn];
void tarjan(int u){
s.push(u);
inq[u] = 1;
low[u] = dfn[u] = index++;
for(int i = head[u];i!=-1;i = e[i].nxt){
int v = e[i].v;
if(!dfn[v]) {
tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(inq[v]) low[u] = min(low[u],dfn[v]);
}
if(low[u] == dfn[u]){
scc++;
while(s.top()!=u){
int v = s.top();s.pop();
id[v] = scc;
inq[v] = 0;
}
s.pop();
id[u] = scc;
inq[u] = 0;
}
}
void sol(){
memset(dfn,0,sizeof dfn);memset(low,0,sizeof low);memset(inq,0,sizeof inq);
memset(in,0,sizeof in);memset(out,0,sizeof out);
index = 1;scc = 0;
while(s.size()) s.pop();
for(int i = 1;i <= n;++i) if(!dfn[i]) tarjan(i);
if(scc == 1){
cout<<1<<"\n"<<0;return;
}
for(int i = 0;i < cnt;++i){
int u = e[i].u;
int v = e[i].v;
if(id[u] == id[v]) continue;
in[id[v]]++;
out[id[u]]++;
}
int a = 0,b = 0;
for(int i = 1;i <= scc;++i){
if(!in[i]) a++;
if(!out[i]) b++;
}
cout<
题目链接
题意:一棵树,边权为1,查询两个结点之间的距离,多次查询
题解:先DFS求各点到根的距离,对于查询u,v,dis[u] + dis[v] - 2*dis[lca(u,v)]即为答案。
AC代码:
#include
#include
#define pb push_back
using namespace std;
typedef pair P;
const int maxn = 4e4 + 50;
int n,m;
vector g[maxn],q[maxn];
vector
query;
int lca[maxn];
void init(){
query.clear();
scanf("%d %d",&n,&m);
for(int i = 0;i <= n;++i) g[i].clear(),q[i].clear();
for(int i = 1; i < n;++i){
int u,v,w;
scanf("%d %d %d",&u,&v,&w);
g[u].pb( P(v,w) );
g[v].pb( P(u,w) );
}
for(int i = 0; i < m;++i) {
int u,v;scanf("%d %d",&u,&v);
query.pb( P(u,v) );
q[u].pb( P(v,i) );
q[v].pb( P(u,i) );
}
}
int vis[maxn];
int fa[maxn];
int fnd(int x){
if(x == fa[x]) return x;
return fa[x] = fnd(fa[x]);
}
int dis[maxn];
void tarjan(int u,int w){
fa[u] = u;
dis[u] = w;
vis[u] = 1;
for(int i = 0;i < g[u].size();++i){
int v = g[u][i].first;
if(!vis[v]) tarjan(v,w + g[u][i].second),fa[v] = u;
}
for(int j = 0;j < q[u].size();++j){
int v = q[u][j].first;
if(!vis[v]) continue;
int id = q[u][j].second;
lca[id] = fnd(v);
}
}
void sol(){
for(int i = 0;i <= n;++i) fa[i] = i,vis[i] = dis[i] = 0;
tarjan(1,0);
for(int i = 0;i < query.size();++i){
int u = query[i].first;
int v = query[i].second;
printf("%d\n",dis[u] + dis[v] - 2*dis[lca[i]]);
}
}
int main(){
int T;cin>>T;
while(T--)init(),sol();
}
题目链接
题意:给一张无向图,求所有桥中权值最小的一个
题解:tarjan求桥,注意求出权值为0的话要输出1,因为要有人运炸弹
#include
#include
using namespace std;
const int maxn = 1e3 + 50;
const int inf = 0x3f3f3f3f;
int n,m;
struct node{
int v,w,nxt;
}e[maxn*maxn];
int cnt = 0;
int head[maxn];
void add(int u,int v,int w){
e[cnt].v = v;
e[cnt].w = w;
e[cnt].nxt = head[u];
head[u] = cnt++;
}
void init(){
cnt = 0;
memset(head,-1,sizeof head);
while(m--){
int u,v,w;scanf("%d%d%d",&u,&v,&w);
add(u,v,w);add(v,u,w);
}
}
int dfn[maxn],low[maxn];
int index;
int ans = inf;
void tarjan(int u,int id){
dfn[u] = low[u] = ++index;
for(int i = head[u];i != -1;i = e[i].nxt){
int v = e[i].v;
if(!dfn[v]){
tarjan(v,i);
low[u] = min(low[u],low[v]);
if(low[v] > dfn[u]) ans = min(ans,e[i].w);
}
else if(i != (id^1)) low[u] = min(low[u],dfn[v]);
}
}
void sol(){
memset(dfn,0,sizeof dfn);
memset(low,0,sizeof low);
index = 0;
ans = inf;
tarjan(1,-1);
for(int i = 1;i <= n;++i){
if(!dfn[i]){
cout<<0<
POJ3694
题意:给一张无向图,然后有q次操作,每次操作会添加一条无向边,每次操作完之后都要输出当前操作结束后的图的割边的个数。
题解:首先把边双连通分量通过并查集缩点,同时把割边标记出来并记录割边个数。缩点之后图变成了一棵树,然后操作的时候,就根据dfn,顺着两个点往上找到lca,沿途遇到的割边全都去掉,更新答案,顺便也用并查集缩一下点。仔细想想可以发现,当两个点往上爬到同一个并查集的时候,他们已经在同一个双连通分量了,再往上爬也不会有割边出现,所以可以直接跳出。(然而对于这题,一些优化不加也可以过
这是一开始的双连通分量没缩点,查询的时候缩点,用时1297ms
这是一开始双连通分量缩点了,查询的时候也缩点了,但是查询的时候直到爬到lca才结束,用时563ms
这是两个点爬到同一个并查集就结束更新,用时360ms
AC代码:
#include
#include
#include
#include
using namespace std;
const int maxn = 2e5 + 50;
const int inf = 0x3f3f3f3f;
int n,m,q;
struct node{
int v,nxt;
}e[maxn*5];
int cnt = 0;
int head[maxn];
void add(int u,int v){
e[cnt].v = v;
e[cnt].nxt = head[u];
head[u] = cnt++;
}
void init(){
cnt = 0;
for(int i = 0;i <=n ;++i) head[i] = -1;
while(m--){
int u,v;scanf("%d%d",&u,&v);
add(u,v);add(v,u);
}
}
int dfn[maxn],low[maxn],pre[maxn],inq[maxn],isgb[maxn];
int fa[maxn];// 用来缩点
int fnd(int x){if(x == fa[x]) return x;return fa[x] = fnd(fa[x]);}
int index;
int ans = 0;
void tarjan(int u,int id){
dfn[u] = low[u] = ++index;
inq[u] = 1;
for(int i = head[u];i != -1;i = e[i].nxt){
int v = e[i].v;
if(i == (id^1)) continue;
if(!dfn[v]){
pre[v] = u;
tarjan(v,i);
if(low[v] > dfn[u]) isgb[v] = 1,ans++;
else fa[v] = fnd(u);//缩点
low[u] = min(low[u],low[v]);
}
else if(inq[v]) low[u] = min(low[u],dfn[v]);
}
inq[u] = 0;
}
void link(int u,int v){
int fx = fnd(u),fy = fnd(v);
if(fx == fy) return ;
fa[fx] = fy; return ;
}
void lca(int u,int v){
while(dfn[u]!=dfn[v]){
if(dfn[u] > dfn[v]){
if(isgb[u]) ans--,isgb[u] = 0;
link(u,pre[u]);//缩点
u = pre[u];
}
else {
if(isgb[v]) ans--,isgb[v] = 0;
link(v,pre[v]);//缩点
v = pre[v];
}
if(fnd(u) == fnd(v)) break;
}
}
int ca = 0;
void sol(){
for(int i = 0;i <= n;++i) isgb[i] = inq[i] = dfn[i] = low[i] = 0,fa[i] = i;
index = 0;ans = 0;
tarjan(1,-1);
int q;cin>>q;
printf("Case %d:\n",++ca);
while(q--){
int u,v;scanf("%d %d",&u,&v);
int ru = fnd(u);
int rv = fnd(v);
if(ru != rv){
lca(u,v);
}
printf("%d\n",ans);
}
printf("\n");
}
int main(){
while(~scanf("%d %d",&n,&m)){
if(n == 0 && m == 0) break;
init();
sol();
}
}
这题求LCA部分网上很多写法是先预处理所有点的深度,操作u和v的时候先提升到同一个深度再一起往上跑,但是其实我们可以利用dfn的性质直接找到lca。