贪心算法(11):水滴问题
Posted 中学生编程与信息学竞赛自学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了贪心算法(11):水滴问题相关的知识,希望对你有一定的参考价值。
本课程是从少年编程网转载的课程,目标是向中学生详细介绍计构和算法。编程学习最好使用计算机,请登陆 www.3dian14.org (免费注册,免费学习)。
本课继续讲解贪心算法相关的习题:水滴问题。
已知一个长度为L的管道中,有N个水滴,它们分布于管道的不同的位置。 每个水滴以不同的速率同时向管道开口端方向移动。 当某水滴追上另一个水滴并与它混合成一个水滴时,混合后的水滴速度会变得与被追上的水滴的原有速度一致。 请你确定最后从管道开口端出来的水滴的数量。
【例题1】请参考下图
假设管道长度L=12,用绿色圆圈表示的5个水滴的位置为{0, 3, 5, 8, 10},而箭头表示水滴移动的方向,也就是所有水滴同时向右移动。表示某个水滴的圆圈中的数字表示该水滴的向右移动速度,分别为{1, 3, 1, 4, 2}。
我们知道移动速度快的水滴会追上移动速度慢的水滴,混合成一个水滴,然后继续以被追上的水滴原有速度继续移动。直到从管道开口处滴落。现在我们来分析看看,上面的例题1中,最后从管道中滴落的水滴数。
分析过程如下:
从x=10和x=8开始的水滴同时向右移动。由于x=8的水滴速度比x=10处的水滴速度快,1秒后,它将在x=12处相遇,从而汇聚成一个水滴滴落。
从x=5和x=3开始的水滴将会汇聚成变成单一的水滴,它们在1秒时于x=6处相互混合,之后以速度1继续向右移动,最后于7秒时于管道开口端滴落。
从0开始的水滴,由于其速度为1,它不会追上其他水滴,它不与其他任何水滴混合,所以最后它自己从管道中滴落。
除此以外,不再有其他的水滴滴落了,所以答案是3。
整个过程请参考下面的动图。
思路分析
考虑使用贪心算法来解决这个问题。
一个水滴将于另一个水滴混合如果它满足下面的三个条件:
1. 水滴的速度要快于待混合的水滴;
2. 较快的水滴的要位于较慢的水滴之后,也就是离管道开口端较远。
3. 较快的水滴要能在较慢的水滴离开管道之前追上较慢的水滴。
我们使用一对数组来存储水滴的位置以及它到达管道开口端所需的时间。然后我们根据水滴的位置对数组进行排序。现在我们可以清楚地知道水滴位于管道的位置以及它们独自到达管道开口端所需的时间。更多的时间意味着更小的速度,更少的时间意味着更快的速度。现在,在较慢水滴之前的所有水滴都不会与之混合,而在较慢的水滴之后所有的水滴与下一个较慢的水滴混合,依此类推。
例如,在上例中,5个水滴到达终点的时间分别是{12, 3, 7, 1, 1},这里5个水滴根据位置排序,分别对应第0个、第1个、第2个、第3个和第4个水滴。
第0个水滴移动最慢,它不会与其他的水滴混合滴落,也就是它将独自滴落。
第1个水滴比第2个水滴移动快,所以它们将会混合,1和2混合后的速度是原第2个水滴的速度,而第2个水滴比第3个水滴慢,因此1和2的混合水滴将从管道中滴落而不会与第3个水滴混合。而第3个水滴将会与第4个水滴混合,因为它们到底管道开口处的时间一样。
因此我们只需要对时间序列{12, 3, 7, 1, 1}找出局部最大值,局部最大值的数目就是水滴的数目。
对应上例,我们可以找到3组局部最大值:{12},{3,7},{1,1},它们对应最后滴落的三个水滴。
算法描述
算法描述如下:
1)计算每个水滴独自移动到管道开口处的时间
2)将时间按水滴最初的位置从左到右排序
3)在时间序列中从右到左依次找出局部最大值的个数
我们可以使用堆栈stack来记录所需时间的局部最大值。
一个局部最大值对应一个一起滴落的混合水滴,局部最大值的数目就是混合滴落的水滴数目。而堆栈中在最后局部最大值后剩余的元素个数,就对应独自滴落的水滴数目。
因此最后的答案是堆栈局部最大值的数加上最后局部最大值后剩余元素数。
代码实现
我们还是使用C++的STL来简化编程。具体的代码实现如下:
#include <bits/stdc++.h>
using namespace std;
int drops(int length, int position[], int speed[], int n)
{
// 以pair方式保存水滴的位置和该水滴单独到达管道开口处所需的时间
// 并且保存在向量中
vector<pair<int, double> > m(n);
int i;
for (i = 0; i < n; i++) {
// 保存水滴的初始位置
m[i].first = position[i];
//p是某水滴距离管道开口出的距离
int p = length - position[i];
// p除以水滴的速度就是该水滴单独到达管道开口处所需的时间
//注意这里乘以1.0是为了将int p转成double类型
m[i].second = p * 1.0 / speed[i];
}
//依据水滴原来的位置由小到大对vector记录的所有水滴进行排序
sort(m.begin(), m.end());
int k = 0; // k记录了最后得到的水滴数
//运用栈 s 来记录下个较慢的水滴,它可能与当前的水滴混合
stack<double> s;
// we traverse the array demo right to left
// to determine the slower drop
//从右向左遍历向量中的水滴,来确定较慢的水滴
for (i = n - 1; i >= 0; i--)
{
//栈为空,把当前的水滴时间入栈
if (s.empty()) {
s.push(m[i].second);
}
//如果当前的水滴比栈顶的时间更长,
//表示栈顶时间对应的水滴将独自滴落,
//因此最后滴落的水滴总数需要加1,
//同时取出栈顶元素,并且将新的时间放在栈顶
if (m[i].second > s.top())
{
s.pop();
k++;
s.push(m[i].second);
}
}
// 向量遍历完成后,从栈中剩余的元素,
// 它们对应的水滴都将成为滴落的水滴
// 因此需要将它们加到结果中
if (!s.empty())
{
s.pop();
k++;
}
return k;
}
// 主程序
int main()
{
int length = 12; // 管道长度
int position[] = { 10, 8, 0, 5, 3 }; // 每个水滴的初始位置
int speed[] = { 2, 4, 1, 1, 3 }; // 每个水滴的初始速度
int n = sizeof(speed)/sizeof(speed[0]);
cout << drops(length, position, speed, n);
return 0;
}
下面的动图描述了程序运行的过程。
请点击阅读原文来观看动画交互课件。
以上是关于贪心算法(11):水滴问题的主要内容,如果未能解决你的问题,请参考以下文章