P7910 [CSP-J 2021] 插入排序

文章介绍了插入排序算法,以及针对给定数组的两种操作:修改元素值和查询元素在排序后的相对位置。讨论了如何优化插入操作以减少时间复杂度,并提供了C/C++和Pascal的代码示例以及解题思路。

题目描述

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

假设比较两个元素的时间为 \mathcal O(1)O(1),则插入排序可以以 \mathcal O(n^2)O(n2) 的时间复杂度完成长度为 nn 的数组的排序。不妨假设这 nn 个数字分别存储在 a_1, a_2, \ldots, a_na1​,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;
		}

这下面是 Pascal 的示范代码:

for i:=1 to n do
	for j:=i downto 2 do
		if a[j]<a[j-1] then
			begin
				t:=a[i];
				a[i]:=a[j];
				a[j]:=t;
			end;

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

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

1~x~v1 x v:这是第一种操作,会将 aa 的第 xx 个元素,也就是 a_xax​ 的值,修改为 vv。保证 1 \le x \le n1≤x≤n,1 \le v \le 10^91≤v≤109。注意这种操作会改变数组的元素,修改得到的数组会被保留,也会影响后续的操作。

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

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

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

输入格式

第一行,包含两个正整数 n, Qn,Q,表示数组长度和操作次数。

第二行,包含 nn 个空格分隔的非负整数,其中第 ii 个非负整数表示 a_iai​。

接下来 QQ 行,每行 2 \sim 32∼3 个正整数,表示一次操作,操作格式见【题目描述】。

输出格式

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

输入输出样例

输入 

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

输出

1
1
2

说明/提示

【样例解释 #1】

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

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

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

【样例 #2】

见附件中的 sort/sort2.in 与 sort/sort2.ans

该测试点数据范围同测试点 1 \sim 21∼2。

【样例 #3】

见附件中的 sort/sort3.in 与 sort/sort3.ans

该测试点数据范围同测试点 3 \sim 73∼7。

【样例 #4】

见附件中的 sort/sort4.in 与 sort/sort4.ans

该测试点数据范围同测试点 12 \sim 1412∼14。

【数据范围】

对于所有测试数据,满足 1 \le n \le 80001≤n≤8000,1 \le Q \le 2 \times {10}^51≤Q≤2×105,1 \le x \le n1≤x≤n,1 \le v,a_i \le 10^91≤v,ai​≤109。

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

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

测试点n \len≤Q \leQ≤特殊性质
1 \sim 41∼410101010
5 \sim 95∼9300300300300
10 \sim 1310∼131500150015001500
14 \sim 1614∼168000800080008000保证所有输入的 a_i,vai​,v 互不相同
17 \sim 1917∼198000800080008000
20 \sim 2220∼22800080002 \times 10^52×105保证所有输入的 a_i,vai​,v 互不相同
23 \sim 2523∼25800080002 \times 10^52×105

