CSP-J 2021 插入排序(详细思路)

简介: CSP-J 2021 插入排序(详细思路)

题目描述:


插入排序是一种非常常见且简单的排序算法。小 Z 是一名大一的新生,今天 H 老师刚刚在上课的时候讲了插入排序算法。

假设比较两个元素的时间为 O(1),则插入排序可以以 O(n*n) 的时间复杂度完成长度为 n 的数组的排序。不妨假设这 n 个数字分别存储在 a1,a2,··· ,an 之中,则如下伪代码给出了插入排序算法的一种最简单的实现方式:这下面是 C/C++ 的示范代码。


for(int i=1;i<=n;i++)
{
  for(int j=i;j>=2;j--){
  if(a[j]<a[j-1]){
  int t=a[j-1];
    a[j-1]=a[j];
    a[j]=t;
  }
  }
}


为了帮助小 Z 更好的理解插入排序,小 Z 的老师 H 老师留下了这么一道家庭作业:

H 老师给了一个长度为 n 的数组 a,数组下标从 1 开始,并且数组中的所有元素均为非负整数。小 Z 需要支持在数组 a 上的 Q 次操作,操作共两种,参数分别如下:

1 x v : 这是第一种操作,会将 a 的第 x 个元素,也就是 ax 的值,修改为 v。保证1 ≤ x ≤ n,1 ≤ v ≤ 10^9。注意这种操作会改变数组的元素,修改得到的数组会被保留,也会影响后续的操作。

2 x : 这是第二种操作,假设 H 老师按照上面的伪代码对 a 数组进行排序,你需要告诉 H 老师原来 a 的第 x 个元素,也就是 ax,在排序后的新数组所处的位置。保证 1 ≤ x ≤ n。注. 意. 这种操作不会改变数组的元素,排序后的数组不会被保留,也不会影后续的操作。

H 老师不喜欢过多的修改,所以他保证类型 1 的操作次数不超过 5000。

小 Z 没有学过计算机竞赛,因此小 Z 并不会做这道题。他找到了你来帮助他解决这个问题。


**输入:**输入的第一行包含两个正整数 n,Q,表示数组长度和操作次数。保证 1 ≤ n ≤8,000,1 ≤ Q ≤ 2 × 10^5。

输入的第二行包含 n 个空格分隔的非负整数,其中第 i 个非负整数表示 ai。保证1 ≤ ai ≤ 10^9。

接下来 Q 行,每行 2 ∼ 3 个正整数,表示一次操作,操作格式见题目描述。


**输出:**对于每一次类型为2的询问,输出一行一个正整数表示答案。


样例输入:


3 4
3 2 1
2 3
1 3 2
2 2
2 3


样例输出:


1
1
2


提示:


【样例1解释】

在修改操作之前,假设 H 老师进行了一次插入排序,则原序列的三个元素在排序结束后所处的位置分别是 3,2,1。

在修改操作之前,假设 H 老师进行了一次插入排序,则原序列的三个元素在排序结束后所处的位置分别是 3,1,2。

注意虽然此时 a2 = a3,但是我们不能将其视为相同的元素。

【数据范围】

对于所有测试数据,满足1 ≤ n ≤ 8,000,1 ≤ Q ≤ 2×10^5,

1 ≤ x ≤ n, 1 ≤ v,ai ≤ 10^9。

对于所有测试数据,保证在所有 Q 次操作中,至多有 5000 次操作属于类型一。

各测试点的附加限制及分值如下表所示。


4d10fb61757943173fad5cd42f2172e2_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBALkFzaHku,size_20,color_FFFFFF,t_70,g_se,x_16.png


思路:


两种操作:

1.修改序列中的值

2.查找排序后的位置


修改序列中的值可以通过O(1)的时间复杂度解决,而求排序后的这个数的位置,我们可以通过一个小公式求出:


这个数排序后的位置=所有的数中比其小的数+他前面和他相等的的数+1;


因为在样例一解释中相同的数视为不同的元素所以要加上所求数前面和他相等的数;


例如:

3 2 2

求 第三个元素 排序后的位置

根据公式就是:0+1+1=2

排序后的位置就是2,这个例子就是样例的最后一组查询;


知道了如何查找位置后我们简单的看一下查找位置的复杂度为O(n);


根据之前的推导,我们先写出一种解法


#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const ll maxx = 1e18;
const int N = 1e5+10;
const int p = 1e4;
const double eps = 1e-8;
ll n,q,cnt1,cnt2;//cnt1 与其相等,cnt2比其小
ll a[8001];
ll k,k1,k2;
int main()
{
    cin>>n>>q;
    for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
    for(int i=1;i<=q;i++)
    {
        cnt1=0;cnt2=0;
        scanf("%lld",&k);
        if(k==1)
        {
            scanf("%lld %lld",&k1,&k2);
            a[k1]=k2;//修改
        }
        else if(k==2)
        {
            scanf("%lld",&k1);
            for(int i=1;i<k1;i++)
            {
                if(a[i]==a[k1]) cnt1++;
                if(a[i]<a[k1]) cnt2++;
            }
            for(int i=k1+1;i<=n;i++)
            {
                if(a[i]<a[k1]) cnt2++;
            }
            printf("%lld\n",cnt1+cnt2+1);
        }
    }    
}



我第一次做的很急,没来得及算复杂度,然后TLE了,我们算一下这个程序的最坏复杂度:


最大查询次数为2×10^5次,每次复杂度为O(n),最大为8000,最坏复杂度就是

1.6×10^9 果然爆了;


优化:


