题解:P16955「NLOI Round1」宇宙冷漠
题目大意
给定 \(n\) 条直线 \(y=k_i x + b_i\),\(q\) 次询问,每次询问第 \(l\) 到第 \(r\) 条直线是否交于同一点。
问题转化
如果直接求多线共点,时间复杂度太高了,但是注意到这些直线是静态的,也就是不会发生变化,那么我们可以先求相邻两线的交点,然后判断某区间内是否都是同一个交点。
细化
这个新问题涉及到以下几个方面:
- 如何求两条直线的交点
- 如何快速判断两个交点是否是同一个点
- 如何判断区间内是否都是同一个交点
我们一个一个说。
整体思路
求两直线的交点其实就是加减乘除,但是平行或重合的“交点”的定义是个问题。
判断两个交点是否是同一个点看起来只要判断 \(x\) 坐标和 \(y\) 坐标都相等就行了,事实上这是可行的线段树做法。
如何快速判断区间内是否都是同一个交点呢?如果我的序列元素不是两个数 \((x,y)\),而是一个数 \(k\) 呢?可以对序列求前缀和,然后判断左端点的数乘以区间长度是否等于区间和。
那么如何把可能为分数的交点 \((x,y)\) 映射为一个独一无二的数呢?可以选择哈希,也可以开一个 map 登记表,碰到一个新的交点就发一张新号牌。
至此整体思路已经出现:求出相邻直线的交点,将其映射到一个数,然后对所有交点映射的数列求前缀和,然后快速判断询问区间是否都是同一个交点。
特殊情况
现在还有最后两个刺头:平行和重合。
如果两线平行,不存在交点,那么映射的数字应该是什么呢?这可不能乱设,就连 \(+\inf\) 也不行,比如我有三线平行,当我查询这三条线时显然有 \(\inf \times 2 = \inf + \inf\),算出来是成立的,就会被误判成共点。
我的处理方式是将交点映射的数字设为 \(-1\),其他的交点编号从 \(1\) 开始,然后在求前缀和的时候将当前元素和 \(0\) 比最大值,再加到前缀和数组中,也就是只累加正数的交点编号:
这样,平行线对和的贡献就是 \(0\)。
在判断区间 \([l,r]\) 是否全为同一交点时,检查:
- 如果左端点 \(l\) 对应的是有效交点(\(p_l > 0\)),而区间内混入了平行线,那么左边的和会少掉平行线本该贡献的 \(p_l\),等式不成立。
- 如果左端点本身就是平行线(\(p_l = -1\)),则等式右边是负数,而左边前缀和非负,等式同样不成立。
这样无论平行线出现在区间的哪个位置,都能正确返回 No,不会干扰到真正共点情况的判断。
重合更加不好用映射的方式处理,因为它既可以和左边的连起来,也可以和右边的连起来。举个例子:
其中 \(1\) 和 \(2\) 表示坐标的映射,\(\texttt{coincident}\) 表示两线共点。如果查询区间 \([1,5]\),那么重合的编号应该为 \(1\),以契合查询;但如果查询区间 \([5,8]\),那么重合的编号应该为 \(2\),与前面矛盾。
我的处理方式是将重合的直线作缩点操作,就是重复的只记录一个。然后在查询的时候将左右端点变为当前数组的左右端点。
代码细节
- 为了避免精度问题,在存入 map 中时,我同时记录了 \(x\) 的分子和分母,以及 \(y\) 的分子和分母,并且约分;
- 特判区间长度为 \(1\) 时输出
Yes; - 在进行缩点操作后,后续的循环不能再是从 \(1\) 到 \(n\);
复杂度分析
如果使用哈希来映射坐标,时间复杂度较低,为 \(O(n+q)\);如果像我一样使用 map,时间复杂度为 \(O(n \log n + q)\)。
空间复杂度均为 \(O(n)\)。
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
#define div() cout << "-----------------------\n";
#define debug(n) cout << #n << " = " << n << "\n";
#define deb(n) cout << #n << " = " << n << " ";
int n,q;
int k[1000005],b[1000005],p[1000005],tk[1000005],tb[1000005];
map<pair<pair<int,int>,pair<int,int>>,int>mp;
int sum[1000005];int tot;int idx;int skip[1000005];
// 约分函数
inline void simplify(int &x,int &y){int gc = __gcd(x,y);x /= gc;y /= gc;}
inline void work(int l){
int r = l+1;
int k1 = k[l];int k2 = k[r];int b1 = b[l];int b2 = b[r];
// 特判
if(k1 == k2 && b1 != b2){
p[l] = -1;return;
}
int xnum = b2-b1;int xdeno = k1-k2;// x坐标的分子和分母
int ynum = k1*b2-k2*b1;int ydeno = k1-k2;// y坐标的分子和分母
simplify(xnum,xdeno);simplify(ynum,ydeno);// 约分
if(!mp[{{xnum,xdeno},{ynum,ydeno}}]){++idx;mp[{{xnum,xdeno},{ynum,ydeno}}] = idx;}
p[l] = mp[{{xnum,xdeno},{ynum,ydeno}}];
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin >> n >> q;
for(int i = 1;i <= n;i++){cin >> tk[i] >> tb[i];}
// 缩点操作,skip[i]表示前i条直线,跳过了多少个
k[++tot] = tk[1];b[tot] = tb[1];
for(int i = 2;i <= n;i++){
if(tk[i] != tk[i-1] || tb[i] != tb[i-1]){
k[++tot] = tk[i];b[tot] = tb[i];
skip[i] = skip[i-1];
}
else{skip[i] = skip[i-1]+1;}
}
for(int i = 1;i < tot;i++){work(i);}
// 求前缀和
for(int i = 1;i < tot;i++){sum[i] = sum[i-1]+max(0ll,p[i]);}
// 查询
for(int i = 1;i <= q;i++){
int l,r;cin >> l >> r;
if(l == r){puts("Yes");continue;}
l -= skip[l];r -= skip[r];
r--;// 要减去1,因为tot条直线只有n-1个交点,这类似于种树问题
if(sum[r]-sum[l-1] == p[l]*(r-l+1)){puts("Yes");}
else {puts("No");}
}
return 0;
}
浙公网安备 33010602011771号