图算法小结(并查集)

(0)目录

图算法小结(prime与dijkstra对比)

图算法小结(并查集)

哈夫曼树 之 建树和编解码

一:起因

(1)关于图的算法一般是比较复杂的,自己在这方面也是比较弱的,首先是图的存储问题 和 遍历问题:

存储分为两种,邻接矩阵 和 临街表;遍历分为DFS 和 BFS两种,非常类似于二叉树的先跟遍历和层次遍历。

(2)图在实际应用中是非常广泛的,这与万物归一,万物相连的理论是一致的,两个物体之间有着千丝万缕的联系,我们成这种联系建立的网络为图(带权图);联系的强弱为边的权重。

(3)图的一些复杂的算法,也是建立在两种遍历图的思想的基础上进行的,所以我们先从简单的谈起。

(4)图的相关知识:

图的定义:
       很简单,G(V,E), V、E分别表示点和边的集合。       

图的表示:
       主要有两种,邻接矩阵和邻接表,前者空间复杂度,O(V2),后者为O(V+E)。因此,除非非常稠密的图(边非常多),一般后者优越于前者。

图的遍历:
       宽度遍历BFS(start):    (1) 队列Q=Empty,数组bool visited[V]={false...}. Q.push(start);
                                             (2) while (!Q.empty()){
                                                       u = Q.pop();  visited[u] = true;   //遍历u结点
                                                       foreach (u的每一个邻接结点v) Q.push(v);
                                                    }   
       深度遍历DFS(start):     (1) 栈S=Empty, 数组bool visited[V]={false...}. S.push(start);
                                               (2) while (!S.empty()){
                                                       u = S.pop();
                                                       if (!visited[u]) visited[u] = true;   //遍历u结点
                                                       foreach (u的每一个邻接结点v) S.push(v);
                                                    }
      两个算法很相似,主要区别在于一个使用队列,一个使用栈。队列是先入先出,所以访问u以后接下来就访问u中未访问过的邻接结点;而栈的后进先出,当访问u后,压入了u的邻接结点,在后面的循环中,首先访问u的第一个临接点v,接下来又将v的邻接点w压入S,这样接下来要访问的自然是w了。

最小生成树:
       (1)Prime算法:    (1) 集合MST=T=Empty,选取G中一结点u,T.add(u)
                                  (2) 循环|V|-1次:
                                        选取一条这样的边e=min{(x,y) | x in T, y in V/T}
                                        T.add(y); MST.add(e);
                                  (3) MST即为所求
       (2) Kruskal算法   (1) 将G中所有的边排序并放入集合H中,初始化集合MST=Empty,初始化不相交集合T={{v1}, {v2}...}},也即T中每个点为一个集合。
                                    (2)  依次取H中的最短边e(u,v),如果Find-Set(u)!=Find-Set(v)(也即u、v是否已经在一棵树中),那么Union(u,v) (即u,v合并为一个集合),MST.add(e);
                                    (3) MST即为所求


       这两个算法都是贪心算法,区别在于每次选取边的策略。证明该算法的关键在于一点:如果MST是图G的最小生成树,那么在子图G'中包含的子生成树MST' 也必然是G'的最小生成树。这个很容易反正,假设不成立,那么G'有一棵权重和更小的生成树,用它替换掉MST',那么对于G我们就找到了比MST更小的生成树,显然这与我们的假设(MST是最小生成树)矛盾了。
       理解了这个关键点,算法的正确性就好理解多了。对于Prime,T于V/T两个点集都会各自有一棵生成树,最后要连起来构成一棵大的生成树,那么显然要选两者之间的最短的那条边了。对于Kruskal算法,如果当前选取的边没有引起环路,那么正确性是显然的(对给定点集依次选最小的边构成一棵树当然是最小生成树了),如果导致了环路,那么说明两个点都在该点集里,由于已经构成了树(否则也不可能导致环路)并且一直都是挑尽可能小的,所以肯定是最小生成树。

最短路径:
       这里的算法基本是基于动态规划和贪心算法的,经典算法有很多个,主要区别在于:有的是通用的,有的是针对某一类图的,例如,无负环的图,或者无负权边的图等。
       单源最短路径(1) 通用(Bellman-Ford算法):
                               (2) 无负权边的图(Dijkstra算法):
                               (3) 无环有向图(DAG) :
       所有结点间最短路径:
                               (1) Floyd-Warshall算法:
                               (2) Johnson算法:

关键路径 和 拓扑排序:

二:代码示例

(1)最简单的图的遍历问题

Lake Counting

Sample Input

10 12
W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.
Sample Output

3

要求输出图中连通分支的个数,最简单的图遍历问题。

图的常见遍历方式有两种:深度优先遍历和广度优先遍历,他们的作用是将图中的每个点都访问一遍,只不过是顺序不同。

如果把图中的每条边长都相等(比如都是1)的话,深度优先遍历过程就是尽可能深的去访问节点,具体过程为:

1.任意选定一个点p0作为遍历的起点

2.当访问到某一个节点p1时,如果p1下面有未被遍历的子节点,我们就接着访问p1的某一个未被遍历子节点,并标记该子节点被访问过,

