IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 算法设计与分析-----贪心法 -> 正文阅读

[数据结构与算法]算法设计与分析-----贪心法

一、贪心法

1、定义

? 贪心法的基本思路是在对问题求解时总是做出在当前看来是最好的选择,也就是说贪心法不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解。

? 人们通常希望找到整体最优解,所以采用贪心法需要证明设计的算法确实是整体最优解或求解了它要解决的问题。

2、贪心法具有的性质

1、贪心选择性质

? 所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。

也就是说,贪心法仅在当前状态下做出最好选择,即局部最优选择,然后再去求解做出这个选择后产生的相应子问题的解。

2、最优子结构性质

? 如果一个问题的最优解包含其子问题的最优解,则称此问题具有最优子结构性质。

? 问题的最优子结构性质是该问题可用动态规划算法或贪心法求解的关键特征。

3、贪心法的算法框架

SolutionType Greedy(SType a[],int n)
//假设解向量(x0,x1,…,xn-1)类型为SolutionType,其分量为SType类型
{  SolutionType x={}//初始时,解向量不包含任何分量
   for (int i=0;i<n;i++)	  //执行n步操作
   {  SType xi=Select(a);	  //从输入a中选择一个当前最好的分量
      if (Feasiable(xi))	  //判断xi是否包含在当前解中
	 solution=Union(x,xi);	  //将xi分量合并形成x 
   }
   return x;			  //返回生成的最优解
}

5、求解活动安排问题

? 【问题描述】假设有一个需要使用某一资源的n个活动所组成的集合S,S={1,…,n}。该资源任何时刻只能被一个活动所占用,活动i有一个开始时间bi和结束时间ei(bi<ei),其执行时间为ei-bi,假设最早活动执行时间为0。

? 一旦某个活动开始执行,中间不能被打断,直到其执行完毕。若活动i和活动j有bi≥ej或bj≥ei,则称这两个活动兼容。

? 设计算法求一种最优活动安排方案,使得所有安排的活动个数最多。

? 【问题求解】假设活动时间的参考原点为0。一个活动i(1≤i≤n)用一个区间[bi,ei)表示,当活动按结束时间(右端点)递增排序后,两个活动[bi,ei)和[bj,ej)兼容(满足bi≥ej或bj≥ei)实际上就是指它们不相交。

? 用数组A存放所有的活动,A[i].b(1≤i≤n),存放活动起始时间,A[i].e存放活动结束时间。

i1234567891011
开始时间130535688212
结束时间4567891011121315

产生最大兼容活动集合的过程:

活动1 √

活动2 ′

活动3 ′

活动4 √

活动5 ′

活动6 ′

活动7 ′

活动8 √

活动9 ′

活动10 ′

活动11 √

代码如下:

#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define MAX 51

struct Action
{	int b;						//活动起始时间
	int e;						//活动结束时间
	bool operator<(const Action &s) const	//重载<关系函数
	{
		return e<=s.e;			//用于按活动结束时间递增排序
	}
};
int n=11;
Action A[]={{0},{1,4},{3,5},{0,6},{5,7},{3,8},{5,9},{6,10},{8,11},{8,12},{2,13},{12,15}};	//下标0不用
//求解结果表示
bool flag[MAX];					//标记选择的活动
int Count=0;					//选取的兼容活动个数
void solve()					//求解最大兼容活动子集
{
	memset(flag,0,sizeof(flag));//初始化为false
	sort(A+1,A+n+1);			//A[1..n]按活动结束时间递增排序
	int preend=0;				//前一个兼容活动的结束时间
	for (int i=1;i<=n;i++)
	{	if (A[i].b>=preend)
		{	flag[i]=true;		//选择A[i]活动
			preend=A[i].e;
		}
	}
}
int main()
{
	solve();
	printf("求解结果\n");
	printf("  选取的活动:");
	for (int i=1;i<=n;i++)
		if (flag[i])
		{
			printf("[%d,%d] ",A[i].b,A[i].e);
			Count++;
		}
	printf("\n  共%d个活动\n",Count);
}

