最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划
题目描述
给定 $n$ 个点的 凸多边形 中每个 顶点 的 权值 $w_i$
我们可以把该 凸多边形 划分成 n - 2 个 三角形(三角形不能相交,这样就能保证是 $n-2$ 了)
每次划分三角形的 费用 为 三个顶点权值 的 乘积
求一个划分 方案,$s.t.$ 方案的 费用总和 最小
分析
这是一个经典的 图形学 问题 — 三角剖分
因为我们现实中常见的一些 多边形图形 存储到计算机中,需要转存为一个个 像素点
那么如何存储一个 多边形 最简单的方案就是把它转化为 多个三角形 进行存储
也就是 三角剖分 问题,不只是 凸多边形,还能解决 凹多边形, 有孔多边形 等问题
对应有 $O(n^3)$ 的暴力算法,$O(n^2)$ 的 $ear clipping$ 算法,$O(n\log n)$ 的 $Delaunay$ 算法
还有很复杂的线性算法
以上和本题都没啥关系,大家有兴趣学习的可以自行翻阅文献和资料
回归本题,本题是一个给定的 凸多边形 求 三角剖分 的最小费用方案
很显然一个 凸多边形的剖分方案 并不唯一:
在 选定 多边形中 两个点 后,找出 三角形 的 第三个点 的方案有 $n-2$ 个
然后还要分别 划分 他的 左右两块区域
因此我们就会想到用 记忆化搜索 或者 区间DP 来进行处理
记忆化搜索介绍 可以参考这篇 加分二叉树【记忆化搜索思想】
区间DP介绍 可以参考这篇 环形石子合并【区间DP+环形区间问题】
本题解采用 区间DP 的方式进行求解
闫氏DP分析法
状态表示—集合$f_{l,r}:$ 当前划分到的多边形的左端点是 $l$,右端点是 $r$ 的方案
状态表示—属性$f_{l,r}:$ 方案的费用最小
状态计算—$f_{l,r}:$
$$ f_{l,r} = min(f_{l,k} + f_{k,r} + w_l \times w_k \times w_r) \quad (l \lt k \lt r) $$
区间DP 在状态计算的时候一定要 认真 划分好 边界 和 转移,对于不同题目是不一样的
然后本题非常的嚣张,直接用样例的 $5$ 的点告诉我们答案会爆 $int$ 和 $long long$
并且没有 取模 要求,那就只能上 高精度 了 用Python了
Code
时间复杂度: $O(n^3)$ 区间DP
#include <iostream>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 55;
int n;
int w[N];
vector<int> f[N][N];
bool cmp(vector<int> &a, vector<int> &b)
{
if (a.size() != b.size()) return a.size() < b.size();
for (int i = a.size() - 1; i >= 0; i -- )
if (a[i] != b[i])
return a[i] < b[i];
return true;
}
vector<int> add(vector<int> a, vector<int> b)
{
vector<int> c;
int t = 0;
for (int i = 0; i < a.size() || i < b.size(); i ++ )
{
if (i < a.size()) t += a[i];
if (i < b.size()) t += b[i];
c.push_back(t % 10);
t /= 10;
}
while (t) c.push_back(t % 10), t /= 10;
return c;
}
vector<int> mul(vector<int> a, LL b)
{
vector<int> c;
LL t = 0;
for (int i = 0; i < a.size(); i ++ )
{
t += b * a[i];
c.push_back(t % 10);
t /= 10;
}
while (t) c.push_back(t % 10), t /= 10;
return c;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
//区间DP
//此处初始状态f[l][l+1] = 0 初始就是空vector,不用额外设置
for (int len = 3; len <= n; len ++ )
{
for (int l = 1, r; (r = l + len - 1) <= n; l ++ )
{
//初始化初始状态
for (int k = l + 1; k < r; ++ k)
{
//w_l * w_k * w_r
auto new_val = mul(mul({w[l]}, w[k]), w[r]);
//f[l][k] + f[k][r] + cost
new_val = add(add(new_val, f[l][k]), f[k][r]);
//f[l][r] = min(f[l][r], f[l][k] + f[k][r] + w_sum)
if (f[l][r].empty() || cmp(new_val, f[l][r])) f[l][r] = new_val;
}
}
}
//输出最终答案
auto res = f[1][n];
for (int i = res.size() - 1; i >= 0; i -- ) printf("%d", res[i]);
puts("");
return 0;
}
为什么
w[l]
不用转换成vector啊{w[l]}
用了花括号,在 $C++11$ 语法里可以直接初始化成一个 $vector$好的,谢谢
学到了
orz,学到了
orz,学到了
ORZ,学到了
or2,学到了
想问一下作者为啥以1和n为底划分就是最优解,为啥不能是其他划分方式呢
if (f[l][r].empty() || cmp(new_val, f[l][r])) f[l][r] = new_val;
我觉得是因为l,r
每次得到的都是能组成三角形的最小值,不知道我理解的对不对因为在任何完整方案中,边 1-n 一定会被包含在一个三角形中
stO Orz
为什么要有一个比较的函数?
__int128(?)
大佬高精度乘法这里为什么不用去前导零?
非要杠的话,前导0存在的情况是b=0,这里的b>0
大佬,这个题不用断环成链是不是因为求的是三个点的乘积,这个没有顺序而言导致的啊
mul乘法这里的b 为什么要用LL? 题给权重不是小于10^9吗
一乘就爆 int 了
t += b * a[i]; 如果b不用LL,这句话会爆int。
Orz
大佬,为啥不需要断环成链啊?
能结合环形石子合并来详细解释下吗,这两题的核心区别在哪里啊?
谢谢大佬
剖分的起点不会影响答案的大小,任意点看作起点求得的答案一定相同
那请问下,为什么“环形石子合并”这题剖分的起点就会影响答案的大小呢?这两题的区别在哪里啊?
环形石子合并的这个“起点”不是表示我从哪里开始合成,它表示的是我通过某种顺序合并石子,经过n-1次合并之后就能将所有的石子合并成一堆,此时相当于所有的石子之间总共连上了n-1条边,必然存在一个断开处,那么你就可以把这种合成方式看做是对这一条断开的链进行基础版的石子合并,由于你合成石子的顺序不同,那么最终断开处不同,也就呈现出起点不同的结果,那么我们只需要对所有的情况求最值就是答案。这个题只是对这个环的区域进行划分,不涉及顺序问题。
因为环形石子给定n个数,只会合并n - 1。状态表示是f[l, r], 在区间[l, r]合并石子所需的最小费用 。例如样例4594并不是最优解, 最优解是45944594 中的4459。而这道题中f[1, n], 表示的是区间左端点是1,右端点是r的方案的费用最小, 是可以被表示出来的, 也就是说区间[1, n]都可以被表示出来。
大佬,为什么最后需要f[l][r].empty()为0呢
没算过的状态初始化
个人理解:
//如果为空直接赋值,如果不为空比较后赋值
如果去掉判断空,f未初始化一直为0,不会比new_val小,不会更新
因为前面没有初始化f[l][r] = INF
y总的数组那个,第一次
mul
是不是只用到了a[0]
呀?是的
高精度乘不是要从低位到高位吗
这样mul({w[l]}, w[k])为什么也对啊
要不你再详细解释一下你的问题
代码中的这个地方 auto new_val = mul(mul({w[l]}, w[k]), w[r]);
比如w[l]=123,将{w[l]}变为vector高精度乘的时候,不是应该321吗
你认为的
w[l] = {3, 2, 1}
实际上是
w[l] = {123, 0, 0}
(这两位0物理上不存在)我明白了,谢谢大佬 Orz
彩铅佬OrzOrz太感谢了
%%%%%%%%%%%%%
大佬可以解释一下为什么可以不用枚举从每条边开始呢
其实可以用__int128)
int128 yyds
大佬,请问这一题为什么不用断环为链呢?
1 和 n 必定是某个三角形的一条边,所以可以之间把他们左右区间左右端点