《趣学算法》之贪心算法(下)

本篇文章接上(上)的经典例应用:
④一场说走就走的旅行——最短路径

当你想要旅行的地点有多个,想要计算最短路线,估算所需时间时,该如何计算?

思考过程】这是一个求单源最短路径的问题。给定有向带权图G=(V,E),其中每条边的权是非负实数。此外,给定V中的一个顶点,成为源点。现在要计算从源到所有其他各顶点的最短路径长度,这里路径长度指路上各边的权之和。如何求源头点到其他各点的最短路径呢?

算法设计】(1)数据结构。设置地图的带权邻接矩阵为map[] [] ,即如果从源点 u 到顶点 i 有边,就令 map[u] [i] 等于 <u,i> 的权值,否则 map[u] [i]=∞(无穷大);采用一维数组 dist[i] 来记录从源点到 i 顶点的最短路径长度;采用一维数组 p[i] 来记录最短路径上 i 顶点的前驱。
(2)初始化。令集合 S={u},对于集合V-S中的所有顶点 x ,初始化 dist[i]=map[u][i],如果源点 u 到顶点 i 有边相连,初始化 p[i]=u,否则 p[i]=-1。
(3)找最小。在集合 V-S 中依照贪心策略来寻找使得 dist[j]具有最小值的顶点 t ,即 dist[t]=min(dist[j] | j 属于V-S集合),则顶点 t 就是属于集合V-S中距离源点 u 最近的顶点。
(4)加入S战队。 将顶点 t 加入集合S中,同时更新V-S。
(5)判结束。 如果集合V-S为空,算法结束,否则转(6)。
(6)借东风。在(3)中已经找到了源点到 t 的最短路径,那么对集合V-S中所有与顶点 t 相邻的顶点 j ,都可以借助 t 走捷径。如果 dist[j] > dist[t] + map[t] [j],则 dist[j] = dist[t] + map[t] [j],记录顶点 j 的前驱为 t,有 p[j] = t,转(3)。
由此,可求得从源点 u 到图G的其余各个顶点的最短路径及长度,也可通过数组 p[]逆向找到最短路径上经过的城市。

代码实现

#include <cstdio>
#include<iostream>
#include<cstring>
#include<windows.h>
#include<stack>
using namespace std;
const int N = 100;  //城市的个数可修改
const int INF = le7;  //初始化无穷大为10000000
int map[N] [N],dist[N],p[N],n,m;  //n为城市的个数,m为城市间路线的条数
bool flag[N];   //如果flag[i]等于ture,说明顶点i已经加入到集合S;否则顶点i属于集合V-S
void Dijkstra(int u){
	for(int i=1;i<n;i++){ 
		dist[i]=map[u][i];  //初始化源点u到其他各个顶点的最短路径长度
		flag[i] = flase;
		if(dist[i] == INF)
			p[i]=-1;  //源点u到该顶点的路径长度为无穷大,说明顶点i与源点u不相邻
		else
			p[i]=u;  //说明顶点i与源点u相邻,设置顶点i的前驱p[i]=u
	}
	dist[u]=0;
	flag[u]=ture;  //初始时,集合S中只有一个元素:源点u
	for(int i=1;i<n;i++){  
		int temp = INF,t=u;
		for(int j=1;j<=n;j++){  //在集合V-S中寻找距离源点u最近的顶点t
			if(!flag[j]&&dist[j]<temp){
				t = j;
				temp = dist[j];
			}
			if(t==u)   return;  //找不到t,跳出循环
			flag[t]=ture;  //否则,将t加入集合
			for(int j=1;j<=n;j++){  //更新集合V-S中与t邻接的顶点与源点u的距离
				if(!flag[j]&&map[t][j]<INF){  //!flag[j]表示j在V-S中
					if(dist[j]>(dist[t]+map[t][j])){
						dist[j]=dist[t]+map[t][j];
						p[j]=t;
					}
				}
			}
		}
	}
}
int main(){
	int u,v,w,st;
	system("color 0d");
	cout<<"请输入城市的个数:"<<endl;
	cin>>n;
	cout<<"请输入城市之间的路线的个数:"<<endl;
	cin>>m;
	cout<<"请输入城市之间的路线以及距离:"<<endl;
	for(int i=1;i<=n;i++){   //初始化图的邻接矩阵
		for(int j=1;j<=n;j++){
			map[i][j]=INF;  //初始化邻接矩阵为无穷大
		}
	}
	while(m--){
		cin>>u>>v>>w;
		map[u][v]=min(map[u][v][w]);  //邻接矩阵储存,保留最小距离
	}
	cout<<"请输入小明所在的位置:"<<endl;
	cin>>st;
	Dijkstra(st);
	cout<<"小明所在的位置:"<<endl;
	for(int i=1;i<=n;i++){
		cout<<"小明:"<<st<<" - "<<"要去的位置:"<<i<<endl;
		if(dist[i] == INF)
			cout<<"Sorry,无路可达"<<endl;
		else
			cout<<"最短距离为:"<<dist[i]<<endl;
	}
	return 0;
}

