解题报告:Cube Stacking
关键字:ACM/ICPC,解题报告,并查集,路径压缩
题目大意
有最多30000个标有标号的小方块,输入最多100000次操作,其中每次操作包括以下两种:
M x y 将含x的方块堆堆到含y的方块堆上去
C x 计算标号x的方块的下方有多少方块
说明:不会有这样的操作, M x y ,其中x和y在同一堆上。
要求输出每一次计算C操作的结果。
解题思路
初看此题像是模拟题,只要模拟30000个方块,并依照它的指示操作,似乎并非很困难。可是,如果有以下的操作(对于n 个方块,t次操作,t>n):
step1 用约n次不到的操作可以将n个方块叠在一个堆上,
step2 用剩余t-n次操作要求堆顶下面含有的方块数
这样,总共的计算量约有(t-n+1)*n,复杂度约为O(tn),最糟情况有约3e9次计算。对于单case 1000ms来说,很容易超时。
加速算法的有效手段是用空间换时间,通过预先记录部分方块的值,来避免每次都做重复的计算。首先想到的是记录每一堆的总数,即 a堆有 2 4 5 7;b堆有 1 3 6 ,则记录sum(a)=4 sum(b)=3,那么在通过一次M 4 3(将a堆放在b堆上),c( ai )=i+sum(b) ,c( bi )=i。其中i还是要通过迭代找到元素ai停止来求得。光记录总数是不够的,因为它的最糟情况和上面的最糟情况类似,几乎没有改进。
因此,可以记录c()来避免多次迭代求i。
公式改为c( ai )=c( ai )+sum(b) ,为了使堆的表示简化,采用并查集的形式,这样y的代表元素是find_set(y),公式再修改为: (xi表示x的堆中的第i个元素)
c( xi )=c( xi )+sum(find_set(y)) 初始c(i)=0 sum(i)=1
想法虽好,但实现不好,因为程序需要始终更新c(xi),这个操作同样费事,比如每次对同一个堆上加一块方块,这样更新c(xi)的计算量就是(n-1)*(n-2)/2,是O(n*n),实质上还是平方级的。
为了替换每次更新的辛苦操作,我们用les[ ]数组记录c(xi)上方的元素个数,可以从下面这个方程了解les的作用:
c( xi )=sum(find_set(x))-les( xi ) 初始c(i)=0 sum(i)=1
这样,不需要维持一个c数组,只要在执行C操作时,求一下c就可以了。而原本c的维护改为了les的维护。乍看之下,les也不易维护,但结合并查集的路径压缩,不但能灵活更新les数据,并且可以将并查集的路径压缩得很好。
例如对于 a b c d e f g (左边为堆底,右边为堆顶,即c(f)=5 ,以g为该堆代表)
现在将 h i j k 堆在其上方,合并操作如下:
1 les[g]=sum[k] 先更新les[g],a到g元素在查找时边压缩路径边更新
2 sum[k]+=sum[g] 更新sum[k],因为以k为该堆的代表。
而find_set()需要实现如下操作:
当前状态:sum[k]=12 les为在合并操作后的les状态,路径是已压缩
a b c d e f g h i j k
parent g g g g g g k k k k k
les 6 5 4 3 2 1 4 3 2 1 0
例如find_set(a), 则因为递归会调用find_set(f),更新les[ f ]=les[g] +les[f]=5,并压缩f,f 更新完毕;返回到find_set(h),更新les[ h ]=les[g]+les[h]=6,并压缩h。如此反复,便可以得到:
a b c d e f g h i j k
parent k k k k k k k k k k k
les 10 9 8 7 6 5 4 3 2 1 0
虽然用到了递归,但是深度是随着 find_set 的调用而减少直至2层,所以只要经常调用 find_set 路径就可以很快得压缩了,那么递归的消耗根本不算什么。
有了les和sum的计算,所求的c就可以通过 c=sum-les-1 (减一是因为我取堆顶les为0,如果要避免减一则需要在sum的更新和les初始时做些改动,其实是无碍的)
解题程序
C++程序 Mem: 440K Time:169MS
#define MAX 30050 // 根据题意,最多30000个方块
int p[MAX], sum[MAX],les[MAX]; // p记录集合代表,sum和les辅助计算
void init() { // 初始化工作
for (int i = 0; i < MAX; i++) {
p[i] = i; sum[i]=1; les[i]=0;
}
}
void link(int x, int y) { // 连接两个集合
p[y] = x; les[y]=sum[x]; sum[x]+=sum[y];
}
int getles(int top,int c){ // 压缩路径,更新les数组
if(p[c]!=top){
les[c]+=getles(top,p[c]); // 更新c的les值
p[c]=top; // 压缩路径
}
return les[c];
}
int find_set(int d) { // 查找元素在集合的代表
int t=p[d];
if(d!=p[d]){
t=find_set(p[d]);
getles(t,d); // 查找的同时压缩路径
}
return p[d]; // 因为压缩了,所以p[d]才是集合代表
}
void union_set(int x, int y) { // 合并两个集合
link(find_set(x),find_set(y));
}
int main(){
int p,x,y;
char op;
scanf("%d",&p); // 输入操作数目
init();
while(p--){
scanf("/n%c",&op); // 输入操作
switch(op){
case 'M':
scanf("%d%d",&x,&y);
union_set(x,y); // 合并堆操作
break;
case 'C':
scanf("%d",&x);
printf("%d/n",sum[find_set(x)]-les[x]-1); // 输出c操作
break;
}
}
}
心得体会
这是一道比较好的关于数据结构的题目,它的主要考点是并查集的概念。但是能够想到并查集并不能帮助我们解题,因为题目的要求比简单的并查集要复杂得多。为了解决这道题目,需要修改并查集的union_set和find_set两个基本操作,而在find_set中如果知道路径压缩的方法便能更好得解决这道题目了。
通过这题的练习,我学到了两点,其一学习数据结构不能只学习表面,需要深入才能应用自如;其二是针对不同的问题要善于修改一些常用的结构和工具,以灵活应付各种题型对于解题是相当有好处的。
相关链接
Pku1988:
http://acm.pku.edu.cn/JudgeOnline/showproblem?problem_id=1988