POI2000 迷宫的墙
题意描述
给出一棵有 \(n-1\) 个节点树,每个节点都有一种颜色,一共只有三种颜色,每个非叶子节点的出度为 \(3\)。
每个节点都指向编号比自己大的节点。
树以 \(1\) 号节点为根,树的所有叶子节点都指向节点 \(n\),而且每个节点的三条边是有顺序的,编号为 \(1,2,3\)。
针对每一条 \(1\to n\) 的路径,我们可以用一次遍历的颜色和边的编号来代表它,从而得到原树的颜色路径集合。
如果树中存在某些节点,删除它们之后 将剩余节点重新连边 发现仍然可以构造出与原来相同的颜色路径集合。
那么它们被认为是可以删除的,求删除之后的树的总结点数。
算法分析
倒序处理+Hash
这道题看上去很不可做,给定条件很多而且复杂,主要问题是思维的转化。
给出方法:一个节点是可删除的,当且仅当树中存在它的等价节点。
其中两个节点是等价的,仅当两者颜色相同,两者子节点也是等价的(或相同的),而且子节点排列顺序相同。
证明:
两个节点指向相同或等价节点,表示我完全可以将编号较小的节点删去,
将指向它的边重新指向编号较大的节点,从而完成一次完美的删除。
由于子节点排列顺序相同,且两者颜色相同,所以保证了路径上遍历的颜色和边的编号均相同。
值得注意的是,根据定义,如果两个节点是等价的,那么以两者为根的子树形成了一一对应的等价关系。
相当于我们找到了两个同构的子树,显然可以任意删去一个。
代码实现
根据上面的定义,需要判断两个节点是否为等价节点,必须事先判断其子节点。
所以我们采用编号从大到小倒序处理。
对于每个节点,我们将它的子节点与它的颜色组合在一起,进行一次 Hash,存入 map 中。
当出现相同的 Hash 值时,就将当前节点(编号较小)替换为 map 中原有的节点(编号较大)。
具体流程是酱紫的:
- 设 \(to(i)\) 表示 \(i\) 节点的等效节点,首先令 \(to(i)=i,ans=n\)。
- 倒序处理节点,对于每一个节点 \(i\),设其子节点为 \(A,B,C\),其颜色为 \(c(i)\)。
- 令 \(Hash=c(i)\times n^3+to(A)\times n^2+to(B)\times n+to(C)\)。(可以证明不存在 Hash 冲突)
- 如果 \(mp(Hash)=j\),则令 \(to(i)=j,ans--\)。
- 否则令 \(mp(Hash)=i\)。
- 重复执行 \(2\to 5\),直至结束,当前的 \(ans\) 即为答案。
#include<map>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 6010
using namespace std;
int n;
int c[N],to[N];
int a[N][5];
map<long long,int>mp;
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
int main(){
//freopen("lab.in","r",stdin);
//freopen("lab.out","w",stdout);
n=read();
char s[2];
for(int i=1;i<n;i++){
scanf("%s",s);
if(s[0]=='N') c[i]=1;
else if(s[0]=='Z') c[i]=2;
else c[i]=3;
for(int j=1;j<=3;j++) a[i][j]=read();
}
for(int i=1;i<=n+1;i++) to[i]=i;
int ans=n;
for(int i=n-1;i>=1;i--){
long long now=c[i];
for(int j=1;j<=3;j++) a[i][j]=to[a[i][j]];
for(int j=1;j<=3;j++) now=now*n+a[i][j];
if(mp.find(now)==mp.end()) mp[now]=i;
else{ans--;to[i]=mp[now];}
}
printf("%d\n",ans);
return 0;
}