实验三 贪心算法
一、实验目的
1、掌握贪心法的基本思想方法;
2、了解适用于贪心法求解的问题类型,并能设计相应贪心算法;
3、掌握贪心算法复杂性分析方法,分析问题复杂性。
二、实验内容和要求
实验要求:通过上机实验进行算法实现,保存和打印出程序的运行结果,并结合程序进行分析,上交实验报告和程序文件。
实验内容:
1、使用贪心算法解决最小生成树问题。
2、使用贪心算法实现找零:当前有面值分别为2角5分,1角,5分,1分的硬币,请给出找n分钱的最佳方案(要求找出的硬币数目最少)。
3、使用贪心算法解决单源最短路径问题。
三、算法思想分析
1、贪心算法
贪心算法是指:在对问题进行求解时,总是做出在当前看来是最好的选择。即不从整体最优上加以考虑,仅做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题它能产生整体最优解或者整体最优解的近似解。
2、贪心算法的基本思想
① 建立数学模型来描述问题;
② 把求解的问题分成若干个子问题;
③ 对每一子问题求解,得到子问题的局部最优解;
④ 把子问题的解局部最优解合成原来解问题的一个解。
3、使用贪心算法解决最小生成树问题
(1)相关概念
生成树:一个无向图中的连通、无环的生成子图。
最小生成树:给定图G(V,E)以及对应的边的权重,获取一棵总权重最小的生成树。
割:图G=<V,E>是一个连通无向图,割(S,V-S)将图G的顶点集V划分为两部分。
横跨:给定割(S,V-S)和边(u,v),u∈S,v∈V-S,称边(u,v)横跨割(S,V-S)。
轻边:横跨割的所有边中,权重最小的称为横跨这个割的一条轻边。
(2)Prim算法
(3)Kruskal算法
步骤1:将连通网G=<V,E> 中的边按权值从小到大进行排列;
步骤2:初始状态为只有n个顶点而无边的非连通图T = (V,{ }),图中每个顶点自成一个连通分量;
步骤3:在E中选择权值最小的边,若该边依附的顶点落在T中不同的连通分量上(即不形成回路),则将此边将入到T中,否则舍去此边而选择下一条权值最小的边;
步骤4:重复步骤3,直到T中所有的顶点都在同一连通分量上为止。
4、使用贪心算法实现找零
找零问题是典型的贪心问题,但是并不代表所有的找零都能用贪心算法找到最优解。通过分析,我们可以证明本题满足贪心选择性质——一个问题的整体最优解可通过一系列局部的最优解的选择达到,并且每次的选择可以依赖以前作出的选择,但不依赖于后面要作出的选择。
我们可以直接先一直选取面值最大的硬币,等到剩余所需的找零值小于最大硬币面值时,选取面值次大的硬币,依照上述过程依次减小面值,直到实现找零。该过程直接用while循环即可解决。
5、使用贪心算法解决单源最短路径问题
单源最短路径算法,又称Dijkstra算法,其目的是寻找从一个顶点到其余各顶点的最短路径,解决有权图中最短路径问题。
(1)相关解释
观测域:假设起点为v点,观测域便为v的所有邻接点;
点集V:图中所有点的集合;
点集S:已经找到最短路径的终点集合;
数组D:存储观测域内能观测到的最短路径。如:D[i]对应在观测域中能观测到的到顶点i的最短路径;
邻接矩阵a:存储有权图中边的信息。如:a[i][j]表示在有权图中点i和点j之间边的权值;如果两点之间没边,则用负数-1表示。
(2)算法步骤
四、程序代码
1、使用贪心算法解决最小生成树问题
//prim和kruskal贪心算法解决最小生成树
#include<iostream>
#include<cstring>
using namespace std;
int n; //结点个数
int m; //边条数
int map[101][101]; //无向图
int pre[101];
int u[101], v[101], edge[101]; //u,v分别为两个点,edge为两个点之间的边
int find(int x) {
int root = x;
while (pre[root] != root) root = pre[root];
return root;
}
//最小生成树prim算法
void prim() {
int count = 0;
cout << "prim算法求解:" << endl;
int dist[101]; //dist[i]存放树中各点到i点的最短边权值
int closest[101]; //closest[i]存放树中哪个点到i点的边为最短边
bool s[101]; //记录i点是否已连入
//初始化
for (int i = 1; i <= n; i++) {
dist[i] = map[1][i];
s[i] = false;
closest[i] = 1;
}
s[1] = true;
for (int i = 1; i < n; i++) {
//最小生成树只有n-1条边
int min = 100000;
int k = 1;
for (int j = 2; j <= n; j++) {
//找最小边
if ((dist[j] < min) && (!s[j])) {
min = dist[j];
k = j;
}
}
cout << k << "-->" << closest[k] << ": " << min << endl;
count += min;
s[k] = true;
for (int j = 1; j <= n; j++)
{
if ((map[k][j] < dist[j]) && (!s[j])){
//如果新的结点到j的边比原来的结点到j的边小,就用新结点替换掉原结点
dist[j] = map[k][j];
closest[j] = k;
}
}
}
cout << "最小权重和为:" << count << endl;
}
//最小生成树kruskal算法
void kruskal() {
cout << "kruskal下解为:" << endl;
int minNum, fu, fv;
int count = 0;
int total = n - 1;
while (total > 0){
int min = 10000000;
for (int i = 1; i <= m; i++){
//找最小值
if (u[i] == -1 || v[i] == -1) continue;
if (edge[i] < min) {
min = edge[i];
minNum = i;
}
}
fu = find(u[minNum]);
fv = find(v[minNum]);
if (fu != fv){
//不连通,就连接两个点
cout << v[minNum] << "-->" << u[minNum] << ": " << edge[minNum] << endl;
count += edge[minNum];
pre[fu] = fv;
total--;
}
edge[minNum] = 100000000; //改变已经找到的最小值
u[minNum] = -1;
v[minNum] = -1;
}
cout << "最小权重和为:" << count << endl;
}
int main()
{
cout << "输入结点个数以及边条数:" << endl;
cin >> n >> m;
int i, a, b, tem;
memset(map, 0x3f, sizeof(map));
cout << "输入对应两结点序号以及两点间边的权重:" << endl;
for (i = 1; i <= n; i++) pre[i] = i;
for (i = 1; i <= m; i++) {
cin >> a >> b;
cin >> tem;
map[a][b] = map[b][a] = tem;
u[i] = a;
v[i] = b;
edge[i] = tem;
}
prim();
kruskal();
return 0;
}
2、使用贪心算法实现找零
//利用贪心算法找零
#include <iostream>
using namespace std;
int main() {
int number;
cout << "请输入你的找零值(单位:分):";
cin >> number;
double a[4]={25,10,5,1}; //保存已有面值硬币(单位:分)
double b[4]={0,0,0,0}; //保存找出的每种硬币的数量
for(int i = 0; i < 4; i++) {
int x = 0; //记录硬币数量
while(number >= a[i]) {
x++;
number -= a[i];
}
b[i] = x;
}
cout<<"25 分: "<<b[0]<<endl;
cout<<"10 分: "<<b[1]<<endl;
cout<<"5 分: "<<b[2]<<endl;
cout<<"1 分: "<<b[3]<<endl;
return 0;
}
3、使用贪心算法解决单源最短路径问题
#include <iostream>
using namespace std;
int n; //顶点个数
int m; //边数
int a [10000][10000]; //邻接矩阵
int dist [10000]; //dist[i]记录顶点v到i点的距离
int pre [10000]; //pre[i]记录i点前驱节点
int v = 1 ; //顶点
void dijkstra();
int main() {
cout << "请输入图的顶点个数和边的条数:" << endl;
cin >> n >> m ;
//初始化邻接矩阵
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
a[i][j] = -1;
}
}
cout << "请输入图的路径长度(格式:起点 终点 长度):" << endl;
for(int i = 1; i <= m; i++) {
int start, end, length ;
cin >> start >> end >> length;
if(start >= 1 && start <= n && start >= 1 && end <= n) {
//无向有权图
a[start][end] = length;
a[end][start] = length;
}
}
dijkstra();
}
void dijkstra() {
if(v > 0 && v <= n) {
bool s[n]; //顶点是否放入的标志
//初始化
for(int i = 1; i <= n; i++) {
dist[i] = a[v][i]; //初始化为 v 到 i 的距离
s[i] = false; //初始化顶点未放
if(dist[i] == -1) {
pre[i] = 0; //v到i无路,i的前驱节点置空
}else {
pre[i] = v;
}
}
dist[v] = 0; //v到v的距离是0
s[v] = true; //顶点放入
for(int i = 1; i <= n - 1; i++) {
int temp = 1000000;
int u = v; //u为下一个被放入的节点
//这个for循环为第二步,观测域为v的观测域
//遍历所有顶点找到下一个距离最短的点
for(int j = 1; j < n; j++) {
//j未放入,且v到j有路,且v到当前节点路径更小
if(!s[j] && dist[j] != -1 && dist[j] < temp) {
u = j;
temp = dist[j]; //temp始终为最小的路径长度
}
}
s[u] = true; //将得到的下一节点放入
//这个for循环为第三步,用u更新观测域
for(int k = 1; k <= n; k++) {
if(!s[k] && a[u][k] != -1) {
int newDist = dist[u] + a[u][k];
if(newDist < dist[k] || dist[k] == -1) {
dist[k] = newDist;
pre[k] = u;
}
}
}
}
}
cout << v << "节点是源节点!" << endl;
for(int i = 2; i <= n; i++) {
cout <<"节点" << v << "到节点" << i << "最短距离是:" << dist[i] << " ;前驱点是:" << pre[i] << endl;
}
}
五、结果运行与分析
1、使用贪心算法解决最小生成树问题
(1)Prim算法求解
(2)Kruskal算法求解
时间复杂度:O(mlogn),其中n为顶点数、m为边的条数。
Prim算法与Kruskal算法的区别:
Kruskal算法只与边有关,因此适合求稀少图的最小生成树;而Prim算法只与端点相关,因此适合求较密图的最小生成树。并且,由于Kruskal只需对权重边做一次排序,而Prim算法则必须做多次排序,因此Kruskal算法在速率上比Prim算法快。
2. 使用贪心算法实现找零
3. 使用贪心算法解决单源最短路径问题
六、心得与体会
本次是算法分析与设计的第三次实验,主要是应用贪心算法分别解决最小生成树问题、找零问题以及单源最短路径问题。
对于最小生成树,其运用的Prim算法和Kruskal算法我在大一的数据结构课上已经学习过了。不仅了解了具体步骤,还编写过代码,更在考试中考过,所以对于解决本次实验的这一问题比较得心应手。
对于找零问题,通过简单的分析,就能知道可以从最大面值的硬币开始分析起,直到其不能使用后再分析次大面值的硬币。整个思路是很清晰明了的。
对于单源最短路径问题,我们在大一的离散课上也有所涉及,所以思路的回顾较为快速。虽然在代码实现中,出现了一些问题,但最终在课上得以解决。
通过三个实验内容的分析、设计与实现,我掌握了贪心法的基本思想方法:先建立数学模型来描述问题,然后把求解的问题分成若干个子问题,再对每一子问题求解,得到子问题的局部最优解,最后把子问题的解局部最优解合成原来解问题的一个解。
并且,我也了解到并不是所有的问题通过贪心算法都能得到整体最优解,关键是贪心策略的选择。利用贪心法求解的问题应具备贪心选择性质和最优子结构性质。要确定一个具体问题是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。因此,在实际应用中什么问题可以用贪心算法确定,需要具体问题具体分析。