(个别地方字符无法识别,原题出自于洛谷P7910 [CSP-J 2021] 插入排序

思路

这道题乍一看

嘶~

在讲什么

怎么看不懂

往往这种时候

就不要慌

要仔细读题

其实内容十分简单

就是说给你几个数字

有两种操作——插入和输出数组位置

首先看样例

3 4  3个数,操作4次

3 2 1    分别输入3个数

2 3  数组[3]所处的位置

1 3 2    将数组第三个元素更改为2

2 2       数组[2]所处的位置

2 3       数组[3]所处的位置

既然要输出原始的位置,就一定有一个变量存储

这里我们使用结构体

struct node//创建结构体
{
    int num;//定义存储数字的变量
    int id;//定义存储编号的变量
}a[8000];

讲几个要点

既然有排序

又要用结构体存储

那么我们可以使用sort函数

 1 sort(a+1,a+n+1,cmp);//按升序排序 

注意因为有结构体

所以sort中的cmp需要根据实际自己设计

bool cmp(node x,node y)//cmp函数,用于sort排序和升序降序判断
{
    if(x.num!=y.num)return x.num<y.num;//如果前面的数字和后面的数字不同,按数字升序排序
    else return x.id<y.id;//否则按编号升序排序
}

再看两个操作

1.修改

也可以说是更改过去的数组元素的值

所以一定有一个遍历寻找

for(int j=1;j<=n;j++)//遍历寻找要更改的数字
{
    if(a[j].id==u)//如果找到了
    {
        a[j].num=w;//更改
        break;//跳出循环
    }
}

切记,每一次更改都要带一次排序

 1 sort(a+1,a+n+1,cmp);//按升序排序 

那么

插入就完成了

2.输出

还是遍历寻找

只要输入的编号和过去的编号一样就输出并跳出循环

for(int j=1;j<=n;j++)//遍历寻找
{
    if(a[j].id==k)//找到原始编号与k相同的
    {
        cout<<j<<endl;//输出当前编号
        break;//跳出循环
    }
}


所以代码大致框架就出来了

只需要处理一下细节

代码

#include<bits/stdc++.h>
using namespace std;
struct node//创建结构体
{
    int num;//定义存储数字的变量
    int id;//定义存储编号的变量
}a[8000];
bool cmp(node x,node y)//cmp函数,用于sort排序和升序降序判断
{
    if(x.num!=y.num)return x.num<y.num;//如果前面的数字和后面的数字不同,按数字升序排序
    else return x.id<y.id;//否则按编号升序排序
}
int main()
{
    int i,j,n,k,q,w,u;//定义变量
    cin>>n>>q;//输入n和q
    for(i=1;i<=n;i++)//输入数字
    {
        cin>>a[i].num;//保存数字
        a[i].id=i;//保存原始编号
    }
    sort(a+1,a+n+1,cmp);//按升序排序
    for(i=1;i<=q;i++)//执行q次操作
    {
        cin>>w;//输入操作方式
        if(w==1)//判断操作方式
        {
            //更改
            cin>>u>>w;//输入更改的编号和数字
            for(int j=1;j<=n;j++)//遍历寻找要更改的数字
            {
                if(a[j].id==u)//如果找到了
                {
                    a[j].num=w;//更改
                    break;//跳出循环
                }
            }
        sort(a+1,a+n+1,cmp);//按升序排序
        }
        else
        {
            //输出
            cin>>k;//输入想要输出的数字的编号
            for(int j=1;j<=n;j++)//遍历寻找
            {
                if(a[j].id==k)//找到原始编号与k相同的
                {
                    cout<<j<<endl;//输出当前编号
                    break;//跳出循环
                }
            }
        }
     }
    return 0;//结束程序
}

是不是很简单?

不不不

奥赛的题最主要的问题就是超时

这道也不例外

只能拿76分

问题

这道题超时的地方主要有两个地方

插入和排序

1.修改

这道题说过了,老师不喜欢过多的修改

所以最多插入操作有5000次

每一次都要遍历和快排

n<=8000

所以时间复杂度为n^2log(n)

最坏打算时n=8000

时间复杂度差不多为1.5*10^8

所以需要优化

这里我们需要另外一种排序方式

因为在修改前数组还是按升序排序

修改后只改变了一个元素

比如说

1 2 3 4 5

把3改为6

1 2 6 4 5

这个时候我们只需要让6和前后元素比较

如果比前面大或如果比后面小

那么交换

这里用到swap函数和前面编写的cmp函数

for(int j=1;j<=n-1;j++)if(cmp(a[j],a[j+1])==0)swap(a[j],a[j+1]);//向前遍历按升序排序
for(int j=n;j>=2;j--)if(cmp(a[j-1],a[j])==0)swap(a[j-1],a[j]);//向后遍历按升序排序
for(j=1;j<=n;j++)b[a[j].id]=j;//更新编号,这个一会讲

2.查找并输出

它的时间复杂度为n^2

n<=8000

最坏打算时

时间复杂度为1.6*10^9

我们可以通过保存编号

当寻找时直接可以输入编号

 1 for(i=1;i<=n;i++)b[a[i].id]=i;//用累计排序保存id的编号顺序 

然后操作2的步骤就可以改为

//输出
cin>>k;//输入想要输出的数字的编号
cout<<b[k]<<endl;//输出并换行

经过这些优化

我们就得到了满分代码

代码

#include<bits/stdc++.h>
using namespace std;
struct node//创建结构体
{
    int num;//定义存储数字的变量
    int id;//定义存储编号的变量
}a[8000];
bool cmp(node x,node y)//cmp函数,用于sort排序和升序降序判断
{
    if(x.num!=y.num)return x.num<y.num;//如果前面的数字和后面的数字不同,按数字升序排序
    else return x.id<y.id;//否则按编号升序排序
}
int main()
{
    int b[100005],i,j,n,k,q,w,u;//定义变量,b可以不用初始化
    cin>>n>>q;//输入n和q
    for(i=1;i<=n;i++)//输入数字
    {
        cin>>a[i].num;//保存数字
        a[i].id=i;//保存原始编号
    }
    sort(a+1,a+n+1,cmp);//按升序排序
    for(i=1;i<=n;i++)b[a[i].id]=i;//用累计排序保存id的编号顺序
    for(i=1;i<=q;i++)//执行q次操作
    {
        cin>>w;//输入操作方式
        if(w==1)//判断操作方式
        {
            //更改
            cin>>u>>w;//输入更改的编号和数字
            for(int j=1;j<=n;j++)//遍历寻找要更改的数字
            {
                if(a[j].id==u)//如果找到了
                {
                    a[j].num=w;//更改
                    break;//跳出循环
                }
            }
            for(int j=1;j<=n-1;j++)if(cmp(a[j],a[j+1])==0)swap(a[j],a[j+1]);//向前遍历按升序排序
            for(int j=n;j>=2;j--)if(cmp(a[j-1],a[j])==0)swap(a[j-1],a[j]);//向后遍历按升序排序
            for(j=1;j<=n;j++)b[a[j].id]=j;//更新编号
        }
        else
        {
            //输出
            cin>>k;//输入想要输出的数字的编号
            cout<<b[k]<<endl;//输出并换行
        }
    }
    return 0;//结束程序
}

最后

喜提AC

<think>题目:P7910 [CSP-J 2021] 插入排序 题目要求我们对一个数组进行插入排序,并要求在排序过程中输出每个元素在排序后的位置(即每个元素在排序后数组中的下标,注意数组下标从1开始)。 但是,题目有一个重要的约束:我们不需要每次都对整个数组进行排序,而是有多次操作。操作分为两种: 1. 操作1:修改数组中的某个元素的值。 2. 操作2:查询某个元素在排序后的数组中的位置。 然而,如果每次操作2都进行完整的插入排序,时间复杂度会很高(因为操作最多有5000次,数组最多有5000个元素,最坏情况O(n^2)操作,5000*5000=25e6,可能勉强过,但题目要求更高效)。 因此,我们需要一种更高效的方法。 注意:题目要求的是稳定排序(即相同元素保持原来的顺序),因此我们需要记录每个元素的初始位置(下标)。 思路: 我们可以维护一个当前有序的数组(或者使用链表?),但是题目有修改操作,所以我们需要动态维护每个元素的位置。 另一种思路:我们注意到插入排序的特点是每次插入一个新元素时,前面的元素已经有序。但题目有修改操作,修改一个元素的值后,可能破坏有序性,需要重新调整。 然而,题目要求我们输出当前元素在排序后的数组中的位置,我们可以考虑不显式地维护整个有序数组,而是通过其他方式快速得到每个元素的位置。 我们可以这样考虑: 1. 每个元素有两个属性:值(val)和初始下标(id)。由于排序要求稳定,当两个元素的值相等时,按照初始下标(下标小的在前)排序。 2. 我们可以用一个结构体数组来存储元素:{val, id, pos},其中pos表示当前这个元素在排序后的位置(我们动态维护)。但是修改一个元素的值后,我们需要更新这个元素的位置,并且其他元素的位置也可能发生变化。 由于修改操作最多5000次,查询操作最多5000次,总操作次数最多10000次,我们可以考虑每次修改后,只对修改的元素进行重新定位(类似插入排序中一次插入操作),这样时间复杂度为O(n),总操作次数10000*5000=50e6,在C++中可能勉强通过(但是题目n最大5000,5000*10000=5e7,在1秒内可能比较危险,需要更高效)。 因此,我们需要更高效的方法。 参考题解思路: 维护一个数组a,存储每个元素的值和原始下标(结构体)。另外,我们维护一个数组rank,其中rank[i]表示原始下标为i的元素在有序数组中的位置(即排序后的下标,从1开始)。 同时,我们维护一个有序数组b,b中存储的是结构体(值,原始下标),并且这个数组是有序的(按值非降序,值相等则按原始下标升序)。 操作: 1. 初始化:将a数组复制到b数组,然后对b数组排序。然后根据b数组更新rank数组:遍历b数组,令rank[b[i].id] = i+1(因为下标从1开始)。 2. 修改操作(操作1 x c):将a[x]的值修改为c。注意,这个修改会影响b数组中的对应元素(即原始下标为x的元素)。因此,我们需要在b数组中找到这个元素(之前的值),将其值修改为c,然后为了保持b数组有序,需要将这个元素移动到合适的位置(类似于插入排序的一次调整)。 具体步骤: - 首先在b数组中找到原始下标为x的元素(我们可以通过维护一个指针数组,或者通过遍历找到?)。我们可以用一个数组pos,其中pos[i]表示原始下标为i的元素在b数组中的下标。这样我们就可以直接定位到b数组中该元素的位置。 - 然后修改这个元素的值。 - 由于修改后,这个元素可能比前面的元素小,也可能比后面的元素大,所以我们需要将其向前或向后移动,直到有序。 - 注意:移动过程中,我们需要更新被移动的元素的pos(实际上,我们移动的是结构体,所以移动过程中,我们需要交换位置,同时更新交换的两个元素的pos)。 - 但是,如果我们只移动这一个元素,那么移动的方向(向前或向后)需要分别处理。我们可以这样: 先尝试向前扫描,如果当前元素比前一个小,则交换(实际是向前冒泡),直到到达正确位置;然后向后扫描,如果比后一个大,则交换(向后冒泡),直到正确位置。 - 移动完成后,b数组重新有序。同时,我们需要更新rank数组:因为b数组的下标位置发生了变化,所以每个元素在b数组中的位置发生了变化。但是我们可以通过遍历b数组来更新rank:rank[b[i].id] = i+1。但是这样每次修改后都需要O(n)更新rank数组,总操作次数可能达到10000*n,即5000万,可能超时。 另一种方法:在移动过程中,我们只改变了相邻元素的位置,所以我们可以只更新相邻交换的两个元素的rank(但是实际上,移动一个元素可能会影响很多元素的位置)。 实际上,在移动过程中,我们每次交换两个元素,那么这两个元素的位置都发生了变化,所以我们需要更新这两个元素的rank(即更新这两个元素在b数组中的位置对应的rank)。但是,移动一个元素可能需要交换多次(最多n次),那么修改一次操作的时间复杂度是O(n),总操作次数10000次,最坏10000*5000=50e6,在C++中可能勉强通过(因为常数小),但实际题目n=5000,操作最多10000,50e6次操作在C++中可能1秒内可以完成(一般1e8次操作1秒,但这是最坏情况,且常数较大)。 但是,我们需要注意,题目中操作1(修改操作)最多5000次,操作2最多5000次,所以总修改操作最多5000次。因此,最坏情况5000次修改操作,每次移动整个数组(5000次交换),总操作次数5000*5000=25e6,这个在C++中是可行的。 具体步骤: 1. 定义结构体: struct Node { int val; // 值 int id; // 原始下标 }; 2. 数组: Node a[5005]; // 原始数组,a[i]表示原始下标为i的元素(题目中下标从1开始) Node b[5005]; // 排序数组,大小为n int pos[5005]; // pos[i]表示原始下标为i的元素在b数组中的下标 int rk[5005]; // rk[i]表示原始下标为i的元素在排序后的位置(即rank,下标从1开始) 3. 初始化: for (int i = 1; i <= n; i++) { a[i].val = 初始值; a[i].id = i; b[i] = a[i]; } sort(b+1, b+n+1, [](const Node &x, const Node &y) { if (x.val != y.val) return x.val < y.val; return x.id < y.id; }); for (int i = 1; i <= n; i++) { pos[b[i].id] = i; // 原始下标为b[i].id的元素在b数组中的位置是i rk[b[i].id] = i; // 注意:排序后的位置就是i(因为我们从1开始存储,所以位置就是i) } 4. 修改操作(操作1:将第x个元素(原始下标为x)的值修改为c): int old_val = a[x].val; a[x].val = c; // 更新原始数组 b[pos[x]].val = c; // 更新b数组中的值 // 现在我们需要调整b数组,使b数组重新有序 int p = pos[x]; // 当前在b数组中的位置 // 向前调整:如果前面的元素比当前大,则交换(注意相等时比较id,因为稳定,id小的在前面,所以如果当前元素的id比前面的小?但是值相等时,我们要求原始顺序,但修改后值可能变了) // 注意:向前调整时,当前元素的值变小了,所以需要向前移动 while (p > 1) { if (b[p].val < b[p-1].val || (b[p].val == b[p-1].val && b[p].id < b[p-1].id)) { // 交换b[p]和b[p-1] swap(b[p], b[p-1]); // 更新交换的两个元素的pos:因为它们在b数组中的位置发生了变化 pos[b[p].id] = p; pos[b[p-1].id] = p-1; p--; } else { break; } } // 然后向后调整:如果当前元素比后面的元素大,则交换 while (p < n) { if (b[p].val > b[p+1].val || (b[p].val == b[p+1].val && b[p].id > b[p+1].id)) { swap(b[p], b[p+1]); pos[b[p].id] = p; pos[b[p+1].id] = p+1; p++; } else { break; } } // 注意:调整后,整个b数组有序,但是我们还需要更新所有元素的rank吗?实际上,我们在交换的时候已经更新了每个元素的pos,但我们需要的是每个元素在排序后的位置(即b数组中的位置),而我们的rk数组可以通过pos数组得到:因为原始下标为i的元素在b数组中的位置是pos[i],那么它的排序后的位置就是pos[i](因为b数组的下标就是排序后的位置,从1开始)。 // 所以事实上,我们并不需要单独维护rk数组,查询时直接用pos[i]即可。 5. 查询操作(操作2 x):直接输出pos[x](因为pos[x]就是原始下标为x的元素在b数组中的位置,也就是排序后的位置)。 注意:为什么我们不需要重新计算整个rank?因为我们维护的b数组始终有序,且每个元素在b数组中的位置由pos记录。在交换调整时,我们同时更新了相关元素的pos值。所以查询时,pos[x]就是答案。 但是,这里有一个问题:在交换调整时,我们只更新了被交换的两个元素的pos值。而移动一个元素时,它可能会交换多次,每次交换我们都更新了这两个元素的pos值,所以所有被交换过的元素的pos都是正确的。而其他元素的pos值没有变化(因为它们在b数组中的位置没有变化)。 因此,我们只需要在交换时更新两个元素的pos值即可。 然而,在修改操作中,我们修改了b数组,同时更新了被交换的元素的pos。这样,每次修改操作后,所有元素的pos值都是正确的。 但是,我们修改操作中只更新了被交换的两个元素的pos,而移动过程中每个交换都更新了,所以被移动的元素和沿途交换过的元素的pos都更新了。没有交换的元素pos不变,而且它们的位置也没有变,所以正确。 初始化时,我们建立了b数组,并且初始化了每个元素的pos。然后每次修改操作,我们只更新被移动的元素和它交换的元素的pos。因此,整个数组的pos都是正确的。 所以,我们不需要rk数组,只需要pos数组。查询操作直接输出pos[x]即可。 但是,注意:我们的b数组是从1到n的,所以位置就是1~n。 然而,在交换时,我们使用swap交换两个结构体,这样b数组中的结构体被交换了。交换后,b[p]和b[p-1](或b[p+1])互换了,所以它们的id对应的pos需要更新。 因此,代码实现: 注意:我们需要两个数组:a数组(记录原始数组,每次修改操作需要更新a[x].val),b数组(排序数组),pos数组(记录每个原始下标对应的元素在b数组中的位置)。 步骤: 初始化:读入数组,建立a数组,复制到b数组,对b数组排序,然后遍历b数组设置pos[b[i].id]=i。 然后处理操作: 操作1:修改第x个元素(原始下标x)的值为c a[x].val = c; 在b数组中找到这个元素的位置:p = pos[x],然后更新b[p].val为c。 然后尝试向前调整(冒泡)和向后调整(冒泡),每次交换时更新两个元素的pos值(即交换的两个结构体的id对应的pos值)。 操作2:查询原始下标为x的元素在排序后的位置:输出pos[x] 但是,这里有一个细节:在向前调整时,我们比较的条件是当前元素比前一个元素小,或者相等但id更小(因为稳定,初始下标小的在前面)。在向后调整时,当前元素比后一个元素大,或者相等但id更大(因为如果相等,那么后一个元素的id应该更大,如果当前元素的id大于后一个元素的id,那么就应该交换,因为我们要将id小的放在前面)?实际上,我们的稳定排序规则是:值相等时,id小的排在前面。因此,在向后调整时,如果当前元素的值大于后一个元素,那么交换;如果相等,并且当前元素的id大于后一个元素的id,那么也应该交换(因为这样不符合稳定性:id大的反而排在了id小的前面)。所以条件是正确的。 但是注意:在向前调整时,我们比较的是当前元素(b[p])和前一个元素(b[p-1]): 如果b[p]的值小于b[p-1],那么交换; 如果相等,并且b[p].id < b[p-1].id,那么交换?但是,如果当前元素的id比前一个元素的id小,那么根据规则,当前元素应该排在前一个元素的前面(因为id小),所以应该交换。所以条件正确。 但是,在向后调整时,条件为:当前元素的值大于后一个元素,或者等于后一个元素并且当前元素的id大于后一个元素的id。因为如果当前元素的值大于后一个元素,显然要交换;如果等于,但是当前元素的id大于后一个元素的id,那么当前元素应该排在后面,但是现在它在前面,所以需要交换,把它放到后面去。所以条件正确。 但是,我们需要注意:在向前调整后,当前元素已经被移动到了前面的位置,然后我们向后调整时,应该从新的位置开始向后调整。所以代码中先向前调整,然后从调整后的位置再向后调整。 然而,这里有一个问题:向前调整后,当前元素已经移动到了一个位置p',然后从p'开始向后调整。有可能向前调整后,当前元素已经在正确的位置了,那么向后调整就不会进行。同样,向后调整时,如果已经正确,则停止。 但是,有没有可能向前调整后,当前元素的位置已经满足条件,但是向后调整时发现后面有需要交换的?实际上,向前调整后,当前元素和前面的元素已经有序(即前面的元素都不大于当前元素,并且如果相等的话,当前元素的id比前面的大或者相等?注意,相等时我们要求id小的在前面,所以如果当前元素的值和前面相等,那么当前元素的id应该大于等于前面的id。因为在向前调整时,我们只交换了当前元素小于前一个元素(值小于,或者值相等但是id小于)的情况。所以调整后,当前元素的值大于等于前面的值,并且如果值相等,当前元素的id不小于前面的id。这样,当前元素和后面的元素可能无序(因为后面的元素可能比当前元素小,或者相等但id更小),所以需要向后调整。 同样,向后调整后,当前元素的值小于等于后面的元素,并且如果相等,当前元素的id不大于后面的id。所以调整后处于正确位置。 因此,我们需要先向前调整再向后调整,或者先向后调整再向前调整?实际上,修改后的元素可能大于后面的元素,也可能小于前面的元素。所以我们需要两个方向调整。但是,如果我们先向后调整,然后再向前调整,也是可以的。不过,这里我们选择先向前调整(处理可能小于前面的情况),再向后调整(处理可能大于后面的情况)。 但是,如果我们修改后的值变小了,那么它可能需要向后调整吗?不会,因为变小了,它应该向前移动,而不会大于后面的元素(原本有序,修改后变小,所以它可能小于后面的元素?但是后面的元素原本大于等于修改前的值,现在修改后变小了,所以后面的元素肯定大于等于修改后的值吗?不一定,因为后面的元素可能也是修改过的?不对,我们每次修改操作后都会进行调整,所以整个数组总是有序的。但是,当我们修改一个元素时,我们只更新了它,然后调整它,所以调整过程中,我们假设其他位置是有序的。 所以,修改后,这个元素的值变了,它可能破坏局部有序性:如果值变小了,那么它需要向前移动;如果值变大了,那么它需要向后移动。因此,我们可以判断:如果新值小于旧值,那么只需要向前调整;如果新值大于旧值,那么只需要向后调整;如果相等,那么位置可能不变,但是也可能因为id的关系需要调整?不对,因为值相等时,我们还有id的比较。即使值不变,但是如果我们修改的值导致它在相等的情况下id不符合条件?不对,id是不会变的。所以当值不变时,位置不需要调整。但是,当我们修改值(即使值不变)时,我们仍然进行了更新,然后进行了调整。但是,如果值不变,那么条件判断中,向前调整:当前元素的值等于前一个元素的值,那么我们需要比较id:如果当前元素的id小于前一个元素的id,那么交换;否则停止。向后调整类似。 但是,如果我们修改的值不变,那么调整过程会进行吗?不会,因为在初始化时,数组已经有序,所以修改后值不变,那么当前元素和相邻元素的比较条件(值相等时,id的大小关系)应该已经满足?不一定,因为可能这个元素被修改后,与相邻的元素相等,但是id的关系可能不满足(比如初始化时,这个元素在相邻元素中间,但是修改操作修改了其他元素,导致这个元素相邻的元素发生了变化?不对,我们每次修改操作只修改一个元素,所以其他元素的相对位置并没有改变。而且,在之前的调整中,整个数组是有序的。所以,如果修改一个元素的值(不变),那么它在原来的位置应该仍然有序。因此,我们可以优化:当修改的值不变时,我们不需要进行调整。但是题目没有保证修改的值不同,所以我们需要处理。 然而,由于我们无法保证修改的值不变,所以我们需要进行调整。但是,如果修改的值不变,那么调整过程会立即停止(因为向前调整时,当前元素的值等于前一个元素的值,并且当前元素的id一定大于等于前一个元素的id吗?不一定,因为排序规则是值相等时id小的在前,所以当前元素在初始化时已经满足:它的id应该大于等于前面的元素的id(因为前面元素的id小于它)。所以当值不变时,向前调整的条件不满足(因为值相等且当前id>=前面的id),所以不会交换。同样,向后调整的条件也不满足(当前值等于后面值,且当前id<=后面的id?因为后面元素的id比当前大,所以当前id<=后面id成立)。所以调整过程不会进行。 因此,我们可以不判断值是否变化,直接进行调整。因为如果值不变,调整过程不会交换任何元素。 但是,如果修改的值变化了,那么我们需要调整。而且,调整方向取决于变化:如果新值小于旧值,那么需要向前调整(因为新值变小,可能小于前面的元素)。如果新值大于旧值,那么需要向后调整(因为新值变大,可能大于后面的元素)。但是,我们也可以不判断方向,直接先向前调整再向后调整(或者反过来)。因为如果新值变小,那么向前调整会把它向前移动,然后向后调整时,由于它前面的元素都比它小(包括现在在它前面的元素,因为数组原来有序,它前面的元素在原来位置时已经比它小,现在它向前移动了,新的前面的元素更小,新的后面的元素是原来位置的元素,仍然比它大),所以不会向后调整。同样,如果新值变大,那么向前调整不会进行(因为新值比前面的元素大),然后向后调整会把它向后移动。 但是,有没有可能同时需要向前和向后?比如,修改后,这个元素的值变得比前面的小,同时也比后面的小?不可能,因为数组原本有序,前面的元素小于等于旧值,后面的元素大于等于旧值。修改后,如果新值小于前面的某个元素,那么它需要向前移动;而移动后,它后面的元素是原来的元素,这些元素原本大于等于旧值,而新值小于旧值,所以新值一定小于这些元素(因为旧值<=后面的元素,而新值<旧值,所以新值<后面的元素)。所以,它不需要向后调整。同样,如果新值大于旧值,那么它后面的元素原本>=旧值,而新值>旧值,所以新值可能大于后面的某个元素(如果后面的元素等于旧值,那么新值大于旧值,所以大于后面的元素)。所以需要向后调整,而不会向前调整(因为前面的元素<=旧值,而新值>旧值,所以新值>=前面的元素)。 但是,这里有一个问题:数组原本有序,但是修改一个元素后,这个元素可能小于前面的元素,也可能大于后面的元素?不可能同时发生,因为数组原本有序:前面的元素<=旧值<=后面的元素。所以: 如果新值<旧值,那么新值可能小于前面的元素(因为前面的元素<=旧值,但是可能大于新值),也可能小于后面的元素(但是后面的元素>=旧值>新值,所以一定大于新值,所以不需要向后调整,因为后面的一定更大)。 如果新值>旧值,那么新值可能大于后面的元素(后面的元素>=旧值,但可能小于新值?因为后面的元素可能只比旧值大一点),也可能大于前面的元素(但是前面的元素<=旧值<新值,所以一定小于新值,所以不需要向前调整)。 所以,我们只需要一个方向调整:新值小于旧值则向前调整,新值大于旧值则向后调整。这样我们可以省略另一个方向的调整。 但是,还有一种情况:修改后,值不变,但是位置可能因为id的关系需要调整?我们说过,原来有序,所以id的顺序已经满足要求(值相等的元素按id升序排列),所以修改值不变时,位置不需要调整。但是,如果修改后值不变,但是我们在向前调整时,会判断:如果当前值等于前一个值,并且当前id小于前一个id,那么交换。这种情况可能发生吗?在初始化排序后,值相等的元素中,id小的在id大的前面,所以当前元素的前一个元素(值相等)的id一定小于当前元素。因此,当前元素的id一定大于前一个元素的id,所以不会交换。向后调整也一样:后面元素的id一定大于当前元素,所以不会交换。 但是,如果修改操作不是修改值,而是修改了另一个元素,导致这个元素相邻的元素发生了变化?不对,我们每次修改只修改一个元素。所以这个元素相邻的元素并没有被修改(除了可能被交换位置)。在修改这个元素之前,整个数组有序,所以相邻的元素满足条件。修改后,值不变,所以相邻关系不变,不会触发交换。 因此,我们可以根据新值和旧值的大小关系来决定调整方向,以减少不必要的比较。但是,为了代码简洁,我们也可以不做方向判断,直接进行两个方向的调整(先向前再向后)。因为调整过程中,只会有一个方向实际发生交换(新值小于旧值则向前调整,新值大于旧值则向后调整,新值等于旧值则两个方向都不动)。这样,每个修改操作最多移动n次(即整个数组),但是平均情况下可能很快。 考虑到操作最多5000次,最坏情况每次移动整个数组(5000次交换),总交换次数25e6,在C++中应该可以接受(因为每次交换是常数操作,所以总操作次数25e6,大约2500万次操作,在1秒内可以完成)。 所以,我们采用两个方向都调整的方法。 但是,在向后调整时,我们需要注意:当我们向前调整后,当前元素已经移动到了一个新的位置,然后从这个位置开始向后调整。而如果我们先向后调整再向前调整,也是可以的。不过,这里我们按照先向前再向后。 代码实现: 注意:数组下标从1开始。 具体代码: 步骤: 1. 输入n, q 2. 输入数组,初始化a[i].val和a[i].id=i 3. 复制到b数组,然后对b数组排序(按val升序,val相等则id升序) 4. 初始化pos数组:遍历b数组,令pos[b[i].id]=i 5. 处理q次操作: while (q--) { 输入操作类型op; if (op == 1) { 输入x和c; 更新a[x].val和b[pos[x]].val为c; int p = pos[x]; // 当前在b数组中的位置 // 先向前调整 while (p > 1) { // 如果当前元素应该排在前面一个元素的前面(即当前元素比前一个元素小,或者相等且id小),则交换 if (b[p].val < b[p-1].val || (b[p].val == b[p-1].val && b[p].id < b[p-1].id)) { swap(b[p], b[p-1]); // 更新交换的两个元素的pos pos[b[p].id] = p; pos[b[p-1].id] = p-1; p--; } else { break; } } // 然后向后调整 while (p < n) { if (b[p].val > b[p+1].val || (b[p].val == b[p+1].val && b[p].id > b[p+1].id)) { swap(b[p], b[p+1]); pos[b[p].id] = p; pos[b[p+1].id] = p+1; p++; } else { break; } } } else if (op == 2) { 输入x; 输出pos[x]; // 注意题目要求的位置是从1开始的,而我们的pos[x]就是从1开始的。 } } 但是,这里有一个问题:我们在修改操作中,先更新了a[x].val,然后更新了b[pos[x]].val。注意,在调整过程中,我们使用的是b[p]的值,而b[p]的值已经被更新为c。 另外,在调整过程中,我们修改了b数组和pos数组,所以它们保持最新状态。 但是,当我们向前调整时,p在变化,所以b数组中当前元素的位置在变化。 注意:在向前调整时,我们交换了b[p]和b[p-1],然后p--,所以当前元素现在在位置p-1上(即交换后它在b[p-1]的位置上)。然后我们继续比较新的位置p-1和p-2。所以p的更新正确。 向后调整同理。 然而,在两次调整之间,我们使用了同一个变量p:向前调整后的p作为向后调整的起始位置。这是正确的,因为向前调整后,当前元素的位置就是p(新的位置)。 但是,在向前调整的过程中,我们可能会移动多次,每次移动都更新了p。所以当向前调整结束后,p就是当前元素在b数组中的当前位置。然后向后调整从这个位置开始。 因此,代码逻辑正确。 但是,我们需要注意边界:p>1时才能向前调整,p<n时才能向后调整。 最后,我们来写代码。 注意:我们使用了结构体数组a和b,以及数组pos。由于n最大5000,q最多5000次操作,所以空间足够。 但是,在最坏情况下,每次修改操作都需要O(n)的时间(比如每次修改一个元素,让它变成最小值,那么每次都要向前移动整个数组,即n次交换),总操作次数5000*n=25000000,即2500万次交换(每次交换包括两次swap和两次更新pos,swap结构体包括两个int,所以交换操作是常数时间,但常数较大)。在C++中,2500万次操作可能刚好在1秒内(取决于机器速度,竞赛环境通常1e8次操作/秒,所以2500万次操作大约0.25秒)。 因此,我们编写代码。 注意:输入输出数据量较大,考虑使用scanf和printf。 代码实现如下: </think>以下是针对 P7910 [CSP-J 2021] 插入排序问题的 C++ 解决方案。该方案通过维护一个有序数组 `b` 和位置映射数组 `pos`,实现了高效的修改和查询操作。 ### 解决方案代码 ```cpp #include <iostream> #include <algorithm> using namespace std; const int MAXN = 5005; struct Node { int val; int id; }; int n, q; Node a[MAXN]; // 原始数组 Node b[MAXN]; // 排序数组 int pos[MAXN]; // 记录每个元素在 b 中的位置 int main() { scanf("%d %d", &n, &q); for (int i = 1; i <= n; i++) { scanf("%d", &a[i].val); a[i].id = i; b[i] = a[i]; } // 对 b 数组排序(稳定排序) sort(b + 1, b + n + 1, [](const Node &x, const Node &y) { if (x.val != y.val) return x.val < y.val; return x.id < y.id; }); // 初始化 pos 数组 for (int i = 1; i <= n; i++) { pos[b[i].id] = i; } while (q--) { int op; scanf("%d", &op); if (op == 1) { int x, c; scanf("%d %d", &x, &c); // 更新原始数组 a[x].val = c; // 获取元素在 b 中的位置 int p = pos[x]; b[p].val = c; // 更新排序数组中的值 // 向前调整(向左冒泡) while (p > 1) { if (b[p].val < b[p-1].val || (b[p].val == b[p-1].val && b[p].id < b[p-1].id)) { swap(b[p], b[p-1]); pos[b[p].id] = p; pos[b[p-1].id] = p-1; p--; } else { break; } } // 向后调整(向右冒泡) while (p < n) { if (b[p].val > b[p+1].val || (b[p].val == b[p+1].val && b[p].id > b[p+1].id)) { swap(b[p], b[p+1]); pos[b[p].id] = p; pos[b[p+1].id] = p+1; p++; } else { break; } } } else if (op == 2) { int x; scanf("%d", &x); printf("%d\n", pos[x]); // 直接输出位置 } } return 0; } ``` ### 代码解释 1. **数据结构设计**: - `Node` 结构体:包含 `val`(元素值)和 `id`(原始下标) - `a[]`:存储原始数组数据 - `b[]`:维护有序的数组(插入排序后结果) - `pos[]`:映射每个元素的原始下标到其在 `b` 中的位置 2. **初始化**: - 读入初始数组并复制到 `b` 数组 - 对 `b` 数组排序(稳定排序:值不同时按值升序,值相同时按 id 升序) - 初始化 `pos` 数组,记录每个元素在有序数组中的位置 3. **操作处理**: - **操作1(修改元素值)**: 1. 更新 `a[x]` 和 `b` 中对应元素的值 2. 获取元素当前位置 `p = pos[x]` 3. **向前调整**:若当前元素应排在前面(值更小,或值相等但 id 更小),则与前面元素交换并更新位置 4. **向后调整**:若当前元素应排在后面(值更大,或值相等但 id 更大),则与后面元素交换并更新位置 - **操作2(查询位置)**: - 直接输出 `pos[x]`(即元素在有序数组中的位置) 4. **时间复杂度**: - 修改操作:最坏 O(n),平均 O(1)(局部调整) - 查询操作:O(1) - 总体:O(qn),在 CSP-J 数据范围内可接受 5. **稳定排序保障**: - 排序和调整时同时考虑元素值和原始 id - 确保值相同的元素保持初始相对顺序 该方案通过动态维护有序数组和位置映射,高效处理了插入排序中的修改和查询操作。 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值