凸多边形的划分
题目描述
给定一个具有 N 个顶点的凸多边形,将顶点从 1 至 N 标号,每个顶点的权值都是一个正整数。
将这个凸多边形划分成 N-2 个互不相交的三角形,求所有这些三角形顶点权值乘积之和的最小值。
输入描述
- 第一行输入一个整数 N,表示顶点个数。
- 第二行输入 N 个整数,依次为顶点 1 至顶点 N 的权值。
输出描述
输出仅一行,为这些三角形顶点权值乘积和的最小值。
示例
输入
5
121 122 123 245 231
输出
12214884
备注
- 对于 100% 的数据,有 N ≤ 50。
- 每个顶点的权值均小于 10^9。
题解:
二、问题建模:为什么用区间 DP?
这个问题与经典的 矩阵链乘法 和 石子合并 类似,都是典型的 区间动态规划(Interval DP) 问题。
我们希望找到一种划分方式,将整个凸多边形划分为若干个三角形,使得所有三角形的顶点权值乘积之和最小。
我们可以把原问题看作是从顶点 i
到顶点 j
构成的一个子多边形的最优划分问题,这样就可以使用 DP 来递推求解。
三、状态定义
设 dp[i][j]
表示从第 i
个顶点到第 j
个顶点所构成的子多边形(包含这两个顶点),划分成若干三角形后,其所有三角形顶点乘积和的最小值。
注意:这里的 i
到 j
是连续编号的顶点,且顺序不变。
四、初始化条件
- 当子区间的长度为 1 或 2 时,无法构成三角形,因此代价为 0:
for (int len = 1; len <= 2; len++)
{
for (int i = 1; i + len - 1 <= n; i++)
{
int j = i + len - 1;
dp[i][j] = 0;
}
}
- 其他情况的
dp[i][j]
初始化为一个非常大的值,表示尚未计算或不可达。
五、状态转移方程
对于任意一个子区间 [i, j]
,我们尝试枚举中间分割点 k (i < k < j)
,将 [i, j]
分割成两个子区间:
- 左边:
[i, k]
- 右边:
[k, j]
此时会形成一个三角形 (i, k, j)
,它的贡献是 w[i] * w[k] * w[j]
。
于是有状态转移:
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + w[i] * w[k] * w[j]);
其中 k
的取值范围是 i < k < j
。
六、遍历顺序
我们按照子区间长度从小到大进行枚举:
// 区间长度从 3 开始
for (int len = 3; len <= n; ++len)
{
// 枚举起点
for (int i = 1; i + len - 1 <= n; ++i)
{
// 计算终点
int j = i + len - 1;
// 执行状态转移。。。。。。
}
}
这种顺序确保我们在计算 dp[i][j]
时,所依赖的 dp[i][k]
和 dp[k][j]
已经被计算过。
七、数据类型选择
由于每个顶点的权值可以高达 1e9
,而一个三角形的乘积可以达到 1e27
,多个三角形的总和可能会超出 long long
的范围(约为 1e18
)。因此我们使用 C++ 中的 __int128
类型来防止溢出。
同时,为了打印 __int128
类型的数值,需要自定义打印函数。
八、完整代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using lll = __int128; // 定义 __int128 类型别名,方便使用
// __int128 的打印函数:
// 标准库不支持直接打印 __int128,因此需要自定义打印函数
void print128(lll x) {
if (x == 0) // 特判0
{
cout << '0';
return;
}
if (x < 0) // 处理负数,先打印负号,再取正
{
cout << '-';
x = -x;
}
string s = "";
// 将数字转换为字符串,取模得到最低位,累加字符
while (x > 0)
{
s += (x % 10 + '0'); // 取个位数字并转成字符
x /= 10; // 去掉个位数字,向高位进位
}
// 因为是从低位向高位依次加入字符,字符串是反向的,需反转
reverse(s.begin(), s.end());
cout << s; // 输出字符串
}
int main()
{
ll n;
cin >> n; // 输入顶点个数(多边形点数)
vector<ll> w(n + 1, 0); // 权值数组,1-based 索引,w[i] 为第 i 个顶点的权值
for (ll i = 1; i <= n; i++)
{
cin >> w[i];
}
// 定义一个很大的数 N 作为无穷大(INF),用来初始化 DP 表
// N = 2^(121),非常大,远大于可能出现的最大代价,防止溢出
lll N = 2;
for (ll i = 0; i < 120; i++)
{
N *= 2;
}
// 初始化 DP 数组,dp[i][j] 表示区间 [i,j] 的最小剖分代价
// 一开始赋值为无穷大 N,表示暂时不可达或未计算
vector<vector<lll>> dp(n + 1, vector<lll>(n + 1, N));
// 区间长度为 1 或 2 的子区间不构成三角形,代价为 0
// 所以这些区间 dp 值初始化为 0
for (ll len = 1; len <= 2; len++)
{
for (ll i = 1; i + len - 1 <= n; i++)
{
ll j = i + len - 1;
dp[i][j] = 0;
}
}
// 区间 DP 主要部分:从长度 3 到 n 的区间逐步计算最小代价
for (ll len = 3; len <= n; len++)
{
for (ll i = 1; i + len - 1 <= n; i++)
{
ll j = i + len - 1;
// 尝试所有可能的中间分割点 k,将区间 [i,j] 分成 [i,k] 和 [k,j]
for (ll k = i + 1; k < j; k++)
{
// 代价由三部分组成:
// 左子区间最优代价 dp[i][k]
// 右子区间最优代价 dp[k][j]
// 当前剖分三角形代价 w[i] * w[k] * w[j]
lll cost = dp[i][k] + dp[k][j] + (lll)w[i] * w[k] * w[j];
// 取所有分割点中的最小代价
dp[i][j] = min(dp[i][j], cost);
}
}
}
// 打印最终结果,dp[1][n] 即整个多边形的最小剖分代价
print128(dp[1][n]);
cout << "\n";
return 0;
}
九、模拟构建 dp 表(以 n=5 为例)
假设权值数组为:
Index: 1 2 3 4 5
Value: 121 122 123 245 231
我们逐步填充 dp[i][j]
:
i \ j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 0 | 0 | ? | ? | 最终结果 |
2 | N | 0 | 0 | ? | ? |
3 | N | N | 0 | 0 | ? |
4 | N | N | N | 0 | 0 |
5 | N | N | N | N | 0 |
随着 DP 过程推进,逐步填入每个 dp[i][j]
的最小值,最终得到 dp[1][5]
即为答案。
十、常见问题 & 注意事项
问题 | 解释 |
---|---|
为什么要使用 __int128 ? | 防止乘积溢出 long long 范围(1e18) |
为什么不能贪心? | 每一步的选择会影响后续结构,必须全局最优 |
是否可以用其他方法? | 如记忆化搜索,但 DP 更直观易实现 |
是否可以优化空间? | 可以压缩部分维度,但对本题意义不大 |
十一、总结
项目 | 内容 |
---|---|
算法类型 | 区间动态规划(Interval DP) |
时间复杂度 | O(n³) |
空间复杂度 | O(n²) |
核心技巧 | 区间划分 + 最小化三角形乘积和 |
注意事项 | 使用 __int128 防止溢出 |
类似题目 | 矩阵链乘法、石子合并、最长回文子串 |