2021牛客寒假算法基础集训营2:F 牛牛与交换排序

本文介绍了《牛牛与交换排序》这一算法问题,解析了如何通过固定翻转区间长度来优化暴力解法,将时间复杂度降至O(n)。文章详细阐述了使用deque优化翻转操作的过程,并给出了C++代码实现,帮助读者理解算法思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

F 牛牛与交换排序

题目链接

此题首先我们可以得知,对于给定的序列, k k k 一定是固定的(已经排好序的除外)。这里的 k k k 一定等于 e − s + 1 e - s + 1 es+1 (s,e见下方代码)。用反证法即可证明,主要的证明条件是翻转区间长度固定且每次只能翻转左端点比上次翻转的左端点更大的区间。

    for (int i = 1; i <= n; ++i) {
        if (a[i] != i) {
            s = i;
            goal = i;
            break;
        }
    }
    for (int i = s + 1; i <= n; ++i) {
        if (a[i] == s) {
            e = i;
            break;
        }
    }

证得了上面的k一定固定后,那么首先这题的暴力的解法就出来了。从第一个错位的元素开始(即上述代码中的s),每次判断区间的左右端点哪个端点会等于目标元素。如果都不等则不可能成功。如果右端点等于目标元素,则将此区间反转。然后依次往后遍历,最后再看无法作为左端点的一些点是否同样排好序即可。那么显然这是一个 O ( n 2 ) O(n^2) O(n2) 的解法,我们需要想办法优化这种解法。

这里可以优化的地方就是翻转区间,使用deque可以将一次翻转区间的时间复杂度降到 O ( 1 ) O(1) O(1) 。现在我们枚举每个需要判断的右端点(每个可能要翻转的区间的右端点)。

for (int i = e; i <= n; ++i) //枚举每个需要判断的区间的右端点

假设对于 i i i 号右端点而言,我们都可以得知当前前 k − 1 k - 1 k1 个元素的状态,那么将右端点插在其尾部,就一定可以得到 i i i号区间整个区间的状态,且可以更新下一个区间的前 k − 1 k - 1 k1 个状态。那么此时用deque就可以实现这个想法,我们用deque保存当前前 k − 1 k - 1 k1 个元素,设置一个变量 t t t 表示当前deque中应当顺着看还是倒着看。那么当我们遍历到第 i i i 号右端点时,如果deque中的序列是要顺着看的,那么此时右端点理应插在deque尾部,即此时序列的真正的尾部,如果deque中的序列是倒序的,那么真正的尾部理应在deque的头部,那么右端点 a [ i ] a[i] a[i] 应插在deque头部。这样 i i i号区间就更新成功。然后判断左右端点哪个是目标元素,如果是右边就pop_back, t t t 置为倒序状态,pop_back的理由是因为我们要保证deque中是区间的前 k − 1 k-1 k1 个元素。那么如果是左端点就pop_front,然后 t t t 设为顺序就好。这样就实现了对于 i i i 号右端点而言,假设我们可以得知当前前 k − 1 k - 1 k1 个元素的状态,就一定可以通过此方式得到 i i i号区间整个区间的状态,且更新下一个区间的前 k − 1 k - 1 k1 个元素 的状态。所以我们只要知道第一个区间的前 k − 1 k - 1 k1 个元素的状态,就可以不断往后 O ( 1 ) O(1) O(1) 更新

代码实现

#include <iostream>
#include <cstdio>
#include <deque>
using namespace std;

int n, s, e, t = 1, goal;
int a[100005];
deque<int> de;

int main() {
#ifndef ONLINE_JUDGE
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    for (int i = 1; i <= n; ++i) {
        if (a[i] != i) {
            s = i;
            goal = i;
            break;
        }
    }
    if (!s) {
        printf("yes\n1");
        return 0;
    }
    for (int i = s + 1; i <= n; ++i) {
        if (a[i] == s) {
            e = i;
            break;
        }
    }
    for (int i = s; i < e; ++i) {
        de.push_back(a[i]);
    }
    for (int i = e; i <= n; ++i) {
        if (t) de.push_back(a[i]);
        else de.push_front(a[i]);
        if (de.back() == goal) {
            de.pop_back();
            t = 0;
        }
        else if (de.front() == goal) {
            de.pop_front();
            t = 1;
        }
        else {
            printf("no");
            return 0;
        }
        goal++;
    }
    if (t) {
        for (auto i : de) {
            if (i != goal++) {
                printf("no");
                return 0;
            }
        }
    }
    else {
        for (auto i : de) {
            if (i != n--) {
                printf("no");
                return 0;
            }
        }
    }
    printf("yes\n%d", e - s + 1);
}
### 关于2020年寒假算法基础集训营中的欧几里得算法2020年的寒假算法基础集训营中,确实存在涉及欧几里得算法的相关题目。具体来说,在第四场竞赛的第一题即为“A. 欧几里得”,该题目的核心在于利用扩展欧几里得定理来解决问题[^5]。 #### 扩展欧几里得算法简介 扩展欧几里得算法主要用于求解形如 ax + by = gcd(a, b) 的线性不定方程的一组特解(x,y),其中gcd表示最大公约数。此方法不仅能够计算两个整数的最大公因数,还能找到满足上述条件的具体系数x和y。 对于给定的数据范围较小的情况可以直接通过递归来实现;而对于较大数据则需考虑效率优化问题。下面给出了一段基于C++语言编写的用于解决此类问题的模板代码: ```cpp #include<bits/stdc++.h> #define int long long using namespace std; // 定义全局变量存储结果 int x, y; void ex_gcd(int a, int b){ if(b == 0){ x = 1; y = 0; return ; } ex_gcd(b, a % b); int tmp = x; x = y; y = tmp - (a / b) * y; } ``` 这段程序实现了经典的扩展欧几里得算法逻辑,并且可以作为处理类似问题的基础工具函数调用。 #### 实际应用案例分析 回到原题本身,“A. 欧几里得”的解答思路就是先预处理斐波那契数列前若干项数值存入数组`a[]`内以便快速查询,之后针对每一次询问直接输出对应位置处两相邻元素之和即可得出最终答案。这实际上巧妙运用到了广为人知的裴蜀定理——任意一对互质正整数都可由它们自身的倍数组合而成,而这里正是借助了这一性质简化了解决方案的设计过程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值