题意理解
一排编号123...的箱子,一共N个,1<=N<=30000,执行两个操作,一个是把编号i箱子所在的组放到编号j箱子所在的组之上,标记为M i j,一个是查询编号i箱子下面有几个箱子,标记为C i。操作总次数为P,1<=P<=100,000,结果范围每次查询的结果。
问题分析
用并查集数据结构
一步转化,根据并查集的思路,每次合并更新后,合并后集合的每个点都更新父节点,但理解题意,查询的结果是节点 i之下有几个节点。这里有个矛盾,要想效率快,必须路径压缩,路径压缩后,每个集合的节点只记录最远祖先节点。这样势必不能记录中间每个节点位置,不能记录节点位置,就无法计算节点之下有几个节点。思路一个是向上记录操作,思路一个是求向下某个结果。。。
二步转化,用差值,并查集是上,但也是每次从小到大,每次记录向上的增长量,这个增量细化成两类,单集合总节点数量的增长,单节点向上节点数量的增长。这样就实现了对向下量的计算转化成向上量差值的计算。
三步转化,也因为并查集为了效率考虑,只记录了每个节点最远祖先节点,中间节点情况在每次合并时并不能兼顾。考虑这个问题,在计算单集合总节点数量和单节点向上节点数量时,每次合并只会更新合并前节点所在集合的最远祖先节点信息;每次查询时更新单节点的向上节点数量信息和最远祖先编号。在计算单节点之下有几个节点时,先更新单节点的祖先节点的信息(不仅仅是最远祖先),这些信息包括所踩到的祖先节点的向上节点数量和最远祖先节点。 然后取出单节点最远祖先节点的集合总节点数,减去单节点的向上节点数,再减1.
三步转化附,合并和查询在这里做了很复杂的扩展,合并所完成的任务只是更新合并在所在集合的最远祖先节点;查询所完成的任务是更新单节点的沿途向上的最远祖先节点和沿途向上的节点数量。并不是更新每个节点,每次向上跳的步长是不定的,由这个节点距离它第一次合并的集合的最远祖先节点决定。
其他
scanf输入单个可见字符的技巧:scanf如果用%c接受输入字符,会误接受回车换行。可以用%s接受,接受变量用char a[3],然后取第一个字符即可。
此题思路超出我的思考范围,想了一个周末,勉强理解了意思。挺值的。
有些题难想好几天想不通是正常的,只要一直想着,总可以想明白的。
并查集有一个“路径压缩”的动作,对性能提升非常大。对于这题,如果不考虑路径压缩,每个集合实际上都是一颗单支树,是查询效率最低的情况。
这题太细了,几个数组运用的出神入化。
另一个思路就是,不是每次规整的遍历完所有要遍历的任务,而是做有限的事情,再需要是更新必要的信息,更新时也不是更新所有的信息,而是能跳步就可以跳步。不是死板的遍历。。。
代码链接
https://github.com/xierensong/learngit/blob/master/poj/p1988.cpp