[Gym 102798K] Tree Tweaking
一、题目
给出一个 \(n\) 个点的排列 \(p\),现在要把这 \(n\) 个点按顺序建立二叉查找树,问把 \([L,R]\) 这段区间重排之后所得搜索树的最小深度和是多少。
\(n\leq 10^5,R-L< 200\)
二、解法
首先要知道如何建树,虽然我们不知道二叉搜索树怎么建但是知道 \(\tt Treap\) 怎么建,考虑二叉搜索树其实就是第 \(i\) 个点的 \(heap[i]=i\) 的 \(\tt Treap\),我们考虑依据这个性质构建。
先把所有点按照 \((p_i,i)\) 放在坐标系上,然后发现要满足堆性质就要选纵坐标最小的点当根,然后向两边连左右儿子,横坐标自然满足二叉查找树的性质,发现这就是笛卡尔树,直接拿栈构建即可。
下面是 \(p=\{3,1,2,5,4\}\) 利用上述方法建出的树:
然后考虑重排 \([L,R]\) 其实就是任意交换 \(y\in[L,R]\) 这些点的 \(x\) 坐标,我们把这些点染色,考虑每个极大染色连通块内都只改变内部的结构,而且不会相互影响,其他连通块的结构都不变。原因很简单,因为重排 \([L,R]\) 并不改变其他点和染色点的偏序关系。
那么考虑规划重排过程,对每个极大连通块分别考虑,因为子树大小和等于深度和,那么我们最小化染色点的子树大小和即可。设连通块大小为 \(m\),我们可以把这个连通块所接的 \(m+1\) 个子树全部取出来,把它们的 \(siz\) 按中序遍历排列成 \(a\),每次拿一个根然后把区间划分成两部分,这是显然的区间 \(dp\),设 \(f(l,r)\) 为规划区间 \([l,r]\) 所得的最小子树和:
时间复杂度 \(O(n+(R-L)^3)\)
三、总结
二叉搜索树可以转成笛卡尔树建立,这启示我们简单结构可以从复杂结构反推
树重排规划问题联想区间 \(dp\),因为它的枚举断点过程其实就是树枚举根的过程。
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
#define ll long long
int read()
{
int num=0,flag=1;char c;
while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;
while(c>='0'&&c<='9')num=(num<<3)+(num<<1)+(c^48),c=getchar();
return num*flag;
}
int n,m,L,R,a[M],b[M],st[M],siz[M],ch[M][2];
ll f[205][205],s[M],ans;
void dfs(int u)
{
if(!u || b[u]>R) {s[++m]=siz[u];return ;}
dfs(ch[u][0]);dfs(ch[u][1]);
}
ll solve(int x)
{
m=0;dfs(x);
for(int i=1;i<=m;i++) s[i]+=s[i-1];
for(int i=m;i>=1;i--)
for(int j=i+1;j<=m;j++)
{
f[i][j]=1ll<<60;
for(int k=i;k<j;k++)
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
f[i][j]+=j-i+s[j]-s[i-1];
}
return f[1][m];
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
a[i]=read(),b[a[i]]=i;
L=read();R=read();
for(int i=1;i<=n;i++)
{
while(m && b[st[m]]>b[i]) ch[i][0]=st[m--];
if(m) ch[st[m]][1]=i;
st[++m]=i;
}
for(int i=n;i>=1;i--)
siz[a[i]]=siz[ch[a[i]][0]]+siz[ch[a[i]][1]]+1;
ans=L==1?solve(a[1]):0;
for(int i=1;i<=n;i++) if(b[i]<L || R<b[i])
{
ans+=siz[i];
if(L<=b[ch[i][0]] && b[ch[i][0]]<=R)
ans+=solve(ch[i][0]);
if(L<=b[ch[i][1]] && b[ch[i][1]]<=R)
ans+=solve(ch[i][1]);
}
printf("%lld\n",ans);
}