④神秘电报密码——哈夫曼编码

摩尔斯电码(Morse code)由点dot(.)、划dash(-)两种符号组成。它的基本原理是:把英文字母表中的字母、标点符号和空格按照出现的频率排序,然后用点和划的组合来表达这些字母、标点符号和空格,使频率最高的符号具有最短的点划组合。

算法设计】(1)确定合适的数据结构。编写程序前需要考虑的情况有:①哈夫曼树没有度为1的结点,则一棵有 n 个叶子结点的哈夫曼树共有 2n-1 个结点(n-1)次的 “合并” ,每次产生一个新结点); ②构成哈夫曼树后,为求编码,需从叶子结点出发走一条从叶子到根的路径。 ③译码需要从根出发走一条从根到叶子的路径,那么我们需要知道每个结点的权值、双亲、左孩子、右孩子和结点的信息。
(2)初始化。构造 n 棵结点为 n 个字符的单节点树集合 T = {t1,t2,t3,···,tn},每棵树只有一个带权的根节点,权值为该字符的使用频率。
(3)如果 T 中只剩下一棵树,则哈夫曼树构造成功,跳到步骤(6)。否则,从集合 T 中取出没有双亲且权值最小的两棵树 ti 和 tj ,将它们合并成一棵新树 zk,新树的左孩子为 ti,右孩子为 tj,zk的权值为 ti 和 tj 的权值之和。
(4)从集合 T 中删去 ti 和 tj ,加入 zk。
(5)重复以上(3)、(4)步。
(6)约定左分支上的编码为“0”,右分支上的编码为“1”。从叶子结点到根节点逆向求出每个字符的哈夫曼编码,从根节点到叶子结点路径上的字符组成的字符串为该叶子结点的哈夫曼编码。

代码实现

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdlib>
using namespace std;
#define MAXBIT 100
#define MAXVALUE 10000
#define MAXLEAF 30
#define MAXNODE MAXLEAF*2-1
typedef struct{
	double weight;
	int parent;
	int lchild;
	int rchild;
	char value;
} HNodeType;  //结点结构体

typedef struct{
	int bit[MAXBIT];
	int start;
}HCodeType;   //编码结构体

HNodeType HuffNode[MAXNODE];  //定义一个结点结构体数组
HCodeType HuffNode[MAXLEAF];  //定义一个编码结构体数组

