题解
有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->a
和 c->b
)
(2, 要么 两个小球的运动方向是: a->c
和 b->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), 使得 VD1
与VD2
, 形成一个 平行四边形
由向量的 (平行四边形)法则, 绿色向量(指向左侧也可以
) 一定与 (时刻线) 相平行;
又因为, 绿色向量 等于 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 vx1−vx2vy1−vy2=a→vy1−a∗vx1=vy2−a∗vx2
上面分析了 (在未来相撞), 下面再看 在(过去相撞), 如上图中的 下侧
通过向量平移后, 紫色向量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 - VD2
与 VV1 - 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
vx1−vx2vy1−vy2=a→vy1−a∗vx1=vy2−a∗vx2
令一个小球的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 vx1−vx2vy1−vy2=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, B1
和 A2, 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));
}