【算法分析】算法的主要时间花费在排序上,排序时间为O(nlog2n),所以整个算法的时间复杂度为O(nlog2n)。

6、求解最优装载问题

问题描述】有n个集装箱要装上一艘载重量为W的轮船,其中集装箱i(1≤i≤n)的重量为wi。

? 不考虑集装箱的体积限制,现要选出尽可能多的集装箱装上轮船,使它们的重量之和不超过W。

问题求解】第5章讨论了简单装载问题,采用回溯法选出尽可能少的集装箱个数。这里的最优解是选出尽可能多的集装箱个数,并采用贪心法求解。

? 当重量限制为W时,wi越小可装载的集装箱个数越多,所以采用优先选取重量轻的集装箱装船的贪心思路。

? 对wi从小到大排序得到{w1,w2,…,wn},设最优解向量为x={x1,x2,…,xn},显然,x1=1,则x’={x2,…,xn}是装载问题w’={w2,…,wn},W’=W-w1的最优解,满足贪心最优子结构性质

代码如下:

#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define MAXN 20						//最多集装箱个数

int w[]={0,5,2,6,4,3};				//各集装箱重量,不用下标0的元素
int	n=5,W=10;

int maxw;							//存放最优解的总重量
int x[MAXN];						//存放最优解向量

void solve()						//求解最优装载问题
{
	memset(x,0,sizeof(x));			//初始化解向量
	sort(w+1,w+n+1);				//w[1..n]递增排序
	maxw=0;
	int restw=W;						//剩余重量
	for (int i=1;i<=n &&  w[i]<=restw;i++)
	{
		x[i]=1;						//选择集装箱i
		restw-=w[i];				//减少剩余重量
		maxw+=w[i];					//累计装载总重量
	}
}
int main()
{
	solve();
	printf("最优方案\n");
	for (int i=1;i<=n;i++)
		if (x[i]==1)
			printf("  选取重量为%d的集装箱\n",w[i]);
	printf("总重量=%d\n",maxw);
}

【算法分析】算法的主要时间花费在排序上,时间复杂度为O(nlog2n)。

二、 贪心法实验

1、实验一 求解田忌赛马问题

? 【问题描述】二千多年前的战国时期,齐威王与大将田忌赛马。双方约定每人各出300匹马,并且在上、中、下三个等级中各选一匹进行比赛,由于齐威王每个等级的马都比田忌的马略强,比赛的结果可想而知。现在双方各n匹马,依次派出一匹马进行比赛,每一轮获胜的一方将从输的一方得到200银币,平局则不用出钱,田忌已知所有马的速度值并可以安排出场顺序,问他如何安排比赛获得的银币最多。

? 输入:输入包含多个测试用例,每个测试用例的第一行正整数n(n≤1000)马的数量,后两行分别是n个整数,表示田忌和齐威王的马的速度值。输入n=0结束。

? 输出:每个测试用例输出一行,表示田忌获得的最多银币数。

输入样例:

3
92 83 71
95 87 74
2
20 20
20 20
2
20 19
22 18
0

样例输出:

200
0
0

问题求解】田忌的马速度用数组a表示,齐威王的马速度用数组b表示,将a、b数组递增排序。采用常识性的贪心思路,分以下几种情况:

(1)田忌最快的马比齐威王最快的马快即a[righta]>b[rightb],则两者比赛(两个最快的马比赛),田忌赢。因为此时田忌最快的马一定赢,而选择与齐威王最快的马比赛对于田忌来说是最优的,下图中“■”代表已经比赛的马,“□”代表尚未比赛的马,箭头指向的马速度更快。

(2)田忌最快的马比齐威王最快的马慢即a[righta]<b[rightb],则选择田忌最慢的马与齐威王最快的马比赛,田忌输。因为齐威王最快的马一定赢,而选择与田忌最慢的马比赛对于田忌来说是最优的。

