状态精简是一类极其重要的方法,在动态规划、组合计数中的应用尤为普遍。先来看一些习题:
1.LA 4380(CERC 2008) Counting Heaps
题意:给出一颗$n(1 \leq n \leq 500000)$个结点的有根树,要求给结点编号为$1 \sim n $,使得不同结点的标号不同,且每个非根结点标号比父节点小(满足堆性质)。求方案总数除以$m$的余数$(2 \leq m \leq 10^9)$。当$n=5$时有如下$8$种方案。
分析:我们可以这样给结点分配编号:按照从小到大的顺序分配,最小的必然是树根,次小的分配给根的子结点中的某一个...。也就是给出一个满足父结点小于所有子树结点的一个拓扑序,那么维护一个下一次可以被分配为分配最小编号的结点队列,直到队列为空,初始状态只有根节点在队列中。这样遍历所有节点一次就找到了一个方案,方法可行但无效,复杂度太高。为什么?因为我们考虑得太细致了,把所有细节都兼顾的代价就是无法承受的复杂度,虽然方法对于答案是充分的,但是却并不必要。计数并不等同于枚举。更好的方法?深入挖掘问题的实质。实际上该问题是有局部性质的:我们固定每棵子树的所有结点编号的集合,那么总的方案数就是确定的,因为满足局部拓扑序必然不会破坏整体,而局部的相对顺序已经由子树的形态给出,我们不需要知道赋给每个结点的编号是具体是几,而只需要知道它是第几大。我们设$f(i)$为根节点为$i$的子树在所有候选编号集合固定的情况下的方案数,那么考虑$i$的一个子节点$j$,我们用$g(u)$表示以$u$为根结点子树的大小,那么$j$子树的编号可取集合总数为$\textrm{C}_{g(i) - 1}^{g(j)}$,于是$j$子树的总方案数为$\textrm{C}_{g(i) - 1}^{g(j)}f(j)$,这样剩下部分(将$j$子树从$i$子树中删去)的总方案数与当前$j$子树的总方案数的乘积即为$i$子树的总方案数。即有$f(i)=f(j)\textrm{C}_{g(i) - 1}^{g(j)}f(i\setminus j)$其中$f(i\setminus g)$表示将$j$子树从$i$子树中删去得到的子树。我们设$i$子树有$p$个子结点,第$i$个为$n_i$,应用上式可以得到$f(i)=\prod_{j=1}^{p}{f(n_j)\textrm{C}_{g(i)-1-\sum_{k<p}{g(n_k)}}^{g(n_j)}}$,于是$O(n)$时间即可解决。