tarjan算法 习题

dfs树与tarjan算法

标签(空格分隔): 517coding problem solution dfs树 tarjan


Task 1

给出一幅无向图\(G\),在其中给出一个dfs树\(T\),选出\(G\)中的一个边集\(E\),使得在所有T-Simple-Circle(即最多有1条边不在\(T\)中的路径)包含至少一个\(E\)中的元素,最小化\(E\)中元素个数。
对于100%的数据 \(1 \leq n \leq 2 \times 10^3, 1 \leq m \leq 2\times 10^4\)
考虑到选取边一定是在\(T\)中,所以对于一个简单环构成的路径,连接着当前节点和它的祖先节点这一条树上的路径,这条路径上必须选择一条边被选择。

将边下放到其对应子节点,

考虑到一个贪心,只需要将这些链按照深度较浅的节点深度递减排序,每一次如果当前链没有被点覆盖,那么就将深度较浅的点覆盖,这样线段本身被覆盖然后对后续的线段有不低于放在深度较浅的点之下的点的效果。

虽然可以树链剖分达到\(O(n{ log_2 }^2 n)\)的复杂度,但没有必要。

最终复杂度是\(O(n^2)\)

# include<bits/stdc++.h>
using namespace std;
const int N=2e3+10,M=2e4+10;
struct rec{ int pre,to; }a[M<<1];
struct node{ int u,v; }rec[N];
int n,m,tot=1,g;
int head[N],dep[N],pre[N];
bool mark[N],inTree[M<<1];
vector<node>V;
void clear()
{
    tot=1;
    memset(a,0,sizeof(a)); memset(rec,0,sizeof(rec));
    memset(dep,0,sizeof(dep)); memset(head,0,sizeof(head));
    memset(pre,0,sizeof(pre)); memset(mark,false,sizeof(mark));
    memset(inTree,false,sizeof(inTree)); V.clear();
}
void adde(int u,int v,bool flag)
{
    a[++tot].pre=head[u];
    a[tot].to=v;
    head[u]=tot;
    if (flag) inTree[tot]=1;
}
void dfs(int u,int fa)
{
    dep[u]=dep[fa]+1;
    for (int i=head[u];i;i=a[i].pre){
        int v=a[i].to; if (v==fa) continue;
        pre[v]=u; dfs(v,u);
    }
}
bool cmp(node x,node y){return dep[x.v]>dep[y.v];}
bool check(int s,int t)
{
    while (pre[s]!=t) {
        if (mark[s]) return true;
        s=pre[s];
    }
    if (mark[s]) return true;
    g=s;
    return false;
}
int main()
{
    //freopen("data.in","r",stdin); 
    while (true) {
        scanf("%d%d",&n,&m); if (!n&&!m) break;
        for (int i=1;i<n;i++) {
            int u,v; scanf("%d%d",&u,&v);
            adde(u,v,1); adde(v,u,1);
            rec[i]=(node){u,v};
        }
        dfs(1,0);
        for (int i=n;i<=m;i++) {
            int u,v; scanf("%d%d",&u,&v);
            adde(u,v,0); adde(v,u,0);
        }
        for (int i=1;i<n;i++) {
            int u=rec[i].u;
            for (int w=head[u];w;w=a[w].pre){
                int v=a[w].to; if (dep[v]>dep[u]||inTree[w]) continue;
                V.push_back((node){u,v});
            }
            u=rec[i].v;
            for (int w=head[u];w;w=a[w].pre){
                int v=a[w].to; if (dep[v]>dep[u]||inTree[w]) continue;
                V.push_back((node){u,v});
            }
        }
        sort(V.begin(),V.end(),cmp);
        int ans=0;
        for (int i=0;i<V.size();i++) {
            int s=V[i].u,t=V[i].v;
            if (check(s,t)) continue;
            else mark[g]=1,ans++;
        }
        printf("%d\n",ans);
        clear();
    }
    return 0;
}

Task 2

在无向图\(G\)中取任意一个生成树,求出这些生成树必然会经过哪些边。
对于100%的数据$2 \leq N \leq 10^4, 1\leq M \leq 10^5 $

如果一条边是割边,那么它必然会出现在生成树中。
本题转化为求出所有的割边。

使用tarjan算法,其中dfn表示无向图的一棵dfs树中节点的dfs序编号,low表示节点经过一条不在dfs树上的边能连接到的节点中dfs序最小值。

显然,割边的判断条件是对于边\((u,v)是割边\),当且仅当$low_{v} > dfn_{u} $

如有重边,仍然可以处理,显然只需要记一下当前进入该节点的边编号(可以用成对变换)如果当前回到父亲节点的这条边不是来的那条边,就说明这两个节点直接含有重边了,一旦含有重边,那么子节点的low值就可以用父亲的dfs序值更新了。

这道题就做完了,\(O(n)\)

