【全程NOIP计划】分治与倍增
【全程NOIP计划】分治与倍增
分治
分治就是分而治之,化整为零的思想
分治的原理是把困难的大问题化解为简单的小问题
然后先解决小问题,然后根据小问题的答案解决大问题
它本质上不是一个算法,而是一个思想
比如快速幂,倍增求lca,归并排序,线段是cdq分治,甚至后缀数组FFT都利用了分治思想
分治虽然不经常出题,但是也非常有用
快速幂
快速幂可以在\(O(logb)\)的时间复杂度内计算\(a^b\)
方法很简单
注意到如果b是偶数,那么就可以先计算\(a^{\frac{b}{2}}\),这样问题的规模就减少了一半
如果b是奇数,那么我们就可以计算\(a^{\frac{b}{2}}\)平方之后再乘a,问题规模同样以不大的代价减少了一般
这里的\(a^b\)就是大问题,而\(a^{\frac b 2}\)就是小问题了
long long f(long long a,long long b,long long c)
{
long long temp=f(a,b/2)
if(b&1)
return temp*temp%c;
else
return temp*temp*a%c;
}
long long ksm(long long x,long long y,long long z)
{
long long res=1%p;
for( ;y;y>>=1)
{
if(y&1)
res=res*x%z;
x=x*x%z;
}
return res;
}
递归实现和迭代实现
实际上都是分治解决的问题,但是方向相反
递归往往是从大问题开始,慢慢递归到小问题,然后进行合并
迭代呢就是要先解决好小问题,从最小的解决,并不断合并得到大问题
从后面的二分和归并排序,还有cdq分治都可以看到这一点
P1045 麦森数
题目
输出\(2^{p-1}\)的后五百位数
思路
这里有一个神奇的数学技巧,但是这里用高精度加快速幂就可以了
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
using namespace std;
unsigned long long a[505]={1};
int main()
{
int p;
cin>>p;
cout<<(int)(p*log10(2))+1<<'\n';
for( ;p>0;p-=60)
{
unsigned long long f=0;
for(int i=0;i<500;i++)
{
if(p>60)
a[i]<<=60;
else a[i]<<=p;
a[i]+=f;
f=a[i]/10;
a[i]%=10;
}
}
a[0]-=1;
for(int i=499;i>=0;i--)
{
cout<<(char)(a[i]+'0');
if(i%50==0) cout<<'\n';
}
return 0;
}
快速幂的特别用途
计算模意义下的出发
根据费马小定理
如果模数p为质数,那么a模p意义下的逆元就是\(a^{p-2}\)
也就是说\(a*a^{p-2}mod\space p=1\)
如果给一个数乘以一个\(a^{p-1}\),就相当于在模p意义下给这个数除了一个a
在一些计数题目或者期望计算题目中,由于数值可能比较大或者精度比较大,出题人都会让你输出模p意义下的答案
P1850 换教室
思路
这道题目一个期望DP
期望就是数学期望
它的物理意义是,假设一个随机事件执行无穷次,这个随机事件结果的平均值就是期望
公式定义是\(\sum P(x) f(x)\),也就是说吧所有可能出现的值成圣这个值出现的概率,再加起来
那么们就会注意到,事件x发生的概率为\(P(x)\),一般来说是一个分数,可能出现的的次数除总次数
那么如果在模质数意义下输出期望,计算\(P(x)\)的时候最好使用快速木和费马小定理来做除法
P4071 排列计数
题目
求有多少种1到n的排列a,慢慢组序列恰好有m个位置i,使得\(a_i=i\)
答案对\(10^9+7\)取模
思路
这个题需要先进性一系列的数学推导
我们只需要知道最后的答案一长串
最后用的时候要用快速幂就对了
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#define int long long
using namespace std;
const long long maxn=1000005,p=1e9+7;
int T;
long long f[maxn];
long long inv[maxn],d[maxn];
inline int read()
{
int 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*10+ch-48;ch=getchar();}
return x*f;
}
long long ksm(long long a,long long b,long long p)
{
long long ans=1%p;
for( ;b;b>>=1)
{
if(b&1)
ans=(long long)ans*a%p;
a=(long long)a*a%p;
}
return ans;
}
signed main()
{
T=read();
f[0]=1;
for(int i=1;i<maxn;i++)
{
f[i]=f[i-1]*i%p;
inv[i]=ksm(f[i],p-2,p);//快速幂求逆元
}
d[1]=0,d[2]=1,d[3]=2;
for(int i=4;i<maxn;i++)
d[i]=(i-1)*(d[i-1]+d[i-2])%p;
while(T--)
{
int n=read(),m=read();
if(n==m+1)
cout<<0<<'\n';
else if(m==n)
cout<<1<<'\n';
else if(m==0)
cout<<d[n]<<'\n';
else
cout<<(long long)(f[n]*inv[m]%p*inv[n-m]%p*d[n-m]%p)<<'\n';
}
return 0;
}
归并排序
三大基本排序:选择排序,冒泡排序和归并排序
其中的归并排序不仅仅是一个排序的方式,它的分治思想要在其他地方思想
分治排序的方法
首先调用\(f(l,r)\),然后将l,r划分为两个子问题\([l,mid]和[mid+1,r]\)
对于左右两半区间分别排序排好之后,再把两个排好序的区间合并为一个大的有序区间,这个区间合并是比较好做的,因为如果从小向大向大的区间里面填数,那么每次一定从两个小区间的最小端取数,只需要从两个最小值中选取高呢更小的那个,然后放进大区间里面
和快速幂类似,每次分治都会使问题的规模减少至少一半
一次你至多logn次分值之后区间长度就会减少为1
这时子问题就非常容易解决了,长度为1的区间是根本不需要排序的
整个归并排序就是不断把大问题分成小问题,直至小问题非常容易解决为止,再逐步把小问题变成大问题,合并,就非常简单了
代码(求逆序对)
int cnt=0;
int q[41000];
void f(int l,int r)
{
if(l>=r) return ;
int mid=(l+r)>>1;
f(l,mid),f(mid+1,r);
int head1=l,head2=mid+1;
for(int i=l;i<=r;i++)
{
if(head2<=r &&(head1>mid||a[head2]<a[head1]))
q[i]=a[head2++];
else
q[i]=a[head1++],cnt+=(head2-mid-1);
}
for(int i=1;i<=r;i++)
a[i]=q[i];
return ;
}
主定理与渐进符号
假设有递归式\(T(n)=aT(n/b)+f(n)\),\(f(n)\)为递归外的,其中n是问题规模,a是递推子问题数量,n/b是每个子问题的规模
有以下三条
1.若\(f(n)=O(n^{log_b^a-\epsilon}),\epsilon>0\)那么则\(T(n)=\Theta(n^{log_b^a})\)
2.若\(f(n)=\Theta(n^{log_b^a})\)那么\(T(n)=\Theta(n^{log_b^a}logn)\)
3.若\(f(n)=\Omega(n^{log_b^a+\epsilon}),\epsilon>0\),且对于某个常数,C<1和所有充分大的n有\(af(\frac n b)\leq cf(n)\),那么\(T(n)=\Theta(f(n))\).
正如上面,有一些符号并不是原来的符号,因为我打不出来,
渐进符号正如其名,随着问题规模n不断增加,计算量h(n)的增加速度不小于f(n),不大于f(n),或者几乎和f(n)一样
而并不是说计算量近似等于f(n),因为当n比较小的时候,可能会出现计算量远远超过f(n)的情况
此外,在渐进符号里面有三种意义不同的符号:\(O,\Theta ,\Omega\)
这些都是渐进符号,对于前两个符号,他们经常在复杂度分析中出现
\(O(f(n))\)的就是指问题的复杂度不超过\(f(n)\),即O规定了问题的上界
\(\Omega(f(n))\)指问题的复杂度不小于\(f(n)\),即\(\Omega\)规定了问题的下界
而当复杂度同时满足\(O(f(n))\)和\(\Omega(f(n))\)的时候,我们就说问题的复杂度满足\(\Theta(f(n))\)
我们再来看一下主定理
第一条和第三条的\(\epsilon\)其实是为了严谨做出出的修正,我们意会的时候可以忽略他们
这样三条定理就可以理解为
1.如果分治外的操作不比\(n^{log_b^a}\)多,那么总的复杂度久违\(\Theta (n^{log_a^b})\),更近一步地,可以理解为如果分治外的操作比分治操作增速更慢,那么分治操作占主要复杂度
2.如果分治外的操作数量级和\(n^{log_b^a}\)相当,那么总的复杂度\(\Theta(n^{log_b^a}logn)\),这是最常见的情况,如果数量级相当,那么总的复杂度还要乘上一个\(logn\)
3.如果分治外的操作数量级不比\(n^{log_b^a}\)小,那么当n很大的时候,总的复杂度会趋向于\(\Theta(f(n))\),也就是说,如果分治外的操作非常慢,那么算法就会被这些操作占主导,分治的作用减弱
归并排序的复杂度分析
回到归并排序,首先分治排序的b和a都是2,也就是说问题规模每次减少一半,问题数量每次增加一倍
那么分治外的操作就要和\(\Theta(n^{log_2^2})\)也就是\(\Theta(n)\)来相比
显然,把两半序列合并成一个序列,n个元素中的每一个都要访问到
因此,分治外操作的复杂度为\(\Theta f(n)\)
所以根据主定理,归并排序的总复杂度就是\(\Theta(nlogn)\)的
现在你知道\(nlogn\)的复杂度是怎么来的了
话说,如果是三分归并排序呢?
速度会更快,但是复杂度并没有变
更快地原因是因为递归层数会降低,但是复杂度还是由主定理算出来的
\(a=3,b=3,f(n)=n\)
那么主定理就告诉我们,\(T(n)=\Theta(nlogn)\)
但是层数不是降为\(log_3^n\)了吗?
事实上,三分之后\(\Theta(nlog_3^n)=\Theta(\frac{nlogn}{log_3^2})=\Theta(nlogn)\)
这里用了换底公式,因为\(log_3^2)\)是一个常数,在复杂度中不分析
P1908 逆序对
题目
给你一个序列a,长度为n,问你其中逆序对的长度
思路
这里使用归并排序来做
此外还可以用树状数组,线段树等来做
但他们的做法都没有分治优秀
我们还是把大问题分成小问题
\(f(l,r)\)表示求区间内\([l,r]\)内部的逆序对个数,而\(f(l,r)\)被分解为\(f(l,mid)\)和\(f(mid+1,r)\)两个小问题来解决
不如假设\(f(l,mid)\)和\(f(mid+1,r)\)都已经成功解决了
那么我们知道了左半边的逆序对个数,右半边的逆序对个数,现在需要考虑的只有一个在左边,一个在右边的逆序对个数
实际上非常好解决
归并排序的做法就是把两边中剩余中最小的数选取一个放到大区间中
当右边的一个数加入到大区间时,它对答案的贡献就是左边剩下的逆序对数
因为左边剩下的数字都比它大,并且都比它左
这样就可以统计跨左右两边的逆序对的个数,结合左边的逆序对个数和右边的逆序对个数,我们就可以得到整个区间的逆序对个数
然后继续将答案向上传递,就是这样就解决了
但是如果他们没有成功完成咋办?
可以证明它们成功完成了
数学归纳法:
1.\(f(k,k)\)显然正确
2.如果\(f(l,mid)\)和\(f(mid+1,r)\)正确,那么\(f(l,r)\)正确
3.归纳可得\(f(1,n)\)成立
代码见上归并排序
本人有一种树状数组的写法
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int maxn=500005;
int tree[maxn],rank[maxn],n;
long long ans;
struct point{
int num,val;
}a[maxn];
int lowbit(int x)
{
return x&(-x);
}
bool cmp(point q,point w)
{
if(q.val==w.val)
return q.num<w.num;
return q.val<w.val;
}
void add(int x,int y)
{
for( ;x<=n;x+=lowbit(x)) tree[x]+=y;
}
int ask(int x)
{
int ans=0;
for( ;x;x-=lowbit(x)) ans+=tree[x];
return ans;
}
int main()
{
std::ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].val;
a[i].num=i;
}
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++)
rank[a[i].num]=i;
for(int i=1;i<=n;i++)
{
add(rank[i],1);
ans+=(i-ask(rank[i]));
}
cout<<ans<<endl;
return 0;
}
P1966 火柴排队
题目
给你两盒火柴,每盒火柴有n根火柴,每盒火柴的高度不同,一开始两盒火柴是排好序的,现在每次你可以交换相邻两个火柴的顺序,问你最少的交换次数,使\(\sum(a_i-b_i)^2 \rightarrow min\)
思路
公式技巧变换
变形之后式子就变成了一个式子,其中有一个常数,另外有一个\(\sum a_ib_i\),让这个数最大就可以了
这是一个经典贪心问题,给a,b从小到大排序,然后对应的匹配就好了
因此只要动一边就好了
然后问题就转化为,给你一个原序列x,和一个目标序列t,问你最少多少次变化吧s变化成t需要注意的是,火柴的高度是各不相同的,因此s的位置和t的位置是一一对应的,不存在一个位置可以移动到多个位置的情况
因此问题就可以转化给一个排列s,问多少步可以转化为1,2,3,4……n
这里需要知道一个问题,交换相邻的两个数字,只会让序列的逆序对增加或者减去一个
因为是相邻的两个数的内部交换,因此所有其他的数和他们的顺序关系并没有改变,实际上,只有他们自己的顺序改变了
而要变化到1,2,3……n,我们是不能交换两个顺序的数字的,因为早晚也会换回来,而任何时候又不缺逆序来交换,当没有逆序交换的时候,我们就得到了目标序列
因此整个转化就是不断交换相邻的逆序,每次逆序对-1,最少需要的步数就是逆序对的个数
实际上就是求逆序对个数
我是拿树状数组过掉的
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int maxn=500005;
const int mod=99999997;
int tree[maxn],rank[maxn],n;
long long ans;
struct point{
int num,val;
}a[maxn],b[maxn];
int lowbit(int x)
{
return x&(-x);
}
bool cmp(point q,point w)
{
if(q.val==w.val)
return q.num<w.num;
return q.val<w.val;
}
void add(int x,int y)
{
for( ;x<=n;x+=lowbit(x)) tree[x]+=y,tree[x]%=mod;
}
int ask(int x)
{
int ans=0;
for( ;x;x-=lowbit(x)) ans+=tree[x],ans%=mod;
return ans;
}
int main()
{
std::ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].val;
a[i].num=i;
}
for(int i=1;i<=n;i++)
{
cin>>b[i].val;
b[i].num=i;
}
sort(a+1,a+1+n,cmp);
sort(b+1,b+1+n,cmp);
for(int i=1;i<=n;i++)
rank[a[i].num]=b[i].num;
for(int i=1;i<=n;i++)
{
add(rank[i],1);
ans+=(i-ask(rank[i]));
ans%=mod;
}
cout<<ans<<endl;
return 0;
}
CDQ分治
cdq分治一般用于解决涉及到多维偏序的问题
偏序可以简单地理解为大小关系,多维偏序就是对多个维度的大小关系有要求
cdq分治的思想主要是区间分治,然后计算左边对右边区间产生的贡献
实际上,求逆序对就是一个cdq分治
逆序对中的第一位实际上就是数组下表,因为输入是按照下标顺序输入的,所以被我们忽略了
什么叫左边区间右边区间产生贡献呢?
首先由于这是一个分治的过程,所以我们假设左右两边已经计算完了
因此只需要考虑跨区间的贡献
由于分治开始之前第一位就有序,所以第一位的左半区间一定比右半边区间要小
那么跨区间的点对就自带第一维的大小关系的性质,接下来只需要考虑第二维
如果再有第三维关系,就只能用数据结构了,用树状数组和线段树维护
cdq分治与数据结构的关系
逆序对既可以用归并排序算,也可以用树状数组算
这就是因为值域线段树或者值域树状数组维护的实际上也是一个偏序关系
数据结构相比分治,最大的优势实际上动态维护
也就是说可以随时修改,随时查询,不像离线分治,只能提前知道所有信息,然后统一求解
代价就是常数大,代码长,空间大,而且许多属性维护起来很困难,但是用分治计算就会简单很多
所以不要无脑上数据结构,不妨想想有没有分治的做法
P3810 【模板】三维偏序(陌上花开)
题目
有n个点,在三维空间中坐标为\(x_i,y_i,z_i\),表示他们的位置
现在问你对于每个点\(i\),有多少个\(j\)满足\(x_j \le x_i ,y_j \le y_i ,z_j \le z_i\)
思路
二维线段树太难了,空间复杂度太大了,动态开点很麻烦,常数非常大
所以我们还是要用cdq分治
第一维对\(x\)排序,第二维用归并排序对\(y\)排序
这样就能找出所有的\(x_j \le x_i,y_j\le y_i\)的点对,和逆序对的思路是一样的
第三维有权值树状数组维护就可以了,\(t[i]\)表示坐标\(i\)出现了多少次,然后树状数组能够查询\(j\le i\) 的\(t[j]\)的和,也就是找到了\(z_j \le z_i\)的数量
麻烦得是相等的情况比较难处理需要好好想一想
需要好好想想
这道题是省选入门题
以下来自@撤云
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int maxn=200005;
struct node{
int x,y,z,id;
}a[maxn];
int c[maxn*4],k,n,b[maxn],qyj[maxn],f[maxn];
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int y)
{
for( ;x<=k;x+=lowbit(x))
c[x]+=y;
}
int ask(int x)
{
int res=0;
for( ;x;x-=lowbit(x))
res+=c[x];
return res;
}
bool cmp1(const node &a,const node &b)
{
if(a.x!=b.x)
return a.x<b.x;
if(a.y!=b.y)
return a.y<b.y;
return a.z<b.z;
}
bool cmp2(const node &a,const node &b)
{
if(a.y!=b.y)
return a.y<b.y;
if(a.z!=b.z)
return a.z<b.z;
return a.x<b.x;
}
void cdq(int l,int r)
{
if(l==r)
return ;
int mid=(l+r)>>1;
int flag;
cdq(l,mid),cdq(mid+1,r);
sort(a+l,a+r+1,cmp2);
for(int i=l;i<=r;i++)
(a[i].x<=mid)?add(a[i].z,1),flag=i:b[a[i].id]+=ask(a[i].z);
for(int i=l;i<=r;i++)
if(a[i].x<=mid)
add(a[i].z,-1);
}
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>a[i].x>>a[i].y>>a[i].z,a[i].id=i;
sort(a+1,a+1+n,cmp1);
for(int i=1;i<=n; )
{
int j=i+1;
while(j<=n&&a[j].x==a[i].x&&a[j].y==a[i].y&&a[j].z==a[i].z)
j++;
while(i<j)
qyj[a[i].id]=a[j-1].id,i++;
}
for(int i=1;i<=n;i++)
a[i].x=i;
cdq(1,n);
for(int i=1;i<=n;i++)
f[b[qyj[a[i].id]]]++;
for(int i=0;i<n;i++)
cout<<f[i]<<'\n';
return 0;
}
P2345 MooFest G
题目
数轴上有n头牛,每头牛的坐标为\(x_i\),听力为\(y_i\),如果第i头奶牛和第j头奶牛说话,会发出\(max(v_i,v_j)*|x_i-x_j|\)的音量
假设每两头牛都在互相说话,问总的音量大小
思路
太秀了,可以同时跟一堆牛同时说话
先看看题目要求的公式
式子两部分都很麻烦,不管是max还是绝对值都很难大规模快速运算,要想办法拆掉
回想递归分治的过程中,左边区间的某一个维度一定大于右边区间
使我们这样就可以很好地解决式子中的一个
我们一开始先按\(v_i\)排序,这样左边区间的\(v_i\)一定小于右边区间的\(v_i\),那么跨区间之间的点对,一定是右边的点提供\(v_i\),这样就可以把max消掉了
绝对值怎么消掉呢?
既然已经对v排序了,那么归并排序的过程中就要对x排序
之前已经说了很多次了,每当右边的一个点被拿出,那么左边没有被拿出的,x一定大于右边的,而已经被拿出的一定小于左边的
分开计算这两种情况,就把绝对值消掉了
只需要单独统计一下被拿出和没有被拿出的个数,以及被拿出和没有被拿出的坐标和,就可以计算出答案了
void f(int l,int r)
{
if(l>=r) return ;
int mid=(l+r)>>1;
f(l,mid);
f(mid+1,r);
int head1=l,head2=mid+1;
long long lsum=0,rsum=0;
long long lcnt=0,rcnt=mid-l+1;
for(int i=1;i<=mid;i++)
rsum+=a[i].x;
for(int i=l;i<=r;i++)
{
if(head2<=r&&(head1>mid||a[head2].x<a[head1].x))
{
ans+=a[head2].y*(lcnt*a[head2].x-lsum);
ans+=a[head2].y*(rsum-rcnt*a[head2].x);
}
else
{
lsum+=a[head1].x;
rsum-=a[head1].x;
lcnt++;
rcnt--;
q[i]=a[head1++];
}
}
for(int i=l;i<=r;i++)
a[i]=q[i];
}
bool cmp(nds,x,nds,y)
{
return x.y==y.y? x.x<y.x : x.y<y.y;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&a[i].y,&a[i].x);
sort(a+1,a+n+1,cmp);
f(1,n);
printf("%lld\n",ans);
return 0;
}
实际上原题是\(10^4\)级别的数据,暴力也能过掉/kkk
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int maxn=20005;
int n;
int v[maxn],x[maxn];
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>v[i]>>x[i];
long long ans=0;
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
ans+=(max(v[i],v[j])*abs(x[i]-x[j]));
}
cout<<ans<<endl;
return 0;
}
P1228 地毯填补问题
思路
分治你一般是自顶向下,考虑如何把大问题分成小问题
但是这道题必须从小问题入手,考虑如何拓展为大问题
简单模拟k=1,2,3的啥情况
我们可以发现规律
先把整个大巨星分成四块,找到公主在哪块,然后递归地去填这一块
填完之后,把中间2*2剩下的3个块用一个图形填满,然后递归剩下的三个
由于剩下的三块每块恰好有一个,因此我们也可以完全套用之前的做法去解决
更近一步,你可以发现当k=1的时候,这个策略适用
2*2的块可以分成四个块,公主的那一块已经满了,而剩下的三块就是其他情况中的,中间2 乘 2剩下的三个块
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
int x,y;
int k;
void dfs(int x,int y,int a,int b,int l)
{
if(l==1) return;
if(x-a<=l/2-1&&y-b<=l/2-1)
{
cout<<a+l/2<<" "<<b+l/2<<" "<<1<<'\n';
dfs(x,y,a,b,l/2);
dfs(a+l/2-1,b+l/2,a,b+l/2,l/2);
dfs(a+l/2,b+l/2-1,a+l/2,b,l/2);
dfs(a+l/2,b+l/2,a+l/2,b+l/2,l/2);
}
else if(x-a<=l/2-1 && y-b>l/2-1)
{
cout<<a+l/2<<" "<<b+l/2-1<<" "<<2<<'\n';
dfs(a+l/2-1,b+l/2-1,a,b,l/2);
dfs(x,y,a,b+l/2,l/2);
dfs(a+l/2,b+l/2-1,a+l/2,b,l/2);
dfs(a+l/2,b+l/2,a+l/2,b+l/2,l/2);
}
else if(x-a>l/2-1 && y-b<=l/2-1)
{
cout<<a+l/2-1<<" "<<b+l/2<<" "<<3<<'\n';
dfs(a+l/2-1,b+l/2-1,a,b,l/2);
dfs(a+l/2-1,b+l/2,a,b+l/2,l/2);
dfs(x,y,a+l/2,b,l/2);
dfs(a+l/2,b+l/2,a+l/2,b+l/2,l/2);
}
else
{
cout<<a+l/2-1<<" "<<b+l/2-1<<" "<<4<<'\n';
dfs(a+l/2-1,b+l/2-1,a,b,l/2);
dfs(a+l/2-1,b+l/2,a,b+l/2,l/2);
dfs(a+l/2,b+l/2-1,a+l/2,b,l/2);
dfs(x,y,a+l/2,b+l/2,l/2);
}
}
int main()
{
std::ios::sync_with_stdio(false);
cin>>k>>x>>y;
int qyj=1<<k;
dfs(x,y,1,1,qyj);
return 0;
}
P1429 平面最近点对
思路
我们可以用分治来做
首先对其中一个坐标排序,然后就可以按照序列分治的方式
\(f(l,mid)\)和\(f(mid+1,r)\),负责解决两边区间内部的贡献,接下来只需要考虑跨过中线的贡献
不难想象,最近点对一定不会离这个中线很远,而且距离中线的距离一定会小于两边内部的最短距离
然后两两枚举左右两边的点对计算距离
因此这样能显著加速
但是还是无法解决一个极端情况
就是两边的点横坐标非常集中,还是容易被卡成\(n^2\)
怎么办?
首先,对于左边的一个点\((x,y)\),假设左右两边区间内部的最近点对距离为d
右边的点坐标范围必须在\((mid,y-d)\)到\((mid+d,y+d)\)这样一个d*2d的长方形上
朝珠这个范围的点一定没有比d大的,否则就没有意义了
然后很容易知道,在这样一个d*2d的长方形之内,至多有六个点
因为如果超过6个点,则无论如何分配点的位置,距离都会小于d
因此需要枚举每一个左边的点,再枚举右边对应方形内的点,就可以把分治外操作的复杂度压缩到\(f(n)=O(n)\)
这样用主定义算出来的总的复杂度就是\(O(nlogn)\)
发现直接用快排加黑科技也很牛逼
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
using namespace std;
const int maxn=200005;
int n;
double x[maxn],y[maxn];
int per[maxn];
inline int read()
{
int 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*10+ch-48;ch=getchar();}
return x*f;
}
double dist(int i,int j)
{
return (double)sqrt((double)(x[i]-x[j])*(x[i]-x[j])+(double)(y[i]-y[j])*(y[i]-y[j]));
}
bool cmp(int i,int j)
{
if(x[i]==x[j])
return y[i]<y[j];
return x[i]<x[j];
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
x[i]=read(),y[i]=read(),per[i]=i;
double ans=99999999.9;
sort(per+1,per+1+n,cmp);
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=i+6;j++)
ans=min(ans,dist(per[i],per[j]));
}
printf("%.4lf\n",ans);
return 0;
}