图算法小结(并查集)

(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 
#include 

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#include 
#include
#include

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;iclosedge[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]>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

(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 
#include 

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

你可能感兴趣的:(c/c++成长之路)