题目
传送门:有账号才行。
题目描述
幻想乡有
N
N
N 个区域,从
1
1
1 到
N
N
N 编号,神奈子希望通过修建“御神渡”来让所有的区域互相可达,“御神渡”可以看做两个区域之间的一个双向道路。在
i
i
i 区域与
j
j
j 区域之间修建“御神渡”,需要在
i
i
i 区域耗费
C
i
C_i
Ci 的神力新建神社,在
j
j
j 区域耗费
C
j
C_j
Cj 的神力新建神社,然后将两个神社配对。如果一个区域和其他多个区域建立“御神渡”,那么在这个区域要为每个“御神渡”建一个神社,即多个“御神渡”不能共用神社。
“御神渡”会促进两个区域的人口流动,而人口的流动就会带来文化的交流、信仰的增多。如果 i i i 区域的人口为 A i A_i Ai 而 j j j 区域有的人口为 A j A_j Aj ,则神奈子将会得到 A i × A j A_i\times A_j Ai×Aj 的神力。
神奈子希望修建尽量少的“御神渡”,使得 N N N 个区域互相可达。在这个前提下,神奈子还希望自己总神力的减少量最小(注意该值可能为负,即神奈子的神力不减反增)。
输入格式
第一行一个非负整数
N
N
N ,表示区域的数量。
第二行 N N N 个非负整数 A i A_i Ai ,分别表示 N N N 个区域的人口。数据保证每个区域的人口互不相等。
第三行 N N N 个非负整数 C i C_i Ci ,分别表示在 N N N 个区域建神社要花费的神力。
输出格式
输出一个整数,表示神奈子总神力的减少量的最小值。
数据范围与约定
对于所有数据,
N
≤
1
0
5
,
A
i
≤
2
×
1
0
6
,
C
i
≤
1
0
9
N\le 10^5,A_i\le 2\times10^6,C_i\le 10^9
N≤105,Ai≤2×106,Ci≤109 。
思路
很明显,将 ( u , v ) (u,v) (u,v) 的边权视作 C u + C v − A u × A v C_u+C_v-A_u\times A_v Cu+Cv−Au×Av(即在 u u u 和 v v v 之间建立“御神渡”的净花费),则原题等价于求这张图里的最小生成树。
耿直的暴力
暴力求最小生成树, O ( n 2 ) \mathcal O(n^2) O(n2) 。因为是完全图。
斜率优化
思考为什么这道题可以有比最小生成树更快的算法?必然是因为 边权的特殊性。
不难证明,所有与 u u u 相连的边中最小的一条一定是生成树中的边。(可以用 p r i m \tt prim prim 或 k r u s k a l \tt kruskal kruskal 的过程证明,或模仿二者的反证法来证明)。怎么找最小的一条边?
考虑 斜率优化。推一推斜率优化的式子,发现是 C u − C v < A x ( A u − A v ) C_u-C_v<A_x(A_u-A_v) Cu−Cv<Ax(Au−Av) 时 u u u 更优(考虑边 ⟨ x , u ⟩ \langle x,u\rangle ⟨x,u⟩ 和 ⟨ x , v ⟩ \langle x,v\rangle ⟨x,v⟩ 谁边权更小)。那么假定 A v < A u A_v<A_u Av<Au ,第 x x x 个点定义为 ( C x , A x ) (C_x,A_x) (Cx,Ax) ,上式等价于 K u v < A x K_{uv}<A_x Kuv<Ax 。不难发现,维护的形状是下凸包。
先求凸包,按照 A A A 值排序后跑, O ( n log n ) \mathcal O(n\log n) O(nlogn) 。然而现实很残酷——只有 60 p t s \tt{60pts} 60pts !
改进版
为什么不是 100 100 100 分到手?因为有可能 试图连接自环!
也就是说,斜率优化求决策点的时候,最佳决策点是自己 我也很绝望啊。
那么我们迫切的需要一个新方法:考虑每一个点的时候,只把其他的拿出来做凸包。回到了 O ( n 2 ) \mathcal O(n^2) O(n2) ……
等等!好像对于 [ 1 , m i d ] [1,mid] [1,mid] 中的每一个 x x x , ( m i d , r ] (mid,r] (mid,r] 中的任意一个点都可以当做决策点。这提示我们 分治!
用 w o r k ( l , r ) work(l,r) work(l,r) 表示处理 [ l , r ] [l,r] [l,r] 内部的互相贡献,首先递归,其次 交叉计算贡献。
这很像 c d q \tt{cdq} cdq分治,能做到 O ( n log n ) \mathcal O(n\log n) O(nlogn) 。应该有 80 80 80 分了吧?
正解
为什么还不过,去™的垃圾题
因为 连接一次不够……有可能只连接了 n 2 \frac{n}{2} 2n 条边(两个点 选择了同一条边……)
好吧,把点染色,一个联通块中的点染一种颜色,问题转化为 某一颜色向异色点中连边。
我们把颜色拿来排个序,一样可以分治……个屁!为什么不能分治了?因为交叉计算贡献的时候,得依靠 A A A 值有序才行!
这个问题好像有点熟悉……有两个维度 ( x , y ) (x,y) (x,y),要求 x x x 和 y y y 都有序。二维偏序?
做过二维偏序板题吗?做过的都明白用的方法是 c d q \tt cdq cdq分治!
现在这个题,好像有点板?(大雾)
C D Q CDQ CDQ 分治,先对颜色排序,再归并 A A A 值, O ( n log 2 n ) \mathcal O(n\log^2 n) O(nlog2n) 。
关于复杂度:当前有 m m m 种颜色,则至少连接 m 2 \frac{m}{2} 2m 条边,也就是颜色减少一半。开始有 n n n 种颜色,最多做 log n \log n logn 次生成树。生成树复杂度与上解法三同, O ( n log n ) \mathcal O(n\log n) O(nlogn) 。实际上,很少有数据能够卡到做 log n \log n logn 次生成树(大多数做个 2 2 2 到 5 5 5 次就搞定了),运行起来海星。
代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
struct Point{ // 向量(或者点)
int x, y;
Point(int X=0,int Y=0):x(X),y(Y){ }
Point operator+(const Point &that)const{
return Point(x+that.x,y+that.y);
}
Point operator-(const Point &that)const{
return Point(x-that.x,y-that.y);
}
friend long long operator*(const Point &a,const Point &b){
return 1ll*a.x*b.y-1ll*a.y*b.x;
}
friend Point operator*(const int &u,const Point &v){
return Point(u*v.x,u*v.y);
}
};
const int MaxN = 100000;
int allColor, n, fa[MaxN];
inline int findColor(int a){
if(fa[a] != a) // 并查集判断颜色相同否
fa[a] = findColor(fa[a]);
return fa[a];
}
struct Node{
Point p; int color;
bool operator < (const Node &that)const{
if(color != that.color)
return color < that.color;
return this->lessThan(that);
}
bool lessThan(const Node &that)const{
return p.x < that.p.x;
}
} node[MaxN];
long long edgeValue(int i,int j){ // 暴力计算边的权值
return -1ll*node[i].p.x*node[j].p.x+node[i].p.y+node[j].p.y;
}
int ys[MaxN]; // 将颜色离散化
int* convexHull(int l,int r,int *result){
int *top = result;
for(int i=l; i<r; ++i){
while(top-result >= 2)
if((node[i].p-node[*(top-1)].p)*(node[*(top-1)].p-node[*(top-2)].p) >= 0)
-- top; // 维护下凸包
else break;
*(top ++) = i;
}
return top;
}
int temp[MaxN];
pair<long long,int> choice[MaxN];
Node temp2[MaxN]; // to sort it
void dealWith(int L,int R,int l,int r){
// [L,R] for color. [l,r) for index.
if(L == R) return ;
int MID = (L+R)>>1, mid;
for(mid=l; mid<r; ++mid)
if(ys[node[mid].color] > MID)
break;
dealWith(L,MID,l,mid); // left part
dealWith(MID+1,R,mid,r); // right part
Point xl; // K
for(int i=mid, *now=temp, *tail=convexHull(l,mid,temp); i<r; ++i){
xl = Point(1,node[i].p.x);
while(tail-now >= 2)
if((node[*(now+1)].p-node[*now].p)*xl >= 0)
++ now; else break;
// choice: first是边权, second是与谁相连
if(choice[node[i].color].second == -1 or edgeValue(i,*now) < choice[node[i].color].first)
choice[node[i].color] = make_pair(edgeValue(i,*now),node[*now].color);
}
for(int i=l, *now=temp, *tail=convexHull(mid,r,temp); i<mid; ++i){
xl = Point(1,node[i].p.x);
while(tail-now >= 2)
if((node[*(now+1)].p-node[*now].p)*xl >= 0)
++ now;
else break;
if(choice[node[i].color].second == -1 or edgeValue(i,*now)<choice[node[i].color].first)
choice[node[i].color] = make_pair(edgeValue(i,*now),node[*now].color);
}
for(int i=l, j=mid, k=l; i<mid or j<r; )
if(j == r or (i < mid and node[i].lessThan(node[j])))
temp2[k ++] = node[i ++];
else // 将其排序
temp2[k ++] = node[j ++];
for(int i=l; i<r; ++i)
node[i] = temp2[i];
}
long long ans = 0;
bool setMST(){
for(int i=0; i<n; ++i){
fa[i] = i; ys[i] = 0;
choice[i].second = -1;
}
sort(node,node+n); allColor = 0;
for(int i=0; i<n; ){
ys[node[i].color] = ++allColor;
for(; i<n and ys[node[i].color] == allColor; ++i);
}
if(allColor == 1)
return true; // finished!
dealWith(1,allColor,0,n);
for(int i=0; i<n; ++i){
if(not ys[node[i].color])
continue;
ys[node[i].color] = 0;
if(choice[choice[node[i].color].second].second == node[i].color and node[i].color > choice[node[i].color].second)
continue; // they two choose the same edge
fa[node[i].color] = choice[node[i].color].second;
ans += choice[node[i].color].first;
}
for(int i=0; i<n; ++i)
node[i].color = findColor(node[i].color);
return false;
}
int main(){
scanf("%d",&n);
for(int i=0; i<n; ++i){
node[i].color = i;
scanf("%d",&node[i].p.x);
}
for(int i=0; i<n; ++i)
scanf("%d",&node[i].p.y);
while(not setMST());
cout << ans;
return 0;
}