前言
NEFU,计算机与控制工程学院,基于C/C++的算法设计与分析课程
实验三 贪心算法设计
环境
操作系统:Windows 10
IDE:Visual Studio Code、Dev C++ 5.11、Code::Blocks
说明
“实验三 贪心算法设计” 包含以下问题
- 最优服务次序问题
- 区间相交问题
- 汽车加油问题
其他联系方式:
Gitee:@不太聪明的椰羊
B站:@不太聪明的椰羊
一、实验目的
掌握贪心算法的基本思想,掌握贪心算法的设计步骤及算法实现。
二、实验原理
算法总体思想:对于一个具体的问题,怎么知道是否可用贪心算法解此问题,以及能否得到问题的最优解?
许多可以用贪心算法求解的问题中看到这类问题一般具有2个重要的性质:贪心选择性质和最优子结构性质。
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
说明:
贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。
贪心法产生最优解的条件:
- 贪心选择性:若一个优化问题的全局最优解可以通过局部最优选择得到,则该问题具有贪心选择性。一个问题是否具有贪心选择性需要证明。
- 最优子结构:若一个优化问题的优化解包含它的子问题的优化解,则称其具有最优子结构。
三、实验内容
1、最优服务次序问题
(1)问题描述:
设有n 个顾客同时等待一项服务。顾客i需要的服务时间为ti, 1<=i <= n 。应如何安排n个顾客的服务次序才能使平均等待时间达到最小?平均等待时间是n 个顾客等待服务时间的总和除以n。
(2)编程任务:
对于给定的n个顾客需要的服务时间,编程计算最优服务次序。
(3)数据输入:
第一行是正整数n,表示有n 个顾客。接下来的1行中,有n个正整数,表示n个顾客需要的服务时间。
(4)结果输出:
计算出的最小平均等待时间。
(5)输入示例
10
56 12 1 99 1000 234 33 55 99 812
(6)输出示例
532.00
1.1 分析
假设原问题为T(先假设只有一个服务点),我们已知某个最优服务系列,即最优解为 A={t(1),t(2),….t(n)} (其中t(i)为第i个用户需要的服务时间),则每个用户等待时间为:
T(1)=t(1);T(2)=t(1)+t(2);...T(n)=t(1)+t(2)+t(3)+…+t(n);
则总等待时问,即最优值为:
TA=T(1)+T(2)+T(3)+...+T(n)=n*t(1)+(n-1)*t(2)+…+(n+1-j)*t(i)+…+2*t(n-1)+t(n);
由于平均等待时间是n个顾客等待时间的总和除以n,故本题实际上就是求使顾客等待时间的总和最小的服务次序。
贪心策略: 最短服务时间优先,先将服务时间排序,然后注意后面的等待服务时间既包括等待部分,也包括服务部分。对服务时间最短的顾客先服务的贪心选择策略,首先对需要服务时间最短的顾客进行服务,即做完第一次选择后,原问题T变成了需对n-1个顾客服务的新问题T'。新问题和原问题相同,只是问题规模由n减小为n-1。基于此种选择策略,对新问题T',选择n-1顾客中选择服务时间最短的先进行服务,如此进行下去,直至所有服务都完成为止
算法实现:采用最短服务时间优先的贪心策略。首先将每个顾客所需要的服务时间从小到大排序。然后申请2个数组: st[]是服务数组,st[j]为第j个队列上的某一个顾客的等待时间;su[]是求和数组,su[j]的值为第j个队列上所有顾客的等待时间。
算法复杂性分析:程序主要是花费在对各顾客所需服务时间的排序和贪心算法,即计算平均服务时间上面。其中,贪心算法部分只有一重循环影响时间复杂度,其时间复杂度为O(n),而排序算法的时间复杂度为O(nlogn),因此,综合来看算法时间复杂度为O(nlogn)。
1.2 代码
#include <iostream>
#include <stack>
#include <queue>
#include <iomanip>
#define maxsize 1000
using namespace std;
typedef struct{
int t[maxsize+1]; //存每个人服务时间
int w[maxsize + 1];//存每个人等待服务时间
double avgw;//存平均等待时间
int n;
}ServeList;
void InsertSort(ServeList &L)//直接插入排序
{
for(int i=2; i<=L.n; i++)
if(L.t[i] < L.t[i-1])
{
L.t[0] = L.t[i];//r[0]存为哨兵单元
L.t[i] = L.t[i-1];
int j;
for (j = i - 2; L.t[0] < L.t[j]; j--)
L.t[j+1] = L.t[j];
L.t[j+1] = L.t[0];
}
}
void minwait(ServeList &L)
{
L.t[0] = 0;
L.w[0] = 0;
L.avgw = 0;
int i;
for (i = 1; i <= L.n; i++)
{
L.w[i] = L.t[i] + L.w[i - 1];//当前人的等待服务时间 = 前一个人的等待服务时间 + 当前人的服务时间
L.avgw += L.w[i];
}
L.avgw = L.avgw / L.n;
cout << "最小平均等待时间:";
cout << fixed << setprecision(2) << L.avgw;
}
int main()
{
ServeList L;
int x, i, n;
cin >> n;
L.n = n;
i = 1;
while(i<=n)
{
cin >> x;
L.t[i] = x;//从t[1]开始存数
i++;
}
InsertSort(L);
/*for(int i=1; i<=L.n; i++)
cout<<L.t[i]<<" ";*/
minwait(L);
return 0;
}
/*
10
56 12 1 99 1000 234 33 55 99 812
*/
1.3 测试
2、区间相交问题
(1)问题描述:
给定x 轴上n 个闭区间。去掉尽可能少的闭区间,使剩下的闭区间都不相交。
(2)编程任务:
给定n 个闭区间,编程计算去掉的最少闭区间数。
(3)数据输入:
第一行是正整数n,表示闭区间数。接下来的n行中,每行有2 个整数,分别表示闭区间的2个端点。
(4)结果输出:
计算出的去掉的最少闭区间数。
(5)输入示例
3
10 20
10 15
20 15
(6)输出示例
2
2.1 分析
问题理解:
每个闭区间由两个端点表示,左端点 l 和右端点 r。
我们需要移除尽可能少的区间,使得剩下的区间互不相交。
贪心策略:
首先,我们将所有区间按照右端点 r 进行排序。排序的目的是希望每次选择的区间右端点尽可能小,这样可以留更多的空间给后面的区间,从而减少移除的区间数目。
排序后,从第一个区间开始,依次遍历每个区间:如果当前区间的左端点大于上一个选中区间的右端点,表示它们不相交,可以选择保留这个区间。
更新当前选中区间的右端点为当前区间的右端点。统计可以保留的区间数,即不相交区间的最大数量。
最后输出未选区间数(即n-cnt)。
实现:
使用一个变量 pre 记录上一个选中区间的右端点。使用变量 cnt 记录可以保留的区间数。
2.2 代码
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 1024;
struct Qu {
int l, r;
}I[M];
bool cmp(Qu x, Qu y) { //按右端点排序
return x.r < y.r;
}
int main() {
int n, l, r;
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> l >> r;
if(l<=r)
{
I[i].l = l;
I[i].r = r;
}
else
{
I[i].l = r;
I[i].r = l;
}
}
//sort(begin, end, cmp),
//begin为指向待sort()的数组的第一个元素的指针
//end为指向待sort()的数组的最后一个元素的下一个位置的指针
//cmp参数为排序准则,cmp参数可以不写,如果不写的话,默认从小到大进行排序。
sort(I + 1, I + n + 1, cmp);
int cnt = 1, pre = I[1].r;
for(int i = 2; i <= n; i++) {
if(I[i].l > pre) { //当前区间与前一个不相交
cnt++;//cnt个区间不相交
pre = I[i].r; //更新右端点
}
}
cout << n - cnt << endl;//输出去掉的最少闭区间数
return 0;
}
2.3 测试
3、汽车加油问题
(1)问题描述:一辆汽车加满油后可行驶nkm。旅途中有若干加油站。设计一个有效算法,指出应在哪些加油站停靠加油,使沿途加油次数最少。
(2)算法设计:对于给定的n和k个加油站位置,计算最少加油次数。
(3)数据输入:n:表示汽车加满油后可行驶nkm
k:旅途中有k个加油站
k+1个整数:表示第k个加油站与第k-1个加油站之间的距离。第0个加油站表示出发地,汽车已加满油。第k+1个加油站表示目的地。
(4)数据输出:最少加油次数和具体在哪几个加油站加油。
例如: n=7 k=7
K+1个整数:1 2 3 4 5 1 6 6
最优值:4
3.1 分析
问题分析:有几种情况:设加油次数为k,每个加油站间距离为a[i];i=0,1,2,3……n
(1)始点到终点的距离小于n,则加油次数k=0。
(2)始点到终点的距离大于n。
A 加油站间的距离相等,即a[i]=a[j]=L=N,则加油次数最少k=n;
B 加油站间的距离相等,即a[i]=a[j]=L>N,则不可能到达终点;
C 加油站间的距离相等,即a[i]=a[j]=L,k=[n/N]+1(n%N!=0);
D 加油站间的距离不相等,即a[i]!=a[j],则加油次数k通过以下算法求解。
算法思路:汽车行驶过程中,应走到自己能走到并且离自己最远的那个加油站,在那个加油站加油后再按照同样的方法贪心。
算法实现:先检测各加油站之间的距离,若发现其中有一个距离大于汽车加满油能跑的距离,则输出no solution。否则,对加油站间的距离进行逐个扫描,尽量选择往远处走,不能走了就让num++,最终统计出来的num便是最少的加油站数。
算法复杂性分析:想要知道在哪个加油站加油必须遍历所有的加油站,且不需要重复遍历,所以时间复杂度问O(n)。
3.2 代码
#include <iostream>
using namespace std;
#define M 1024
int main() {
int n, k, d[M];
cin >> n >> k;
int l = 0, num = 0; //l记录当前走过的路程,n表示一次加满油能走的最远路程,num记录加油次数
for (int i = 1; i <= k + 1; i++)
{
cin >> d[i];
if(d[i] > n) { //无法到达下一个加油站
cout << "No Solution" << endl;
return 0;
}
if(l + d[i] > n) { //必须在当前加油站加油
l = d[i]; //更新当前位置,加油后汽车能行驶最大距离变成 n,所以当前走过的路程更新为 d[i]
num++;//加油次数加1
}
else { //尽量往远处走
l += d[i]; //更新当前位置,当前走过的路程 + d[i]
}
}
cout << num << endl;
return 0;
}
/*
例如:
n=7 k=7
K+1个整数:1 2 3 4 5 1 6 6
最优值:4
*/