四边形优化原理

引入

  • 四边形不等式应用于DP优化,相信大家大多是接触区间DP问题才看到了这个知识点,关于它的起源,网上绝大多数文章都没有提到,这个问题最初来源于高德纳(Knuth)教授1971年研究最优二叉搜索树问题的一篇论文,后来储枫教授深入研究了这个东西,并在1980年提出并证明了它,四边形不等式可以将区间DP从O(n3)的时间复杂度优化到O(n2)

内容

不等式介绍

  • 首先什么是四边形不等式呢?设w是整数定义下的一个二元函数,形式如下
    设 a ≤ b ≤ c ≤ d 设a\leq b\leq c\leq d abcd 有 w ( a , c ) + w ( b , d ) ≤ w ( a , d ) + w ( b , c ) 有w(a,c)+w(b,d)\leq w(a,d)+w(b,c) w(a,c)+w(b,d)w(a,d)+w(b,c)
  • 在一般的文章中都使用的是i、i’、j、j’这四个元素,比较正规,但是 ’ 这个符号不太好辨认,所以这里我用a,b,c,d代替
  • 为什么叫四边形不等式?我画一个四边形如下
    在这里插入图片描述
  • 在这个四边形里面, a d + b c > a c + b d ad+bc\gt ac+bd ad+bc>ac+bd显然成立,为什么?考虑 △ o a c \triangle oac oac △ o b d \triangle obd obd,根据三角形性质,显然有 o a + o c > a c oa+oc\gt ac oa+oc>ac o b + o d > b d ob+od\gt bd ob+od>bd两边分别相加有 o a + o d + o c + o b > a c + b d oa+od+oc+ob\gt ac+bd oa+od+oc+ob>ac+bd也就是 a d + b c > a c + b d ad+bc\gt ac+bd ad+bc>ac+bd,那什么时候相等,当abcd四点共线的时候相等,这也就是四边形不等式 a d + b c ≥ a c + b d ad+bc\geq ac+bd ad+bcac+bd

优化DP

  • 最常见的区间DP问题是石子合并问题,可以参考洛谷上面的石子合并这道题是区间DP问题,N只有100,O(n3)也可以,但是HDU上面还有一道类似问题题目链接,这道题n的范围放到了1000,这时候O(n3)显然不行,那么需要进行优化。这两道题属于环形区间DP问题,其实也不是很友好,但是由于HDU这道题数据比较强,所以还是以这两道题作为例子。首先给出这道题不优化的程序,
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
const double eps = 1e-6;
int Data[MAXN];
int SUM[MAXN];
int dp[2050][2050];
int s[2050][2050];
int main(){
    
    
    int n;
    while(cin >> n){
    
    
        for(int i=1;i<=n;i++){
    
    
            cin >> Data[i];
            Data[i + n] = Data[i];
        }
        for(int i=1;i<=2*n;i++){
    
    
            SUM[i] = SUM[i - 1] + Data[i];
        }
        for(int i=1;i<=2*n;i++){
    
    
            dp[i][i] = 0;
        }
        for(int len = 1; len < n; len++){
    
    
            for(int i=1;i<=2*n-len;i++){
    
    
                int j=i+len;
                dp[i][j] = INF;
                for(int k=i;k<j;k++){
    
    
                    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + SUM[j] - SUM[i - 1]);
                }
            }
        }
        int ans = INF;
        for(int i=1;i<=n;i++){
    
    
            ans = min(ans, dp[i][i + n - 1]);
        }
        cout << ans << "\n";
    }
    return 0;
}
  • 上面的程序是区间DP问题的一般性框架,可以看到,第一圈for循环是区间长度,不可省略,第二圈for循环是区间的左端点,也不能省略,这两圈是必要的,那么唯一可以优化的就是第三圈的for,这个for是在干嘛呢,这个for循环的作用是寻找区间 [ i , j ] [i,j] [i,j]内的最优分割点,四边形优化就是在这里将 [ i , j ] [i,j] [i,j]缩小,设 d p [ i , j ] dp[i,j] dp[i,j]表示动态规划的一个状态量,定义 s [ i , j ] s[i,j] s[i,j] d p [ i , j ] dp[i,j] dp[i,j]取得最小值对应的 k k k值,可以证明 s [ i ] [ j − 1 ] ≤ s [ i ] [ j ] ≤ s [ i + 1 ] [ j ] s[i][j-1]\leq s[i][j]\leq s[i+1][j] s[i][j1]s[i][j]s[i+1][j]也就是 s [ i ] [ j − 1 ] ≤ k ≤ s [ i + 1 ] [ j ] s[i][j-1]\leq k\leq s[i+1][j] s[i][j1]ks[i+1][j]利用这个不等式,我们可以将k的枚举范围从 [ i , j ] [i,j] [i,j]缩小到 [ s [ i ] [ j − 1 ] , s [ i + 1 ] [ j ] ] [s[i][j-1],s[i+1][j]] [s[i][j1],s[i+1][j]],优化后的程序如下
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
const double eps = 1e-6;
int Data[MAXN];
int SUM[MAXN];
int dp[2050][2050];
int s[2050][2050];
int main(){
    
    
    int n;
    while(cin >> n){
    
    
        for(int i=1;i<=n;i++){
    
    
            cin >> Data[i];
            Data[i + n] = Data[i];
        }
        for(int i=1;i<=2*n;i++){
    
    
            SUM[i] = SUM[i - 1] + Data[i];
        }
        for(int i=1;i<=2*n;i++){
    
    
            dp[i][i] = 0;
            s[i][i] = i;
        }
        for(int len = 1; len < n; len++){
    
    
            for(int i=1;i<=2*n-len;i++){
    
    
                int j=i+len;
                dp[i][j] = INF;
                for(int k=s[i][j-1];k<=s[i+1][j];k++){
    
    
                    int tmp = dp[i][k] + dp[k + 1][j] + SUM[j] - SUM[i - 1];
                    if(dp[i][j] > tmp){
    
    
                        dp[i][j] = tmp;
                        s[i][j] = k;
                    }
                }
            }
        }
        int ans = INF;
        for(int i=1;i<=n;i++){
    
    
            ans = min(ans, dp[i][i + n - 1]);
        }
        cout << ans << "\n";
    }
    return 0;
}
  • 这样就可以通过这道题

