数据结构之图的最小生成树以及图的连通性

一、图的最小生成树算法
1. Prim算法
  • 蛮力法,特点:使用访问数组,三层循环,思路简单,但时间复杂度较高。
#define MaxWeight 1000

struct Node{//边信息
	int v1;//顶点1下标
	int v2;//顶点2下标
	int weight;
};

class Map{
	int len;//顶点数
	string *name;//顶点数组
	int **P;//邻接矩阵
	int *v;//访问数组
	Node *node;//存放边信息
public:
	//图的创建函数...
	
	void Prim(string start){
		int p1, p2, min;
		for(int i=0; i<len; i++){//初始化访问数组
			v[i] = 0;
		}
		p1 = findPoint(start);//起始点下标
		v[p1] = 1;

		for(int i=0; i<len-1; i++){//len个顶点进行len-1次
			min = MaxWeight;
			for(int j=0; j<len; j++){
				for(int k=0; k<len; k++){
					if(v[j] == 1 && v[k] == 0 && p[j][k] < min){
						p1 = j;
						p2 = k;
						min = p[j][k];
					}
				}
			}
			v[p2] = 1;
			//将选出的边加入边集
			node[i].v1 = p1;
			node[i].v2 = p1;
			node[i].weight = min;
		}
	}
	int findPoint(string x){
		int i;
		for(i=0; i<len; i++){
			if(name[i] == x){
				break;
			}
		}
		return i;
	}
};

说明:(1) 在本程序中,邻接矩阵里权值为0的点也会被选中,如果想用0来表示点之间不连接需要做修改;(2) 这里的边集不是必需的,要看题目要求。如果题目不需要保留最小生成树的边信息,只要计算中权重值,直接用一个变量在循环中加总即可。

  • 增量法,特点:使用临时数组记录已选中的点与为被选中的点之间边的信息,每选中一个点就更新一次(主要是更新最小边),同时还可代替访问数组;两层循环,时间复杂度相较蛮力法有所降低。
void Prim(string start){
	int p1, p2, min;
	//temp存放未选中点到已选中点的最小交叉边及其权值
	Node *temp = new Node[len];
	p1 = findPoint(start);
	for(int i=0; i<len; i++){//初始化起始点p1到其余各结点的权值
		temp[i].v1 = p1;
		temp[i].weight = p[p1][i];
	}
	temp[p1].weight = 0;//权值设为0标志已访问
	for(int i=0; i<len-1; i++){
		min = MaxWeight;
		for(int j=0; j<len; j++){
			if(temp[j].weight!=0 && temp[j].weight<min){//在temp中找出权值最小的边
				min = temp[j].weight;
				p2 = j;
			}
		}
		//将边加入边集
		node[i].v1 = temp[p2].v1;
		node[i].v2 = p2;
		node[i].weight = min;

		for(int j=0; j<len; j++){//检查新选中的p2是否使得剩余顶点的最小交叉边权值变小
			if(temp[j].weight!=0 && temp[j].weight>p[p2][j]){
				temp[j].weight = p[p2][j];
				temp[j].v1 = p2;
			}
		}
		temp[p2].weight = 0;//标志已访问
	}
	delete[] temp;
}
  • 总结:其实大部分问题使用蛮力法都可以解决,而且思路十分简单;使用增量法的难点在于每次处理temp数组。在面对具体问题时,要细心分析题目需求,选择恰当的方法来做。另外,Prim算法不一定需要参数表示起点,也可以在函数中自行指定(初始化时设置访问数组即可)。
2. Kruskal算法
  • 特点:事先将图的所有边排序好以便使用,这里用到一个边集;并查集,实际上就是一个数组,标志顶点位于哪个连通分量,以便在选择边时判断是否会产生回路。
//这里提供一种对边排序的方法
bool cmp(const Node& n1, const Node& n2){
	return n1.weight<n2.weight;
}
void SortNode(){//对边进行排序
	int n=0;
	for(int i=0; i<len; i++){
		for(int j=i+1; j<len; j++){//无向图是对称矩阵,存一半的边即可,这里存的是上三角,不包括对角线
			node[n].v1 = i;
			node[n].v2 = j;
			node[n].weight = p[i][j];
			n++;
		}
	}
	sort(node, node+n, cmp);
}		