3.如果p1不存在未被访问的子节点,我们就退回到p1的父节点,并执行第2步

4.执行第2、3步,直到所有节点被访问过。

大家也看出来了,深度优先遍历的是一个递归的过程,因此很容易想到要用到栈这种数据结构,具体实现的时候可以用递归,也可以用栈。看个人习惯了。
广度优先遍历实现就要用到队列了。以下是poj2386不同实现方式的代码

#include <iostream>
#include <string>

using namespace std;

const int MAX_SIZE = 102;
string strs[MAX_SIZE];
int n,m;
int dir[8][2] = {{-1,0},{-1,-1},{0,-1},{1,-1},{1,0},{1,1},{0,1},{-1,1}};
inline bool isBound(int x,int y)
{
    return (x<0 || x>=m || y<0 || y>=n);
}
void dfs(int xindex,int yindex)
{
    strs[xindex][yindex] = '.';
    int i,x,y;
    for(i=0; i<8; ++i)
    {
        x = xindex+dir[i][0];
        y = yindex+dir[i][1];
        if(!isBound(x,y) && strs[x][y]=='W')
        {
           dfs(x,y);
        }
    }

}

int main()
{
    int i,j;
    cin >> m >> n;
    getline(cin,strs[0]);//¿ÕÐйýÂË
    for(i=0; i<m; ++i)
    {
        getline(cin,strs[i]);
        //cout << strs[i] << " i= " << i << endl;
    }
    int ans = 0;
    for(i=0; i<m; ++i)
    {
        for(j=0; j<n; ++j)
        {
            if(strs[i][j]=='W')
            {
                //cout << strs[i][j];
                ans ++;
                dfs(i,j);
            }
        }
    }
    cout << ans << endl;
    cin >> n;
    return 0;
}

(2)最小生成树 ---- prime实现 O(N2)

题意:北极有一些村庄,现需要在这些村庄间建立起通讯,有s个卫星频道,任何两个拥有卫星频道的村庄都可以直接通过卫星进行通讯而无视距离,没有卫星的村庄通过无线电进行通讯,并且这两个村庄的距离不能超过D,D值取决于无线电收发器的功率,功率越大,D值越大,但价格也越高,出于购买费用和维护费用的考虑,所有村庄的无线电收发器都相同,即D值相同,现要求在保证任意两个村庄间都能直接或间接通讯,并且D值最小,输出这个最小值。就是输出最小生成树最大边的权值,但是卫星频道是不确定因素。这道题有个很好的解法及证明。
其实题目意思是将一棵最小生成树转化成一个森林,森林里有S棵树,每棵树配一个卫星频道,并且使得森林里所有边中最长的边的长度最小
其实意思就是可以删除最小生成树中的S-1条边,问剩下的边中最长的是多少
由于建图时每两个点之间都有边,是稠密图,故用Prim法比较好 ---- 有的题目也稍微复杂一点,首先得判断是否联通,再求最小生成树

#include <iostream>
#include<cmath>
#include<algorithm>

using namespace std;

const int INF = 1000000;
const int MAX_SIZE = 501;
double map[MAX_SIZE][MAX_SIZE];
double path[MAX_SIZE];

struct node
{
    int x;
	int y;
}point[MAX_SIZE];
//记录从顶点集U到V-U的代价最小的边的辅助数组定义
struct
{
    int adjvex;
    double lowcost;
}closedge[MAX_SIZE];
bool cmp(double a,double b)//从大到小偏序
{
    return a>b;
}
// 求距离
inline double CalDist(struct node  &a, struct node &b)
{
	double xx = (a.x-b.x);
	double yy = (a.y-b.y);
	return sqrt(xx*xx + yy*yy);
}
//用普里姆算法从第k个顶点出发构造网G的最小生产树T,N个顶点的图
void prim(int k,int n)
{
	int i,j;
    for(j=1;j<=n;j++)//辅助数组初始化
	{
        if(j!=k)
        {
            closedge[j].adjvex=k;
            closedge[j].lowcost=map[k][j];
        }
	}
	closedge[k].lowcost=0; //初始,U={u},这里将0作为访问过的标志
	int l=0;
	for(i=1;i<n;i++)//选择其余n-1个顶点,这个i不无任何实际意义,仅仅是选取n-1个点,记录次数而已
	{
		double min=INF;
		for(j=1;j<=n;j++)//求出T的下一个结点:第k顶点,最小的,不成环(即未访问过)
		{
			if(closedge[j].lowcost!=0&&min>closedge[j].lowcost)
			{
				k=j;
				min=closedge[j].lowcost;
			}
		}
		closedge[k].lowcost=0; //第k顶点并入U集
		path[l++]=map[k][closedge[k].adjvex]; //保存该边,要是求最小生成树的成本,就得改成加和了,要是求最小生成树的最大边权值,就得比较最大值了
		for(j=1;j<=n;j++) //新顶点并入U后重新选择最小边
		{
			if(map[k][j]<closedge[j].lowcost)
			{
				closedge[j].adjvex=k;
				closedge[j].lowcost=map[k][j];
			}
		}// end of for
	}// end of for
}

