贪心算法例题与讲解(1)
文章首发于:My Blog 欢迎大佬们前来逛逛
P2240 部分背包问题
P2240 【深基12.例1】部分背包问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
有点像01背包,但是又不是,01背包的物品的是不能再分的,而本题的物品是可以再分的,因此我们可以根据某个物品的部分重量和单位价值算出它的部分最大价值来作为我们选择的物品。
首先计算单位价值 = 总价值 / 总重量
按照贪心的思想,则单位价值大的一定是越优的,因此排序,单位价值大的排在前面。
按照单位价值排好序的顺序一个一个物品选,若背包的剩余容量不够则取某个物品的部分最大价值,然后退出即可。
//TODO: Write code here
int n,T;
const int N=1e4+10;
struct Node
{
double w,v,pro;
}node[N];
bool cmp(Node a,Node b)
{
if (a.pro>b.pro)
{
return true;
}
return false;
}
signed main()
{
cin>>n>>T;
for (int i=1;i<=n;i++)
{
cin>>node[i].w>>node[i].v;
node[i].pro=node[i].v/node[i].w; //计算单位价值
}
sort(node+1,node+1+n,cmp);//按照单位价值排序
double ans=0;
double temp=T;
for (int i=1;i<=n;i++)
{
if (temp>=node[i].w)
{
ans+=node[i].v;
temp-=node[i].w;
}
else
{
ans+=(temp*node[i].pro);//取最大部分价值
break;
}
}
printf("%.2lf",ans);
#define one 1
return 0;
}
P1223 排队接水
排队接水:一列人排成一队,接水的人有个接水的时间,而其他人都要等这个人接完水,然后再轮流接水,已经接完水的就完成任务了,即不需要算在排队等接水的人当中了。
我们要计算n个人的平均接水时间,因此需要把当前人接水的时间 * 后面等待的总人数
因此我们可以总结出:最先接水的一定是接水所需时间最少的。为什么?
如果你的接水时间是 1000 ,那么除这个人之外的9个人就需要等待 9 * 1000的时间。
如果你的接水时间是 10 ,那么除这个人之外的9个人就需要等待 9 * 10 的时间。
很显然按照接水时间短的先接水的这种做法一定是最优的。
那么就直接按照时间排序即可,然后计算 平均的等待时间,最后再除以一次总人数。
//TODO: Write code here
int n,m;
const int N=1e6+10;
int nums[N],presum[N];
struct Node
{
int num,val;
}node[N];
bool comp(Node a,Node b)
{
if (a.val<b.val)
{
return true;
}
else if (a.val==b.val)
{
return a.num<b.num;
}
return false;
}
signed main()
{
cin>>n;
for (int i=1;i<=n;i++)
{
node[i].num=i;
cin>>node[i].val;
}
sort(node+1,node+1+n,comp);// 按照接水的时间排序,如果时间相等,则按序号
double ans=0;
for (int i=1;i<=n;i++)
{
ans+=(n-i)*node[i].val;//后面的每个人都需要等这个时间
cout<<node[i].num<<" ";
}
cout<<endl;
printf("%.2lf",ans/n);//最后再计算平均值
#define one 1
return 0;
}
P1803 凌乱的yyy / 线段覆盖
这道题其实就是贪心的经典问题:给出任务的开始和结束时间,求总共能完成的任务的最大数量
贪心思路:
结束时间早的一定是最优的(在此不给出证明)
过程如下;
因此把每个任务按照结束时间排序,结束时间早的是最优的。
按照结束时间早正序排序,如果下一个任务的开始时间在上一个任务的进行过程中,则说明还没有完成上一个任务,因此跳过这个任务,只能是下一个任务的开始时间最差的等于上一个任务的结束时间
//TODO: Write code here
int n,m;
const int N=1e6+10;
int nums[N];
struct Node
{
int s,e;
}node[N];
bool comp(Node a,Node b)
{
if (a.e<=b.e) return true;
return false;
}
signed main()
{
cin>>n;
for (int i=1;i<=n;i++)
{
cin>>node[i].s>>node[i].e;
}
//结束时间最早
int ans=0;
int fg=0;
sort(node+1,node+1+n,comp);
for (int i=1;i<=n;i++)
{
if (i==1)//第一个一定是可以的
{
ans++;
fg=i;
}
else
{
if (node[i].s>=node[fg].e) //如果下一个任务的开始在上一个任务的结束,则可以
{
fg=i;
ans++;
}
}
}
cout<<ans;
#define one 1
return 0;
}
P1090 [NOIP2004 提高组] 合并果子
P1090 [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G
题目让我们求合并两堆的最小花费的总和, 因此我们很容易想到最小的两堆一起合并一定是最优的
我们便可以想到直接排序即可,花费小的在前面,然后前两个合并成一个,然后再把这个合并的放进去再排序,最后知道这个队列只有一个元素为止,因此我们每次所统计的两堆的和就是最后的答案。
这道题目的思想是很容易的。
但是如何做到 将两堆取出来合并后的值再放回去 ? 我们可以想到 堆排序。
进而想到堆排序的一个应用:优先队列
我们要制造 小顶堆,即堆顶元素是最小的,然后取出堆的前两个元素,合并后再插入堆
优先队列C++:priority_queue小顶堆形式:
priority_queue<int,vector<int>,greater<int>> q;
大顶堆形式:
priority_queue<int> q;
因此就解决了,当然你也可以手写堆,包含构建初始堆和堆的调整过程
//TODO: Write code here
int n,m;
const int N=1e5+10;
int nums[N];
signed main()
{
cin>>n;
priority_queue<int,vector<int>,greater<int>> q;
for (int i=1;i<=n;i++)
{
int num;
cin>>num;
q.push(num);
}
if (q.size()==1)//特判
{
cout<<q.top();
return 0;
}
int ans=0;
while (q.size()>=2)
{
int num1=q.top(); //取出最小的
q.pop();
int num2=q.top(); //取出第二小的
q.pop();
int sum=num1+num2;
ans+=sum;//注意统计res
q.push(sum); //合并后再插入堆
}
cout<<ans;
#define one 1
return 0;
}
P3817 小A的糖果
让我们求 在相邻的两个盒子的总数不超过 m 的情况下,至少需要吃的数量。
貌似我们可以进行排序? 从小到大,然后从左往右吃
不可以
我们注意到盒子是有先后顺序的,因此不能够改变位置(当然如果你会排序的做法则当我没说)
我们就直接贪心即可:
从左往右遍历,如果当前的盒子 i 的数量 + 后一个盒子 i+1 的数量 超过了规定 m,则我们一定需要在这两个中吃糖果,如何吃呢?
如果我们选择吃 i ,则 我们只会改变 i+1着一种情况,即 【i,i+1】是一组
但是如果我们吃 i+1,则我们不仅改变了【i,i+1】这一组,还可能改变 【 i+1,i+2】下一组,因此吃后面的一定是最优的(求最少的吃的数量)
遍历到后面的每个 i 盒子的时候,它的 i -1个位置的盒子数量 一定被上一种情况吃了。但是我们的第 1 个盒子怎么办呢,它可没有前一个?
我们直接错一下位即可, 让0(实际不存在,从1开始)号盒子 与 1 号成一组,然后我们吃后面的,这样不就吃到 1 号盒子了吗
//TODO: Write code here
int n,m;
const int N=1e5+10;
int nums[N];
signed main()
{
cin>>n>>m;
for (int i=1;i<=n;i++)
{
cin>>nums[i];
}
int ans=0;
for (int i=1;i<=n;i++)
{
int a=i-1,b=i; //从0开始,则保证了1号也能作为第二个盒子
if (nums[a]+nums[b]>m)
{
//尽量吃第二个
ans+=nums[a]+nums[b]-m;//两个的总数 - 规定 = 这组中要吃的数量
nums[b]=m-nums[a]; //全部吃第二个盒子
}
}
cout<<ans;
#define one 1
return 0;
}
P1106 删数问题
在序列中删除 k 位后能够组成的最小的数。
貌似很简单?
直接从左往右,碰到一个 i 位置元素如果比 i-1 位置的元素大 ,则删除 i 位置的元素?
错误的!!
示例:
1 5 9 8 (2) ,删除两个,按照上面的思路,则删除 5 和 9,最后得到 18,但是实际上 15是最优的(删后两个)
1 2 6 5 9 7 (3),删除三个,按照上面的思路,则删除 2 6 和 9,得到了 1 5 7,但是实际上只要 2没被删除则其他的都比这个小。
那么怎么删呢? 观察一下式子,定义:比两边都大,则此位置为山峰
1 5 9(山峰) 8(山峰) :删除山峰,得到 1 5
为什么 8 也是山峰,9比8大啊,因为我们提前删除了9,因此 8 的前面是 5。
1 2 6(山峰) 5 9(山峰) 7(山峰):删除山峰,得到 1 2 5
答案:我们删除山峰位置的元素,则最后的数一定是最小的
实际上山峰在此题只需要表示为:比后面的数大即可
//TODO: Write code here
int n,k;
const int N=1e5+10;
char s[N];
int p=1;
bool vis[N];
signed main()
{
cin>>s>>k;
int len=strlen(s);//原始长度
while (k--)//需要删除 k 个元素,每次循环一次则一定删除一个元素
{
for (int i=0;i<len-1;i++)
{
if (s[i]>s[i+1]) //如果比后面的数字大,则删除 i 位置
{
//后面的位置元素依次覆盖过来,模拟删除 i 位置元素
for (int j=i;j<len-1;j++)
{
s[j]=s[j+1];
}
break;
}
}
len--; //s长度减1
}
int i=0;
while (i<len && s[i]=='0') i++;//处理前导0,记录前0的数量
if (i==len) printf("0");//如果前0的数量等于s的长度,则整个都是0
else
{
//否则从i位置开始(忽略前0),直到最后
for (int j=i;j<len;j++)
{
cout<<s[j];
}
}
#define one 1
return 0;
}
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209592.html