这道题给人的第一感觉像是树形,我们可以按照树形
的思路进行递归处理,统计一个结点左右子树贡献的答案时,我们分别算出是否交换左右子树的答案,取较大值并向上更新(这样是符合最优子结构的)。问题来了,我们如何快速把两棵子树合并并把答案贡献到父节点呢?线段树可以做到。为了让线段树支持合并操作,我们不能再用堆式的存储方法,而要使用动态开点的方法。我们以原二叉树中的每一个叶子节点为根建立线段树,注意我们建立的是权值线段树,
中存储的是原二叉树上权值处在
结点代表的区间中的叶子结点的个数。建出
棵线段树的
空间复杂度看起来不能接受,但实际上,我们使用动态开点的方法,建立的每棵线段树都不是完整的,因为有些结点根本不会用到,而我们只在需要的时候开点,因此,每棵线段树存的只是根节点到叶子结点的一条链,长度为
,总空间复杂度为
,可以接受。在合并的过程中,因为线段存储的是结点个数,我们可以方便地在
的时间内直接算出逆序对个数,自然也不需要考虑如何把逆序对个数存进线段树中。完整代码如下。
#include<cstdio>
#define maxn 400010
#define reg register
#define cmin(_x,_y) (_x<_y?_x:_y)
using namespace std;
int n,tot,w[maxn],lson[maxn],rson[maxn],root[maxn];//原二叉树的数据
int cnt,ls[maxn<<4],rs[maxn<<4],num[maxn<<4];//权值线段树的数据
long long ans,numl,numr;
int read()
{
reg char ch=getchar();reg int in=0;
while(ch>'9'||ch<'0') ch=getchar();
while(ch<='9'&&ch>='0') in=(in<<1)+(in<<3)+ch-'0',ch=getchar();
return in;
}
void input(reg int x)//按照题目要求,递归读入每个非叶子结点的左右儿子
{
w[x]=read();
if(!w[x])
{
lson[x]=++tot,input(tot);
rson[x]=++tot,input(tot);
}
}
int add(reg int x,reg int l,reg int r,reg int v)//动态开点
{
if(l==r) num[x]=1;
else
{
reg int mid=l+r>>1;
if(v<=mid) ls[x]=add(++cnt,l,mid,v);
else rs[x]=add(++cnt,mid+1,r,v);
num[x]=num[ls[x]]+num[rs[x]];//num[x]的意义同上面讲的segtree[x]
}
return x;
}
int merge(reg int x,reg int y)//合并子树并更新答案
{
if(!x||!y) return x+y;
numl+=1ll*num[rs[x]]*num[ls[y]];//numl为不交换左右子树的逆序对个数
numr+=1ll*num[ls[x]]*num[rs[y]];//numr为交换左右子树的逆序对个数
ls[x]=merge(ls[x],ls[y]);
rs[x]=merge(rs[x],rs[y]);
num[x]=num[ls[x]]+num[rs[x]];//递归合并左右子树并pushup
return x;
}
void work(reg int x)//在原二叉树上递归
{
if(w[x]) return;
work(lson[x]),work(rson[x]);
numl=numr=0;
root[x]=merge(root[lson[x]],root[rson[x]]);
ans+=cmin(numl,numr);
}
int main()
{
n=read(),input(++tot);
for(reg int i=1;i<=tot;i++)
if(w[i]) root[i]=add(++cnt,1,n,w[i]);
work(1);
return !printf("%lld",ans);
}
这里讲一下和
的更新方法。我们在合并一个结点的左右子树之前,先更新
和
。因为我们使用的是权值线段树,所以
代表满足
的结点
的个数,而
代表满足
的结点
的个数。如果不交换左右子树,逆序对增加的个数为
,及左子树中满足
的结点
的个数和右子树中满足
的结点
的个数的乘积。根据乘法原理,左子树中的
个节点中的任何一个大于右子树中的
个节点中的任何一个,它们两两之间可以构成逆序对,因此
(
的更新同理)。