数据结构与算法综合训练 / 相关资料 / 第一次
从旧 .com 全量搬运的历史内容,来源路径:/math/课程/数据结构与算法综合训练/resources/第一次/
迁移来源
- 旧站标题:第一次
- 新站标题:数据结构与算法综合训练 / 相关资料 / 第一次
- 旧站路径:/math/课程/数据结构与算法综合训练/resources/第一次/
- 旧页面 ID:
498
实验 1: 渐进分析和排序算法
Task 1 #
- (1) 使用 和 的定义, 证明下面每一个等式:
- a) {{< admonition note “证明” false >}} 取 则, 故
- b) {{< admonition note “证明” false >}} 取 则, 故 .
- c) {{< admonition note “证明” false >}} 取 则, 故 .
- d) {{< admonition note “证明” false >}} 故
- (2) 使用数学归纳法证明 . 其中
note
时, 取 则, .
设 时成立, 即
则存在 使得
当 时.
当 时.
综上 .
Task 2 #
算法设计 #
先将所有点按照 为第一关键字, 为第二关键字排序, 并以点 为分界点, 拆分点集 :
并递归下去, 求出两点集各自内部的最近点对, 设距离为 , 取较小值设为 .
我们将所有横坐标与 的差小于 的点放入集合 而只有这些点内部的点距离可能更优.
同样的, 如果想比 更小, 中点的纵坐标之差也得小于 .
我们设 .
接下来只需对每对 求距离并取最小值即可.
至此我们已经完成了这个问题, 但还有一些实现的细节需要完善, 比如如何快速计算 数组. 这将在复杂度分析中给出具体方式及其正确性证明.
复杂度分析 #
我们先来考虑 的计算方式, 最朴素的想法就是将 中的点按 排序, 就可以得到.
但是如果每次直接排序, 这部分的复杂度就变为 . 超出了预期.
但仔细思考后, 在求 的时候我们已经不需要 的顺序了, 所以在递归过程中我们可以利用归并排序对 进行排序, 由此我们就可以在 的时间复杂度内求出 .
除此之外, 在每次求完 并更新答案时我们的复杂度为 , 而粗略看去 应该是 级别, 那么总复杂度将退化为 . 但经过仔细分析后实则不然, 的大小并没有那么大, 最大大小实际上只有 . 我们进一步考虑 的定义, 发现 内部的点应该落在 这个大小为 的矩形内, 而当我们以中心线 将其分成两个部分.
下面只考虑左半部分, 由于左半边被之前分治的左半边包含, 所以该区域内任意两点的距离应该不小于 , 而该部分的面积为 . 更进一步的, 我们将这个 的矩形分成四个 的小矩形, 则每个小矩形内部最多只会存在一个点, 因为小矩形的对角线长度也仅有 所以在整个 中最多只会存在 个点, 再除去 本身, 就只会剩下 个点.
通过上述分析, 我们分治之后合并的复杂度是 .
那么总复杂度就是 .
代码实现 #
代码如下, 已通过洛谷 P7883 平面最近点对(加强加强版)
const int N=4e5+10;
const ll INF=9e18;
int n;
struct Point{
int x,y;
}a[N],b[N];
bool cmp(Point a,Point b)
{
return a.x<b.x;
}
bool cmp2(Point a,Point b)
{
return a.y<b.y;
}
ll dist(Point a,Point b)
{
return 1ll*(a.x-b.x)*(a.x-b.x)+1ll*(a.y-b.y)*(a.y-b.y);
}
ll solve(int l,int r)
{
if(r-l<=10)
{
ll ans=INF;
sort(a+l,a+r+1,cmp2);
for(int i=l;i<=r;i++)
for(int j=i+1;j<=r;j++)
ans=min(ans,dist(a[i],a[j]));
return ans;
}
int mid=(l+r)>>1;
int px=a[mid].x;
ll ans=INF;
ans=min(solve(l,mid),solve(mid+1,r));
int p1=l,p2=mid+1,p3=l;
while(p1<=mid&&p2<=r)
{
if(a[p1].y<a[p2].y)b[p3++]=a[p1++];
else b[p3++]=a[p2++];
}
while(p1<=mid)b[p3++]=a[p1++];
while(p2<=r)b[p3++]=a[p2++];
for(int i=l;i<=r;i++)a[i]=b[i];
int tot=0;
for(int i=l;i<=r;i++)
if(1ll*abs(a[i].x-px)*abs(a[i].x-px)<ans)
{
for(int j=tot;j&&1ll*(a[i].y-b[j].y)*(a[i].y-b[j].y)<ans;--j)
ans=min(ans,dist(a[i],b[j]));
b[++tot]=a[i];
}
return ans;
}
int main()
{
read(n);
for(int i=1;i<=n;i++)
read(a[i].x),read(a[i].y);
sort(a+1,a+n+1,cmp);
cout<<solve(1,n);
return 0;
}
Task 3 #
以下为各排序算法的主干代码. 均用 组不同规模随机数据, 正序数据和逆序数据进行测试.
插入排序 #
void Insertion(vector<int>&num)
{
int n=num.size();
for(int i=1;i<n;i++)
{
int pos=i;
for(int j=i-1;j>=0;j--)
if(num[i]<num[j])
pos=j;
int val=num[i];
for(int j=i;j>pos;j--)num[j]=num[j-1];
num[pos]=val;
}
return;
}
选择排序 #
void Selection(vector<int>&num)
{
int n=num.size();
for(int i=0;i<n-1;i++)
{
int pos=i;
for(int j=i;j<n;j++)
if(num[j]<num[pos])pos=j;
swap(num[i],num[pos]);
}
return;
}
希尔排序 #
void Shell_1(vector<int>&num)
{
static int H[30];
int n=num.size(),p=0;
H[p]=1;
for(;H[p]<n;p++)
H[p+1]=H[p]<<1;
while (p>=0)
{
int h=H[p];
for (int i = h; i < n; i++)
for (int j = i; j >= h && num[j] < num[j - h]; j -= h)
swap(num[j], num[j - h]);
p--;
}
return;
}
其余两个希尔排序只有 数组发生变化, 变化如下:
for(;H[p]<n;p++)
H[p+1]=((H[p]+1)<<1)-1;
for(;H[p]<n;p++)
H[p+1]=((H[p]<<1|1)*3-1)/2;
快速排序 #
int depth=0;
void Quicksort(vector<int>&num,int l,int r,int dep)
{
depth=max(depth,dep);
if(l==r)return;
int pos=random(l,r); //在 [l,r] 中随机选取点, 利用 c++ rand() 函数实现
int p=num[pos];
swap(num[pos],num[r]);
int i=l,j=r-1;
while(i<j)
{
while(i<j&&num[i]<p)i++;
while(i<j&&num[j]>=p)j--;
swap(num[i],num[j]);
}
if(num[i]>=num[r])swap(num[i],num[r]);
else i++;
if(l<i)Quicksort(num,l,i-1,dep+1);
if(i+1<=r)Quicksort(num,i+1,r,dep+1);
return;
}
归并排序 #
void Mergesort(vector<int>&num,int l,int r)
{
static vector<int>tmp;
if(l==r)return;
int mid=(l+r)>>1;
Mergesort(num,l,mid);
Mergesort(num,mid+1,r);
if(tmp.size()<r-l+1)tmp.resize(r-l+1);
int p1=l,p2=mid+1,p=0;
while(p1<=mid&&p2<=r)
{
if(num[p1]<num[p2])tmp[p++]=num[p1++];
else tmp[p++]=num[p2++];
}
while(p1<=mid)tmp[p++]=num[p1++];
while(p2<=r)tmp[p++]=num[p2++];
for(int i=l;i<=r;i++)num[i]=tmp[i-l];
}
Task 4 #
以下为各排序算法实际运行时间测试, 同一组数据采取运行 次所需时间的平均值.
考虑到选择和插入排序的效率过慢, 故对其余算法进行了更大数据集的测试和单独的图表绘制. 以此更清晰的表示其余算法的效率差异.
随机数据 #

