luogu P4769 [NOI2018] 冒泡排序 - 计数

传送门:https://www.luogu.org/problemnew/show/P4769

题目大意:考虑用冒泡排序的过程求一个排列的逆序对数,由于将pi从i位置移动到pi位置至少需要移动|i-pi|次,而每次交换最多实现2次正向移动,所以逆序对数的下界是1/2 sigma |i-pi|,当然这个下界并不总能达到(比如3 2 1有3个逆序对而计算得到的下界是2),求:给定n和排列p,满足字典序严格大于p且能达到下界的排列有多少个,模998244353。

一道差点把我送退役的题……算了说多了都是泪。

考虑什么时候才能达到下界?显然是所有元素都只会正向移动的时候。

这就意味着:如果pi>i,那么pi左边的数都必须<pi;否则,所有<pi的数都必须出现在pi左边(之所以全部考虑左边,是为了下一步设计dp状态方便)。

于是我们立刻得到了一个状压dp:设dp[s][0/1]表示现在选了s集合的数,字典序是否贴下界的合法方案数。转移时枚举下一个选的数字,讨论一下是否合法即可。

于是我们已经有了一个能解决n<=18,期望得分44的优秀算法,然后呢?

当然是用它来打表了。

进一步分析:

题目可以转化为:要求排列中不存在长度>=3的下降子序列。

因为如果出现的话,那么这个下降子序列中间的元素需要既与左边比它大的元素交换又与右边比它小的元素交换,需要折返一下,显然就不合法了。

这又等价于可以将序列划分为2个上升子序列。

首先我们先不看那个字典序的性质,相信大家打一下表就能发现答案是卡特兰数。

然后我们再想想它的本质是什么:

假设前i个位置中,最大的数是j,那么我们会发现,>j的数当前是可以随便填的,然而<j的数只能限制从小到大按顺序填入(因为这些元素一定被归入同一个上升子序列)。

于是我们就可以设f(i,j)表示还剩余i个数没填,其中后j个是大于当前最大值的“非限制元素”的答案。

转移就是枚举下一个位置填一个限制元素或某一个非限制元素。

如果填限制元素,非限制元素的数量不变;否则假设填入第k个非限制元素,非限制元素的数量就会减少k个。

f(i,j) = sigma k=0~j f(i-1,j-k)

边界是f(0,0) = 1

这其实就是个前缀和:

f(i,j) = f(i,j-1) + f(i-1,j)

擅长打表熟悉组合数的人会很快发现,这东西就是两个组合数相减:

f(i,j) = c(i+j-1,j) - c(i+j-1,j-2)

它的正确性容易验证:

f(i,j-1) + f(i-1,j) = c(i+j-2,j-1) + c(i+j-2,j) - c(i+j-2,j-3) - c(i+j-2,j-2) = c(i+j-1,j) - c(i+j-1,j-2) = f(i,j)

特别地,f(i,0) = 1,f(i,1) = i

这也解释了为什么没有限制时答案是卡特兰数:只需注意到此时的所求是f(n,n)

f(n,n) = c(2×n-1,n) - c(2×n-1,n-2) = C(n)

我们再考虑限制,假设当前做到第i位,给定的排列中这一位是ai,后面有bi个数比它大,前面有ci个数比它小,这两个数组可以用树状数组方便地计算出来,并且现在的“非限制元素”还有nw个。

我们先计算填入的数字pi>ai的情况,然后再令pi=ai继续计算下一位。

首先nw可以与bi取个min,因为填完这一位后非限制元素一定不超过bi个。

然后此时如果nw=0(最大的数被填进去了),则意味着我们后面将别无选择地只能按顺序填入,而这个排列的字典序是严格不大于给定排列的,因此就可以退出了。

否则,我们相当于要求sigma j=0~nw-1 f(n-i,j),根据前缀和它等于f(n-i+1,nw-1),可以O(1)计算。

然后,我们考虑填入pi=ai是否合法:

如果刚刚bi更新了nw,说明ai本身就是一个“非限制元素”,当然合法;

否则,如果ai是当前未填入的元素中最小的(对应ci==ai-1),相当于填了一个最小的“限制元素”,也是合法的;

