`算法题解` `AcWing` 4508. 移动的点

博客详细探讨了如何通过向量分析解决多个小球在不同速度和方向下的相撞问题。首先澄清了直线相交并不一定导致相撞,并引入了速度和时间的概念。接着,通过建立向量模型,分析了未来和过去相撞的情况,得出判断相撞的数学公式,并讨论了特殊情况,如速度相同或轨迹重合。最后,提供了高效的算法思路和代码实现,计算所有可能的相撞对数。
摘要由CSDN通过智能技术生成

题目链接

题解

有n条 (带方向的 直线), 每个直线都有一个自己的速度, 表示: 一个小球, 从无穷远处 朝着这个方向 以该速度 在这个直线上 无终止的运动着.

我们画一条直线(简称为: 时刻线), 与这n个点 有n个交点 (没有交点重合 说明: 相撞 要么在过去 要么在未来),
这些交点表示: 在历史某一时刻 (同一时刻), 这n个小球, 在空间的位置!

这个逻辑非常重要, 仔细理解;


首先, 两条直线相交, 两个小球就会相撞吗?
------[a]------[b]-----> x轴, 令此时两小球的距离为Dist
比如, 时刻线就是x轴, a小球以 (与x轴成45度方向) 以Va速度行驶
b小球以 (与x轴成135度方向) 以Vb速度行驶
结论: 如果Va == Vb, 不管Dist为多少, 则两小球必然相撞
否则, 只要Va != Vb, 不管Dist为多少, 两小球永远不相撞
所以, 不能单看直线!! 直线仅仅是小球的运动轨迹! 还要与(速度)相关
同一条直线, 同一个方向, 然而 不同的速度, 会带来不同的结果

