前言
最近打TC,做到DP的题很多,以树型DP为主。觉得这些题都挺好的,解出来也是用了不少时间。因此,决定写几篇题解在巩固一下。
题意
给出两个数 和 ,对于 的所有全排列,对于每个全排列建笛卡尔树 ,定义 的分值为其所有有两个子节点的节点两子节点在原排列中索引的差值(非负值)之和。求这个所有 的分值之和 对 取模后的值。
约定
分析
乍一看题,并没有什么思路马上冒出来。这时候,去蛮力地分析是没有效果的。我们需要从笛卡尔树的本质去分析。
PART 1 笛卡尔树的本质
注意到笛卡尔树一棵子树对应的是原序列中一段连续的区间,所以对于一棵给定的子树,这个差值大小只会与区间内元素相对的大小关系有关,并不需要考虑它们具体是什么值。也就是说,就像物理中的相对位置,差值的大小和具体的数值并没有什么关系。
PART 2 动态规划
事实上,上面我们找到的性质对后面的推理有极大的帮助。
我们令
为
的全排列的分值之和,考虑
所在的位置
,它会将原排列一分为二,在树上就是根节点一左一右一棵更小的笛卡尔树。我们可以分左右两部分来计算左右两棵子树的贡献。
首先,若
在第一个或者最后一个位置,那么贡献就是
。
当
在中间位置时,我们需要先需要选
个元素放在
的左边,剩余的放在
的右边。这样子的方案数就是
。
接下来我们考虑每种情况的贡献。
首先对于左子树,对于右边的所有可能排列,它都会产生自己的一个贡献,即
。
然后,考虑两颗子树根节点新产生的贡献,我们设左子树根节点的数值为
,右子树根节点数值为
,那么这个贡献就是
,再进一步,变为
,这样就可以分开计算这个贡献。
而对于右边区间的所有排列,左子树根节点的值可以随意选,并且,对于每个确定了的左子树根节点,其他i-2个树又可以任意排列,那么贡献就是
整合一下式子,并加上右子树的贡献,可以得到转移方程:
当然上面的方程需要对 和 特别处理。
最后注意要取模,所以不能直接进行除法,应该乘上2的逆元。(一开始忘记了各种WA然后看不出来)
参考代码
using namespace std;
typedef long long LL;
const int MAXN = 105;
class BearPermutations2 {
public:s
int getSum( int N, int MOD );
private:
int Mod;
void plus(int & x, int d) { x = (x + d) % Mod; }
LL pow2(int ex) {
LL res = 1, pw = 2;
while (ex) { if (ex & 1) res = res * pw % Mod; pw = pw * pw % Mod; ex >>= 1; }
return res;
}
};
int C[MAXN][MAXN], Fac[MAXN], F[MAXN];
int BearPermutations2::getSum(int N, int MOD) {
int i, j; Mod = MOD;
for (C[0][0] = Fac[0] = i = 1; i <= N; i++)
for (Fac[i] = (LL)i * Fac[i - 1] % MOD, C[i][0] = j = 1; j <= i; j++) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD;
memset(F, 0, sizeof(F));
LL inv2 = pow2(MOD - 2); // 阶乘求逆元
for (i = 3; i <= N; i++)
for (F[i] = (F[i - 1] << 1) % MOD, j = 2; j < i; j++)
plus(F[i], ((F[j - 1] + Fac[j] * inv2 % MOD) % MOD * Fac[i - j] % MOD +
(F[i - j] + Fac[i - j + 1] * inv2 % MOD) % MOD * Fac[j - 1] % MOD) % MOD * C[i - 1][j - 1] % MOD);
return F[N];
}
总结
这道题思维上非常具有挑战性,如果从错误的方向思考可能很久也找不出方法。因此,对于这类给定一个概念的题目,如果一般的做法下手困难,一定要从概念的定义出发,寻找一些重要的性质,从而一击解出答案。