# include<bits/stdc++.h>
using namespace std;
const int N=1e4+10,M=1e5+10;
struct rec{ int pre,to; }a[M<<1];
int dfn[N],low[N],n,m,head[N],tot=1;
bool bridge[M<<1];
void clear()
{
    tot=1;
    memset(a,0,sizeof(a)); memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low)); memset(head,0,sizeof(head));
    memset(bridge,false,sizeof(bridge));
}
void adde(int u,int v)
{
    a[++tot].pre=head[u];
    a[tot].to=v;
    head[u]=tot;
}
void tarjan(int u,int id)
{
    low[u]=dfn[u]=++dfn[0];
    for (int i=head[u];i;i=a[i].pre) {
        int v=a[i].to; 
        if (!dfn[v]) { 
            tarjan(v,i);  low[u]=min(low[u],low[v]);
            if (low[v]>dfn[u]) bridge[i]=bridge[i^1]=1;
        } else if (i!=(id^1)) low[u]=min(low[u],dfn[v]);
    }
}
int main()
{
    int T; scanf("%d",&T);
    while (T--) {
        scanf("%d%d",&n,&m);
        for (int i=1;i<=m;i++) {
            int u,v; scanf("%d%d",&u,&v);
            adde(u,v); adde(v,u);
        }
        tarjan(1,0); int cnt=0;
        for (int i=2;i<=tot;i+=2) if (bridge[i]) cnt++;
        printf("%d\n",cnt);
        for (int i=2;i<=tot;i+=2) 
            if (bridge[i]) printf("%d%c",i>>1,(--cnt)==0?'\n':' ');
        if (T!=0) puts("");
        clear();
    }
    return 0;
 } 

Task 3

无向图找割点

我也不知道它的输入为什么这么复杂(是为了掩饰这道题水的本质吗?)

# include <iostream>
# include <cstdio>
# include <cstring>
# define min(a,b) ((a)<(b)?(a):(b))
# define max(a,b) ((a)<(b)?(b):(a))
using namespace std;
const int N=105,M=N*N;
struct rec{ int pre,to; }a[M<<1];
bool cur[N];
int dfn[N],low[N],head[N],root;
int n,m,tot;
void clear()
{
    memset(a,0,sizeof(a)); memset(cur,false,sizeof(cur));
    memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low));
    memset(head,0,sizeof(head)); tot=0;
}
void adde(int u,int v)
{
    a[++tot].pre=head[u];
    a[tot].to=v;
    head[u]=tot;
}
void tarjan(int u)
{
    dfn[u]=low[u]=++dfn[0];
    int flag=0;
    for (int i=head[u];i;i=a[i].pre) {
        int v=a[i].to;
        if (!dfn[v]){ 
            tarjan(v); low[u]=min(low[u],low[v]);
            if (low[v]>=dfn[u]) {
                flag++; 
                if (u!=root || flag>1) cur[u]=1;
            }
        } else low[u]=min(low[u],dfn[v]);
    }
}
int main()
{
    while (true) {
        scanf("%d",&n); if (!n) break;
        while (true) {
            int u; scanf("%d",&u); if (!u) break;
            while (getchar()!='\n') {
                int v; scanf("%d",&v);
                adde(u,v); adde(v,u);
            }
        }
        for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(root=i);
        int ans=0;
        for (int i=1;i<=n;i++) if (cur[i]) ans++;
        printf("%d\n",ans);
        clear();
    }
    return 0;
}

Task 4

有向图\(G\)中各节点都有一个权值,从\(s\)点出发到\(P\)个合法终点中的一个被认为是一条合法路径。选取一条路径,可以重复经过一些点(但是其权值只可以获得一次),最大化经过路径权值和。
对于100%的数据$1 \leq P\leq n,m\leq 5 \times 10^5 $

考虑到如果有环的话,那么最优的走法就是绕一圈环获取所有权值。