原理证明

  • 下面讨论的都是取最小值的四边形优化。下面来证明 s [ i ] [ j − 1 ] ≤ s [ i ] [ j ] ≤ s [ i + 1 ] [ j ] s[i][j-1]\leq s[i][j]\leq s[i+1][j] s[i][j1]s[i][j]s[i+1][j]考虑右侧不等号
    m k [ i , j ] = m [ i , k ] + m [ k , j ] m_k[i,j]=m[i,k]+m[k,j] mk[i,j]=m[i,k]+m[k,j] s [ i ] [ j ] = d s[i][j]=d s[i][j]=d如果说 d d d是最优分割,因为取得是最小值,那么应该有 m k [ i , j ] ≥ m d [ i , j ] m_k[i,j]\geq m_d[i,j] mk[i,j]md[i,j]现在 d d d是最优分割,那么扩展到下一个区间的时候, d d d也仍然应该是最优分割,也就是 m k [ i + 1 , j ] ≥ m d [ i + 1 , j ] m_k[i+1,j]\geq m_d[i+1,j] mk[i+1,j]md[i+1,j]仍然成立,那么根据数学归纳法,如果能够证明这个式子,也就能够说明 s [ i ] [ j ] ≤ s [ i + 1 ] [ j ] s[i][j]\leq s[i+1][j] s[i][j]s[i+1][j]注意根据数学归纳法,我们现在已知条件是 m k [ i , j ] ≥ m d [ i , j ] m_k[i,j]\geq m_d[i,j] mk[i,j]md[i,j],要证明的是 m k [ i + 1 , j ] ≥ m d [ i + 1 , j ] m_k[i+1,j]\geq m_d[i+1,j] mk[i+1,j]md[i+1,j]
  • 将这两个式子整体作差,得到
    ( m k [ i + 1 , j ] − m d [ i , j ] ) − ( m k [ i , j ] − m d [ i , j ] ) (m_k[i+1,j]-m_d[i,j])-(m_k[i,j]-m_d[i,j]) (mk[i+1,j]md[i,j])(mk[i,j]md[i,j])根据 m k [ i + 1 , j ] = m [ i + 1 , k ] + m [ k , j ] m_k[i+1,j]=m[i+1,k]+m[k,j] mk[i+1,j]=m[i+1,k]+m[k,j] m k [ i , j ] = m [ i , k ] + m [ k , j ] m_k[i,j]=m[i,k]+m[k,j] mk[i,j]=m[i,k]+m[k,j] m d [ i + 1 , j ] = m [ i + 1 , d ] + m [ d , j ] m_d[i+1,j]=m[i+1,d]+m[d,j] md[i+1,j]=m[i+1,d]+m[d,j] m d [ i , j ] = m [ i , d ] + m [ d , j ] m_d[i,j]=m[i,d]+m[d,j] md[i,j]=m[i,d]+m[d,j]全部代入化简得到 m [ i + 1 , k ] − m [ i , k ] + m [ i , d ] − m [ i + 1 , d ] m[i+1,k]-m[i,k]+m[i,d]-m[i+1,d] m[i+1,k]m[i,k]+m[i,d]m[i+1,d]因为 i < i + 1 ≤ k ≤ d i\lt i+1\leq k\leq d i<i+1kd根据四边形不等式,有 m [ i , k ] + m [ i + 1 , d ] ≤ m [ i , d ] + m [ i + 1 , k ] m[i,k]+m[i+1,d]\leq m[i,d]+m[i+1,k] m[i,k]+m[i+1,d]m[i,d]+m[i+1,k]整理得到 m [ i + 1. k ] − m [ i , k ] + m [ i , d ] − m [ i + 1 , d ] ≥ 0 m[i+1.k]-m[i,k]+m[i,d]-m[i+1,d]\geq 0 m[i+1.k]m[i,k]+m[i,d]m[i+1,d]0正好对应刚才的化简结果,而这个结果正好对应着 m k [ i + 1 , j ] − m k [ i , j ] ≥ m d [ i + 1 , j ] − m d [ i , j ] m_k[i+1,j]-m_k[i,j]\geq m_d[i+1,j]-m_d[i,j] mk[i+1,j]mk[i,j]md[i+1,j]md[i,j]再移项,回头看一眼已知条件,可以得到 m k [ i + 1 , j ] − m d [ i + 1 , j ] ≥ m k [ i , j ] − m d [ i , j ] ≥ 0 m_k[i+1,j]-m_d[i+1,j]\geq m_k[i,j]-m_d[i,j]\geq 0 mk[i+1,j]md[i+1,j]mk[i,j]md[i,j]0也就是 m k [ i + 1 , j ] ≥ m d [ i + 1 , j ] m_k[i+1,j]\geq m_d[i+1,j] mk[i+1,j]md[i+1,j]
  • 右侧 ≤ \leq 证明完成,左侧 ≤ \leq 证明方式类似,只不过改为考虑 j j j而不是 i i i