否则,就是乱序填入“限制元素”,不合法,就可以退出了。

总复杂度O(n log n),瓶颈其实在于树状数组求bi和ci的部分。

总结:一道绝好的打表找规律题。

代码如下:

#include<bits/stdc++.h>
using namespace std;
int t,n,a[600010];
#define li long long
#define gc getchar()
#define pc putchar
int read(){
    int x = 0,c = gc;
    while(!isdigit(c)) c = gc;
    while(isdigit(c)){
        x = (x << 1) + (x << 3) + (c ^ '0');
        c = gc;
    }
    return x;
}
void print(int x){
    if(x >= 10) print(x / 10);
    pc(x % 10 + '0');
}
int tt[1200010];
void xg(int q){
    for(int i = q;i <= n;i += i & (-i)) ++tt[i];
}
int cx(int q){
    int as = 0;
    for(int i = q;i;i -= i & (-i)) as += tt[i];
    return as;
}
int b[600010],c[600010];
li jc[1200010],nj[1200010];
const int mo = 998244353;
li ksm(li q,li w){
    li as = 1;
    while(w){
        if(w & 1) as = as * q % mo;
        q = q * q % mo;
        w >>= 1;
    }
    return as;
}
int mx1 = 1200005;
inline li zh(li q,li w){
    return jc[q] * nj[w] % mo * nj[q - w] % mo;
}
inline li wk(li q,li w){
    if(w == 0) return 1;
    if(w == 1) return q;
    return (zh(q + w - 1,w) - zh(q + w - 1,w - 2) + mo) % mo;
}
int main(){
    int i,j;
    jc[0] = 1;
    for(i = 1;i <= mx1;++i) jc[i] = jc[i - 1] * i % mo;
    nj[mx1] = ksm(jc[mx1],mo - 2);
    for(i = mx1 - 1;i >= 0;--i) nj[i] = nj[i + 1] * (i + 1) % mo;
    t = read();
    while(t--){
        n = read();
        for(i = 1;i <= n;++i) a[i] = read(),tt[i] = 0;
        for(i = n;i;--i){
            b[i] = n - i - cx(a[i]);
            xg(a[i]);
            c[i] = i - 1 - (n - a[i] - b[i]);

        }
        li as = 0;int nw = n; 
        for(i = 1;i <= n;++i){
            bool fg = b[i] < nw;
            nw = min(nw,b[i]);
            if(nw <= 0) break;
            (as += wk(n - i + 1,nw - 1)) %= mo;
            if(!fg && c[i] != a[i] - 1) break;
        } 
        print(as);pc('\n');
    } 
    return 0;
}

 