于是我们采用了\(tarjan\)算法来缩点,然后问题就转化为在\(DAG\)`上从\(s\)点出发到任意点的最长路了。(环中节点答案是一样的)

可以使用dp做法,将\(DAG\)进行拓扑排序,按照拓扑序做动态规划。

如果某个缩点的拓扑序在\(s\)所在的拓扑序之前,那么\(s\)是不可能走到那里的。

\(f_u\)表示从\(s\)点到\(u\)点的最长路,转移就是\(f_u=max\{f_{from} + \sum w_u\}\)

答案就是包含合法终点的缩点的最大值。

还要注意一些问题,比如说dp初值除了出发点,其他必须全部设为-inf
由于dp的时候是取max的,所以重边应该没什么问题。

# include <iostream>
# include <cstdio>
# include <cstring>
# include <vector>
# include <stack>
# include <queue>
# include <algorithm>
# define int long long
using namespace std;
const int N=5e5+10;
struct rec{ int pre,to; }a[N<<1];
stack<int>s; queue<int>q;
bool ok[N],ins[N];
int dfn[N],low[N],c[N],val[N],w[N],head[N],seq[N],in[N],f[N];
vector<int>E[N],F[N];
vector<int>scc[N];
vector<pair<int,int> >Edge;
int n,m,tot,cnt;
void adde(int u,int v)
{
    a[++tot].pre=head[u];
    a[tot].to=v;
    head[u]=tot;
}
void tarjan(int u)
{
    dfn[u]=low[u]=++dfn[0];s.push(u);ins[u]=1;
    for (int i=head[u];i;i=a[i].pre){
        int v=a[i].to;
        if (!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
        else if (ins[v]) low[u]=min(low[u],dfn[v]);
    }
    if (dfn[u]==low[u]) {
        int v; ++cnt;
        do {
            v=s.top(); s.pop(); ins[v]=0;
            c[v]=cnt; scc[cnt].push_back(v);
            val[cnt]+=w[v];
        } while (v!=u);
    }
}
void toposort(int s)
{
    if (!in[s]) q.push(s);
    for (int i=1;i<=cnt;i++) if (!in[i] && i!=s) q.push(i);
    while (!q.empty()) {
        int u=q.front();q.pop();
        seq[++seq[0]]=u;
        for (int i=0;i<E[u].size();i++) {
            int v=E[u][i]; 
            in[v]--; if (!in[v]) q.push(v);
        }
    }
}
signed main()
{
    scanf("%lld%lld",&n,&m);
    for (int i=1;i<=m;i++) {
        int u,v; scanf("%lld%lld",&u,&v);
        adde(u,v);
    }
    for (int i=1;i<=n;i++) scanf("%lld",&w[i]);
    int s,p; scanf("%lld%lld",&s,&p);
    for (int i=1;i<=p;i++) { int t; scanf("%lld",&t); ok[t]=1;}
    for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i);
    for (int u=1;u<=n;u++)
     for (int i=head[u];i;i=a[i].pre) {
        int v=a[i].to; if (c[u]==c[v]) continue;
        Edge.push_back(make_pair(c[u],c[v]));
     }
    sort(Edge.begin(),Edge.end()); 
    Edge.erase((unique(Edge.begin(),Edge.end())),Edge.end()); 
    for (int i=0;i<Edge.size();i++) {
        int u=Edge[i].first,v=Edge[i].second;
        E[u].push_back(v); in[v]++;
        F[v].push_back(u);
    } 
    toposort(s);
    int pt; for (int i=1;i<=cnt;i++) if (c[s]==seq[i]) { pt=i; break;} 
    memset(f,-0x3f,sizeof(f)); f[c[s]]=val[c[s]]; 
    for (int w=pt+1;w<=cnt;w++) {
        int u=seq[w];
        for (int i=0;i<F[u].size();i++) {
            int v=F[u][i];
            f[u]=max(f[u],f[v]);
        } 
        f[u]+=val[u];
    }
    int ans=0;
    for (int i=1;i<=n;i++) if (ok[i]) ans=max(ans,f[c[i]]);
    printf("%lld\n",ans); 
    return 0;
}

Task 5

有向图\(G\)中至少需加多少条有向边才能使每个节点都能从节点\(s\)出发被访问到。
对于100%的数据 \(1\leq s \leq n,m \leq 5000\)

缩点以后,图变成了\(DAG\)上的问题。

做一遍$ dfs \(找出所有不能和\)s\(所在的点联通的块,对于每一个连通块至少需要一条边才能和\)s$所在的点连通。

考虑到每一个入度为0的连通块其必然会加一条边使得其连接到\(s\)所在的连通块或者其他连通块。

所以答案是 不在\(s\)缩点里的且入度为0的连通块个数。

复杂度\(O(n)\)

# include <bits/stdc++.h>
using namespace std;
const int N=5005;
struct rec{ int pre,to; }a[N<<1];
stack<int>s;
int low[N],dfn[N],head[N],c[N],in[N];
bool ins[N],vis[N];
int n,m,tot,cnt;
vector<int>E[N];
void adde(int u,int v)
{
    a[++tot].pre=head[u];
    a[tot].to=v;
    head[u]=tot;
}
void tarjan(int u)
{
    dfn[u]=low[u]=++dfn[0]; s.push(u); ins[u]=1;
    for (int i=head[u];i;i=a[i].pre){
        int v=a[i].to; 
        if (!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
        else if (ins[v]) low[u]=min(low[u],dfn[v]);
    }
    if (low[u]==dfn[u]) {
        int v; ++cnt;
        do {
            v=s.top();s.pop();ins[v]=0;
            c[v]=cnt; 
        } while (v!=u);
    }
}
void dfs(int u)
{
    vis[u]=1;
    for (int i=0;i<E[u].size();i++) {
        int v=E[u][i]; if (vis[v]) continue;
        dfs(v);
    }
}
int main()
{
    int s; scanf("%d%d%d",&n,&m,&s);
    for (int i=1;i<=m;i++) {
        int u,v; scanf("%d%d",&u,&v);
        adde(u,v);
    }
    for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i);
    for (int u=1;u<=n;u++)
     for (int i=head[u];i;i=a[i].pre) {
        int v=a[i].to; if (c[u]==c[v]) continue;
        E[c[u]].push_back(c[v]); in[c[v]]++;
    }
    dfs(c[s]); int ans=0;
    for (int i=1;i<=cnt;i++)  if (!vis[i] && !in[i] ) ans++;
    printf("%d\n",ans);
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/ljc20020730/p/11291475.html