【题解】洛谷P6186 [NOI Online #1 提高组] 冒泡排序:【冒泡排序】与【逆序对】问题

传送门:洛谷P6186 [NOI Online #1 提高组] 冒泡排序

题意

n n n 元排列 p i p_i pi ,两种操作:
1 x:交换当前排列中 x x x x + 1 x+1 x+1 位置的数。
2 x:输出当前排列经过 x x x 轮冒泡排序交换后的逆序对个数。

输入输出

输入:单组数据, n n n 排列长度, m m m 操作数, n n n 元排列, m m m 个操作。
输出:每个一行,操作 2 2 2 的结果。

数据范围

2 ≤ n , m ≤ 2 e 5 2\leq n,m\leq 2e5 2n,m2e5 ,操作 2 2 2 x x x i n t int int 范围,其他输入数保证合法。

分析

开始毫无头绪,我太笨了,看题解都看了好久,把两种方法整理一下TAT
从逆序对和冒泡排序的本质出发。
逆序对,对于数组中的一个数 x x x 来说,向左找逆序对的个数为它左边比它大的数的个数。
冒泡排序的一轮,本质是对于每个数,如果左边没有比它大的,会向右换到第一个比它大的数左边;反过来说,如果一个数左边有 x x x 个比它大的数,那么每轮让左边最近的大数交换到右边,逆序对个数 − 1 -1 1 x x x 轮后(即 x + 1 x+1 x+1 轮),它开始向右交换。

记原排列为 a [ 1... n ] a[1...n] a[1...n] b [ i ] b[i] b[i] 表示 a [ i ] a[i] a[i] 左边比它大的数的个数,则逆序对的个数为 ∑ i = 1 n b [ i ] \sum_{i=1}^{n} b[i] i=1nb[i]
每一轮冒泡排序,若 b [ 1... n ] b[1...n] b[1...n] 不为 0 0 0 则减 1 1 1,则第 x x x 轮的逆序对个数为 ∑ i = 1 n b [ i ] − ∑ i = 1 n m i n ( b [ i ] , x ) = ∑ b [ i ] > x b [ i ] − x ∑ b [ i ] > x 1 \sum_{i=1}^{n} b[i]-\sum_{i=1}^{n}min(b[i],x)=\sum_{b[i]>x} b[i]-x\sum_{b[i]>x} 1 i=1nb[i]i=1nmin(b[i],x)=b[i]>xb[i]xb[i]>x1
方法一:右边的式子是很明显的两颗权值线段树!
方法二:观察第一个式子,第一项可以在读入数据时建立权值线段树求解得到;第二项 f ( x ) = ∑ i = 1 n m i n ( b [ i ] , x ) = f ( x − 1 ) + b [ i ] ≥ x 的 个 数 f(x)=\sum_{i=1}^{n}min(b[i],x)=f(x-1)+b[i]\geq x的个数 f(x)=i=1nmin(b[i],x)=f(x1)+b[i]x 得到。因为 0 ≤ b [ i ] < n 0\leq b[i]<n 0b[i]<n ,开一个桶 d [ c ] d[c] d[c] 记录 b [ 1... n ] = = c b[1...n]==c b[1...n]==c 的个数,则 f ( x ) = f ( x − 1 ) + ∑ i = x n − 1 d [ i ] = f ( x − 1 ) + n − ∑ i = 0 x − 1 d [ i ] f(x)=f(x-1)+\sum_{i=x}^{n-1}d[i]=f(x-1)+n-\sum_{i=0}^{x-1}d[i] f(x)=f(x1)+i=xn1d[i]=f(x1)+ni=0x1d[i],其中 f ( 0 ) = 0 f(0)=0 f(0)=0。有了 f ( x ) − f ( x − 1 ) = n − ∑ i = 0 x − 1 d [ i ] f(x)-f(x-1)=n-\sum_{i=0}^{x-1}d[i] f(x)f(x1)=ni=0x1d[i],建一棵线段树/树状数组,每次求区间和就可以得到 f ( x ) f(x) f(x) 了!而如果我们把线段树/树状数组略作修改,将第一个节点设为 ∑ i = 1 n b [ i ] \sum_{i=1}^{n} b[i] i=1nb[i],并依次加入 ∑ i = 0 x − 1 d [ i ] − n \sum_{i=0}^{x-1}d[i]-n i=0x1d[i]n 节点,就可以通过一个直接的 [ 1... x ] [1...x] [1...x] 区间求和得到答案!
至此,仅操作 2 2 2 ,即对于一个序列求任意轮冒泡排序后逆序对个数的问题解决了。

操作 1 1 1 会产生什么影响呢?
只交换相邻两个,只能影响到它们俩有关的量。
方法一:直接更新两棵权值线段树就好了!
方法二:考虑交换 a [ i ] , a [ i + 1 ] a[i],a[i+1] a[i],a[i+1] (同步交换 b b b 数组),初始逆序对个数发生 ± 1 \pm 1 ±1 的改变;另外相当于把 d d d 的两个相邻的桶 ( d [ b [ i ] ] 和 d [ b [ i ] − 1 ] , 或 d [ b [ i + 1 ] ] 和 d [ b [ i + 1 ] + 1 ] ) (d[b[i]]和d[b[i]-1],或d[b[i+1]]和d[b[i+1]+1]) (d[b[i]]d[b[i]1]d[b[i+1]]d[b[i+1]+1]) ,做了修改(左+1右-1或反过来),那么只需要修改左边那个桶对应的上述树状数组的节点值即可!

至此,分析完了(吐血)。

代码

注意long long

方法一
int n,m;
int a[N],b[N]; // a是原始排列,b是左边比它大的数的个数
struct BIT
{
    ll tree[N];
    inline int lowbit(int x) { return x & (-x); }
    inline void init() { memset(tree,0,sizeof(tree)); }
    inline void update(int x,int val=1)
    {
        if(!x)
            return;
        //cout<<x<<' '<<val<<":\n";
        while(x<=n)
        {
            //cout<<x<<endl;
            tree[x]+=val;
            x+=lowbit(x);
        }
    }
    inline ll query(int x)
    {
        ll sum=0;
        while(x>0)
        {
            sum+=tree[x];
            x-=lowbit(x);
        }
        return sum;
    }
    inline ll query(int l,int r) { return query(r)-query(l-1); }
}origin,number;
int main()
{
    read(n),read(m);
    for(int i=1;i<=n;i++)
    {
        read(a[i]);
        b[i]=i-1-origin.query(a[i]-1); // 求左边比a[i]大的数的个数
        origin.update(a[i]); // 插入a[i]
    }
    origin.init();
    for(int i=1;i<=n;i++)
    {
        origin.update(b[i],b[i]); // \sum b[i]
        number.update(b[i]); // \sum 1
    }
    int op,x;
    for(int i=1;i<=m;i++)
    {
        read(op),read(x);
        if(op==2)
            printf("%lld\n",x>=n?0:origin.query(x+1,n)-x*number.query(x+1,n));
        else
        {
            if(a[x]>a[x+1])
            {
                origin.update(b[x+1],-b[x+1]);
                number.update(b[x+1],-1);
                b[x+1]--;
                origin.update(b[x+1],b[x+1]);
                number.update(b[x+1],1);
            }
            else
            {
                origin.update(b[x],-b[x]);
                number.update(b[x],-1);
                b[x]++;
                origin.update(b[x],b[x]);
                number.update(b[x],1);
            }
            swap(a[x],a[x+1]);
            swap(b[x],b[x+1]);
        }
    }
    return 0;
}
方法二
int n,m;
int a[N],b[N],d[N]; // a是原始排列,b是左边比它大的数的个数,d是桶
ll tree[N];
inline void update(int x,ll val=1)
{
    if(!x)
        return;
    //cout<<x<<' '<<val<<":\n";
    while(x<=n)
    {
        //cout<<x<<endl;
        tree[x]+=val;
        x+=lowbit(x);
    }
}
inline ll query(int x)
{
    ll sum=0;
    while(x>0)
    {
        sum+=tree[x];
        x-=lowbit(x);
    }
    return sum;
}
int main()
{
    read(n),read(m);
    ll cnt=0; // 记录初始逆序对个数
    for(int i=1;i<=n;i++)
    {
        read(a[i]);
        b[i]=i-1-query(a[i]-1); // 求左边比a[i]大的数的个数
        update(a[i]); // 插入a[i]
        d[b[i]]++;
        cnt+=b[i];
    }
    memset(tree,0,sizeof(tree));
    update(1,cnt); // 第一个节点记录原逆序对数a
    cnt=0; // 用来求d数组的和
    for(int i=0;i<n;i++)
    {
        cnt+=d[i];
        update(i+2,cnt-n); // 注意第一个节点已经加过了,所以是i+2
    }
    int op,x;
    for(int i=1;i<=m;i++)
    {
        read(op),read(x);
        if(op==2)
            printf("%lld\n",x>=n?0:query(x+1));
        else
        {
            if(a[x]>a[x+1])
            {
                update(1,-1); // 总数-1
                b[x+1]--;
                update(b[x+1]+2); // 左+1右-1
            }
            else
            {
                update(1);
                update(b[x]+2,-1); // 左-1右+1
                b[x]++;
            }
            swap(a[x],a[x+1]);
            swap(b[x],b[x+1]);
        }
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值