<think>好的,用户之前让我讲解了冒泡排序,现在他们问如何在洛谷上找到相关的题目。我需要先理解他们的需求。用户可能是在学习排序算法,尤其是冒泡排序,现在想通过练习题目来巩固知识,或者准备比赛。洛谷是一个编程题库平台,用户可能希望找到适合自己水平的题目,用冒泡排序解决。 首先,我需要确认洛谷是否有直接考察冒泡排序的题目。冒泡排序本身效率不高,通常在算法题中可能不会直接要求用冒泡排序实现,因为其他排序方法更高效。但可能有题目适合用冒泡排序的思想解决,或者考察对排序过程的理解。 接下来,我需要回忆或查找洛谷上的题目。例如,可能存在一些需要统计交换次数的题目,类似逆序对,而冒泡排序中的交换次数可能与逆序对有关。或者题目可能直接要求实现冒泡排序,但较为基础。 然后,我需要考虑用户可能的水平。如果用户刚学冒泡排序,可能需要入门题;如果已经掌握,可能需要更难的题目。但直接搜索“冒泡排序”可能结果不多,可能需要用相关关键词,比如排序、交换次数等。 另外,可能需要提醒用户,虽然冒泡排序在某些题目中可以应用,但实际比赛中更常用其他高效排序方法。不过理解冒泡排序的原理对解决特定问题有帮助,比如涉及相邻元素交换的问题。 可能还需要指导用户如何在洛谷上搜索题目,比如使用标签或关键词,或者直接推荐几道相关题目。例如,P1116 车厢重组可能涉及到相邻交换,类似冒泡排序的过程,计算逆序数。P1774 最接近神的人使用逆序对,也可以用冒泡排序的思想来理解交换次数。 需要验证这些题目是否确实适合用冒泡排序的思路解决,或者是否在题解中涉及冒泡排序的概念。同时,要确保推荐的题目难度适合用户,避免过于困难或偏离主题。 最后,可能需要给出具体的题目链接或编号,并简要说明每道题与冒泡排序的关系,帮助用户理解如何应用所学知识。同时提醒用户注意冒泡排序的时间复杂度,避免在数据量大的题目中使用导致超时。</think>以下是洛谷(Luogu)上与**冒泡排序**相关的题目推荐及解题思路说明,帮助你通过实际编程练习巩固冒泡排序的原理和应用: --- ### **1. 直接考察冒泡排序的题目** #### **P1781 宇宙总统(排序基础应用)** - **题目链接**:[P1781 宇宙总统](https://www.luogu.com.cn/problem/P1781) - **关联点**: 虽然题目核心是比较字符串长度和字典序,但可以用冒泡排序的思想实现自定义排序规则(例如:比较两个候选人的票数)。 - **练习意义**: 熟悉冒泡排序的相邻比较和交换逻辑,同时练习处理非数值型数据的排序。 --- ### **2. 基于冒泡排序思想的变式题** #### **P1116 车厢重组(逆序对问题)** - **题目链接**:[P1116 车厢重组](https://www.luogu.com.cn/problem/P1116) - **关键点**: 要求通过相邻交换将火车车厢按升序排列,计算最少交换次数。 - **冒泡排序的关联**: 冒泡排序的交换次数即为数组中**逆序对的数量**,此题本质是求逆序对数目。 - **示例代码**: ```cpp #include <iostream> using namespace std; int main() { int n, cnt = 0; cin >> n; int a[n]; for (int i = 0; i < n; i++) cin >> a[i]; // 冒泡排序统计交换次数 for (int i = 0; i < n-1; i++) { for (int j = 0; j < n-i-1; j++) { if (a[j] > a[j+1]) { swap(a[j], a[j+1]); cnt++; } } } cout << cnt; return 0; } ``` **注意**:此方法时间复杂度为 O(n²),若数据量大需用归并排序求逆序对(如 P1908)。 --- ### **3. 间接应用冒泡排序思想** #### **P1908 逆序对(高效解法)** - **题目链接**:[P1908 逆序对](https://www.luogu.com.cn/problem/P1908) - **关联点**: 冒泡排序的交换次数 = 逆序对数量,但此题数据规模大(n ≤ 5×10^5),直接冒泡会超时。 - **学习意义**: 通过对比冒泡排序的暴力解法与归并排序/树状数组的高效解法,理解时间复杂度的优化思路。 --- ### **4. 推荐练习方式** 1. **基础训练**: - 使用冒泡排序直接解决 **P1116 车厢重组**,体会逆序对与交换次数的关系。 - 尝试用冒泡排序实现 **P1781 宇宙总统** 的排序逻辑。 2. **进阶思考**: - 分析为何 **P1908** 不能直接用冒泡排序(数据规模导致 O(n²) 超时)。 - 对比冒泡排序与快速排序、归并排序的效率差异。 --- ### **5. 如何在洛谷搜索排序题目** 1. **标签筛选**: 在洛谷题库页面选择“排序”或“模拟”标签,筛选适合的题目。 2. **关键词搜索**: 搜索“冒泡排序”“逆序对”“相邻交换”等关键词。 3. **难度选择**: 从普及-(入门)到提高(进阶)逐步提升难度。 --- ### **6. 注意事项** - **时间复杂度**:冒泡排序适用于 n ≤ 1e4 的数据量,更大数据需换用更优算法。 - **实际应用**:编程竞赛中较少直接考察冒泡排序,但理解其原理对分析其他问题(如逆序对)有帮助。 如果需要具体题解的代码实现或进一步分析,可以告诉我题目编号,我会为你详细解答!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值