把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【洛谷6776】[NOI2020] 超现实树(思维)

点此看题面

  • 给定一个由\(m\)棵无标号、区分左右儿子的二叉树组成的集合。
  • 规定一棵树\(T\)能变成\(T'\),当且仅当它能通过若干次操作,每次把一个叶节点替换成一棵任意形状的非空二叉树,最终得到\(T'\)
  • 要求判断是否只有有限种二叉树是无法通过这个集合中的树变得的。
  • 数据组数\(\le100,\sum n,\sum m\le2\times 10^6\)

怎么说呢,这种题目就是做的时候非常难想,而一看题解却又对着结论直呼傻逼。。。

想不到给它加什么标签了,因此便硬凑了个“思维”的标签上去。

一个递归的思路

\(f(V,x)\)表示对于集合\(V\)中的所有树,它们以\(x\)为根的子树是否几乎完备。那么我们要求的实际上就是\(f(T,1)\)

首先,如果在集合\(V\)中存在一棵树,满足它的点\(x\)是叶节点,显然它必然完备了。

否则,我们考虑什么样的情况下多棵树的组合才能让它几乎完备。

发现以\(x\)为根的树有四种:单独一个点、没有左儿子的、没有右儿子的、左右儿子都有的。

其中第一种树显然是无法得到的,但因为它只有这么一种,是有限的,所以无法得到也没关系。

接下来就对于剩余三种分类讨论一下。

没有左儿子的/没有右儿子的

以没有左儿子的情况为例,那么我们需要先找出\(V\)中所有没有左儿子的树的集合\(V_1\),然后其实就是要判断它们的右子树是否几乎完备,即\(f(V_1,rc)\)是否为\(1\)

而没有右儿子的情况,相应的就是找出所有没有右儿子的树的集合\(V_2\),然后判断\(f(V_2,lc)\)是否为\(1\)

注意,只要\(f(V_1,rc)=1\)\(f(V_2,lc)=1\)中有一个不满足,无法得到的树就有无限种。

左右儿子都有的

首先,如果前面的两种都满足了,而在\(V\)中剩下那些左右儿子都有的树的集合中存在某一棵树满足它的左右儿子都是叶节点,那么就能证明\(f(V,x)=1\)了,这应该是显然的。

接下来,我们先证明,对于一棵左右儿子都不是叶节点的树,它是没用的:

  • 如果它能由\(V\)中的其他树变成:显然它是没用的。
  • 如果它不能由\(V\)中的其他树变成:考虑把它的左子树换成单独一个点,得到的新树仍然不可能由其他树变成(否则就能由其他树先变成这棵新树,再变成当前树),而且也不可能由当前树变成。这样一来,我们有无限种方式在这棵新树的右子树中加点,使得得到的树无法生成。也就是说,不管是否有当前树,这个集合都是不完备的,那么当前树显然没用了。

因此,我们只要求出所有左儿子是叶节点的树的集合\(V_3\)和所有右儿子是叶节点的树的集合\(V_4\),判断是否满足\(f(V_3,rc)=f(V_4,lc)=1\)即可。

时间复杂度

由于\(V_1,V_2,V_3,V_4\)无交,对于一棵树我们访问的次数不会超过它的高度,更不会超过它的点数,肯定正确。

注意,具体实现和题解中的说法可能有些差异。我们不必统一每棵树中的节点编号,只要知道当前点对应到集合中每棵树的哪个点即可。

代码:\(O(\sum n)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 2000000
#define pb push_back
using namespace std;
int m,n[N+5];vector<int> lc[N+5],rc[N+5];
struct node {int p,x;I node(CI a=0,CI b=0):p(a),x(b){}};vector<node> V[N+5];
class FastIO
{
	private:
		#define FS 100000
		#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
		#define D isdigit(c=tc())
		char c,*A,*B,FI[FS];
	public:
		I FastIO() {A=B=FI;}
		Tp I void read(Ty& x) {x=0;W(!D);W(x=(x<<3)+(x<<1)+(c&15),D);}
}F;
vector<node>::iterator it;I bool Check(CI T)//检验V[T]集合
{
	#define REP for(it=V[T].begin();it!=V[T].end();++it)
	#define LC lc[it->p][it->x]//左儿子
	#define RC rc[it->p][it->x]//右儿子
	#define Exit(v) return V[T].clear(),v//清空vector并返回v
	if(V[T].empty()) return 0;REP if(!LC&&!RC) Exit(1);//如果为空返回0,如果是叶节点返回1
	REP !LC&&(V[T+1].pb(node(it->p,RC)),0);if(!Check(T+1)) Exit(0);//如果f(V1,rc)=0返回0
	REP !RC&&(V[T+1].pb(node(it->p,LC)),0);if(!Check(T+1)) Exit(0);//如果f(V2,kc)=0返回0
	REP if(LC&&RC&&!lc[it->p][LC]&&!rc[it->p][LC]&&!lc[it->p][RC]&&!rc[it->p][RC]) Exit(1);//如果存在左右儿子都是叶节点的点返回1
	REP LC&&RC&&!lc[it->p][LC]&&!rc[it->p][LC]&&(V[T+1].pb(node(it->p,RC)),0);if(!Check(T+1)) Exit(0);//如果f(V3,rc)=0返回0
	REP LC&&RC&&!lc[it->p][RC]&&!rc[it->p][RC]&&(V[T+1].pb(node(it->p,LC)),0);Exit(Check(T+1));//返回f(V4,lc)
}
int main()
{
	RI Tt,i,j,x,y;F.read(Tt);W(Tt--)
	{
		for(F.read(m),i=1;i<=m;++i) for(F.read(n[i]),V[0].pb(node(i,1)),
			lc[i].pb(0),rc[i].pb(0),j=1;j<=n[i];++j) F.read(x),F.read(y),lc[i].pb(x),rc[i].pb(y);//存储一棵树的信息
		puts(Check(0)?"Almost Complete":"No");//验证对于整个集合,以1为根的树是否完备
		for(i=1;i<=m;++i) lc[i].clear(),rc[i].clear();//清空
	}return 0;
}
posted @ 2020-12-18 18:51  TheLostWeak  阅读(94)  评论(0编辑  收藏  举报