枚举分割点的 dp
什么样的题适合枚举分割点?
- 特征: 一个规模为
i的问题可以拆成"左半部分 + 当前分割点"或"两个子规模之和"; - 状态:
dp[i]表示规模为i的最优值或方案数; - 内层: 枚举分割点
j, 合并两侧结果; - 复杂度: 两层循环, 一般
O(n^2);
整数拆分如何枚举分割点?
- 问题: 把正整数拆成至少两个正整数之和, 使乘积最大 (343);
- 状态:
dp[i]为i拆分后的最大乘积; - 决策: 第一个因子取
j(1 <= j < i), 剩余部分i-j有两种选择;- 不再拆: 乘积为
j * (i - j); - 继续拆: 乘积为
j * dp[i-j];
- 不再拆: 乘积为
- 方程:
dp[i] = max(dp[i], j * (i - j), j * dp[i - j]); - 初值:
dp[0] = dp[1] = 0, 两者无法拆分; - 答案:
dp[n];
const integerBreak = (n) => {
const dp = new Array(n + 1).fill(0);
for (let i = 2; i <= n; i++) {
for (let j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], (i - j) * j, dp[i - j] * j);
}
}
return dp[n];
};
不同的二叉搜索树如何枚举分割点?
- 问题: 求
n个节点能构成多少种结构不同的二叉搜索树 (96); - 状态:
dp[i]为i个节点能构成的二叉搜索树数量; - 决策: 枚举根节点的排名
j(1 <= j <= i);- 左子树有
j-1个节点, 右子树有i-j个节点; - 左右子树结构相互独立, 方案数相乘;
- 左子树有
- 方程:
dp[i] += dp[j-1] * dp[i-j]; - 初值:
dp[0] = 1, 表示空树也算一种结构; - 答案:
dp[n];
const numTrees = (n) => {
const dp = new Array(n + 1).fill(0);
dp[0] = 1;
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= i; j++) dp[i] += dp[j - 1] * dp[i - j];
}
return dp[n];
};
两个问题为什么形式相同?
- 共同点: 都在规模
i上枚举切分位置, 把问题分成两个独立子规模; - 差别: 整数拆分合并时取乘积最大值, 二叉搜索树合并时取方案数相乘再求和;
- 推广: 只要满足"子问题独立 + 可合并", 都能套用这一结构, 如矩阵连乘、多边形三角剖分 (见 区间 dp);