【深圳大学算法设计与分析】实验五 桥

实验目的

(1)   掌握图的连通性。

(2)   掌握并查集的基本原理和应用。

实验内容与结果

1. 桥的定义

在图论中,一条边被称为“桥”代表这条边一旦被删除,这张图的连通块数量会增加。等价地说,一条边是一座桥当且仅当这条边不在任何环上。一张图可以有零或多座桥。

                                     

      图 1 没有桥的无向连通图                          图 2 有16个顶点和6个桥的图(桥以红色线段标示)

2. 求解问题

找出一个无向图中所有的桥。

3. 算法

1)基准算法

For every edge (u, v), do following

a) Remove (u, v) from graph

b) See if the graph remains connected (We can either use BFS or DFS)

c) Add (u, v) back to the graph.

基准算法的思想是:遍历每一条边,分别移去该边,观察图的连通性是否有变化;换而言之,即观察图的连通分量的个数是否相同。

设计基准算法:

1.用矩阵存储图,遍历所有边,逐一选取每一条边。

2.先用dfs遍历全图,得到连通分量个数并记录。

3.移去该边后,再次用dfs遍历全图,得到新的连通分量个数并记录。

4.判断前后连通分量个数是否相同,若不相同,则该边为桥。

伪代码如下:

在dfs函数中,深度遍历当前节点所能达到的节点,并将其设为已经访问,直到能访问的节点全部访问完,即该节点所在连通分量已经全部遍历时返回;

countConnectedComponents函数遍历所有未访问过的节点并进入dfs函数,该节点所在连通分量已经全部遍历后返回时,记录连通分量个数的countlian变量加一,当该图所有节点均遍历后,返回连通分量个数;

isBridge函数用来判断边(u,v)是否是桥,其首先将该边从图中移除后获取连通分量个数,接着将该边添加回图中后再次获取连通分量个数,最后判断前后连通分量个数是否相等,若不相等,则代表该边为桥,输出该边为桥的信息后,记录桥的总数的qiaoshu变量加一。

运行小图的结果如下,正确输出为边的桥,以及桥的总数。

运行中图的结果如下

基准算法复杂度分析:

该算法的时间复杂度主要在dfs函数部分和countConnectedComponents函数部分,算法复杂度为O(n2)

2)应用并查集设计一个比基准算法更高效的算法。

优化领接表数据结构,添加增添和删除某条边的功能。

并查集:并查集是一种树型的数据结构,用于处理一些不相交集合的合并以及查询问题。我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。简而言之,并查集就是用来对集合进行合并与查询操作的一种数据结构。

其通常有功能:

合并:将两个不相交的集合合并成一个集合;

查询:查询两个元素是否属于同一集合。

路径压缩:把沿途所有节点的父亲节点均设为根节点。这样可以大大降低并查集查询操作过程中的递归向上的查询深度,从而在大规模数据下极大地缩短算法在查找所耗费的时间。