时间复杂度分析

  • 这三层 f o r for for循环最外侧是 O ( n ) O(n) O(n)的,现在考虑内侧两层 f o r for for,这里还是以石子合并问题作为例子,但是降低难度,不要环形,改成直线,程序如下
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
const double eps = 1e-6;
int Data[MAXN];
int SUM[MAXN];
int dp[2050][2050];
int s[2050][2050];
int main(){
    
    
    int n;
    while(cin >> n){
    
    
        for(int i=1;i<=n;i++){
    
    
            cin >> Data[i];
            Data[i + n] = Data[i];
            SUM[i] = SUM[i - 1] + Data[i];
            dp[i][i] = 0;
            s[i][i] = i;
        }
        for(int len = 1; len < n; len++){
    
    
            for(int i=1;i<=n-len;i++){
    
    
                int j=i+len;
                dp[i][j] = INF;
                for(int k=s[i][j-1];k<=s[i+1][j];k++){
    
    
                    int tmp = dp[i][k] + dp[k + 1][j] + SUM[j] - SUM[i - 1];
                    if(dp[i][j] > tmp){
    
    
                        dp[i][j] = tmp;
                        s[i][j] = k;
                    }
                }
            }
        }
        cout << dp[1][n] << "\n";
    }
    return 0;
}
  • 考虑内侧两层 f o r for for,每个 k k k的循环次数是 s [ i + 1 [ j ] − s [ i ] [ j − 1 ] + 1 s[i+1[j]-s[i][j-1]+1 s[i+1[j]s[i][j1]+1次,那么经过 n − l e n n-len nlen次循环过后,内侧两层 f o r for for循环次数为(累加) ∑ i = 1 n − l e n s [ i + 1 ] [ j ] − s [ i ] [ j − 1 ] + 1 = s [ 2 ] [ j ] − s [ n − l e n ] [ j − 1 ] + n − l e n \sum_{i=1}^{n-len}s[i+1][j]-s[i][j-1]+1=s[2][j]-s[n-len][j-1]+n-len i=1nlens[i+1][j]s[i][j1]+1=s[2][j]s[nlen][j1]+nlen = n − l e n + s [ 2 ] [ j ] − s [ n − l e n ] [ j − 1 ] =n-len+s[2][j]-s[n-len][j-1] =nlen+s[2][j]s[nlen][j1]n后面都是常数,所以内侧两层 f o r for for的时间复杂度是 O ( n ) O(n) O(n),所以三层 f o r for for总的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

总结

  • 上面的证明是比较粗略的,储枫教授在她的论文中详细证明了四边形不等式在DP上面的应用,里面用到了几个引理,证明的过程很多,非常完整详细,如果想深入研究可以拿来作为参考,论文是全英的。其实也没有必要弄通它的全部,还是那句话,吾生也有涯,而知也无涯,以有涯逐无涯,殆矣。

参考文献

  1. 算法竞赛入门到进阶 区间DP
  2. 罗老师的博客https://blog.csdn.net/weixin_43914593/article/details/105150937
  3. 百度百科

猜你喜欢

转载自blog.csdn.net/roadtohacker/article/details/115281427
今日推荐