(3)田忌最快的马与齐威王最快的马的速度相同即a[righta]=b[rightb],又分为以下3种情况:

? ① 田忌最慢的马比齐威王最慢的马快即a[lefta]>b[leftb],则两者比赛(两个最慢的马比赛),田忌赢。因为此时齐威王最慢的马一定输,而选择与田忌最慢的马比赛对于田忌来说是最优的。

? ② 田忌最慢的马比齐威王最慢的马慢,并且田忌最慢的马比齐威王最快的马慢,即a[lefta]≤b[leftb]且a[lefta]<b[rightb],则选择田忌最慢的马与齐威王最快的马比赛,田忌输。因为此时田忌最慢的马一定输,而选择与齐威王最快的马比赛对于田忌来说是最优的。

? ③ 其他情况,即a[righta]=b[rightb]且a[lefta]≤b[leftb]且a[lefta]≥b[rightb],则a[lefta]≥b[rightb]=a[righta],即a[lefta]=a[righta],b[leftb]≥a[lefta]=b[rightb],即b[leftb]=b[rightb],说明比赛区间的所以马的速度全部相同,任何两匹马比赛都没有输赢。

上述过程看出每种情况对于田忌来说都是最优的,因此最终获得的比赛方案也一定是最优的。

代码如下:

#include <stdio.h>
#include <algorithm>
using namespace std;
#define MAX 1001

int n;
int a[MAX];
int b[MAX];

int ans;
void solve()					//求解算法
{
	sort(a,a+n);				//对a递增排序
	sort(b,b+n);				//对b递增排序
	ans=0;
	int lefta=0,leftb=0;
	int righta=n-1,rightb=n-1;
	while (lefta<=righta)		//比赛直到结束
	{
		if (a[righta]>b[rightb])	//田忌最快的马比齐威王最快的马快,两者比赛
		{
			ans+=200;
			righta--;
			rightb--;
		}
		else if (a[righta]<b[rightb])	//田忌最快的马比齐威王最快的马慢,选择田忌最慢的马与比齐威王最快的马比赛
		{
			ans-=200;
			lefta++;
			rightb--;
		}
		else						//田忌最快的马与齐威王最快的马的速度相同
		{
			if (a[lefta]>b[leftb])	//田忌最慢的马比齐威王最慢的马快,两者比赛
			{
				ans+=200;
				lefta++;
				leftb++;
			}
			else
			{
				if (a[lefta]<b[rightb])	//否则,用田忌最慢的马与齐威王最快的马比赛
					ans-=200;
				lefta++;
				rightb--;
			}
		}
	}
}
int main()
{
	while (true)
	{
		scanf("%d",&n);
		if (n==0) break;
		for (int i=0;i<n;i++)
			scanf("%d",&a[i]);
		for (int j=0;j<n;j++)
			scanf("%d",&b[j]);
		solve();
		printf("%d\n",ans);
	}
	return 0;
}

【算法分析】算法的主要时间花费在排序上,时间复杂度为O(nlog2n)。

2、实验二 求解多机调度问题

问题描述】设有n个独立的作业{1,2,…,n},由m台相同的机器{1,2, …,m}进行加工处理,作业i所需的处理时间为ti(1≤i≤n),每个作业均可在任何一台机器上加工处理,但未完工前不允许中断,任何作业也不能拆分成更小的子作业。

多机调度问题要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。

问题求解】贪心法求解多机调度问题的贪心策略是最长处理时间作业优先,即把处理时间最长的作业分配给最先空闲的机器,这样可以保证处理时间长的作业优先处理,从而在整体上获得尽可能短的处理时间。

按照最长处理时间作业优先的贪心策略,当m≥n时,只要将机器i的[0,ti)时间区间分配给作业i即可;

当m<n时,首先将n个作业依其所需的处理时间从大到小排序,然后依此顺序将作业分配给空闲的处理机。

