题目大意
有n条水平线段, 左端点为l[i], 右端点为r[i], 可以左右平移, 线段平移的消耗是平移的距离, 现在要你把这些线段平移到存在一条竖直线段与所有水平线段相交, 求最小的总消耗
题目思路
nlog(n)
- 首先, 能够得到最小消耗的那条竖直线段肯定能够在线段的端点处取到, 如果不在端点处, 将这条竖线向左平移到左边最近的端点, 向右平移到右边最近的端点这两个操作肯定是一个消耗增加, 一个消耗减小, 或者两个都不变, 所以在端点处一定能取到最小值
- 最先想到的做法肯定是将所有2n个端点排序, 然后一个端点一个端点扫过去的到最小消耗
- 设完全在竖直线段左侧的线段数量为x, 完全在右侧线段数量为y
- 那么从一个端点到另一个端点的变化是两端点距离*(x-y), 因为端点排过序, 所以在两个端点移动不会改变x, y(线段只会从被竖线中间穿过或交于一个端点变成交于另一个端点), 所以只要每次移动后再更新x, y就好了
- 这样加上排序效率是nlogn
题解还提到了一种O(n)的解法
- 很容易看出, 竖线从左到右扫描的时候, 消耗肯定是先减小后增大的, 所以会在中间某个位置取到最小值
- 因为从端点i移动到端点i+1(端点排好了序), 消耗的变化是dis(i, i+1)*(x-y); 所以, 当x==y的时候, 就是最小值
- 如果x==y, 可能是有n个端点完全在竖线左边, n个端点完全在竖线右边, 还可能是(n-x)完全在竖线左边, (n-x)个端点完全右边, 然后然后有x条线段被竖线穿过, 两种情况, x==y都可以在第n个端点和第n+1个端点中取到
- 所以竖线肯定在第n个端点或者第n+1个端点处
- 所以只需要找出排序在第n个端点的位置, 然后对每条线段计算需要的平移的距离
求数组第k大元素是一个经典问题, 有O(n)的求法, STL里面又有nth_element()可以在O(n)求出第k大元素
nth_element()
nth_element(a, a+k, a+n);
之后a[k]就是第k大元素, 并且a[k]之前元素都小于a[k], a[k]之后元素都大于a[k]
代码
#include <bits/stdc++.h>
using namespace std;
int n, l[111111], r[111111], s[222222];
int main(){
scanf("%d", &n);
for(int i=0; i<n; ++i){
scanf("%d%d", l+i, r+i);
s[i*2] = l[i];
s[i*2+1] = r[i];
}
nth_element(s, s+n-1, s+2*n);
int t = s[n-1];
long long ans = 0;
for(int i=0; i<n; ++i)
if(r[i]<t) ans += (t-r[i]);
else if(l[i]>t) ans += (l[i]-t);
cout << ans << endl;
return 0;
}
三分
想不到是第n个端点就是能得到最小消耗的竖线位置
但很容易想到消耗是沿端点从左到右先减少后增加的
这样就可以利用三分, 代码也很精简, 而且更容易想到
#include <bits/stdc++.h>
using namespace std;
int n, l[111111], r[111111], s[222222];
long long cal(int p)
{
long long ans = 0;
for(int i=0; i<n; ++i)
if(r[i]<p) ans += p-r[i];
else if(l[i]>p) ans += l[i]-p;
return ans;
}
int main(){
scanf("%d", &n);
for(int i=0; i<n; ++i){
scanf("%d%d", l+i, r+i);
s[i*2] = l[i];
s[i*2+1] = r[i];
}
sort(s, s+2*n);
int l=0, r=2*n-1, lmid, rmid;
long long tl, tr;
while(l<r)
{
lmid = l + (r-l)/2;
rmid = lmid + (r-lmid)/2;
tl = cal(s[lmid]);
tr = cal(s[rmid]);
if(tl<tr) r = rmid-1;
else if(tl == tr) l = lmid, r = rmid;
else l = lmid+1;
}
cout << cal(s[l]) << endl;
return 0;
}