即直线的交点:
(1, 要么相撞)
(2, 要么一个小球比如此时在这个交点处, 另一小球 此时一定不在这个位置
… 或者说, 另一个小球, 要么已经以前在这个位置, 要么以后未来会在这个位置


这样直觉的分析, 非常的不严谨, 而且容易判断出错, 严格的分析:

暂不考虑两个轨迹重合的情况, 先只分析: 两个直线相交的情况 (这个交点, 必然不在 时刻线上)

在这里插入图片描述

如果小球在c点相撞, 也就是: (要么未来在c相撞) (要么过去在c相撞)
这个表述等价于: 两个小球的运动方向, 需要指向 (时刻线)的同一侧, 也就是方向相同
请注意, 这里的小球运动方向, 是否相同, 不是以坐标系的角度, 而是 (时刻线)的角度.

也就是:
(1, 要么 两个小球的运动方向是: c->ac->b)
(2, 要么 两个小球的运动方向是: a->cb->c)
否则, 其他两种运动方向, 就意味着: (一个小球到达c点, 另一小球 在过去/未来 到达c点), 总之, 没有相撞;

判断运动方向是否相同, 可以类比 (判断两个点, 是否在直线的同一侧), 通过三个向量, 是否叉积, 判断两个叉积的乘积 > 0

总之, 此时两个小球运动方向相同;

但是, 此时也不一定相撞; 因为还涉及一个量: 速度;

令Va为 小球1 在轨迹上的速度, Vb为 小球2 在轨迹上的速度; (两者都是标量; 为正数, 仅代表速度大小, 没有方向)
(如果带有方向 即正负号, 也可以; 但要保证, 如果上面的方向相同 则同号, 否则异号)

只有: 时间 = S a c V a = S b c V b 时间 = \frac{S_{ac}}{ Va} = \frac{S_{bc}}{ Vb} 时间=VaSac=VbSbc, 两个时间相同时, 才说明是相撞!
其中, S距离是常量, V也是常量, 自然可以求出时间;

因此得到暴力做法:

for( i小球)
	for( j小球)
		if( 两个轨迹相交 && 方向相同 && 时间相同)
			相撞
		else if( 轨迹重合)
			分析...

这是O(n^2), 肯定不行的;

而且, 这个算法, 只是针对 (两直线 相交一点) 的情况
而对于 (一个直线与一个点) 和 (两个重合的直线), 也有可能会相撞, 只能特判;

向量

用 速度向量来表示: 速度, 记作VV: vector velocity
… (一个向量, 可以表示: 速度大小和方向, 但无法表示: 是哪个小球! 因为向量是可以平移的
… (故他只能表示: 朝这个方向, 具有这个速度; 但不知道是从哪个位置坐标的
… (换句话说, 哪个位置都可以, 任意点 都可以具有这样的一个速度向量, 速度向量 和 小球起始位置, 是两个不相干的量)

用 位移向量来表示: 位移, 记作: VD: vector distance
表示: 朝这个方向, 走了这么多的距离;
同样, 他也不能表示, 是从哪个起始点开始 走了这样的位移; 换句话说, 从哪个点开始都可以.

两者的关系是: (如果两个向量的方向相同: VD = VV * t, t为时间)


这样定义的好处是: ( 比如两个小球1 2在 时刻线的位置是: a b)
(假如他俩是在未来会相撞, 则存在t时间, VD1 = VV1 * t, VD2 = VV2 * t,
… 使得: <以a点为起点的 位移向量VD1终点> 等于 <以b点为起点的 位移向量VD2终点>
(假如他俩是在过去相撞, 则存在t时间, VD1 = VV1 * t, VD2 = VV2 * t,
… 使得: <以a点为终点的 位移向量VD1起点> 等于 <以b点为终点的 位移向量VD2起点>

这个定义非常的本质, 他不像上面 有 (比如两条线相交)的局限, 使用向量表示, 非常的本质化;
不管是 (两个直线相交) (两个直线重合) (一个直线一个点), 不管是什么情况, 只要会相撞, 则必然满足: 上面分析的情况.
这就是用向量定义的好处


先看在未来相撞 (下图中的 上侧)

在这里插入图片描述图中的红色向量为: 位移向量; 蓝色向量为: 速度向量;

两个位移向量, 的终点 重合;
又因为: 向量可以平移, 我们将VD1进行平移 (也可以平移VD2), 使得 VD1VD2, 形成一个 平行四边形
由向量的 (平行四边形)法则, 绿色向量(指向左侧也可以) 一定与 (时刻线) 相平行;
又因为, 绿色向量 等于 VD1 - VD2 (或者如果反过来则为VD2 - VD1) 向量减法的平行四边形法则

即: VD1 - VD2向量 与 y = ax + b 平行; (换句话说, 绿色向量VD1 - VD2, 可以平移到 y=ax+b上面去)

令时间为t, 则: VD1 - VD2 = t( VV1 - VV2)
因为涉及到 (平行), 斜率, 我们将向量 具体到 (x, y)的形式;
VV1 = (vx1, vy1), VV2 = (vx2, vy2)
绿色向量为: { t * (vx1 - vx2), t * (vy1 - vy2)}, 他与y = ax + b 平行, 是什么意思呢?

一个向量, 如果要看他的斜率, 其实他和直线是一样的, 只不过他可以平移 (即b是任意的), 但不管怎么平移, 斜率是不变的

一个向量{x, y}, 他的斜率是: y x \frac{y}{x} xy

因此得到: v y 1 − v y 2 v x 1 − v x 2 = a → v y 1 − a ∗ v x 1 = v y 2 − a ∗ v x 2 \frac{ vy1 - vy2}{ vx1 - vx2} = a \quad \rightarrow \quad vy1 - a * vx1 = vy2 - a * vx2 vx1vx2vy1vy2=avy1avx1=vy2avx2


上面分析了 (在未来相撞), 下面再看 在(过去相撞), 如上图中的 下侧
通过向量平移后, 紫色向量VD1 - VD2, 在直线y=ax+b上, 也说明: 该向量与直线平行; 同样得到上面一样的公式.


在这里插入图片描述

绿色向量为: { t * (vx1 - vx2), t * (vy1 - vy2)}, 他与y = ax + b 平行;
而: 绿色向量 平行于 向量{vx1 - vx2, vy1 - vy2}; 因此, 判断这个向量即可;
而这个向量, 代表什么呢?

绿色向量是: VD1 - VD2, 则这个向量是: VV1 - VV2, 他俩是平行的;
从向量来看, 他俩是倍数关系, 所以平行;
而从(三角形)来看, 通过平移三角形的一条边, 会得到: 相似三角形

从图中来看, 只有 VV1 与 VV2共终点的 , 才会相撞;
因为: 三角形1 与 三角形2 是相似三角形;
也就是, 对于三角形1 (因为VV1 与 VV2共终点的, 所以, 他是一个三角形; 否则不能构成三角形)
此时, 移动VV2边, 直到将其起点, 移动到b点; 这个过程, 就是 (是三角形的 平移一条边操作),
结果是: 得到了一个新的三角形 (两个端点在a b点);
因为, 这两个三角形是 相似的; 即VD1 = t * VV1; VD2 = t * VV2, 他们的t是相同的;

这是从另一个角度 来分析: VD1 - VD2VV1 - VV2 平行; 也就是: (x, y) 与 (tx, ty)是平行的;


再来看: v y 1 − v y 2 v x 1 − v x 2 = a → v y 1 − a ∗ v x 1 = v y 2 − a ∗ v x 2 \frac{ vy1 - vy2}{ vx1 - vx2} = a \quad \rightarrow \quad vy1 - a * vx1 = vy2 - a * vx2 vx1vx2vy1vy2=avy1avx1=vy2avx2
令一个小球的F值 = vy - a * vx, 那么, 两个小球的F值相同, 意味着什么呢?
从上面的分析指导, 意味着, 两个小球会相撞!
那么, 比如3个小球, 他们的F值都相同; 肯定意味着: 任意两个小球, 都会相撞;
但是: 这3个小球, 相撞于同一点吗?
… 答案是否定的; 其实这要从定义出发, 两个小球相撞, 说明F值相同; 并不能推出: 所有F值相同的, 相撞于同一点; (两者是无关联的)
比如: 你可以画3个 (速度向量)的终点 共于同一点; 这就说明( 任意两个小球, 都是相撞的), 即上面说的, 3个F值是相同的
… (假如这3个小球, 在时刻线(此时) 就在这3个速度向量的 (起点), 那么: 这3个小球, 确实相撞于同一点
… (但是, 你此时将任意两个速度向量平移 (向量平移后, 于原向量等价) 即依然是任意两个小球是相撞的
… … 此时, 你延长, 三条线的会有3个交点, 这3个交点, 就是相撞点; 则 这3个小球, 不相撞于同一点

但是, 其实研究这个问题没有意义.
因为, 他对求答案 其实没有帮助;
假如F值 为 K 的小球有n个, 因为其两两相撞 (不管所有点是否在同一点相撞),
n个小球, 对答案的贡献是: n * (n-1)

即, 根据F值的不同, 对所有小球进行分类, 然后对每一类的小球, 进行ans += n * (n-1)操作

这就是答案;

但是由于 v y 1 − v y 2 v x 1 − v x 2 = a \frac{ vy1 - vy2}{ vx1 - vx2} = a vx1vx2vy1vy2=a中, 分母不能为0的条件, 所以, 此时必须要特判;

对于两个小球, 如果他们的vx相同, 则一定不会相撞;
… 因为, 分母不能为0, 此时式子!= a; 而如果相撞, 式子必然会= a (如果这样分析, 觉得太粗略, 见以下)
… 因为, 当两个速度向量的vx相同, 你的VV1 - VV2 要么是(垂直x轴) 要么是(0向量), 这两种情况, 都不会相撞;

故, 只要两个vx相同的速度向量, 他们一定不相撞, 即(对答案一定无贡献)
我们记作, 这样的两个小球a b 为: 非法小球对; (注意, a c可能是合法的)
反正, 只要两个小球的vx不同, 就是合法的对, 否则就是非法的对.

但是, 我们上面的做法: (根据F值的不同, 对所有小球进行分类, 然后对每一类的小球, 进行ans += n * (n-1)操作)
会包含有: 非法小球对; 也就是, 此时求出的ans是错误的; 要把分母为0的情况, 给抛除掉

假如a b两个小球 是 非法小球对, 且他对此时的ans有贡献, 那么: 这两个小球的F值, 必然相同; (否则无贡献)
即: vy1 - a*vx1 = vy2 - a*vx1, 得到: vy1 == vy2

即, 只有: 两个小球的 速度向量 完全相同(vx1 = vx2, vy1 = vy2), 他才会对ans有贡献, 而他俩是非法的, 不应该有贡献;

那么, 贡献有多大呢?
单看F值为K的集合: 即该集合里, 所有小球的F值均为K, 有n个小球
… a个小球为(vx1, vy1): A1, A2, .., Aa
… b个小球为(vx2, vy2): B1, B2, .., Bb
… c个小球为(vx3, vy3): C1, C2, .., Cc

他们此时在ans的贡献是: n * (n-1), (由于这个值 等于 不同数对的个数, 我们可以将他对ans的贡献, 看作是: 不同数对的个数)
记作: Sum = n * (n - 1)

但是, 我们知道, A1, A2 他俩对ans是无贡献的; (因为他俩的 vx, vy都相同)
因此, 要减去Ai, Aj 对答案的贡献 (等于: a * (a - 1))

同理, 要减去Bi, Bj 对答案的贡献 (等于: b * (b - 1))

同理, 要减去Ci, Cj 对答案的贡献 (等于: c * (c - 1))

这是符合加法原理的! 但你必须这样严谨的分析, 而不是凭感觉

即: Sum - a*(a-1) - b*(b-1) - c*(c-1), 这个值等于: 2*a*b + 2*a*c + 2*b*c
2*a*b 意味着: (从a里挑1个, 再从b里挑1个), 会产生2的贡献, 即一共会产生2*a*b的贡献;
注意, 从a里挑1个, 挑A1 还是 挑A2, 对答案都是有贡献的;
A1, B1A2, B1, 他俩对答案的贡献是: 2 + 2, 是不同的
… 这就是为什么, 要这样严谨的分析, 而不是凭感觉; 感觉上可能认为, 是要将a个相同小球, 化为1个.

代码

代码很短, 但却不简单…

int n, a, b;
scanf("%d%d%d", &n, &a, &b);

map< pair< int, int>, int> map_x;
unordered_map< Ll_, int> map_f;

Ll_ ans = 0;
FOR_( i, 1, n, 1){
    int x, vx, vy;
    scanf("%d%d%d", &x, &vx, &vy);
    
    map_x[ {vx, vy}] ++;
    
    Ll_ f = vy - Ll_( a) * vx;
    map_f[ f] ++;
}
for( auto it = map_f.begin(); it != map_f.end(); ++it){
    int cont = (*it).second;
    ans += ( Ll_( cont) * ( cont - 1));
}
for( auto it = map_x.begin(); it != map_x.end(); ++it){
    int cont = (*it).second;
    ans -= ( Ll_( cont) * ( cont - 1));
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值