[usOJ5677]御神渡

题目

传送门:有账号才行。

题目描述
幻想乡有 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 N105,Ai2×106,Ci109

思路

很明显,将 ( u , v ) (u,v) (u,v) 的边权视作 C u + C v − A u × A v C_u+C_v-A_u\times A_v Cu+CvAu×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) CuCv<Ax(AuAv) 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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值