正序数据 #

逆序数据 #

总结 #
从理论分析上看, 插入排序和选择排序的时间复杂度均为 级别, 而快排和归并排序均为 , 而实际测试中, 这两种算法的实际用时也确实远超过其余算法.
而对于希尔, 快排, 归并和 C++ stl 中的 sort 函数. Shell_1 在小数据与其他算法时间差距不大, 但当数据规模进一步扩大后, 用时增长速度明显大于其余算法. 而 Shell_2 在大数据规模下也有较差的表现.
而快排和归并, 则一直用时接近, 只有细微差别. C++ stl 中的 sort 函数表现出了较优的性能.
对于不同的数据性质选择, 快排, 归并和希尔用时变化不大.
而插入排序, 如果是从后往前寻找插入位置时, 当数据为正序理论复杂度为 , 实际运行效率也很高.
Task 5 #
快排层数 #

在 这 12 组数据测试结果中, 从图中可以看出递归深度大概是一个 级别.
而从理论分析上看, 快排轴值的选取期望就是中间值, 故理论上递归层数就应该是 级别.
快排优化 #

通过设定阈值分别为 和 , 在 这些数据中, 可以看出, 优化过的两份算法均略优于没优化过的快排. 但优化幅度并不大, 只是常数上的优化, 并不影响算法复杂度.
而对于不同的阈值, 由此实验看出 比 快了几毫秒, 基本区别不大.
\subsection{螺丝螺母匹配问题}:
由于我们无法直接比较螺丝或螺母的大小, 所以考虑先找到一个轴值, 并利用轴值与相对的类别进行比较.
具体的, 类似快排, 我们先选取一个轴值螺母, 将这个螺母与区间内每个螺丝比较, 找到与该轴值螺母匹配的螺丝, 记为轴值螺丝.
接下来利用轴值螺母对所有螺丝做快排的操作, 利用轴值螺丝对所有螺母做快排的操作.
之后只需类似快排的递归调用即可.
程序实现:
int match(int nut,int bolt)
{
return nut==bolt?0:nut>bolt?1:-1;
}
void Quicksort(vector<int>&nuts,vector<int>&bolts,int l,int r)
{
if(l==r)return;
int pos_nut=random(l,r);
int p_nut=nuts[pos];
int pos_bolt=l,p_bolt;
for(int i=l;i<=r;i++)
if(match(p_nut,bolts[i])==0)pos_bolt=i;
p_bolt=bolts[pos_bolt];
swap(nut[pos_nut],nut[r]);
swap(bolt[pos_bolt],bolt[r]);
int i=l,j=r-1;
while(i<j)
{
while(i<j&&match(nut[i],p_bolt)==-1)i++;
while(i<j&&match(nut[j],p_blot)>=0)j--;
swap(num[i],num[j]);
}
if(match(nut[i],p_bolt)==1)swap(nut[i],nut[r]);
else i++;
i=l,j=r-1;
while(i<j)
{
while(i<j&&match(p_nut,bolt[i])==1)i++;
while(i<j&&match(p_nut,bolt[i])<=0)j--;
swap(num[i],num[j]);
}
if(match(p_nut,bolts[i])==-1)swap(bolts[i],bolts[r]);
else i++;
if(l<i)Quicksort(nuts,bolts,l,i-1);
if(i+1<=r)Quicksort(nuts,bolts,i+1,r);
return;
}
讨论
评论
正在加载评论...