H 老师不喜欢过多的修改,所以他保证类型 1 的操作次数不超过 5000。


这句话很重要,优化思路就是以这句话想出来的;


优化思路就是修改次数这么少,我们不如做一个预处理,然后每次查询直接输出,每次修改的时候再处理O(n)的遍历;


每次查找位置的复杂度为O(1)

而每次修改的复杂度为O(n)(这里的复杂度一会给出解释)

最坏复杂度大概是8000×8000(预处理)+5000×8000(修改)+(200000-5000)*1 大约是 1e8的复杂度,优化了一个数量级;下面给出代码


#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const ll maxx = 1e18;
const int N = 1e5+10;
const int p = 1e4;
const double eps = 1e-8;
int n,q,cnt1,cnt2;//cnt1 等于的 //cnt2小于的 
int a[8001];
int left1[8001];//左边等于的 
int all1[8001];//全部小于的
int k,k1,k2;
void cc(int k1,int k2)//k1 位置   k2大小   原来值a[k1] 
{
  int cntt=0,cntts=0;//cntt修改点等于的    cntts修改点小于的 
  for(int i=1;i<k1;i++)
  {
  if(a[i]==k2) cntt++; 
  if(a[i]<k2) cntts++;//本点属性
  if(a[i]>a[k1]) all1[i]--;
  if(a[i]>k2) all1[i]++;//左边(比其小修改)
  }
  left1[k1]=cntt;
  for(int i=k1+1;i<=n;i++)
  {
  if(a[i]==k2) left1[i]++;
  if(a[i]==a[k1]) left1[i]--;//右边点(和其相等属性)修改
  if(a[i]>a[k1]) all1[i]--;
  if(a[i]>k2) all1[i]++;//右边(比其小)修改
  if(a[i]<k2) cntts++;//本点属性
  }
  a[k1]=k2;
  all1[k1]=cntts;
}
int main()
{
  scanf("%d %d",&n,&q); 
  for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
  for(int i=1;i<=n;i++)
  {
  cnt1=0;cnt2=0;
  for(int j=1;j<=n;j++)
  {
    if(a[j]==a[i]) cnt1++;
    if(i-j==1) left1[i]=cnt1; //当处理到这个数左边的时候记录
    if(a[j]<a[i]) cnt2++;
  }
  all1[i]=cnt2; 
  }//预处理 
//  for(int i=1;i<=n;i++)
//  cout<<all1[i]<<" "<<left1[i]<<endl;//debug
  for(int i=1;i<=q;i++)
  {
  cnt1=0;cnt2=0;
  scanf("%d",&k);
  if(k==1)
  {
    scanf("%d %d",&k1,&k2);
    cc(k1,k2);//修改
//    for(int i=1;i<=n;i++)
//    cout<<all1[i]<<" "<<left1[i]<<endl;//debug
  }
  else if(k==2)
  {
    scanf("%d",&k1);
    printf("%d\n",all1[k1]+left1[k1]+1);//查询
  }
  }
}


在修改的时候,程序会变得难写不少,因为当一个点发生变化,所有预处理的值都会受到影响,但是我们也要尝试来模拟修改,当一个点发生变化的时候

1.这个点的两个属性都会发生改变

2.这个点左右所有的点(比其小)这个属性会发生改变

3.这个点右边的点(左边和其相等这个属性)会发生改变


而这些属性的改变只要一个循环就能全部求出


只要每个点都遍历一遍

减去修改前点的影响再加上新点的影响即可


因此复杂度也是O(n);


这题写出来的时候开心到不行,第一次做出我觉得有难度的一个题,也许这就是解题的快了吧;


加油!


目录
相关文章
|
搜索推荐 算法 索引
冒泡排序算法的实现和优化~
冒泡排序算法的实现和优化~
|
13天前
|
搜索推荐
插入排序算法的讲解和代码
【10月更文挑战第12天】插入排序是一种基础的排序算法,理解和掌握它对于学习其他排序算法以及数据结构都具有重要意义。你可以通过实际操作和分析,进一步深入了解插入排序的特点和应用场景,以便在实际编程中更好地运用它。
|
27天前
|
搜索推荐 算法 数据可视化
深入解析冒泡排序算法
深入解析冒泡排序算法
20 4
|
搜索推荐 算法
|
12月前
|
搜索推荐 Java
冒泡排序算法的Java实现及优化
冒泡排序算法的Java实现及优化
|
机器学习/深度学习 搜索推荐 算法
【排序算法】冒泡排序(改进版)的思想分析与代码实现详解
【排序算法】冒泡排序(改进版)的思想分析与代码实现详解
282 0
【排序算法】冒泡排序(改进版)的思想分析与代码实现详解
|
编解码 算法 网络协议
【算法基础】快速排序解析
快速排序是一种分治排序方法,通过多次比较和交换来实现排序,其基本操作是将无序表不断拆分和交换,直到拆分到最小时,整个表就成为了一个有序表,从而得到一个新的、记录数量增1的有序表。
119 0
【算法基础】快速排序解析
|
编解码 算法 网络协议
【算法基础】冒泡排序解析
在我们数组排序中,每一个数组元素根据大小比对,小的元素不断向前移动,如同气泡在冒出一样,我们称这种排序方法为冒泡排序。
163 0
|
存储 编解码 移动开发
【算法】直接选择排序解析
直接选择排序是指每次都从剩余数据中选出最大或者最小的,将其排在已经排好的有序表后面。
216 0
【算法】直接选择排序解析
|
存储 编解码 算法
【算法基础】希尔排序解析
希尔排序的基本思想是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
112 0