【题目描述】
给定一个具有 N 个顶点的凸多边形,将顶点从 1 至 N 标号,每个顶点的权值都是一个正整数。将这个凸多边形划分成 N−2 个互不相交的三角形,试求这些三角形顶点的权值乘积和至少为多少。
【输入】
输入第一行为顶点数 N
第二行依次为顶点 1 至顶点 N 的权值。
【输出】
输出仅一行,为这些三角形顶点的权值乘积和的最小值。
【输入样例】
5
121 122 123 245 231
【输出样例】
12214884
【提示】
数据范围与提示:
对于 100% 的数据,有 N≤50,每个点权值小于 109 。
在算法竞赛中,我们经常会遇到一类题目:数据规模N很小(例如),但涉及的数值运算却非常巨大,轻易就能挤爆
long long。
今天我们要看的这道经典例题------凸多边形的划分 ,就是这样一个典型的组合题:它既需要区间动态规划 的思维,又需要处理超高精度的数值运算。
题目简述
给定一个具有N个顶点的凸多边形(),每个顶点都有一个权值
(
)。我们将这个多边形划分成N-2个互不相交的三角形。
划分的代价定义为:所有三角形的三个顶点的权值乘积之和。求最小的划分代价。
核心思路:区间动态规划
面对这种几何图形的分割问题,初学者往往容易陷入"几何切分"的死胡同。其实,我们应该将多边形的顶点看作一个线性的序列 。
我们的目标是解决整个区间[1, N]的问题。根据 DP 的思想,我们可以将其拆解为更小的子区间问题。
1. 状态定义
定义dp[i][j]表示:将顶点i到顶点j构成的子多边形(即顶点序列)划分成三角形所能得到的最小权值和。
-
目标:求dp[1][N]。
-
边界条件:当区间长度为 2 时(例如dp[i][i+1]),只有两个点,无法构成三角形,代价为 0。
2. 状态转移方程
如何计算大的区间dp[i][j]?
我们可以逆向思考:在最终的划分方案中,底边(i, j)一定属于某一个三角形。假设这个三角形的第三个顶点是k。
为了能构成三角形(i, k, j),顶点k必须严格位于i和j之间(即i<k<j)。
一旦选定了k,这个三角形就把问题划分成了三部分:
-
左子问题:顶点i到k的最小划分代价,即dp[i][k]。
-
右子问题:顶点k到j的最小划分代价,即dp[k][j]。
-
当前三角形代价 :
。
我们遍历所有可能的k,取最小值:
dp[i][j] =
隐藏的陷阱:数据溢出
DP 方程有了,看起来问题解决了。但且慢,看看数据范围:
每个权值。
计算一个三角形的代价需要三个权值相乘:。
-
int最大值约为。
-
long long最大值约为。
很显然, 远远超出了
long long的承受范围。如果我们直接用 long long 计算,结果一定会溢出变成负数,导致答案错误。
物理外挂:__int128
在标准 C++ 中,要处理这么大的数,通常需要手写高精度结构体(BigInt)。但在竞赛环境中(使用 GCC 编译器),我们有一个"物理外挂"可以使用:__int128。
什么是 __int128?
它是一个 GCC 编译器扩展提供的 128 位整数类型,能存储高达的数值,足以应对本题的
。它的加减乘除运算和普通整数一样方便。
唯一的缺点:I/O 问题
因为 __int128 不是 C++ 标准类型,标准的输入输出流(cin / cout)并不认识它。如果我们直接写 cout << dp[1][N],编译器会报错。
因此,我们需要手写一个简单的输出函数(快写)。利用递归思想,将大数字拆解成一个个字符输出。
cpp
//手写输出函数
void print(int128 x){
if(x>9) print(x/10);//递归打印前面的位
putchar(x%10+'0');//打印最后一位
}
完整代码
结合区间 DP 的逻辑和 __int128 的高精度处理,我们可以写出最终的完整代码。
cpp
//这道题数据范围大 要用高精度 第一种用__int128
#include <iostream>
#include <algorithm>//min函数
#include <cstdio>//putchar
using namespace std;
typedef __int128 int128;
int n;
int a[110];//存每个点权值
int128 dp[110][110];//dp[i][j]代表i-j顶点区间内的权值乘积和的最小值
//手写输出函数
void print(int128 x){
if(x>9) print(x/10);//递归打印前面的位
putchar(x%10+'0');//打印最后一位
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];//存每个点权值
//将DP数组初始化为一个极大的值(无穷大)
//利用位运算(int128)1 << 120 可以安全地构造一个超大数
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dp[i][j]=(int128)1<<120;
}
}
for(int i=1;i<n;i++){//一个点或两个点所构成的区间无法构成三角形,因此权值为0
dp[i][i]=0;
dp[i][i+1]=0;
}
dp[n][n]=0;
//枚举区间长度len,从3开始,2个无法构成三角形
for(int len=3;len<=n;len++){
for(int i=1;i<=n-len+1;i++){//左端点
int j=i+len-1;//右端点
//枚举分割点k,k必须严格位于i和j之间
//k如果等于i或者j无法构成三角形
for(int k=i+1;k<j;k++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+int128(a[i])*a[k]*a[j]);
}
}
}
print(dp[1][n]);
return 0;
}
总结
这道题是区间DP分割类的经典例题。它教会了我们两件事:
-
思维转换:将几何图形的分割问题转化为线性区间的合并问题。
-
高精度意识 :在计算乘积时,一定要估算最大可能的数值范围,当
long long不够用时,果断使用__int128(在允许的环境下)或手写高精度。