void Kruskal(){//边集事先排好序
	int p1, p2, q1, q2;
	int k = 0, count = 0;
	for(int i=0; i<len; i++){//刚开始每个结点都是不同的连通分量
		v[i] = i;
	}
	while(count<len-1){
		p1 = node[k].v1;
		p2 = node[k].v2;
		if(v[p1] != v[p2]){
			count++;//记录选出的边数
			q1 = v[p1];
			q2 = v[p2];
			for(int i=0; i<len; i++){//将所有q1值换成q2,属于同一个连通分量
				if(v[i] == q1) v[i] = q2;
			}
		}
		k++;
	}
}
二、题目
1. 道路建设 (Ver. I)

题目描述
有N个村庄,编号从1到N,你应该建造一些道路,使每个村庄都可以相互连接。
两个村A和B是相连的,当且仅当A和B之间有一条道路,或者存在一个村C使得在A和C之间有一条道路,并且C和B相连。
现在一些村庄之间已经有一些道路,你的任务就是修建一些道路,使所有村庄都连通起来,并且所有道路的长度总和是最小的。
输入
测试数据有多组
第一行是整数N(3 <= N <= 100),代表村庄的数量。 然后是N行,其中第i行包含N个整数,这些N个整数中的第j个是村庄i和村庄j之间的距离(距离是[1,1000]内的整数)。
然后是整数Q(0 <= Q <= N *(N + 1)/ 2),接下来是Q行,每行包含两个整数a和b(1 <= a 输出
对于每组测试数据
输出一个整数,表示要构建的道路的长度总和最小值
样例输入
3
0 990 692
990 0 179
692 179 0
1
1 2
样例输出
179

分析:本题采用Prim算法较简单。主要在于如何处理已建成的道路:在邻接矩阵中将权值设为0即可。

#include
#include
using namespace std;
 
#define MaxWeight 1000
 
class Map{
    int VertexNum;
    int **matrix;
    int *visited;
public:
    Map(int t){
        VertexNum = t;
        matrix = new int*[VertexNum];
        for(int i=0; i<VertexNum; i++){
            matrix[i] = new int[VertexNum];
            for(int j=0; j<VertexNum; j++){
                cin >> matrix[i][j];
            }
        }
    }
    void set(int i, int j){//注意是无向图
        matrix[i][j] = 0;
        matrix[j][i] = 0;
    }
    void Prim(){
        int sum = 0, min;
        int pos; 
        visited = new int[VertexNum]();
        visited[0] = 1;//设置起点 
         
        for(int i=0; i<VertexNum-1; i++){
            min = MaxWeight;
            for(int j=0; j<VertexNum; j++){
                for(int k=0; k<VertexNum; k++){
                    if(visited[j] == 1 && visited[k] == 0 && matrix[j][k] < min){
                        pos = k;
                        min = matrix[j][k];
                    }
                }
            }
            visited[pos] = 1;
            sum += min;
        }
         
        cout<<sum<<endl;
    }
    ~Map(){
        if(visited){
            delete[] visited;
        }
         
        for(int i=0; i<VertexNum; i++){
            delete[] matrix[i];
        }
        delete[] matrix;
    }
};
 
int main(){
    int t;
    while(cin >> t){
        Map m(t);
        int n;
        cin >> n;
        while(n--){
            int a, b;
            cin >> a >> b;
            m.set(a-1, b-1);
        }
        m.Prim();
    }
     
    return 0;
}
//思路:道路建成的村庄将邻接矩阵中的值设为0
//使用Prim算法的简单版本,每次找到符合要求的边将权值加起来即可 
2.图的应用之——图的连通

题目描述
给定一个图的邻接矩阵,请判断该图是否是连通图。连通图:任意两个顶点之间都有路径。
–程序要求–
若使用C++只能include一个头文件iostream;若使用C语言只能include一个头文件stdio
输入
第1行输入一个整数k,表示有k个测试数据
第2行输入一个整数n,表示有n个结点
从第3行起到第n+2行输入一个邻接矩阵,其中Matrix[i,j]=1表示第i,j个结点之间有边,否则不存在边。
接下来是第2到第k个测试数据的结点数和邻接矩阵
输出
输出Yes or No表示图是否是强连通图
样例输入
2
4
0 1 1 1
1 0 1 1
1 1 0 1
1 1 1 0
7
0 1 0 0 0 0 0
0 0 1 1 0 0 0
1 0 0 0 0 0 0
1 0 1 0 0 0 0
0 0 0 0 0 1 1
0 1 0 0 0 0 0
0 0 0 1 0 1 0
样例输出
Yes
No

分析:强连通图需要图中每个顶点都能到达所有顶点。这里用DFS,再计数即可。

#include
using namespace std;
 
class Map{
    int VertexNum;
    int **matrix;
    int *visited;
    void DFS(int v, int& count){
        visited[v] = 1;
        count++;
 
        for (int j = 0; j < VertexNum; j++){
            if(matrix[v][j] && !visited[j]){
                DFS(j, count);
            }
        }
    }
public:
    void CreateMap();
    void DFSTraverse();
};
void Map::CreateMap(){
    cin >> VertexNum;
    matrix = new int *[VertexNum];
    for (int i = 0; i < VertexNum; i++){
        matrix[i] = new int[VertexNum];
        for (int j = 0; j < VertexNum; j++){
            cin >> matrix[i][j];
        }
    }
}
void Map::DFSTraverse(){
    visited = new int[VertexNum];
    int sum = 0;
    for (int i = 0; i < VertexNum; i++){
        int count = 0;
        for (int j = 0; j < VertexNum; j++){
            visited[j] = 0;
        }
        DFS(i, count);
        if(count == VertexNum){
            sum++;
        }
    }
    if(sum == VertexNum){
        cout << "Yes" << endl;
    }else{
        cout << "No" << endl;
    }
}
 
int main(){
    int k;
    cin >> k;
    while(k--){
        Map m;
        m.CreateMap();
        m.DFSTraverse();
    }
 
    return 0;
}
3. 图的顶点可达闭包

题目描述
给定有向图的邻接矩阵A,其元素定义为:若存在顶点i到顶点j的有向边则A[i,j]=1,若没有有向边则A[i,j]= 0。试求A的可达闭包矩阵A*,其元素定义为:若存在顶点i到顶点j的有向路径则A*[i,j]=1,若没有有向路径则A*[i,j]= 0。
输入
第1行顶点个数n
第2行开始的n行有向图的邻接矩阵,元素之间由空格分开
输出
有向图的可达闭包矩阵A*,元素之间由空格分开
样例输入
4
0 1 0 1
0 0 1 0
0 0 0 0
0 0 0 0
样例输出
0 1 1 1
0 0 1 0
0 0 0 0
0 0 0 0

#include
#include
using namespace std;

#define MaxWeight 1000

class Map{
	int VertexNum;//顶点数
	int **matrix;
	int *visited;
	void DFS(int v, int &index){
		visited[v] = 1;
		matrix[index][v] = 1;
		
		for(int i=0; i<VertexNum; i++){
			if(!visited[i] && matrix[v][i]){
				DFS(i, index);
			}
		}
	}
public:
	Map(){
		cin >> VertexNum;
		matrix = new int*[VertexNum];
		for(int i=0; i<VertexNum; i++){
			matrix[i] = new int[VertexNum];
			for(int j=0; j<VertexNum; j++){
				cin >> matrix[i][j];
			}
		}
	} 
	~Map(){
		for(int i=0; i<VertexNum; i++){
			delete[] matrix[i];
		}
		delete[] matrix;
	}
	void DFSTraverse(){
		int flag;
		visited = new int[VertexNum];
		
		for(int i=0; i<VertexNum; i++){
			//初始化访问数组 
			for(int j=0; j<VertexNum; j++){
				visited[j] = 0;
			}
			DFS(i, i);
			//检查顶点能否到达自己
			flag = 0;
			for(int j=0; j<VertexNum; j++){
				if(i != j && matrix[i][j] && matrix[j][i]){
					flag = 1;
					break;
				}
			}
			if(!flag){
				matrix[i][i] = 0;
			}
		}
	}
	void getClosure(){
		DFSTraverse();
		
		for(int i=0; i<VertexNum; i++){
			for(int j=0; j<VertexNum-1; j++){
				cout << matrix[i][j] << ' ';
			}
			cout << matrix[i][VertexNum-1] << endl;
		}
	}
};

int main(){
	Map m;
	m.getClosure();
	
	return 0;
}
//思路:使用DFS,途经的点在矩阵上标记为1
//顶点能否到达自己在于中途其他顶点能否到达该顶点,DFS后判断即可 

你可能感兴趣的:(数据结构,数据结构)