void HuffmanTree(HNodeType HuffNode[MAXNODE],int n){  //构造哈夫曼树
	int i,j,x1,x2;  //i、j:循环变量。  x1、x2:构造哈夫曼树不同过程中最小权值结点在数组中的序号。
	double m1,m2;  //m1、m2:构造哈夫曼树不同过程中两个最小权值结点的权值。
	for(i=0;i<2*n-1;i++){  //初始化存放哈夫曼树数组HuffNode[]中的结点
		HuffNode[i].weight = 0;  //权值
		HuffNode[i].parent = -1;  
		HuffNode[i].lchild = -1;  
		HuffNode[i].rchild = -1; 
	}
	for(i=0;i<n;i++){  //输入n个叶子结点的权值
		cout <<"Please input value and weight of leaf node "<< i+1 <<endl; 
		cin >>HuffNode[i].value>>HuffNode[i].weight;
	}
	for(i=0;i<n-1;i++{  //构造哈夫曼树
		m1 = m2 = MAXVALUE;  //m1、m2中存放两个无父结点且结点权值最小的两个结点
		x1 = x2 = -1;  //找出所有结点中权值最小、无父结点的两个结点,并合并之为一棵二叉树
		for(j=0;j<n+i;j++){
			if(HuffNode[j].weight<m1 && HuffNode[j].parent == -1){
				m2 = m1;
				x2 = x1;
				m1 = HuffNode[j].weight;
				x1 = j;
			}
			else if(HuffNode[j].weight <m2 && HuffNode[j].parent == -1){
				m2 =HuffNode[j].weight;
				x2 = j;
			}
		}
		//设置找到的两个子节点 x1、x2的父结点信息
		HuffNode[x1].parent = n+i;
		HuffNode[x2].parent = n+i;
		HuffNode[n+i].weight = m1+m2;
		HuffNode[n+i].lchild = x1;
		HuffNode[n+i].rchild = x2;
		cout <<"x1.weight and x2.weight in round"<<i+1<<"\t"<<HuffNode[x1].weight<<"\t"<<HuffNode[x2].weight<<endl;  //用于测试
	}
}

void HuffmanCode(HCodeType HuffCode[MAXLEFT],int n){ //哈夫曼树编码
	HCodeType cd;  //定义一个临时变量来存放求解编码时的信息
	int i,j,c,p;
	for(i=0;i<n;i++){
		cd.start = n-1;
		c = i;
		p = HuffNode[c].parent;
		while(p! = -1){
			if(HuffNode[p].lchild == c)
				cd.bit[cd.start]=0;
			else
				cd.bit[cd.start]=1;
			cd.start--;  //求编码的低一位
			c=p;
			p=HuffNode[c].parent; //设置下一循环条件
		}
		//把叶子结点的编码信息从临时编码cd中复制出来,放入编码结构体数组
		for(j=cd.start+1;j<n;j++)
			HuffCode[i].bit[j] =cd.bit[j];
		HuffCode[i].start = cd.start;
	}
}

int main(){
	int i,j,n;
	cout <<"Please input n: "<<endl;
	cin>>n;
	HuffmanTree(HuffNode,n); 
	HuffmanCode(HuffCode,n);
	for(i=0;i<n;i++){
		cout<<HuffNode[i].value<<":Huffman code is: ";
		for(j=HuffCode[i].start+1;j<n;j++)
			cout<<HuffCode[i].bit[j];
		cout<<endl;
	}
	return 0;
}

⑤沟通无限校园网——最小生成树

校园网是为学校师生提供资源共享、信息交流和协同工作的计算机网络。校园网是一个宽带、具有交互功能和专业性很强的局域网络。如果一所学校包括多个学院及部门,也可以形成多个局域网络,并通过有线或无线方式连接起来。原来的网络系统只局限于以学院、图书馆为单位的局域网,不能形成集中管理一级各种资源的共享,个别学院还远离大学本部,这些情况严重地阻碍了整个学校的网络化需求。现在需要设计网络电缆布线,将各个单位的局域网络连通起来,如何设计能够使费用最少呢?

扫描二维码关注公众号,回复: 4486650 查看本文章

问题分析】对于 n 个顶点的连通图,只需 n-1 条边就可以使这个图连通, n-1 条边要想保证图连通,就必须不含回路,所以我们只需要找出 n-1 条权值最小且无回路的边即可。

算法设计】(1)确定合适的数据结构。设置带权邻接矩阵C存储图G,如果图G中存在边(u,x),令C[u] [x] 等于边(u,x)上的权值,否则,C[u] [x] =∞ ;bool数组 s[] ,如果 s[]=true,说明顶点 i 已加入集合 U。
(2)初始化。令集合 U={u0} ,u0∈V ,并初始化数组 closest[]、lowcost[]和s[]。
(3)在 V-U 集合中找 lowcost 值最小的顶点 t ,即 lowcost[t]=min{ lowcost [ j ] | j ∈V-U },满足该公式的顶点 t 就是集合V-U中连接集合U的最邻近点。
(4)将顶点 t 加入集合U。
(5)如果集合V-U为空,算法结束;否则,转(6)。
(6)对集合V-U中的所有顶点 j ,更新其 lowcost[ ]和closest[ ]。更新公式:if( C[t][j] < lowcost[j] ){ lowcost[j] = C[t][j]; closest[j]=t; },转(3)。

代码实现

#include<iostream>
using namespace std;
const int INF = 0x3fffffff;
const int N = 100;
bool s[N];
int closest[N];
int lowcost[N];
void Prim(int n,int u0,int c[N][N]){  //顶点个数 n、开始顶点u0、带权邻接矩阵c[N][N]
	//s[i] = true; 表明顶点i已加入最小生成树的顶点集合U,否则顶点i属于集合V-U,将最后的相关的最小权值传递到数组 lowcost
	s[u0] = true;  //初始时,集合中U只有一个元素,即顶点u0
	int i;
	int j;
	for(i=1;i<=n;i++){
		if(i!=u0){
			lowcost[i] = c[u0][i];
			closest[i] = u0;
			s[i] = false;
		}
		else
			lowcost[i] = 0;
	}
	for(i=1;i<=n;i++){
		int temp = INF;
		int t=u0;
		for(j=1;j<=n;j++){  //在集合中V-U中寻找距离集合U最近的顶点 t
			if((!s[j])&&(lowcost[j]<temp)){
				t = j;
				temp = lowcost[j];
			}
		}
		if(t == u0)
			break;   //找不到 t,跳出循环
		s[t] = true;   //否则,将t加入集合U
		for(j=1;j<=n;j++){    //更新lowcost和closest
			if((!s[j])&&(c[t][j]<lowcost[j])){
				lowcost[j] = c[t][j];
				closest[j] = t;
			}
		}
	}
}

int main(){
	int n,c[N][N],m,u,v,w;
	int u0;
	cout <<"输入结点数 n 和边数 m: "<<endl;
	cin >> n >> m;
	int sumcost = 0;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			c[i][j] = INF;
	cout << "输入结点数u,v和边值w: "<<endl;
	for(int i=1;i<=m;i++){
		cin >> u >> v >> w;
		c[u][v] = c[v][u] = w;
	} 
	cout <<"输入任一结点u0: "<<endl;
    cin >> u0;   //计算最后的lowcost的总和,即为最后要求的最小的费用之和
    Prim(n,u0,c);
    cout << "数组lowcost的内容为: " <<endl;
    for(int i=1;i<=n;i++)
    	cout << lowcost[i] << " ";
    cout <<endl;
    for(int i=1;i<=n;i++)
    	sumcost += lowcost[i];
    cout << "最小的花费是: " <<sumcost << endl <<endl;
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_34624515/article/details/84881799
今日推荐