题意:
给你一棵每个非叶子节点都有两个叶子的二叉树,只有叶子上有值,你可以选择交换任意的一个点的左右子树,使得最后的树按照前序遍历之后所有叶子的值形成的数列的逆序对最少。叶子个数<=200000
此题树的读入方式比较奇怪,是先读入一个n,表示叶子的个数,再递归的读入,如果当前是个0,那么接下来会递归到左右子树,如果是有值的,那么就意味着当前这个点是叶子,它的值就是读入的值。
题解:
这次不是全机房都会,而是全机房都熟练掌握的算法, 我却不会了QAQ。感觉他们都飞速进步,只有我那么菜,要加把劲啊QAQ。
最近刚开始学线段树合并。我来介绍一下线段树合并,来作为我简单的学习笔记。
相信大家对线段树都已经比较熟悉了。有时候,我们需要解决这样一个问题,我们在两棵线段树上都维护了一些信息,现在要合并两棵线段树的信息,得到一棵新的线段树。由于空间原因,我们通常需要使用动态开点线段树,有时候还需要离散化一下。我们用递归的方式完成重建,就是分别对线段树的左右子树进行重构。在重构的时候我们进行信息合并,如果记录的是和的话就相加,最值就再取一个新的最大或最小之类的,还是像往常的有些线段树一样,合并还是可能会成为难点的吧。然后如果到了一个节点,存在有一棵树没有这个节点,那么我们就可以返回了,返回之前,如果是有一棵树有这个子树,那么我们直接继承那棵子树的信息即可。
update on 2021.8.16 :
一直不太会这个东西的复杂度证明,这几天胡思乱想+四处询问+网上找讲解了很久,终于(我觉得是)想明白了一个证明方法。这个东西网上时间复杂度的讲解真的是乱七八糟,什么都有,说法都有,但是就是不能理解,甚至某THU大佬告诉我去搞个势能分析就行,我直接懵逼。
先说空间复杂度的证明。我们设插入权值次数为 m m m,在插入一个权值时像主席树一样,新建了 l o g m logm logm个节点,总复杂度是 m l o g m mlogm mlogm。对于合并一棵树和它的一个子树,我们过程中是可以不用新开节点的。当然,如果新开节点,复杂度也应该没有错,因为每条从根到线段树上一个叶子链只会在两棵线段树都有这个叶子时被新建一次,这样每次新建都会少一个叶子,换句话说,这个新建过程也是 m l o g m mlogm mlogm的,所以新建可能空间大了一倍常数。
再说时间复杂度。首先新建节点的总时间复杂度是 m l o g m mlogm mlogm的。比较关键的是考虑合并过程的复杂度。考虑每次合并,只有两棵树都有某个叶子时才会递归到底,每次合并后,下次合并的树就相当于少了一个需要考虑的叶子节点,换句话说,每一个叶子到根的那条链最多只会被合并掉一次,所以递归到底的总复杂度是 m l o g m mlogm mlogm的。对于没有递归到底,中途继承了某个线段树的节点的情况,一定是在某个要合并的叶子到根的路径上的另一个分支出现的,所以这个过程的复杂度相当于对于单个合并的叶子是 l o g m logm logm,总复杂度也是 m l o g m mlogm mlogm,相当于增大了一倍常数。于是总的复杂度是 m l o g m mlogm mlogm的。可以发现,这个复杂度和 n n n关系并不大,关键在于向线段树中插入了多少个数。
当然,上面的 l o g m logm logm可能更小,在离散化后是 l o g 不 同 的 权 值 个 数 log不同的权值个数 log不同的权值个数。
线段树合并大体就是这么一个思想,这个题可以看作一个线段树合并的模板题。下面来介绍一下这个题的写法。
我们这个题是建权值线段树,权值线段树维护该权值区间的数出现的次数,并且使用动态开点的方法。我们每次是合并两个子树原来的线段树。
我们考虑一棵子树内的逆序对是由哪些部分产生的:首先是左右子树内部,然后是左右子树之间。左右子树内部的逆序对可以递归下去做,所以我们着重考虑左边的子树对右边的子树的影响。我们可以交换左右子树,所以对于一个确定的权值区间,产生的逆序对可以是在原树左子树的右区间出现的所有数和原树右子树的左区间的所有数出现的次数相乘,满足一个乘法原理的关系还是比较显然的。然后我们再递归到左右权值区间继续求值。对于一个子树,在合并的时候顺便求出交换和不交换的逆序对,由于是否交换当前点的左右子树是不会对子树外的逆序对数产生影响的,所以在当前子树贪心地选更小的那种情况就可以了。
然后就做完了,下面就是代码了。
代码:
#include <bits/stdc++.h>
using namespace std;
int n,cnt;
long long ans,ans1,ans2;
struct node
{
int l,r;
long long s;
}tr[8000010];
inline int read()
{
int x=0;
char s=getchar();
while(s>'9'||s<'0')
s=getchar();
while(s>='0'&&s<='9')
{
x=x*10+s-'0';
s=getchar();
}
return x;
}
inline void update(int &rt,int l,int r,int y)
{
if(!rt)
rt=++cnt;
++tr[rt].s;
if(l==r)
return;
int mid=(l+r)>>1;
if(y<=mid)
update(tr[rt].l,l,mid,y);
else
update(tr[rt].r,mid+1,r,y);
}
inline void merge(int &l,int r)
{
if(!l||!r)//有一棵树没有这分个分支
{
l=l+r;//直接继承那个分支
return;
}
tr[l].s+=tr[r].s;
ans1+=tr[tr[l].r].s*tr[tr[r].l].s;
ans2+=tr[tr[l].l].s*tr[tr[r].r].s;
merge(tr[l].l,tr[r].l);
merge(tr[l].r,tr[r].r);
}
inline void solve(int &x)
{
int opt,l,r;
x=0;
opt=read();
if(!opt)
{
solve(l);
solve(r);
ans1=0;
ans2=0;
x=l;
merge(x,r);
ans+=min(ans1,ans2);
}
else
update(x,1,n,opt);
}
int main()
{
n=read();
int qwq=0;
solve(qwq);
printf("%lld\n",ans);
return 0;
}