图算法小结(并查集)

news/2024/7/3 12:52:38 标签: 图的算法, Prime算法, Kruskal算法, 拓扑排序

(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;
}


http://www.niftyadmin.cn/n/865421.html

相关文章

说说如何在 Linux 中修改某个账户的密码

root 账号&#xff0c;执行以下命令&#xff1a; passwd xxxxxx 表示账户名称&#xff0c;执行之后&#xff0c;会要求输入两遍新密码。 当看到上图中的红色框内容&#xff0c;就说明密码修改成功啦O(∩_∩)O哈哈~

图算法小结(prime与dijkstra对比) CodeBlocks再次安装

一&#xff1a;CodeBlocks再次安装&#xff0c;无法编译问题 &#xff08;0&#xff09;最近自己自行从网上下载了一个CodeBlocks进行安装&#xff08;ps竟然下载了一个不带有任何编译器的裸的exe文件&#xff1b;如果想用GUN GCC编译器&#xff0c;即缺少MinGW文件夹&#xf…

说说 Python 的继承

如果要编写的类是另一个类的特殊版本时&#xff0c;那么就可以使用继承 。原有的类称为父类 &#xff0c; 新类称为子类 。 子类继承了父类的所有属性和方法&#xff0c; 同时子类还可以自定义自己的属性和方法。 1 继承写法 定义子类的实例时&#xff0c; 可以通过 子类的 _…

优化算法 无处不在

一&#xff1a;起因 &#xff08;0&#xff09;优化算法&#xff08;Optimization Algorithm&#xff09;,即求目标函数的最优值问题&#xff1b;如何评价你的当前解的值是最优的&#xff1f;这就需要构造评价函数&#xff1b;如何从当前的位置&#xff08;解&#xff09;更新…

说说在 Python 中如何导入类

随着我们不断地在一个文件中添加新的功能&#xff0c; 就会使得文件变得很长。 即便使用了继承&#xff0c;也抑制不住类的成长。为了解决这一问题&#xff0c;我们可以将类存储在模块中&#xff0c; 然后在主程序中导入所需的模块&#xff0c;这样可以让文件尽可能保持整洁&am…

Qt+OpenCV打开视频文件并在窗口界面上显示

1、新建一个Qt Widgets Application&#xff0c;工程配置文件&#xff08;.pro文件&#xff09;内容如下&#xff1a; #------------------------------------------------- # # Project created by QtCreator 2018-12-11T12:57:12 # #--------------------------------------…

VMware 下安装Ubuntu的吐血经历

一&#xff1a;起因 &#xff08;1&#xff09;自己学习Linux的历程 自己一直想着在Linux下面练习、学习一下Python&#xff0c;以及C编程&#xff1b;shell编程也顺带&#xff1b;今天突然来了兴趣&#xff0c;就开始安装了。 &#xff08;2&#xff09;血泪史 话说&#…

零基础学习Shell编程

一&#xff1a;起因 &#xff08;0&#xff09;也许由于一时的冲动使得你开始关注并学习shell编程&#xff1b;亦许由于是“道听途说”shell的威力很大&#xff1b;亦许由于shell编程的魅力&#xff1b;亦许由于作为一个coder的偏好&#xff1b;亦许…… &#xff08;1&#…