改造二叉树

Posted on 2019-02-03 09:48  Mocking_Jimmy  阅读(167)  评论(0编辑  收藏  举报

一月20日

Description

什么是二叉搜索树呢?二叉搜索树首先是一棵二叉树。设 \(key[p]\) 表示结点 \(p\) 上的数值。对于其中的每个结点p,若其存在左孩子 \(lch\) ,则 \(key[p]>key[lch]\) 若其存在右孩子 \(rch\) ,则 \(key[p]<key[rch]\) 注意,本题中的二叉搜索树应满足对于所有结点,其左子树中的 \(key\) 小于当前结点的 \(key\) ,其右子树中的 \(key\) 大于当前结点的 \(key\)

现在给定一棵二叉树,可以任意修改结点的数值。修改一个结点的数值算作一次修改,且这个结点不能再被修改。若要将其变成一棵二叉搜索树,且任意时刻结点的数值必须是整数(可以是负整数或0),所要的最少修改次数。相信这一定难不倒你!

Input

第一行一个正整数 \(n\) 表示二叉树结点数。结点从 \(1\thicksim n\)进行编号。

第二行 \(n\) 个正整数用空格分隔开,第 \(i\) 个数 \(a_i\) 表示结点 \(i\) 的原始数值。

此后 \(n - 1\) 行每行两个非负整数 \(fa\) , \(ch\) ,第 \(i + 2\) 行描述结点 \(i + 1\) 的父亲编号 \(fa\) ,以及父子关系 \(ch\) ,( \(ch = 0\) 表示 \(i + 1\) 为左儿子,\(ch = 1\) 表示 \(i + 1\) 为右儿子)。

结点1一定是二叉树的根。

Output

仅一行包含一个整数,表示最少的修改次数。

Sample Input

3
2 2 2
1 0
1 1

Sample Output

2

Data Constraint

20 % \(n <= 10 , a_i <= 100\)

40 % \(n <= 100 , a_i <= 200\)

60 % \(n <= 2000\)

100 % \(n <= 10 ^ 5 , a_i < 2 ^ {31}\)

Solution

比赛时我根本没有看懂题目,所以没有比赛时的暴力思路。。。没办法题意太恶心。

正解:

非常妙的一道题。首先如果看到这是一棵树就往树形 \(dp\) 上想那肯定就想不出来了。注意这是一棵二叉搜索树,所以有一些非常奇妙的性质。熟悉平衡树的人应该知道,二叉搜索树的中根遍历是严格上升的。所以我们将中根遍历求出来就好了。现在的问题变成最少修改一个序列上的一些数,使得其严格上升

首先直接求出 \(LIS\) 的长度再用 \(n\) 一减肯定是错的。我们考虑一下反着做,我们设 \(f_i\) 表示前 \(i\) 个数里最多有多少个不用修改。对 \(i\) 位置上的数 \(a_i\) 如果前面有一个位置 \(j\) 满足 \(a_i-a_j>=i-j\) 的话,我们就可以把 \((i,j)\) 上的数全部修改而且能使得其严格递增。所以就有方程

\[f_i=max(f_j+1)[a_i-a_j>=i-j] \]

也就是 \(a_i-i>=a_j-j\) 那就是所有的数减掉下标之后求一个最长不下降子序列啊,最后拿 \(n\) 减去这个长度就好了

可以 \(O(nlogn)\) 搞定


\(by\) asuldb

摘自 https://35178.blog.luogu.org/solution-p3365

然而 \(O(nlogn)\) 求最长不下降子序列我也不会。

O(nlogn)的算法关键是它建立了一个数组b[]b[i]表示长度为i的不下降序列中结尾元素的最小值,用k表示数组目前的长度,算法完成后k的值即为最长不下降子序列的长度。

具体点来讲:
不妨假设,当前已求出的长度为k,则判断a[i]b[k]

如果 \(b[k]≤a[i]\) ,即 \(a[i]\) 大于长度为k的序列中的最后一个元素,这样就可以使序列的长度增加1,即 \(k=k+1\) ,然后更新 \(b[k]=a[i]\)

如果 \(b[k]>a[i]\) ,那么就在 \(b[1] \to b[k]\) 中找到最大的j,使得 \(b[j] < a[i]\) ,即a[i]大于长度为j的序列的最后一个元素,显然,\(b[j+1]≥a[i]\) , 那么就可以更新长度为 \(j+1\) 的序列的最后一个元素,即 \(b[j+1]=a[i]\)

可以注意到:b[i]单调递增,很容易理解,长度更长了,b[k]的值是不会减小的,更新公式可以用二分查找,所以算法复杂度为 \(O(nlogn)\)


\(by\) Shawn-Xue

摘自 https://blog.csdn.net/axiqia/article/details/77427735

照着上面的方法打,我调了2个小时没调出来,决定放弃。换一种方法。

准确的说,并不是接在谁后面。而是替换掉谁。因为它接在前面的谁后面都是没有意义的,再接也超不过最长的 \(len\) ,所以是替换掉别人。那么替换掉谁呢?就是替换掉那个最该被它替换的那个。也就是在d数组中第一个大于它的。第一个意味着前面的都小于等于它。假设第一个大于它的是d[j],说明d[1..j-1]都小于等于它,那么它完全可以接上d[j-1]然后生成一个长度为j的不下降子序列,而且这个子序列比当前的d[j]这个子序列更有潜力(因为这个数比d[j]小)。所以就替换掉它就行了,也就是 \(d[j]=a[i]\) 。其实这个位置也是它唯一能够替换的位置(前面的替了不满足d[k]最小值的定义,后面替换了不满足不下降序列)

至于第一个大于它的怎么找……STL upper_bound。每次复杂度 \(logn\)


\(by\) lvmememe

摘自 https://www.cnblogs.com/itlqs/p/5743114

终于,在我不懈的努力下,调出了这个代码。。。

#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;

ll n,key[100005],ch[100005][2];
ll a[100005],dive;
ll b[100005],k;

void dfs(ll x)
{
	if(ch[x][0]) dfs(ch[x][0]);
	a[++dive]=key[x];
	if(ch[x][1]) dfs(ch[x][1]);
}
ll find(ll z)
{
	ll l=0,r=k,mid;
	while(l+1<r)
	{
		mid=(l+r)/2;
		if(b[mid]<=z) l=mid;
		else r=mid;
	}
	return r;
}

int main()
{
	scanf("%lld",&n);
	for(ll i=1;i<=n;i++)
		scanf("%lld",&key[i]);
	for(ll i=2,x,y;i<=n;i++)
	{
		scanf("%lld%lld",&x,&y);
		ch[x][y]=i;
	}
	dfs(1);
	for(ll i=1;i<=n;i++)
		a[i]-=i;
	b[0]=-2e9;
	for(ll i=1,j;i<=n;i++)
	{
		if(b[k]<=a[i]) b[++k]=a[i];
		else
		{
			j=find(a[i]);
			b[j]=a[i];
		}
	}
	printf("%lld\n",n-k);
	return 0;
}