例如,有7个独立的作业{1,2,3,4,5,6,7},由3台机器{1,2,3}加工处理,各作业所需的处理时间如下:

作业编号1234567
作业的处理时间214416653

采用贪心法求解的过程如下:

(1)7个作业按处理时间递减排序,其结果如下表所示。

作业编号4256371
作业的处理时间161465432

(2)先将排序后的前3个作业分配给3台机器。此时机器的分配情况为{{4},{2},{5}},对应的总处理时间为{16,14,6}。

作业编号4256371
作业的处理时间161465432

(3)分配余下的作业

代码如下:

#include <stdio.h>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
#define N 100

int n=7;
int m=3;
struct NodeType
{
	int no;							//作业序号
    int t;							//执行时间
	int mno;						//机器序号
	bool operator<(const NodeType &s) const 
	{	return t>s.t;  }				//按t越小越优先出队
};
struct NodeType A[]={{1,2},{2,14},{3,4},{4,16},{5,6},{6,5},{7,3}};
void solve()							//求解多机调度问题
{
	NodeType e;
	if (n<=m)
	{	printf("为每一个作业分配一台机器\n");
		return;
	}
	printf("  排序前");
	for (int s=0;s<n;s++)
		printf(" [%d:%d]",A[s].no,A[s].t);
	printf("\n");
	sort(A,A+n);
	printf("  排序后");
	for (int s=0;s<n;s++)
		printf(" [%d:%d]",A[s].no,A[s].t);
	printf("\n");

	priority_queue<NodeType> qu;		//小根堆
	for (int i=0;i<m;i++)
	{
		A[i].mno=i+1;
		printf("  给机器%d分配作业%d,执行时间为%2d,占用时间段:[%d,%d]\n",
				A[i].mno,A[i].no,A[i].t,0,A[i].t);
		qu.push(A[i]);
	}
	for (int j=m;j<n;j++)
	{
		e=qu.top(); qu.pop();			//出队e
		printf("      出队:e.no=%d,e.t=%d,e.mno=%d\n",e.no,e.t,e.mno); 
		printf("  给机器%d分配作业%d,执行时间为%2d,占用时间段:[%d,%d]\n",
				e.mno,A[j].no,A[j].t,e.t,e.t+A[j].t);
		e.t+=A[j].t;
		qu.push(e);						//e进队
		printf("      进队:e.no=%d,e.t=%d,e.mno=%d\n",e.no,e.t,e.mno); 
	}
}
int main()
{
	printf("多机调度方案:\n");
	solve();
}

【算法分析】排序的时间复杂度为O(nlog2n),两次for循环的时间合起来为O(n),所以本算法的时间复杂度为O(nlog2n)。

3、实验三 哈夫曼编码

问题描述】设要编码的字符集为{d1, d2, …, dn},它们出现的频率为{w1, w2, …, wn},应用哈夫曼树构造最优的不等长的由0、1构成的编码方案。

问题求解】先构建以这个n个结点为叶子结点的哈夫曼树,然后由哈夫曼树产生各叶子结点对应字符的哈夫曼编码。

? 哈夫曼树(Huffman Tree)的定义:设二叉树具有n个带权值的叶子结点,从根结点到每个叶子结点都有一个路径长度。从根结点到各个叶子结点的路径长度与相应结点权值的乘积的和称为该二叉树的带权路径长度,记作:

? 由n个叶子结点可以构造出多种二叉树,其中具有最小带权路径长度的二叉树称为哈夫曼树(也称最优树)。

构造一棵哈夫曼树的方法如下:

(1)由给定的n个权值{w1,w2,…,wn}构造n棵只有一个叶子结点的二叉树,从而得到一个二叉树的集合F={T1,T2,…,Tn}。

(2)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和。即合并两棵二叉树为一棵二叉树。

(3)重复步骤(2),当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。

代码如下:

