大家应该都知道一般我们在区间dp的更新中都需要三层循环来实现,第一层枚举区间长度,第二层枚举区间起始点,第三层就是枚举区间断点来寻求最优解,对于动态规划问题来说n^3的复杂度属实有点高,那我们能不能找到一些方法来优化一下呢?首先我们能够发现for循环的第一层和第二层肯定不能优化,前两层保证了对于任何区间都能够被更新,而第三层只是为了找最优解而设立的,如果我们能缩短第三层for循环的次数,那么肯定能对整个动态规划过程起到优化作用,而四边形不等式就是为了优化第三层for循环而产生的。
先说一下什么样的区间动态规划问题能够使用四边形不等式进行优化,首先是w函数(就是每次进行区间合并所需要消耗的权值)需要具备以下两个性质:
我还是想跟大家先谈论一下w函数的问题,不同的区间问题的w函数是不一样的,一般都是以w[i][j]表示合并第i~j个物品所需要消耗的权值,比如石子合并问题中我们合并第i~j堆石子所需要消耗的权值就是第i~j堆石子的个数之和,这是一个定值,也只有当w只与i和j有关时才能讨论w单调性以及是否满足四边形不等式这些问题,那是不是所有的w[i][j]都只和i以及j有关呢?肯定不是的,举一个简单的例子,我们进行矩阵相乘,最后一次合并i~j个矩阵的计算次数与端点有关系,所以说此时的w并不是一个仅仅与i和j有关的函数,这个就无法进行四边形不等式优化。总之,只有满足w[i][j]是关于i和j的二元函数时才能讨论单调性和四边形不等式的问题。
下面假设w都是关于i和j的二元函数:
如何判断w是否具有单调性以及是否满足四边形不等式呢?这里不建议大家在做题时先对具体问题进行证明,因为证明过于繁琐,倒不如直接先打一个表观察,如果满足再进行不等式优化。
打表代码:
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<algorithm>
#include<map>
#include<cmath>
#include<queue>
using namespace std;
int w(int i,int j)
{
//具体问题具体分析
}
int main()
{
bool flag=true;
//验证单调性
for(int l=1;l<=n;l++)
for(int r=l+2;r<=n;r++)
for(int i=l;i<=r;i++)
for(int j=i;j<=r;j++)
if(w(i,j)>w(l,r)) flag=false;
//验证四边形不等式
for(int l=1;l<=n;l++)
for(int r=l+2;r<=n;r++)
if(w(l,r-1)+w(l+1,r)>w(l,r)+w(l+1,r-1)) flag=false;
if(flag) //符合单调性以及四边形不等式
else //不符合单调性以及四边形不等式
return 0;
}
只是为了验证,n不用特别大,一般取50即可。
下面对这两个引理证明:(符号太多,我就直接写在纸上了)
下面是引理二证明:
最后我们来说一下怎么使用s数组,这个比较简单,直接把最后一重循环换成s[i][j-1]<=k<=s[i+1][j]即可,以石子合并为例:动态转移代码如下:
for(int len=2;len<=n;len++)
for(int i=1;i+len-1<=n;i++)
{
int j=i+len-1;
for(int k=s[i][j-1];k<=s[i+1][j];k++)
{
if(f[i][k]+f[k+1][j]+sum[j]-sum[i-1]<f[i][j])
{
f[i][j]=f[i][k]+f[k+1][j]+sum[j]-sum[i-1];
s[i][j]=k;
}
}
}
s数组初始化应该是s[i][i]=i,在最内层for循环进行更新。
最后说一下复杂度,经过四边形优化后的动态转移复杂度是o(n^2)的
复杂度证明如下:
总地来说,当我们看到区间dp发现数据较大后应该考虑用四边形不等式进行优化,一般能处理3000及以下的数据。
下面我推荐几道还算不错的区间DP题目:
Zuma - 洛谷对应博客:CF607B Zuma(区间DP)_AC__dream的博客-CSDN博客
[NOI1995] 石子合并 - 洛谷对应博客P1880 [NOI1995] 石子合并(区间DP+思维)_AC__dream的博客-CSDN博客
Problem - 149D - Codeforces对应博客(CodeForces - 149D)Coloring Brackets(区间DP)_AC__dream的博客-CSDN博客