题解:P4479 [BJWC2018] 第k大斜率
P4479 题解
题面
前置知识
- 树状数组求逆序对。(不会的看后记。)
- 二维偏序。(不会的可以利用这道题学一学。)
- 知道斜率公式。
思路
首先,\(O(n^2)\) 的大家肯定都没有问题。
接下来就开始考虑正解,首先答案是从大到小第 \(k\) 大的斜率,满足单调性,就可以二分答案。
那二分答案也很好想,只要判断有几个斜率大于等于 \(mid\) 即可,接下来就开始推式子。
首先,一个直线(不妨设 \(x_i<x_j\))的斜率大于 \(num\) 得满足如下式子。
\[\frac{y_i-y_j}{x_i-x_j}>num
\]
\[y_i-y_j>num\times(x_i-x_j)
\]
\[y_i-num\times x_i>y_j-num\times x_j
\]
咱们令 \(t_i=y_i-num\times x_i\),于是问题就转化为满足 \(x_i<x_j,t_i>t_j\) 的数对的个数。
于是这就是二维偏序的问题。
接下来就是要把二维降维成一维偏序。(就是求逆序对。)其实也很简单啦,只要将 \(x\) 数组提前从小到大排序一下(那么前提条件 \(x_i<x_j\) 就满足了)就可以啦。
实现细节
- 二分的边界,这个其实挺好想,就是斜率最大的时候为 \(\frac{y_{max}-y_{min}}{1}=2\times10^8\)。
- 对输入的点进行排序的时候 \(y\) 的关键字。(如果错了就喜提 \(40\) 分的好成绩。)当 \(x_i=x_j\) 时,这时候斜率是不存在的,所以它不能被计入答案,所以要 \(y_i>y_j\)。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int MN=1e5+5;
ll n,k,t[MN],ans,rk[MN];
struct point{ll x,y;}a[MN];
struct node{ll val,id;}q[MN];
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
ll lowbit(ll x){return x&-x;}
void change(ll x, ll v){while(x<=n){t[x]+=v;x+=lowbit(x);}}
ll query(ll x){ll res=0;while(x){res+=t[x];x-=lowbit(x);}return res;}
bool cmp1(point a, point b){return a.x==b.x?a.y>b.y:a.x<b.x;}
bool cmp2(node a, node b){return a.val==b.val?a.id>b.id:a.val>b.val;}
bool check(ll num){
ll res=0;
memset(t,0,sizeof(t));
for(int i=1; i<=n; i++) q[i].id=i,q[i].val=a[i].y-num*a[i].x;//降维。
sort(q+1,q+1+n,cmp2);
for(int i=1; i<=n; i++) rk[q[i].id]=i;//常规离散化。
for(int i=n; i>=1; i--){//常规求逆序对。
res+=query(rk[i]-1);
change(rk[i],1);
}
return res>=k;
}
int main(){
n=read();k=read();
for(int i=1; i<=n; i++) a[i].x=read(),a[i].y=read();
sort(a+1,a+1+n,cmp1);//按照x的升序,y的降序排序。
ll l=-2e8,r=2e8;//见实现细节
while(l<=r){//二分答案。
ll mid=l+r>>1;
if(check(mid)) l=mid+1,ans=mid;
else r=mid-1;
}
write(ans);putchar('\n');
return 0;
}
后记
这里来介绍下树状数组求逆序对。(虽然原题的题解可能会更好。)
首先我们先理解下逆序对的本质是求 \(i<j,a_i>a_j\) 的 \((i,j)\) 的个数。
接下来开始介绍算法。
用树状数组接逆序对需要的一个思想(或者说技巧),把数字看做树状数组的下标。接下来就是一次处理每一个元素,树状数组的下标所对应的元素数值加 \(1\),统计前缀和,这就是逆序对的数量。倒序和正序处理均可,这里介绍倒序。
倒序依次处理每一个数,当前数字的前一个数的前缀和即为以该数为较大数的逆序对个数。
当然,一般求逆序对的时候为了防止因为数据太大,会选择离散化,因为求逆序对只需要每对数的相对大小,这样就可以用树状数组维护了。
代码如下。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int MN=1000005;
ll n,m,x,rk[MN],t[MN],ans;
struct node{ll id,val;}a[MN<<2];
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
ll lowbit(ll x){return x&-x;}
void change(ll x, ll v){while(x<=n){t[x]+=v;x+=lowbit(x);}}
ll query(ll x){ll res=0;while(x){res+=t[x];x-=lowbit(x);}return res;}
bool cmp(node x, node y){if(x.val==y.val)return x.id<y.id;return x.val<y.val;}
int main(){
n=read();
for(int i=1; i<=n; i++) a[i].val=read(),a[i].id=i;
sort(a+1,a+1+n,cmp);
for(int i=1; i<=n; i++) rk[a[i].id]=i;//离散化
ans=0;
for(int i=n; i>0; i--){//倒序求逆序对
change(rk[i],1);
ans+=query(rk[i]-1);
}
printf("%lld\n",ans);
return 0;
}