传送门:洛谷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 2≤n,m≤2e5 ,操作 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]−x∑b[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(x−1)+b[i]≥x的个数 得到。因为
0
≤
b
[
i
]
<
n
0\leq b[i]<n
0≤b[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(x−1)+∑i=xn−1d[i]=f(x−1)+n−∑i=0x−1d[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(x−1)=n−∑i=0x−1d[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=0x−1d[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;
}