P8499 [NOI2022] 挑战 NPC Ⅱ 题解
思路
实际上就是一个超级暴搜题。
注意到题目已经保证了不卡哈希,于是我们考虑用树哈希来判断是否同构。由于给定了根,因此这是很容易做到的。
然后发现题目要求的可以删去的节点个数非常小,只有常数级别,于是我们考虑一些相对暴力的做法:
对于两棵树,我们去遍历这两棵子树中的节点 \(x\) 和 \(y\) 然后求出使得这两棵子树同构的最小代价。
然后我们注意到如果 \(x\) 和 \(y\) 的某些子树同构的话,那么我们就不需要管,于是我们可以先将两个节点的同构的子树全部判掉。
然后我们考虑这个时候 \(x\) 与 \(y\) 剩下的子树的个数一定已经不大于 \(k\) 了,因为我们对于每一种不同构的子树我们都要至少删去一个点才能使得其同构,于是如果不同构的子树个数大于 \(k\) 那么返回一个极大值即可。
由于不同构的子树个数很小,因此我们可以直接暴力枚举 \(x\) 与 \(y\) 的哪两个子树需要删成同构的,求出如果要将其删成同构的最小代价。
于是我们直接去枚举一个全排列,算出这样一一对应出来的贡献,然后对所有的贡献取一个 \(\min\) 即可。
于是操作实际上就三步:去除同构的子树,算出任意一对不同构子树变成同构的贡献(继续 dfs 下去),枚举全排列统计贡献。
边界条件就是如果 \(y\) 所在的子树大小恰好为一,那么我们就要将 \(x\) 删成只有 1 个点,于是返回 \(siz_x-1\) 即 \(x\) 所在子树的点的个数减 1。
复杂度上界大概是 \(O(k\times k!n)\),阈值是在枚举全排列统计贡献那里。然后发现好像不太能过,考虑有非常多的情况显然是没有用的,考虑减一下枝。
比如如果 \(x\) 的大小小于 \(y\) 或者 \(x\) 的子树个数小于 \(y\),那么我们显然不可能将点越删越多,于是直接返回极大值即可。
然后我们在 dfs 的时候不是所有时候删点的次数上界都是 \(k\)。在向下 dfs 的时候,假如说 \(x\) 有 \(cnt\) 个与 \(y\) 的子树不同构的子树,当前可以删除的点数量的上界是 \(lim\),那么 \(x\) 的子树可以删的点的个数的上界就是 \(lim-cnt+1\),因为其他每个不同构的子树至少要删去 1 个点才可能同构。
然后我们加上前面说的如果不同构的子树数目大于了可以删去的点的上界 \(lim\),那么就返回极大值。然后就足以通过此题了,同时可以跑的比较快没有卡常风险。
code
实际上有很多题解的复杂度都不太对,因为在上面“去除同构子树”这一步的时候,很多题解代码的实现都使用了 erase 函数。但这个东西复杂度是错误的,具体可以 看我发的工单。
我同机房的一些同学认为可以使用 set 来维护,我认为有点道理。我个人是使用了 map 来维护某种哈希值的已经被删去的点编号的最小值。
具体的,我使用了 pair 来将一个点子树的哈希值以及这棵子树的编号绑定在一起。由于 pair 的双关键字特性,我在预处理哈希值的时候将一个点所有的子树的哈希值与编号用 vector 存了起来然后排了遍序,这样所有哈希值相同的点都在一起且哈希值相同的点编号从小到大排列。
于是我们可以用二分查找找到对于 \(x\) 的某个子树,第一个与其哈希值相同并且没有被去掉的 \(y\) 的子树,具体的实现可以看代码。
在将 \(y\) 的没有被去掉的子树提取出来的时候,由于对于某种哈希值而言,所有没有被去除的子树在预处理出来的 vector 中一定是连续的,因此我们同样可以用二分查找找出这种哈希值的没有被去掉的子树在 vector 上的区间。
最后由于可能 \(x\) 不同构的子树个数要多余 \(y\) 不同构的子树个数,因此多出来的 \(x\) 的这部分子树需要全部被删掉。于是在枚举全排列的时候其表现形式就是如果其对应的编号大于 \(y\) 不同构的子树个数,那么贡献就加上其大小,也就是全部删完。
最后讲个笑话,这道题写的不卡哈希,于是我自信的写出了自然溢出的树哈希,然后就被卡飞了。
#include<bits/stdc++.h>
using namespace std;
#define ll unsigned long long
#define pii pair <ll,int>
#define mp make_pair
const int N=1e6+7,inf=1e8+7;const ll bas=13,p=1e9+9;
mt19937 rnd(time(0));
int val=rnd(),add=rnd(),n,m,rta,rtb,k;
vector <int> q[N];vector <pii> stk[N];
namespace hs{
	ll f[N];int siz[N],tot[N];
	ll get(ll x){x^=val,x%=p;x<<=10,x%=p;return (x+1919810ull)%p;}
	void dfs(int u){
		siz[u]=1;
		for(int v:q[u]){
			dfs(v);siz[u]+=siz[v];(f[u]+=get(f[v]))%=p;stk[u].push_back(mp(f[v],v));tot[u]++;
		}
		(f[u]+=add)%=p;sort(stk[u].begin(),stk[u].end());
	}
}
using namespace hs;
void init(){for(int i=1;i<=n+m;i++)f[i]=siz[i]=tot[i]=0,q[i].clear(),stk[i].clear();}
map <ll,int> id;
int calc(int x,int y,int lim){
	if(siz[x]<siz[y]||tot[x]<tot[y])return inf;
	if(siz[y]==1)return siz[x]-1;
	vector <int> u,v;
	for(pii a:stk[y]) id[a.first]=0;  //注意到如果不加这一句话可能会在后面遍历 id 的时候漏掉 y 的一些子树(因为那里的查找是基于 id 的) 
	
	for(pii a:stk[x]){
		ll w=a.first;int loc=lower_bound(stk[y].begin(),stk[y].end(),mp(w,id[w]))-stk[y].begin();
		if(loc<tot[y]&&stk[y][loc].first==w) id[w]=stk[y][loc].second+1;
		else u.push_back(a.second);
	}
	int cu=u.size(),cv=0,ans=inf;
	
	if(cu>lim){id.clear();return inf;}
	
	int g[10][10],pai[10];
	for(auto a:id){
		ll w=a.first;
		int loc=a.second;
		int l=lower_bound(stk[y].begin(),stk[y].end(),mp(w,loc))-stk[y].begin();
		int r=lower_bound(stk[y].begin(),stk[y].end(),mp(w+1,0))-stk[y].begin()-1;
		for(int j=l;j<=r;j++)v.push_back(stk[y][j].second),cv++; 
	}
	id.clear();
	
	for(int i=0;i<cu;i++)for(int j=0;j<cv;j++)g[i][j]=calc(u[i],v[j],lim-cu+1);
	
	for(int i=0;i<10;i++)pai[i]=i;
	do{
		int tmp=0;for(int i=0;i<cu&&tmp<ans;i++)tmp+=pai[i]<cv?g[i][pai[i]]:siz[u[i]];
		ans=min(ans,tmp);
	}while(next_permutation(pai,pai+cu));
	return ans;
}
void solve(){
	cin>>n;for(int i=1,x;i<=n;i++){cin>>x;if(x==-1)rta=i;else q[x].push_back(i);}
	cin>>m;for(int i=1,x;i<=m;i++){cin>>x;if(x==-1)rtb=i+n;else q[x+n].push_back(i+n);}
	dfs(rta),dfs(rtb);int ans=calc(rta,rtb,k);
	cout<<(ans<=k?"Yes\n":"No\n");
}
signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int C,T;cin>>C>>T>>k;while(T--)solve(),init();
	return 0;
}
                    
                
                
            
        
浙公网安备 33010602011771号