(用POJ 1160puls来讲解)四边形不等式——区间dp的常见优化方法
本文为深度解析理解四边形不等式,定义、优化和证明的主体部分均摘自 百度百科,加粗的补充部分才是干货精华;初学入门请移步https://blog.csdn.net/noiau/article/details/72514812(超详细入门+合并石子入门问题全解析)
定义
优化
证明
例题讲解
题面(POJ 1160的增强版)
1 2 3 4 5 1000
dp公式的建立
1. n个居民点,m个邮局,想到用dp[i][j]代表建i个邮局时,(排序后)前j个居民点的最近距离总和的最小值。
2. 思考递推关系,dp[i][j]可由dp[i-1][k]和居民点k+1到j之间建立一个新邮局的情况 推出 (k<j)。
则定义d[i][j] = 在居民点i到j上只建立一个邮局时的居民点i到j 的最近距离总和的最小值。
3. 则dp[i][j] = min(dp[i-1][k] + d[k+1][j]),1<=k<j。
4. d数组的求法,d[i][j]中存在递推关系:d[i][j] = d[i][j-1] + (a[j]-a[(i+j)/2])(即新增加一个居民点j后,邮局一定在i,j的中点(取下整)位置,原来i到j-1个居民点的最近距离总和不变)
注意:此处存在一个疑惑,在k+1到j之间建立新邮局后,居民点1到k的最近距离是否改变?
答案是:此公式成立时,居民点1到k的最近距离不会改变。
因为若存在[p,k]的居民点到新邮局的距离比之前的更近,则dp[i-1][p-1] + d[p][j]一定比dp[i-1][k] + d[k+1][j] 更小。
所以此最小值成立时,所有居民点的最近距离不变。(即[1,k]的居民点都离前面的邮局更近,而[k+1,j]的居民点都离后面的新邮局更近)
四边形不等式优化
明显,求d[i][j]需要两层for循环,而求dp[i][j]时需要对i,j,k进行三层循环,复杂度O(n^3),需要进行优化。
可以想到此dp公式包含着d[1][k] + d[k+1][j]的一个子区间之和最小值问题,那么dp[i][j] = min(dp[i-1][k] + d[k+1][j]),1<=k<j 这个复合的地推公式的决策点s[i][j]是否也可以用四边形不等式优化呢?(决策点s(i,j)为使得dp(i,j)取得最小值的k值。)
严格的数学证明需要先证明d数组满足d[i][j]+d[i+1][j+1] <= d[i][j+1]+d[i+1][j](i<=i+1<=j<=j+1)的四边形不等式条件,再证明dp数组也满足此条件,再证明验证s[i,j]对 i 非递减,对 j 非递减。
实际上d[i+1][j+1] - d[i+1][j] = a[j+1] - a[(i+j+2)/2],d[i][j+1] - d[i][j] = a[j+1] - a[(i+j+1)/2],而a[(i+j+2)/2] >= a[(i+j+1)/2]成立,所以d[i][j]满足四边形不等式条件。
再后面的证明其实无需进行,在实际做题时直接打印s[i][j]验证结论成立即可。
这样 dp[i][j] = min(dp[i-1][k] + d[k+1][j]) (s[i,j-1]≤k≤s[i+1,j])
优化后的伪代码
1. 先对居民点坐标数组a进行排序,然后依据递推关系:d[i][j] = d[i][j-1] + (a[j]-a[(i+j)/2]),双for得出d数组。
2. dp[1][j]其实就是d[1][j]。
3. 因为在求dp[i][j]时要使用 s[i,j-1]≤k≤s[i+1,j] 进行优化,那么 j 应该从小到大递推,i 应该从大到小递推,以使用上一次的 j-1和 i+1。
但是dp的递推关系只能是邮局数 i 从小到大递推,所以应该使用 s[i-1,j]≤k≤s[i,j+1]进行优化(本质是决策点s[i,j]对 i 非递减,对 j 非递减),
那么 i 应该从小到大递推,j应该从大到小递推。dp[i][j] = min(dp[i-1][k] + d[k+1][j]) (s[i-1,j]≤k≤s[i,j+1])
4.所以可以先for i in [2,m],再for j in [n,i+1],最后for k in [s[i-1][j], s[i][j+1]],递推出dp[i][j]和s[i][j]。 注意设置对i in [2,m]所有s[i][n+1]的值都为n-1,因为j=n时k最大取n-1。
代码
#include<cstdio> #include<iostream> using namespace std; int a[3003], d[3003][3003], f[3003][3003], s[3003][3003]; int main() { int n, m; cin >> n >> m; for (int i = 0; i < n; i++) cin >> a[i]; sort(a, a + n); for (int i = 0; i < n; i++) { //d[i][i] = 0; for (int j = i + 1; j < n; j++) d[i][j] = d[i][j - 1] + a[j] - a[(i + j) / 2]; f[0][i] = d[0][i]; //f[0][i]表示在前i+1个节点中建0+1个站时的最近距离总和的最小值。 } for (int i = 1; i < m; i++)s[i][n] = n - 2; //准备s[i][n]的初值,s[0][j]的初值直接使用默认值0 for (int i = 1; i < m; i++) { //for (int j = 0; j <= i; j++)f[i][j] = 0; //为了使用s[i][j]的结论,j从后往前递推 for (int j = n - 1; j > i; j--) { f[i][j] = 0x7fffffff; //使用s[i][j]的结论:因为i在最外层,不能由之前得到s[i+1][j],所以变通为s[i-1][j]。 //这样就会需要使用s[i][j+1],需要由之前得到s[i][j+1],所以j从后往前递推 for (int k = s[i - 1][j]; k <= s[i][j + 1]; k++) { if (f[i - 1][k] + d[k + 1][j] < f[i][j]) { f[i][j] = f[i - 1][k] + d[k + 1][j]; s[i][j] = k; } } } } cout << f[m - 1][n - 1]; return 0; }
总结
-
证明w满足四边形不等式,这里w是m的附属量,形如m[i,j]=opt{m[i,k]+m[k,j]+w[i,j]},此时大多要先证明w满足条件才能进一步证明m满足条件
-
证明m满足四边形不等式。(其实证明1之后,基本就可以直接验证决策点s[i][j]是否满足对 i 和 j 都非递减)
-
证明和验证s[i,j-1]≤s[i,j]≤s[i+1,j]
- 使用合适的遍历顺序,利用3.的结论进行优化。