最近公共祖先(LCA:两个点在这棵树上距离最近的公共祖先节点。

寻找方法:两个节点向根节点方向查找,交汇点即最近公共祖先。

为了优化算法,首先我们需要明确桥的一个等价条件:一条边是一座桥当且仅当这条边不在任何环上。即形成环的所有的边均不是桥,我们只需要排除这些边,便能得到桥。

我们可以创建生成树,如下图所示即为多个生成树。生成树上的点,只要任意两个相连,便会形成环,因此我们只需要逐一添加边,并移去生成了环的边,便能得到桥。

以点代边思想:对树而言,除根节点外所有节点有且仅有一个父亲节点。因此生成树上的每一条边其实都可以用这条边的孩子节点来表示。如边(1,2)可以用2表示。那么我们可以用一个数组来表示某条边是否已被访问过,如zu[2]=1。

算法流程:

1.创建并查集并标记非生成树边。

2.创建生成树:

(1)搜索并查集找到所有根节点。

(2)对每个根节点进行dfs遍历,遍历中记录每个节点的父亲节点以及其在树中的高度。

3.将非生成树边(u,v)逐一加入生成树,寻找u和v的最近公共祖先,与此同时记录寻找过程中经过的边。若经过的边未被访问过,则非桥边数+1。

跑小图结果

跑中图结果,由基准算法的218ms缩短到6ms,算法效率提升。

实验总结

通过此次实验,加深了对图和树的知识的理解和运用,学会了并查集数据结构,以及各种优化方法和思想。

基准算法虽然构思简单,容易实现,但是其效率极低,因此需要改进算法,不断优化以提高算法效率。

选择合适的数据结构,如本次实验中的并查集,可以极大的提升运行效率。

实验伪代码

//基准法 
dfs(int index)
	visit[index]=true
	for i=0 to n-1
        if juzhen[index][i] && !visit[i]       
            dfs(i)
        
countConnectedComponents()  
 	for i=0 to n-1
        if !visit[i]
            dfs2(i)
            countlian++
    return countlian
    
isBridge(int u,int v)
	juzhen[u][v] = juzhen[v][u] = false; 
    lian2 = countConnectedComponents()

    juzhen[u][v] = juzhen[v][u] = true
    lian1 = countConnectedComponents()
    
    if lian1 != lian2
         (u,v) is bridge
		 qiaoshu++;		

//领接表数据结构 
struct Edge 
    int bian  
    Edge* next = NULL

    void addbian(int v)    
        Edge* p = this
        while (p->next)
            p = p->next
        Edge* s = new Edge{ v,NULL }
        p->next = s
    void shanchu(int v)
        Edge* p = this;
        while (p->next)        
            if p->next->bian == v
                break
            p = p->next            
        if p->next == NULL
            return
        Edge* s = p->next
        p->next = s->next
 
//并查集
struct DisjointSet 
    int* parent  // 记录顶点的父节点
    int* rank    // 记录顶点的秩(树的高度)
    
    void init(int n) // 构造函数,初始化并查集
        parent = new int[n]
        rank = new int[n]

        for i=0 to n-1 
            parent[i] = i
            rank[i] = 0

    // 查找根节点(路径压缩)
    int find(int x) 
        int root = x
        while root != parent[root]
            root = parent[root]      
        while x != root 
            int next = parent[x]
            parent[x] = root
            x = next       
        return root

    // 合并两个集合(按秩合并)
    void merge(int x, int y) 
        int rootX = parent[x]
        int rootY = parent[y]

        if rootX != rootY        
            if rank[rootX] < rank[rootY] 
                parent[rootX] = rootY
            
            else if rank[rootX] > rank[rootY] 
                parent[rootY] = rootX
            
            else           
                parent[rootY] = rootX
                rank[rootX]++

//寻找最近公共祖先并记录非桥边数 
void lca(int u, int v)
    feiqshu++
    gaou = gao[u]
    gaov = gao[v]

    if gaou >= gaov
        da = u,dagao=gaou,xiao=v, xiaogao = gaov
    else ifgaov > gaou
        da = v, dagao = gaov, xiao = u, xiaogao = gaou
    
    //当二者高度不同
    while dagao != xiaogao
        if dai[da] == 0
            dai[da] = 1
            feiqshu++
        da = fu[da]
        dagao--

    //当二者高度相同
    while da != xiao
        if dai[da] == 0
            dai[da] = 1
            feiqshu++        
        if dai[xiao] == 0
            dai[xiao] = 1;
            feiqshu++;       
        da = fu[da];
        xiao = fu[xiao];

实验代码

基准算法.cpp 

#include <iostream>
#include <cstring>
#include<fstream>
using namespace std;

//# define lujin "D:\\smallG.txt"     //路径选择
# define lujin "D:\\mediumG.txt"
//# define lujin "D:\\largeG.txt"

const int MAXN = 10000;

int n, m;   //n为节点个数,m为边数
bool juzhen[MAXN][MAXN];  // 邻接矩阵表示图结构
bool visit[MAXN];  // 记录顶点的访问状态
bool bridge[MAXN][MAXN];  // 记录边是否为桥
int discoveryTime[MAXN];  // 记录顶点的发现时间
int earliestTime[MAXN];  // 记录顶点的最早发现时间
int timeCounter;  // 全局时间变量
int qiaoshu = 0;    // 桥的个数
int countlian = 0;  //记录连通分量个数

// 深度优先搜索
void dfs(int index, int parent)
{
    visit[index] = true;
    discoveryTime[index] = earliestTime[index] = timeCounter++;

    for (int i = 0; i < n; i++) 
    {
        if (juzhen[index][i]) 
        {
            if (!visit[i]) 
            {
                dfs(i, index);
                earliestTime[index] = min(earliestTime[index], earliestTime[i]);
                if (earliestTime[i] > discoveryTime[index]) 
                {
                    // (vertex, i) 是桥
                    //bridge[index][i] = bridge[i][index] = true;
                    cout << "边 (" << index << ", " << i << ") 是桥" << endl;
                    qiaoshu++;
                }
            }
            else if (i != parent) 
            {
                earliestTime[index] = min(earliestTime[index], discoveryTime[i]);
            }
        }
    }
}

// 判断边是否为桥
void isBridge() 
{
    memset(visit, false, sizeof(visit));
    memset(discoveryTime, 0, sizeof(discoveryTime));
    memset(earliestTime, 0, sizeof(earliestTime));
    timeCounter = 0;

    //juzhen[u][v] = juzhen[v][u] = false;  // 暂时移除边 (u, v) 

     // 对每个连通分量进行深度优先搜索
    for (int i = 0; i < n; i++) 
        if (!visit[i]) 
            dfs(i, -1);

    //juzhen[u][v] = juzhen[v][u] = true;  // 恢复边 (u, v)
}

// 深度优先搜索2
void dfs2(int index)
{
    visit[index] = true;

    for (int i = 0; i < n; i++)
    {
        if (juzhen[index][i] && !visit[i])
        {
            dfs2(i);
        }
    }
}

// 计算连通分量个数
int countConnectedComponents()
{
    countlian = 0;
    memset(visit, false, sizeof(visit));

    for (int i = 0; i < n; i++)
    {
        if (!visit[i])
        {
            dfs2(i);
            countlian++;
        }
    }

    return countlian;
}

// 判断边移去后连通分量数是否变化来判断是否是桥
void isBridge2(int u,int v)
{
    memset(visit, false, sizeof(visit));
    memset(discoveryTime, 0, sizeof(discoveryTime));
    memset(earliestTime, 0, sizeof(earliestTime));
    timeCounter = 0;

    int lian1, lian2;

    juzhen[u][v] = juzhen[v][u] = false;  // 暂时移除边 (u, v) 
    lian2 = countConnectedComponents();

    juzhen[u][v] = juzhen[v][u] = true;  // 恢复边 (u, v)
    lian1 = countConnectedComponents();
    
    if (lian1 != lian2)
        cout << "边 (" << u << ", " << v << ") 是桥" << endl, qiaoshu++;
}



int main() 
{
    // 初始化邻接矩阵和顶点访问状态
    memset(juzhen, false, sizeof(juzhen));
    memset(visit, false, sizeof(visit));

    ifstream ifs(lujin);

    ifs >> n >> m;
    int u, v;
    //输入每条边的起点和终点
    for (int i = 0; i < m; i++) 
    {       
        ifs >> u >> v;
        juzhen[u][v] = juzhen[v][u] = true;
    }
    ifs.close();

    ifs.open(lujin);
    //判断每一条边是否是桥
    /*ifs >> u >> v;
    for (int i = 0; i < m; i++)
    {
        ifs >> u >> v;
        isBridge(u, v);
    }*/
    int qi = clock();
    for (int i = 0; i < m; i++)
    {
        ifs >> u >> v;
        isBridge2(u, v);
    }
    int zhi = clock();
    cout << endl<<"桥的总数为 " << qiaoshu << " 个" << endl;
    cout << "耗时为 " << zhi - qi << " ms" << endl;
}

 

final.cpp

#include <iostream>
#include<cstring>
#include<fstream>
#include<queue>
#include<stack>
#include<algorithm>
using namespace std;

# define lujin "D:\\smallG.txt"     //路径选择
//# define lujin "D:\\mediumG.txt"
//# define lujin "D:\\largeG.txt"

int n, m;
queue<int>root; //记录并查集的所有根节点
int* rootvisit; //记录是否已查找到过该节点
int* fu;        //dfs时记录父亲节点
int* gao;       //dfs时记录在树中的高度
int* visit;    //dfs时记录是否被访问
//int* visit2;    //创建并查集时记录是否被访问
int* dai;       //以点代边数组
int feiqshu = 0;//非桥边个数

//领接表
struct Edge 
{
    int bian;
    //int visited = 0;
    //int flag = 0;   //若为1表示该边为非生成树边
    Edge* next = NULL;

    void addbian(int v)
    {
        Edge* p = this;
        while (p->next)
            p = p->next;
        Edge* s = new Edge{ v,NULL };
        p->next = s;
    }
    void shanchu(int v)
    {
        Edge* p = this;
        while (p->next)
        {
            if (p->next->bian == v)
                break;
            p = p->next;
        }     
        if (p->next == NULL)
            return;
        Edge* s = p->next;
        p->next = s->next;
        //if(s!=NULL)
        //    delete s;
    }
    /*
    Edge(){}
    Edge(int src, int dest) 
    {
        this->src = src;
        this->dest = dest;
    }
    */
};

Edge* bianji; //领接表
Edge* danbianji; //单向领接表

struct DisjointSet 
{
    int* parent;  // 记录顶点的父节点
    int* rank;    // 记录顶点的秩(树的高度)

    // 构造函数,初始化并查集
    void init(int n) 
    {
        parent = new int[n];
        rank = new int[n];

        for (int i = 0; i < n; i++) 
        {
            parent[i] = i;
            rank[i] = 0;
        }
    }

    // 查找根节点(路径压缩)
    int find2(int x) 
    {
        if (x != parent[x]) 
        {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    // 查找根节点(路径压缩)
    int find(int x) {
        int root = x;
        while (root != parent[root]) {
            root = parent[root];
        }

        // 路径压缩
        while (x != root) {
            int next = parent[x];
            parent[x] = root;
            x = next;
        }

        return root;
    }

    // 合并两个集合(按秩合并)
    void merge(int x, int y) 
    {
        int rootX = parent[x];
        int rootY = parent[y];

        if (rootX != rootY) 
        {
            if (rank[rootX] < rank[rootY]) 
            {
                parent[rootX] = rootY;
            }
            else if (rank[rootX] > rank[rootY]) 
            {
                parent[rootY] = rootX;
            }
            else 
            {
                parent[rootY] = rootX;
                rank[rootX]++;
            }
        }
    }
    /*
    // 判断边是否为桥
    bool isBridge(int u, int v) 
    {
        int rootU = find(u);
        int rootV = find(v);

        if (rootU != rootV) 
        {
            return true;
        }

        return false;
    }

    // 销毁并查集
    ~DisjointSet() 
    {
        delete[] parent;
        delete[] rank;
    }
    */
    
};

DisjointSet bcji;



// 深度优先搜索
void dfs(int index,int high)
{
    //cout << index << " 已被dfs遍历" << endl;
    visit[index] = 1;
    gao[index] = high;

    Edge* p = &bianji[index];
    while (p->next)
    {
        if (!visit[p->next->bian])
        {
            fu[p->next->bian] = index;
            dfs(p->next->bian, high + 1);
        }      
        p = p->next;
    }
}


//记录非生成边
struct fei
{
    int u, v;
    fei* next=NULL;
};

fei *feibian=new fei;  

//创建并查集
void createbcji(int i)
{
   // visit2[i] = 1;

    Edge* p = &bianji[i];
    while (p->next)
    {       
        if (bcji.find(i) != bcji.find(p->next->bian))
        {
            bcji.merge(i, p->next->bian);
            //cout << "合并边 (" << i << "," << p->next->bian << ") " << endl;

            danbianji[i].shanchu(p->next->bian);
            danbianji[p->next->bian].shanchu(i);
            createbcji(p->next->bian);
            //p = &bianji[p->next->bian];
            //continue;
        }            
        
        // p->next->flag = 1;//标记非生成边
        p = p->next;
    }
}

// 创建并查集
void createbcji2(int i)
{
    // visit2[i] = 1;

    std::stack<int> stack;
    stack.push(i);

    while (!stack.empty())
    {
        int current = stack.top();
        stack.pop();

        Edge* p = &bianji[current];
        while (p->next)
        {
            int next = p->next->bian;
            if (bcji.find(current) != bcji.find(next))
            {
                bcji.merge(current, next);
                // cout << "合并边 (" << current << "," << next << ") " << endl;

                danbianji[current].shanchu(next);
                danbianji[next].shanchu(current);

                stack.push(next);
            }

            // p->next->flag = 1;//标记非生成边
            p = p->next;
        }
    }
}


//寻找最近公共祖先并记录非桥边
void lca(int u, int v)
{
    feiqshu++;
    int gaou = gao[u];
    int gaov = gao[v];

    int da, xiao, dagao, xiaogao;
    if (gaou >= gaov)
        da = u,dagao=gaou,xiao=v, xiaogao = gaov;
    else if(gaov > gaou)
        da = v, dagao = gaov, xiao = u, xiaogao = gaou;
    
    //当二者高度不同
    while (dagao != xiaogao)
    {
        if (dai[da] == 0)
        {
            dai[da] = 1;
            feiqshu++;
        }
        da = fu[da];
        dagao--;
    }

    //当二者高度相同
    while (da != xiao)
    {
        if (dai[da] == 0)
        {
            dai[da] = 1;
            feiqshu++;
        }
        if (dai[xiao] == 0)
        {
            dai[xiao] = 1;
            feiqshu++;
        }
        da = fu[da];
        xiao = fu[xiao];
    }
}


int main() 
{  
    ifstream ifs(lujin);
    ifs >> n >> m;
    bcji.init(n);    //初始化并查集

    int u, v;
    // 添加边

    fu = new int[n];
    gao = new int[n];
    visit = new int[n];
    //visit2 = new int[n];
    dai = new int[n];
    rootvisit = new int[n];

    for (int i = 0; i < n; i++)
    {
        fu[i] = -1;
        gao[i] = visit[i]  = dai[i] = rootvisit[i] = 0;
    }

    /*数组可以这么用,但是指针数组不可以!!!
    memset(fu, -1, sizeof(fu));
    memset(gao, 0, sizeof(gao));
    memset(visit, false, sizeof(visit));
    memset(dai, 0, sizeof(dai));
    memset(rootvisit, 0, sizeof(rootvisit));
    */

    bianji = new Edge[n];     //初始化领接表
    danbianji = new Edge[n];

     //创建领接表
    for (int i = 0; i < m; i++)
    {
        ifs >> u >> v;      
        bianji[u].addbian(v);
        bianji[v].addbian(u);
        
        //if (u > v)
          //  swap(u, v);
        danbianji[u].addbian(v);
    }

    /*测试领接表
    for (int i = 0; i < n; i++)
    {
        cout<<"i= "<<i<<"   ";
        Edge* p = &danbianji[i];
        while (p->next)
        {
            cout << p->next->bian << " ";
            p = p->next;
        }
        cout << endl;
    }
    */

    /*
    //创建并查集
    for (int i = 0; i < n; i++)
    {
        Edge* p = &danbianji[i];
        while (p->next)
        {

            if (bcji.find(i) != bcji.find(p->next->bian))
                bcji.merge(i, p->next->bian), cout << "合并边 (" << i << "," << p->next->bian << ") " << endl;
            else
            {   //数组feibian记录非生成边
                fei* k = feibian;
                while (k->next)
                    k = k->next;
                fei* s = new fei;
                s->u = i;
                s->v = p->next->bian;
                k->next = s;
                cout << "记录非生成边 (" << s->u << "," << s->v << ") " << endl;
            }
               // p->next->flag = 1;//标记非生成边
            p = p->next;
        }
    }
    */
    //创建并查表
    for (int i = 0; i < n; i++)
        createbcji(i);

    int qi = clock();

    //添加非生成边
    for (int i = 0; i < n; i++)
    {
        //cout << "i= " << i << "   ";
        Edge* p = &danbianji[i];
        while (p->next)
        {
            fei* k = feibian;
            while (k->next)
                k = k->next;
            fei* s = new fei;
            s->u = i;
            s->v = p->next->bian;
            k->next = s;

           // cout << p->next->bian << " ";
            p = p->next;
        }
        //cout << endl;
    }

    //找到并查集的所有根节点 
    for (int i = 0; i < n; i++)
        //cout << "rootvisit[" << i << "]  的值为" << rootvisit[i] << endl;
    for (int i = 0; i < n; i++)
    {
        int r=bcji.find(i);
        //cout << "find( "<< i<<" ) 的值为"<< r<< endl;
        if (rootvisit[r] == 0)
        {
            rootvisit[r] = 1;
            //cout << r << " 进入根节点队列" << endl;
            root.push(r);
        }
    }

    //对每个根节点进行dfs遍历
    while (!root.empty())
    {
        dfs(root.front(),1);
        //cout << root.front() << " 从根节点队列中出队" << endl;
        root.pop();
    }

    //将每条非生成边依次加入生成树
    fei* p = feibian->next;
    while (p)
    {
        //cout << "非生边( " << p->u<<"," <<p->v<< " ) " <<  endl;
        lca(p->u, p->v);    //寻找最近公共祖先并记录非桥边
        p = p->next;
    }
    
    int zhi = clock();

    //检验——输出每个点所在的并查集的根节点
    //for (int i = 0; i < n; i++)
    //    cout << bcji.find(i) <<"   " <<i<<"的父节点是" << bcji.parent[i] << "   高度是" << bcji.rank[i] << endl;
    
    cout << "桥的个数为 " << m - feiqshu << " 个" << endl;
    cout << "耗时为 " << zhi - qi << " ms" << endl;
    /*
    // 判断每条边是否为桥
    for (int i = 0; i < m; i++)
    {
        Edge* p = &bianji[i];
        while (p->next)
        {
            if (bcji.isBridge(i, p->next->bian))
            {
                cout << "边 (" << i << ", " << p->next->bian << ") 是桥" << endl;
            }
            else
            {
                cout << "边 (" << i << ", " << p->next->bian << ") 不是桥" << endl;
            }
            p = p->next;
        }
    }
    */
}

(by 归忆)

  • 22
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

归忆_AC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值