算法设计与分析课-实验-贪心
算法设计与分析课
贪心算法
第一题
最小延迟调度:
贪心算法的基本思想:贪心算法的基本思想为从整体中找到每个小局部的最优解,并将所有局部最优解合并成整体的最优解。能够使用贪心算法解决的问题必须满足以下两个性质:
1、整体的最优解可以通过局部最优解得出。
2、整体能被分成多个局部,且这些局部可以求出最优解。
对于本题的问题,每个客户有自己的服务时间和希望的完成时间。如果对某客户的服务在他希望的完成时间前结束,那么对于该客户的服务则没有延迟,如果对某客户的服务在他希望的完成时间之后结束,则对该客户的服务有延迟,设对客户i的完成时间为F[i],则延迟时间为F[i]+T[i]-D[i]。
所有客户是整体,每个客户是局部,对于每个局部求其最小延迟时间,得到的整体延迟时间则是最小的,因此贪心的策略为怎么求最小延迟时间。
思路1:按照Ti(服务时间)从小到大排。
思路2:按照Di-Ti(结束时间减去服务时间)从小到大排。
思路3:按照Di(结束时间)从小到大排。
然后进行思路的排除,对于思路1,如果客户的服务时间很小,但是结束时间较大,说明他不急着完成服务;如果客户的服务时间很大,但是结束时间较小,说明他急于完成服务。如果让不急于完成的客户先进行调度,急于完成的客户后进行调度,则会造成较大延迟,不可取。
对于思路2,如果某客户不急于完成服务,则Di很大,但同时其Ti也很大,那么Di-Ti就会很小,先让其调度则会占用大量的时间,造成其他客户的服务开始时间延后,产生较大延迟,不可取。
对于思路3,解释为(copy的):
对于上述两条解释,第一条说明当客户的期望结束时间相同的时候,最大延迟与对客户服务的先后顺序无关;第二条说明交换相邻的客户i和j(原来是i先于j调度,且Di>Dj)的服务顺序后,最大延迟只会不变或者减小。
这两条都证明了思路3的正确性,只需按完成时间从小到大安排任务即可(没有空闲时间)。
#include<iostream>
#include<cstring>
#define maxn 1000+5
using namespace std;
//贪心算法
//数组T为对客户的服务时间(时间段),数组D为客户希望的完成时间(时间节点),数组F为事件i开始的时间
//如果在客户希望的完成时间之前则服务没有延迟
//如果在客户希望的完成时间之后完成该服务则产生延迟,延迟时间为该服务结束时间减去客户希望的完成时间
//延迟时间为F[i]+T[i]-D[i]
//每一个客户都有延迟时间,根据贪心的思想,要找所有最大的拖延时间中的最小值,这样就会使得总拖延时间最小。
typedef struct Client
{
int id;
int t;
int d;
int flag;
}Client;
void minArrange(int n, int F[], Client C[], int S[], int &t);
void sortC(int n, Client C[]);
int main()
{
int t=0;
int n;
cin>>n;
int F[maxn];
Client C[maxn];
memset(F, 0, sizeof(F));
for(int i=0; i<n; ++i) cin>>C[i].t;
for(int i=0; i<n; ++i) cin>>C[i].d;
for(int i=0; i<n; ++i)
{
C[i].flag=0;
C[i].id=i;
}
int S[maxn];
memset(S, 0, sizeof(S));
sortC(n, C);
minArrange(n, F, C, S, t);
for(int i=0; i<n; ++i) cout<<S[i]<<" ";
cout<<endl<<t;
return 0;
}
void sortC(int n, Client C[])
{
for(int i=0; i<n; ++i)
{
for(int j=i; j<n-1; ++j)
{
if(C[j].d>C[j+1].d)
{
Client temp=C[j];
C[j].t=C[j+1].t;
C[j].d=C[j+1].d;
C[j].id=C[j+1].id;
C[j+1].t=temp.t;
C[j+1].d=temp.d;
C[j+1].id=temp.id;
}
}
}
}
void minArrange(int n, int F[], Client C[], int S[], int &t)
{
int temp=C[0].d, index=0, time=0;
int j;
for(j=0; j<n; ++j)
{
index=C[j].id;
S[j]=index+1;
F[index]=time;
time+=C[j].t;
//F[index]是第index个开始时间,与客户C[j].id对应,所以下面两行计算时间应该是F[index]而不用F[j]
//(这里错了很久没看出来,如果把开始时间F这个数组写到Client类中会更清晰一点)
if(j==0) t=F[index]+C[j].t-C[j].d;
else if(t<F[index]+C[j].t-C[j].d) t=F[index]+C[j].t-C[j].d;
}
}
第二题
找零钱问题:
思路:零钱数组v是按从小到大排序的(最小的是1),找零钱数额为Y,问题是如何组合支付使得所付零钱的数目是最少的(零钱有n种,但是个数不唯一)。因此直接先选用最大的零钱开始找零,如果无法找零则换次大的零钱找零,以此类推,每次都选能够选择的最大的零钱进行找零,最后选用的零钱数一定是最少的。
#include<iostream>
#define maxn 1000+5
using namespace std;
void charge(int n, int Y, int v[]);
int main()
{
int n, Y;
cin>>n>>Y;
int v[maxn];
for(int i=0; i<n; ++i) cin>>v[i];
charge(n, Y, v);
return 0;
}
void charge(int n, int Y, int v[])
{
cout<<Y<<"=";
int j=n-1, tmp=Y;
while(j>=0)
{
if(tmp>=v[j])
{
if(tmp-v[j]) cout<<v[j]<<"+";
else cout<<v[j];
tmp-=v[j];
}
else
{
--j;
}
}
}
第三题
汽车加油问题:
思路:汽车加满油能行驶n km, 一共有k个加油站,给出的加油站数组共有k+1个,本题中设为从1到k+1,其中第x个数组表示从x-1个加油站到第x个加油站的距离,第0个加油站为出发地,第k+1个加油站为目的地。也即数组元素s[1]为从出发地到第一个加油站的距离,s[k+1]为从第k个加油站到目的地的距离。
首先,汽车加满汽油最多也只跑n km,那么如果有任意两个加油站之间的距离大于n km则不可能到达。其次从s[1]遍历到s[k](不遍历s[k+1],因为s[k+1]在s[k]中考虑过了),设汽车还能跑tmp km,tmp>=s[i]则能到当前加油站,tmp减去s[i]后如果tmp>=s[i+1]则能到下一个加油站,如果不能就要加油,加油次数sum++,记录加油地点为当前加油站。
#include<iostream>
#include<cstring>
#define maxn 1000+5
using namespace std;
//n为汽车能行驶的最大km数,共有k个加油站
//汽车加满油只能跑nkm,如果有两个加油站之间的距离大于n则汽车无法到达目的地,No Solution
//行驶到当前加油站后,如果汽车剩余油量不能支撑汽车行驶到下一个加油站,则需要在当前加油站加油
void AddPetrol(int &sum, int n, int k, int s[], int d[]);
int main()
{
int n, k, sum=0;
cin>>n>>k;
int s[maxn], d[maxn];
memset(d, 0, sizeof(d));
for(int i=1; i<=k+1; ++i) cin>>s[i];
AddPetrol(sum, n, k, s, d);
return 0;
}
void AddPetrol(int &sum, int n, int k, int s[], int d[])
{
//如果有两个加油站大于nkm,则不可能到达
for(int i=1; i<=k+1; ++i)
{
if(s[i]>n)
{
cout<<"No Solution";
return ;
}
}
int tmp=n, t=0;
for(int i=1; i<=k; ++i)
{
if(tmp>=s[i])
{
tmp-=s[i];
}
if(!(tmp>=s[i+1]))
{
sum++;
d[t++]=i;
tmp=n;
}
}
cout<<sum<<endl;
for(int i=0; i<t; ++i) cout<<d[i]<<" ";
}