int main()
{
    int t,m,n;
	int i,j;
    cin>>t;
    while(t--)
    {
        cin>>m>>n;
		// input
        for(i=1;i<=n;i++)
        {    
			cin>>point[i].x>>point[i].y;
		}
		// init dist
        for(i=1;i<=n;i++) //求出毎两个顶点之间的距离
		{
            for(j=1;j<i;j++)
			{
                map[i][j]=map[j][i]=CalDist(point[i],point[j]);
				//cout << map[i][j] << endl;
			}
		}
		for(i=1;i<=n;i++)
		{
			map[i][i]=INF;
		}
		// prime
		prim(1,n);
		sort(path,path+n-1,cmp); //把构成最小生成树的n-1条边从大到小排序
		cout.setf(ios::fixed);//保留两位小数
		cout.precision(2);
		cout<<path[m-1]<<endl;//数组d从下标0开始存储,即第m条边
    }
    return 0;
}

(3)最小生成树的变种 ---- 求最小生成树的最大权值的边,并查集实现kruskal算法 O(eloge)

Help Bessie know the largest amount of water she will ever have to carry:
what is the length of longest road she'll have to travel between any two farms,
presuming she chooses routes that minimize that number? This means, of course,
that she might backtrack over a road in order to minimize the length of the longest road she'll have to traverse.
即农民遍历每一个农场需要的最少带水量,到达每一个农场,他都可以补充水分的。

//poj2395  最小生成树的最大边
#include <cstdio>
#include <cstdlib>

using namespace std;

const int MAX_SIZE = 2500000;
const int MAX_POINT = 2001;

int n;
struct Edge
{
	int ori,des;
	int len;
	int flag;
}E[MAX_SIZE];
int K;
int set[MAX_POINT];
// c里面的qsort函数
int cmp(const void *a,const void *b)
{
	struct Edge *c,*d;
	c=(struct Edge *)a;
	d=(struct Edge *)b;
	return c->len - d->len;
}
// 并查集的建立
void build(int num)
{
	int i;
	for(i=1;i<=num;i++)
	{
	    set[i]=i;
	}
}
int find(int k)
{
	if(set[k]==k)
        return k;
	set[k]=find(set[k]);
	return set[k];
}
void Union(int f1,int f2)
{
	set[f1]=f2;
}

inline int MAX(int a,int b)
{
	return a>b?a:b;
}
int Krustal(const int edges)
{
	int i;
	int ans;
	int f1,f2;
    //初始状态 均为访问过
	for(i=0; i!=edges; ++i)
	{
		E[i].flag=0;
	}
    //用并查集 实现的克鲁斯卡尔核心算法,
    //当然代码需要优化的地方很多,例如当nion的个数等于n-1时,即可停止;
    //以及上下两个for循环可以同时合并到中间的for里面
	for(i=0; i!=edges; ++i)
	{
		f1=find(E[i].ori);
		f2=find(E[i].des);
		if(f1==f2)	continue;
		Union(f1,f2);
		E[i].flag=1;
	}
    // 求最小生成树的最大权边
	ans=0;
	for(i=0; i!=edges; ++i)
	{
		if(E[i].flag)
            ans=MAX(ans,E[i].len);
	}

	return ans;
}
int main()
{
	int n,m;
	int ori,des,len;

	while(scanf("%d%d",&n,&m)!=-1)
	{
		build(n);// 建立并查集
        // input
		for(int i=0; i!=m; ++i)
		{
			scanf("%d%d%d",&ori,&des,&len);
			E[K].ori=ori;
			E[K].des=des;
			E[K].len=len;
		}

		qsort(E,m,sizeof(E[0]),cmp);
		printf("%d\n",Krustal(m));
	}
	return 0;
}

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
根据提供的引用内容,Prim算法确实使用了并查集。Prim算法是一种用于构建最小生成树的贪心算法,它的基本思想是从一个点开始,每次选择一个与当前生成树距离最近的点加入生成树中,直到所有的点都被加入生成树为止。在Prim算法中,我们需要维护一个集合S,表示已经加入生成树的点的集合,以及一个数组d,表示每个点到集合S的距离。每次选择距离集合S最近的点加入集合S中,并更新其他点到集合S的距离。这个过程可以使用堆优化的Dijkstra算法来实现,也可以使用并查集来实现。具体来说,我们可以将所有点分为两个集合,一个集合表示已经加入生成树的点,一个集合表示还没有加入生成树的点。每次选择距离已经加入生成树的点最近的未加入生成树的点加入生成树中,并将这个点加入已经加入生成树的点的集合中。这个过程可以使用并查集来实现,具体来说,我们可以将已经加入生成树的点的集合看作一个并查集,每次加入一个新的点时,我们将这个点加入并查集中,并将这个点与已经加入生成树的点的集合中的点进行合并。这样,我们就可以快速地判断一个点是否已经加入生成树中,以及两个点是否在同一个连通块中。因此,Prim算法使用并查集来维护已经加入生成树的点的集合,以及判断两个点是否在同一个连通块中。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值