四边形不等式
Meaning#
若有函数 ,令 ,若有:
则我们称函数 满足四边形不等式。
General#
若我们在 过程中会用到类似如下形式的方程:
那么,只要代价函数 满足四边形不等式,那么函数 一般也会满足四边形不等式。同时,假设 为当 取得最优值的决策点,即: ,一般 函数也会满足四边形不等式,这样,通过移项我们会得到:
或是:
那么我们选取决策点的转移范围就变小了,对于枚举决策点的复杂度为 的转移方程,它的枚举复杂度经过均摊,这个 将会变成常数级别,常常能使 的 降为 。
Example 1--石子合并#
Problem#
给你环形排列的 堆石子 ,第 堆石子数量为 ,每次选择相邻的两堆合并,每次合并的代价将会是两堆石子的数量之和,问将所有石子合并成一堆的最大与最小代价分别是多少。
First Mentality#
不难想到,首先断环为链,使之变成一条两倍长度的链,然后做 的区间的 。
设 为区间 的石子数量之和, 为合并区间 的最小、最大代价,初始状态为 :
枚举 合并的区间长度 len (2~n)
枚举 合并区间的左端点 i (1~n)
枚举 区间合并点 j (i~i+len-2)
{
fmin[i][i+len-1]=Min(fmin[i][j]+fmin[j+1][i+len-1])+w[i][j]
fmax[i][i+len-1]=Max(fmax[i][j]+fmax[j+1][i+len-1])+w[i][j]
}
这样我们就得到了一个 的 ,虽然对于题目来说确实够用了,但是如果 呢?这时候我们应该考虑如何优化。
最小代价四边形不等式优化#
可以观察到 的转移式满足使用四边形不等式的基本特征,同时, 函数也满足四边形不等式。那么,我们可以考虑使用四边形不等式了:
利用决策点的单调性 来优化第三层循环即可,初始状态为: 。
枚举决策点转移时同时更新 来决策就好。
最大代价决策点优化#
首先有这样一个结论: 的最优决策点必定在 与 两者之间。
考虑如何证明,反证法奉上:
设 与 的差值函数为 ,可以观察到此函数单峰。
- 对于 的情况:
若最优决策点为 ,我们设 ,那么相应转移方程为:
对应的,我们的合并方案为:
那么我们考虑另一种合并方式:
则对应的转移方程为:
由于另一种合并方式必定比原决策更优,即选取决策点 优于 , ,由于函数 在 右侧区间单调递增,则若选取决策点为 且 且 ,那么总有选取 优于 ,那么由此可知对于某个决策点 往右的决策必定单调变优。则对于所有 ,选取 将会最优。
- 对于 :
类似的,设最优决策点为 ,我们设 ,那么对应值为:
同样,我们考虑另一种合并方式:合并 ,则此时的值为
则另一种必定优于原本的决策。
那么对于上面的决策点 往左的决策必定单调变优,因为对于任意 且 都必定有决策点 要更优,则在左端点选取 最优。
得证,故转移决策点仅需在 与 之间选取。
Code#
重点看 就好 = =。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
int n, a[101], fmax[201][101], posi[201][101], fmin[201][101], maxans, minans;
int main() {
cin >> n;
memset(fmin, 0x7f, sizeof(fmin));
minans = 2e9;
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i + n] = a[i];
}
for (int i = 1; i <= 2 * n; i++)
a[i] += a[i - 1], fmin[i][i] = 0, posi[i][i] = i; //初始化 dp 数组与 决策点
for (int i = 1; i < n; i++)
for (int j = 1; j <= 2 * n - i; j++) {
fmax[j][j + i] = max(fmax[j][j + i - 1], fmax[j + 1][j + i]) + a[j + i] -
a[j - 1]; //两个端点取最优
for (int k = posi[j + 1][j + i]; k >= posi[j][j + i - 1]; k--)
if (fmin[j][j + i] >
0ll + fmin[j][k] + fmin[k + 1][j + i] + a[j + i] - a[j - 1])
fmin[j][j + i] =
fmin[j][k] + fmin[k + 1][j + i] + a[j + i] - a[j - 1],
posi[j][j + i] = k; //四边形不等式优化后的决策点枚举
}
for (int i = 1; i <= n; i++) {
minans = min(minans, fmin[i][i + n - 1]);
maxans = max(maxans, fmax[i][i + n - 1]);
}
cout << minans << endl;
cout << maxans;
}
Example 2--山区建小学#
Problem#
一条直线上分布着 个村子 ,第 个村子距离第 个村子的距离为 ,要求在 个村子中选取 个建造小学 ,使得所有村子到距离它最近的小学的距离和最小。输出最小距离和。
First Mentality#
一道很经典的 题。设 为在村庄 中只建立一个小学,所有村庄到这个小学的最小距离和。而这个最小距离和的位置必定为最中间的那个村庄,这也是一个经典的数学证明,即中位数为最小距离和的位置。
设 为对于前 个村庄建立 个小学的最小距离和,那么转移方程为:
函数只需要勤记前缀和即可转移时 求出。
不用疑惑为什么这样转移没有考虑第 个村庄中为什么不会有到前 个村庄的小学距离更短的村庄,因为这样反正不会影响答案使之变劣。
四边形不等式优化#
经过大眼 or 打表观察法, 我们发现 函数又满足四边形不等式!那么我们假设 为 的最优决策点,即 ,我们同样可以猜想套路:
那么初始化为:
完毕。
Code#
才是重点,码风比较仙。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
int n, m, d[501], q[501], h[502], f[501][501], pos[502][501], inf = 1e9;
int work(int l, int r) {
int mid = (l + r) >> 1;
return h[l] - h[mid] - (mid - l) * (h[mid] - h[mid + 1]) + q[r] - q[mid] -
(r - mid) * (q[mid] - q[mid - 1]);
} // w 函数计算
int main() {
cin >> n >> m;
for (int i = 2; i <= n; i++) {
scanf("%d", &d[i]);
q[i] = d[i], h[i - 1] = d[i];
}
for (int l = 1; l <= 2; l++)
for (int i = 1; i <= n; i++) q[i] += q[i - 1]; //初始化前缀和的前缀和
for (int l = 1; l <= 2; l++)
for (int i = n; i >= 1; i--) h[i] += h[i + 1]; //初始化后缀和的后缀和
for (int i = 1; i <= n; i++)
f[i][1] = work(1, i), pos[i][1] = 0; //初始化 dp 数组与决策点
for (int j = 2; j <= m; j++) {
pos[n + 1][j] = n;
for (int i = n; i > j; i--) //由于四边形不等式需要,我们从大到小枚举 i
{
f[i][j] = inf;
for (int k = pos[i][j - 1]; k <= pos[i + 1][j]; k++) {
int tmp = f[k][j - 1] + work(k + 1, i);
if (tmp < f[i][j]) f[i][j] = tmp, pos[i][j] = k;
}
}
}
cout << f[n][m];
}
总结#
四边形不等式是一个套路,虽然不常见,但是应熟记于心以防万一。
同时,考场上要通过数学证明函数满足四边形不等式其实还是蛮困难的,建议不要头铁,打打表证明就好了= = 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通