1. 程式人生 > >牛客D-Where are you /// kruskal+tarjan找無向圖內的環

牛客D-Where are you /// kruskal+tarjan找無向圖內的環

題目大意:

https://ac.nowcoder.com/acm/contest/272/D

在一個無向圖中,給定一個起點,從起點開始走遍圖中所有點

每條邊有邊權wi,表示第一次經過該道路時的花費(第二次及以後經過時花費為0)

此時用最少花費完成可能存在多種方案

求每種方案都必須經過的邊有多少條

 

首先想到最小生成樹

然後想到在得到最短邊時 若存在其他長度相等的邊 這條邊此時就可被替代

但如果沒有長度相等的邊 那麼這條邊就是必須經過的邊

然而這個想法經不起考驗 是錯誤的 如下

 

但是沒有長度相等的邊 就是必須經過的邊 這是毋庸置疑的

那麼再觀察下圖

 

可以看到這是 一個只由長度相等的邊組成的圖

若在這個圖中 我們要得到一棵生成樹的話 邊3-4是必選的 而剩下的1-2、2-3、3-1則任選兩條即可

也就是說在生成樹的所有選擇方案裡 這三條邊不是必須經過的邊

那麼可以發現 在一個只由長度相等的邊組成的圖內 能形成一個環的幾條邊不是在所有的選擇方案裡必須經過的邊

 

再結合kruskal得到最小生成樹的步驟 每次先得到所有相同長度的最短邊建圖

再找到其中的環的個數m 那麼要連線所有的環 必須經過的邊就有m-1條

 

tarjan 求無向圖內的環 就是在 有向圖求強聯通分量 的基礎上進行修改

將 已走過的邊 視為有向 不走其反向邊

那麼當走完這個圖之後 整個圖變成了一個有向圖 此時圖中的強聯通分量就是環

 

如何 將已走過的邊視為有向 呢

首先建圖的過程中 對於一條邊 我們是連了正向就連反向的 也就是這兩條有向邊在儲存過程中的序號是連續的

所以我們從序號2開始存邊的話 序號為 2和3 的兩條有向邊對應一條無向邊 4和5對應一條無向邊 6和7對應一條......

則對於 儲存順序為第 x 的有向邊 其對應的反向邊(即另一條有向邊)順序為 (x^1)

那麼我們在遞迴時將上一條邊的順序 last 作為引數傳過來 不走它對應的反向邊即跳過順序為 last^1 的邊 就可以了

 

#include <bits/stdc++.h>
#define mem(i,j) memset(i,j,sizeof(i))
using namespace std;
const int N=2e5+5;
 
struct EDGE {
    int u,v,w;
    bool operator <(const EDGE& p)const {
        return w<p.w;
    }
}E[N<<1];
struct NODE { int to,nt; }e[N<<1];
int head[N], tot;
void addE(int u,int v) {
    e[++tot].to=v;
    e[tot].nt=head[u];
    head[u]=tot;
}
int dfn[N], low[N], ind;
int fa[N], ans;
int n, m, p;
void init_e() {
    ind=0; mem(dfn,0);
    tot=1; mem(head,0);
}
void init_s() {
    ans=0;
    for(int i=1;i<=n;i++) fa[i]=i;
}
 
int tarjan(int u,int last) {
    dfn[u]=low[u]=++ind;
    int res=0;
    for(int i=head[u];i;i=e[i].nt) {
        if(i==(last^1)) continue; // 是上一條邊的對應反向邊 跳過
        int v=e[i].to;
        if(!dfn[v]) {
            res+=tarjan(v,i);
            low[u]=min(low[u],low[v]);
        } else low[u]=min(low[u],dfn[v]); 
    }
    if(dfn[u]==low[u]) res++;
    return res;
}
 
int getfa(int x) {
    if(x==fa[x]) return x;
    return fa[x]=getfa(fa[x]);
}
void unite(int x,int y) {
    x=getfa(x), y=getfa(y);
    if(x!=y) fa[x]=y;
}
 
void Kruskal() {
    sort(E,E+m);
    for(int i=0,j;i<m;i=j) {
        j=i;
        while(j<m && E[j].w==E[i].w) j++; //找到所有與最短邊相等的邊
        init_e(); // 初始化鄰接表和tarjan需要的陣列
        for(int k=i;k<j;k++) { // 建圖
            int u=getfa(E[k].u), v=getfa(E[k].v);
            if(u==v) continue; // 兩個點已經連起來了
            addE(u,v); addE(v,u);
        }
        for(int k=i;k<j;k++) {
            int u=getfa(E[k].u);
            if(!dfn[u]) ans+=tarjan(u,0)-1; //保證m個環連通 需要m-1條邊
        }
        for(int k=i;k<j;k++)
            unite(E[k].u,E[k].v);
    }
}
 
int main()
{
    while(~scanf("%d%d%d",&n,&m,&p)) {
        init_s();
        for(int i=0;i<m;i++)
            scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].w);
        Kruskal();
        printf("%d\n",ans);
    }
 
    return 0;
}
View Code