总述
前几天面试的时候被问到了这个问题,当时没有get到面试官的考察思路,导致无从下手,面试结束后我仔细思考了一下这道题,发现其中的过程和细节还是挺多的,所以记录下这道题的解题思路和代码。
分析
这道题因为涉及了n个球和随机方向、位置,比较抽象不好整理逻辑,所以最好是自己写一个测试用例,在纸上推演一遍再想思路比较好,我当时就没有从简单的情况开始考虑,感觉是紧张了。
小球碰撞有个特点,就是在最开始的情景下,第一次发生碰撞的一定是相邻的小球,这意味着我们在计算时可以将小球按照位置排序,然后两两计算相遇时间,我们的第一次碰撞时间一定在这第一次遍历中能够得到,接下来就是计算相撞的两个小球之外的球的碰撞时间了,根据前面的思路,第一组小球假设为i,j,两个小球相撞时间最短,然后i和j消失,那么此时下一次碰撞的两个小球一定还是去除了i和j之后相邻的小球,这里显然并不需要重新计算所有的相邻小球的碰撞时间,因为只有i - 1 和j + 1从不相邻变得相邻了,我们只需要更新这一对小球的碰撞时间进去,那么我们怎么持续的得到所有时间里最短的碰撞时间呢?
这里考虑使用一个小顶堆,根据碰撞时间来升序即可,那我们i和j碰撞之后,是不是还要把队列里面i和i-1,j和j+1的碰撞时间去掉?其实也不用,用一个额外的数组标记碰撞过的小球就可以了。
这样一来,我们大概可以先写出两个结构体:
struct Ball
{
int velocity; // 速度
int pos; // 位置
};
struct CollisionEvent
{
double time;
int ball1;
int ball2;
bool operator>(const CollisionEvent& other) const
{
return time > other.time;
}
};
然后是计算两球相撞消耗的时间:
// 确定会碰撞的两球的碰撞时间
double CalculateCollisionTime(const Ball& ball1, const Ball& ball2)
{
// 确保相向或追尾才能发生碰撞
int distance = ball2.pos - ball1.pos;
int relativeVelocity = ball1.velocity - ball2.velocity;
// 相向碰撞的情况
if ((ball1.velocity > 0 && ball2.velocity < 0 && ball1.pos < ball2.pos) ||
(ball1.velocity < 0 && ball2.velocity > 0 && ball1.pos > ball2.pos)) {
relativeVelocity = abs(ball1.velocity) + abs(ball2.velocity);
return static_cast<double>(abs(distance)) / relativeVelocity;
}
// 同向追尾的情况
if ((ball1.velocity > ball2.velocity && ball1.pos < ball2.pos && ball1.velocity > 0) ||
(ball1.velocity < ball2.velocity && ball1.pos > ball2.pos && ball1.velocity < 0)) {
relativeVelocity = abs(ball1.velocity - ball2.velocity);
return static_cast<double>(abs(distance)) / relativeVelocity;
}
// 如果不会发生碰撞,返回 infinity
return numeric_limits<double>::infinity();
}
接下来可以开始写主要的逻辑,那么对小球位置排序时,如果位置相同的两个小球该如何排序?根据推理,应该是速度更慢的小球在左边,速度更快的小球在右边。
// 将小球按位置排序,如果位置相同,则速度小的排在前面
sort(balls.begin(), balls.end(), [](const Ball& a, const Ball& b) {
return a.pos == b.pos ? abs(a.velocity) < abs(b.velocity) : a.pos < b.pos;
});
初始化一个最小堆
// 初始化一个最小堆来存储碰撞事件
priority_queue<CollisionEvent, vector<CollisionEvent>, greater<CollisionEvent>> minHeap;
针对初始状态,两两计算相邻小球的可能碰撞时间,目的是为了找出最早碰撞的时间,这个最早碰撞的时间很关键,并且一定是实际发生了碰撞,初次之外在这里计算的其他时间都有可能实际上没碰着
for (int i = 0; i < balls.size() - 1; ++i) {
double time = CalculateCollisionTime(balls[i], balls[i + 1]);
if (std::isfinite(time)) {
minHeap.push({ time, i, i + 1 }); // 列表初始化
cout << time << ' ' << i << ' ' << balls[i].pos<< endl;
}
}
接下来根据最小堆的内容开始pop最短时间的碰撞,根据前面的思路,我们要记录已经碰撞的小球,因为这两个小球已经消失了,他们参与的其他可能的碰撞事件应该无效了,并且我们应该只需要更新这组已消失小球两侧的小球的碰撞
vector<bool> active(balls.size(), true); // 标记小球是否仍在场
double lastCollisionTime = 0;
// 处理碰撞事件
while (!minHeap.empty())
{
CollisionEvent ce = minHeap.top();
cout << ce.ball1 << ' ' << ce.ball2 << endl;
minHeap.pop();
// 如果小球已消失,跳过该事件
if (!active[ce.ball1] || !active[ce.ball2]) continue;
lastCollisionTime = ce.time;
// 将小球 i 和 j 标记为消失
active[ce.ball1] = false;
active[ce.ball2] = false;
// 更新相邻的有效小球的碰撞事件
int left = ce.ball1 - 1;
int right = ce.ball2 + 1;
cout << left << ' ' << right << endl;
// 仅检查活跃小球对 `left` 和 `right` 之间的碰撞
if (left >= 0 && active[left] && right < balls.size() && active[right]) {
double time = CalculateCollisionTime(balls[left], balls[right]);
if (isfinite(time)) {
minHeap.push({ time, left, right });
}
}
}
return lastCollisionTime;
这样就基本完成了这道题目,完整代码如下,附带一个小的测试用例,可能不全面,因为这道题我没在网上见到过:
#include<iostream>
#include<queue>
#include<vector>
using namespace std;
struct Ball
{
int velocity; // 速度
int pos; // 位置
};
struct CollisionEvent
{
double time;
int ball1;
int ball2;
bool operator>(const CollisionEvent& other) const
{
return time > other.time;
}
};
// 确定会碰撞的两球的碰撞时间
double CalculateCollisionTime(const Ball& ball1, const Ball& ball2)
{
// 确保相向或追尾才能发生碰撞
int distance = ball2.pos - ball1.pos;
int relativeVelocity = ball1.velocity - ball2.velocity;
// 相向碰撞的情况
if ((ball1.velocity > 0 && ball2.velocity < 0 && ball1.pos < ball2.pos) ||
(ball1.velocity < 0 && ball2.velocity > 0 && ball1.pos > ball2.pos)) {
relativeVelocity = abs(ball1.velocity) + abs(ball2.velocity);
return static_cast<double>(abs(distance)) / relativeVelocity;
}
// 同向追尾的情况
if ((ball1.velocity > ball2.velocity && ball1.pos < ball2.pos && ball1.velocity > 0) ||
(ball1.velocity < ball2.velocity && ball1.pos > ball2.pos && ball1.velocity < 0)) {
relativeVelocity = abs(ball1.velocity - ball2.velocity);
return static_cast<double>(abs(distance)) / relativeVelocity;
}
// 如果不会发生碰撞,返回 infinity
return numeric_limits<double>::infinity();
}
double FindLastCollisionTime(vector<Ball> balls)
{
if (balls.size() < 2) {
return 0; // 没有碰撞
}
// 将小球按位置排序,如果位置相同,则速度小的排在前面
sort(balls.begin(), balls.end(), [](const Ball& a, const Ball& b) {
return a.pos == b.pos ? abs(a.velocity) < abs(b.velocity) : a.pos < b.pos;
});
// 初始化一个最小堆来存储碰撞事件
priority_queue<CollisionEvent, vector<CollisionEvent>, greater<CollisionEvent>> minHeap;
vector<bool> active(balls.size(), true); // 标记小球是否仍在场
for (int i = 0; i < balls.size() - 1; ++i) {
double time = CalculateCollisionTime(balls[i], balls[i + 1]);
if (std::isfinite(time)) {
minHeap.push({ time, i, i + 1 }); // 列表初始化
cout << time << ' ' << i << ' ' << balls[i].pos<< endl;
}
}
double lastCollisionTime = 0;
// 处理碰撞事件
while (!minHeap.empty())
{
CollisionEvent ce = minHeap.top();
cout << ce.ball1 << ' ' << ce.ball2 << endl;
minHeap.pop();
// 如果小球已消失,跳过该事件
if (!active[ce.ball1] || !active[ce.ball2]) continue;
lastCollisionTime = ce.time;
// 将小球 i 和 j 标记为消失
active[ce.ball1] = false;
active[ce.ball2] = false;
// 更新相邻的有效小球的碰撞事件
int left = ce.ball1 - 1;
int right = ce.ball2 + 1;
cout << left << ' ' << right << endl;
// 仅检查活跃小球对 `left` 和 `right` 之间的碰撞
if (left >= 0 && active[left] && right < balls.size() && active[right]) {
double time = CalculateCollisionTime(balls[left], balls[right]);
if (isfinite(time)) {
minHeap.push({ time, left, right });
}
}
}
return lastCollisionTime;
}
int main()
{
// 示例:5个小球,位置和速度如下
vector<Ball> balls = { {0, 2}, {2, 1}, {5, -3}, {3, 2}, {6, -1} };
// 计算最后一对小球碰撞的时间
cout << "Last collision time: " << FindLastCollisionTime(balls) << endl;
return 0;
}