倍增
https://www.acwing.com/problem/content/111/
题意:给定(m),对于一个集合(S),取出其中的(m)对元素(如果少于(2m)个元素就取到不能取为止),每一对元素做差平方再求和,对应最大可能的结果为集合的“校验值”,现在给你一个长度为(n)的数列,希望分割成若干段使得每一段的校验值不超过(T),问至少分几段,(n,mleq 5 imes 10^5)。
题解:容易分析出这个校验值的计算方法:将集合的元素按大小排序,每次取出最大的和最小的配对。接着为了让划分段数尽量小,那从左往右考虑,我们就希望每一段尽可能长,于是就变成考虑给定左端点(l),右端点最多能扩展到哪里。
嗯…?等一等,为什么这么“贪心”地从左到右让每段尽可能长是对的?想一想确实是对的:假设现在的(l)最多能够扩展到(r)的位置,(f(l,r)leq TRightarrow f(l,r-1)leq T),那我们猜想会不会出现把(a_r)丢到下一个段里面,答案会更优,如果不会的话我们的结论就是正确的。事实上确实不会,假设原本后一段最多能扩展成((r+1,q)),现在加了一个元素(a_r),如果(a_r)也参与到了“校验值”的运算,那么校验值一定不可能被缩小,甚至很可能被扩大(即往一个集合里丢东西,校验值一定是不会变小的,因为计算方式是取最大)。
嗯于是这个问题就解决了,现在就只要考虑如何扩展区间…嗯,话说上面的分析似乎有很明显的单调性的味道:大区间可以小区间一定可以。二分位置似乎是一个可行的做法,每次扩展一个长度为(L)的区间的复杂度是(O(Llog ^2 L))。
不过这里还有另一种做法,那就是倍增啦~这里用倍增的好处在于可以一边维护一个当前的([l,r])和想要继续扩展的一段([r+1,r+p]),check的时候可以直接(O(p))地来合并以及检验,复杂度可以降一个log。
最终复杂度,这里就直接考虑均摊的情况:最后划分成了(K)段,每段均摊的长度是(frac{N}{K}),总的复杂度就是(O(sum frac{N}{K}log frac{N}{K})=O(frac{N}{K}log^Kfrac{N}{K})=O(Nlog frac{N}{K}))。
平面最近点对
问题描述:平面内(n)个点,求出距离最近的点对。(nleq 10^6)。
(对应的模板题:https://www.luogu.com.cn/problem/P1429)
我会暴力!:我会(O(n^2))地枚举!没准就n方过百万了呢!
我会分治!
这里考虑分治的依据同样在于,如果我们把点集划分成两个集合(S_1,S_2),答案要么来自(S_1,S_2)二者之一,要么就是一个点在(S_1)内,另一个点在(S_2)内(是不是有点像最大子段和)。和其他分治一样,难点还是在于如何快速合并。
暴力地枚举点对,合并的渐进复杂度还是(O(n^2))的,最后总复杂度还会多一个log,这样显然不行,于是我们就得考虑可能成为答案的点还需要满足哪些约束啦。
一维情况的分治:
一维情况很显然可以(O(nlog n))排序再(O(n))地扫一遍。不过我们稍微绕一下,考虑一维情况如何分治地处理。
首先同样是一遍按坐标排序,分成左右两段区间([l,mid],[mid+1,r]),递归地处理(d_1=f(l,mid),d_2=f(mid+1,r)),记(D=min(d_1,d_2)),现在考虑出现第三种情况(最近点对的一个点在左,一个点在右边)会有哪些约束(一个取(mid),另一个取(mid+1),好了每次合并(O(1))地解决啦x肯定不能这么说,这样子就没法拓展了):以(mid)位置上的点(p_m)为分界,假设可能成为答案的点分别为(p_l,p_r),那一定有(dis(p_l,p_m)<D,dis(p_r,p_m)<D).
好了可以拓展了
二维情况也类似地,我们处理出左右区间的(d_1=f(l,mid),d_2=f(mid+1,r)),取(mid)位置的点(p_m),以它为分界,做一条垂直(x)轴的直线,那样满足条件的点对一定在([x_m-D,x_m+D])这个范围内,嗯…不过仔细想想这样子的点数还是可能达到(O(n))级别的,但是还有别的约束吧,比如对于左边的一个点(p_l),右边可能和它配对成为答案的点(p_r)一定满足(y_rin[p_l-D,p_l+D])这个约束,结合前一个约束,对左边每个点,右边可能成为答案的点就落在一个水平长度为(D),竖直长度为(2D)的正方形内了,而这里面的点一定不超过6个:首先对于左边一个点,右边可能成为答案的点集组成的应该是一个弓形,数学直觉告诉我们这里面应该不可能同时存在太多的点,事实也确实如此,基于这个想法,我们把弓形补成一个矩形,矩形的长边至多(2D),短边至多(D),再分成大小相同的6个(frac{1}{2}D imes frac{2}{3}D)的小矩形,每个矩形内点的最远距离是(frac{5}{6}D<D),于是由鸽巢原理我们就知道右边最多有6个可能和组成(p_l)答案的点。
于是这样就可以(O(n))地解决了!
嗯…等一下,怎么快速找到这6个点,每次合并的时候再排序嘛…这样似乎就变成(T(n)=2T(n/2)+O(nlog n))了,考虑对应的递归树,第(k)层(t=2^{k-1})个点的话,第(k)层对应的代价就是
(egin{aligned}O(sum_{i=1}^t frac{n}{t}logfrac{n}{t})=O(frac{n}{t}log^tfrac{n}{t})=O(nlogfrac{n}{t})=O(nlog n-nk)end{aligned})
最后的答案就是(egin{aligned}O(sum_{i=0}^{log n}(nlog n-ni))=O(nlog^2 n)end{aligned}),嗯似乎不是那么优美…(毕竟我可是听说这个问题有(O(nlog n))的做法),嗯看来每次都要排序这个东西得解决一下…
在解决之前插一嘴(毕竟这篇blog就当做复习啦,各种扯远也很正常x),发现算法导论关于主定理的证明一节,一道课后习题能对应直接给出这种递归式的解…对于(T(n)=aT(n/b)+f(n)),如果(f(n)=Theta(n^{log _b a}log ^k n)),则(T(n)=Theta(n^{log _b a}log^{k+1} n)),证明就类似上面递归树的分析(x)
好了现在言归正传,我们要把每次合并优化到线性,似乎也好做:类似归并排序,每次递归顺带维护一个按照(y)值从小到大排序的数列,这个东西可以做到(O(n))的合并。于是就…等一下,按(y)排好序之后(x)怎么办,如果不在(xin[x_m-D,x_m+D])的前提下找,那样就没法保证6个点这个性质了,这样就可能出现对一个(p_l),右边的点会有很多个(x)就不满足条件的点要跳过,跳完之后对下一个(p_{l+1})又要跳回去再重新跳,复杂度看起来有点危…还要再稍微处理一下。
那就先把(x)满足条件的点再另外挑出来,再去用类似双指针的东西来扫描,问题解决√
获得了『(O(nlog n))的算法』(突然中二)
算导上的做法
实现的细节和算导上的稍微有点不同,算导上是先处理一个按(y)排序的数组,每次分治的时候把这个数组(O(n))地拆开,同时书上是直接把左右两边的点合起来处理了(对每个点枚举周围的7个点),原理还是一样的。
另外书上还讨论了怎么处理重复点以及一些拓展问题。
最后丢个代码吧
double solve(int l,int r){
if(l==r)return (1e12);
int mid=(l+r)>>1;
double d=min(solve(l,mid),solve(mid+1,r));
if(ps[mid+1].x-ps[mid].x>d)return d;
vl.clear();vr.clear();
rep(i,l,mid)if(ps[mid].x-qs[i].x<=d)vl.pb(qs[i]);
rep(i,mid+1,r)if(qs[i].x-ps[mid].x<=d)vr.pb(qs[i]);
int p=0,q=0,sl=vl.size(),sr=vr.size();
while(p<=sl-1){
while(q<sr-1&&vr[q].y<vl[p].y-d)q++;
rep(j,q,min(q+5,sr-1))d=min(d,dis(vl[p],vr[j]));
p++;
}
inplace_merge(qs+l,qs+mid+1,qs+r+1,cmp2);
return d;
}
//ps存的是按x排好序的点,qs是在ps的基础上一边递归一边合并的按y排序的点
那就继续扯远-HDU4312
想着找点类似的东西来做,飘着飘着就飘到切比雪夫距离去了…然后就看到了一道题:http://acm.hdu.edu.cn/showproblem.php?pid=4312
想了一会就做掉了,连着也塞进来吧(x)
题意:给平面内(n(nleq 10^5))个点,找一个点使得其他点到它的距离的切比雪夫距离((max(Delta x,Delta y)))之和最小。
题解:一开始有点被吓到,其实冷静一想可以把切比雪夫距离转化成曼哈顿距离(旋转45°+缩小(sqrt 2)倍),接着为了方便处理再把数据都乘上2(这样就都是整数了,最后再除掉就好),于是变成找一个点使得其他点到它的曼哈顿距离之和最小,也就是(sum |x_i-x_p|+|y_i-y_p|)最小,(x,y)可以分开统计,接着对(x)排序,就可以(O(n))地求出每个点作为(p)点的答案,对(y)做同样的处理,这题就愉快滴做完啦~