#include <iostream>
#include <queue>
#include <vector>
#include <string>
#include <map>
using namespace std; 
#define MAX 101
int n;
struct HTreeNode				//哈夫曼树结点类型
{
	char data;					//字符
	int weight;					//权值
	int parent;					//双亲的位置
	int lchild;					//左孩子的位置
	int rchild;					//右孩子的位置
};
HTreeNode ht[MAX];				//哈夫曼树
map<char,string> htcode;			//哈夫曼编码

struct NodeType		//优先队列结点类型
{
	int no;				//对应哈夫曼树ht中的位置
	char data;			//字符
	int  weight;		//权值
	bool operator<(const NodeType &s) const
	{					//用于创建小根堆
		return s.weight<weight;
	}
};
void CreateHTree()						//构造哈夫曼树
{
	NodeType e,e1,e2;
	priority_queue<NodeType> qu;
	for (int k=0;k<2*n-1;k++)	//设置所有结点的指针域
		ht[k].lchild=ht[k].rchild=ht[k].parent=-1;
	for (int i=0;i<n;i++)				//将n个结点进队qu
	{
		e.no=i;
		e.data=ht[i].data;
		e.weight=ht[i].weight;
		qu.push(e);
	}
	for (int j=n;j<2*n-1;j++)			//构造哈夫曼树的n-1个非叶结点
	{
		e1=qu.top();  qu.pop();		//出队权值最小的结点e1
		e2=qu.top();  qu.pop();		//出队权值次小的结点e2
		ht[j].weight=e1.weight+e2.weight; //构造哈夫曼树的非叶结点j	
		ht[j].lchild=e1.no;
		ht[j].rchild=e2.no;
		ht[e1.no].parent=j;			//修改e1.no的双亲为结点j
		ht[e2.no].parent=j;			//修改e2.no的双亲为结点j
		e.no=j;						//构造队列结点e
		e.weight=e1.weight+e2.weight;
		qu.push(e);
	}
}
void CreateHCode()			//构造哈夫曼编码
{
	string code;
	code.reserve(MAX);
	for (int i=0;i<n;i++)	//构造叶结点i的哈夫曼编码
	{
		code="";
		int curno=i;
		int f=ht[curno].parent;
		while (f!=-1)				//循环到根结点
		{
			if (ht[f].lchild==curno)	//curno为双亲f的左孩子
				code='0'+code;
			else					//curno为双亲f的右孩子
				code='1'+code;
			curno=f; f=ht[curno].parent;
		}
		htcode[ht[i].data]=code;	//得到ht[i].data字符的哈夫曼编码
	}
}
void DispHCode()					//输出哈夫曼编码
{
	map<char,string>::iterator it;
	for (it=htcode.begin();it!=htcode.end();++it)
		cout << "    " << it->first << ": " << it->second <<	endl;
}
void DispHTree()					//输出哈夫曼树
{
	for (int i=0;i<2*n-1;i++)
	{
		printf("    data=%c, weight=%d, lchild=%d, rchild=%d, parent=%d\n",
			ht[i].data,ht[i].weight,ht[i].lchild,ht[i].rchild,ht[i].parent);
	}
}
int WPL()				//求WPL
{
	int wps=0;
	for (int i=0;i<n;i++)
		wps+=ht[i].weight*htcode[ht[i].data].size();
	return wps;
}

int main()
{
	n=5;
	ht[0].data='a'; ht[0].weight=4;		//置初值即n个叶子结点
	ht[1].data='b'; ht[1].weight=2;  
	ht[2].data='c'; ht[2].weight=1;  
	ht[3].data='d'; ht[3].weight=7;  
	ht[4].data='e'; ht[4].weight=3;  
	CreateHTree();					//建立哈夫曼树
	printf("构造的哈夫曼树:\n");
	DispHTree();
	CreateHCode();					//求哈夫曼编码
	printf("产生的哈夫曼编码如下:\n");
	DispHCode();					//输出哈夫曼编码
	printf("WPL=%d\n",WPL());
}
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-09-12 13:24:22  更:2021-09-12 13:25:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 11:50:38-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码