美文网首页动态规划
DP训练——斜率优化DP

DP训练——斜率优化DP

作者: 云中翻月 | 来源:发表于2020-02-27 12:42 被阅读0次

    斜率优化DP

    斜率优化DP涉及到的模型较多,在编写习题题解前,先做出如下规律总结。

    如何识别斜率优化DP

    按照正常思路列出DP方程后,如发现其形如f[i]=max(f[j]+v(j)*w(i))的形式,即可使用斜率优化DP。(v(j)w(i)分别表示两个关于j和i的函数)

    斜率优化DP的化简步骤

    1 忽略方程中的minmax符号。
    2 假设外层循环为i时,对于决策点jk(j<k),若需要保证j的决策比k的决策更优,则f[j]+v(j)*w(i)<f[k]+v(k)*w(i)
    3 对上式分离参数,将j,k有关项作为分式(这个分式就是斜率)放在左侧,i有关项放在右侧,并保持jk的相对顺序不变。得\frac { f[j]-f[k]}{ v(j)-v(k)} < w(i)(该不等式中间的符号(小于号或大于号)取决于v(j)v(k)的相对大小)
    4 上式可以抽象为一个点集(v(j),f[j]),于是可以通过如下代码计算斜率。

    int X(int i){
        return v(i);
    }
    int Y(int i){
        return f[i];
    }
    double d(int i,int j){
        return ((double)Y(i)-Y(j))/(X(i)-X(j));
    }
    

    5 根据如下规则可判断凸包的形状。
    如果如上不等式的符号为大于号,则为下凸包(斜率递增)
    如果不等式的符号为小于号,则为上凸包(斜率递减)
    6 根据如下规则可判断维护斜率的方式。
    下凸包对应目标斜率w(i)​递增,上凸包对应目标斜率w(i)递减,满足这两个条件之一时就可以使用双端队列维护凸包。
    下凸包对应目标斜率w(i)​递减,上凸包对应目标斜率w(i)递增,满足这两个条件之一时就可以使用双端队列维护凸包。
    如果目标斜率w(i)​没有单调性,就需要在凸包中二分。
    如果如果v(j)v(k)的相对大小无法确定,就需要用平衡树维护凸包,或者整体二分。

    斜率优化DP的其他注意事项

    1 对于二维状态的斜率优化DP(或者存在转移点限制的斜率优化DP,例如BZOJ4709),需要在内层循环中维护多个单调队列。
    2 因为计算斜率时存在分式计算,所以需要考虑是否存在分母为零的情况,如若存在,则需要特判。(例如在BZOJ3675中,就将其斜率直接设为了INF,即永远不会被算入答案)

    斜率优化DP习题

    BZOJ1597
    题意
    n(n<=50000)个矩形,给定每个矩形的长宽(长宽不可交换)。每次可以选出若干个矩形,代价是矩形中长宽的最值乘积。求选出所有矩形的最小代价。
    题解
    首先通过排序(按矩形的长升序排序,相同则按宽升序排序),去除完全包含于其他矩形的矩形。
    这样剩下的矩形中任意选出一段[j,i],则长度的最大值为land[i].x,宽度的最大值为land[j].y
    然后进行dp。
    状态定义:f[i]表示处理好前i个矩形,需要的最小代价。
    边界:f[0]=0
    目标:f[n]
    转移方程:f[i]=min(f[i],f[j]+land[i].x*land[j+1].y),1<=j<=i-1
    这样的时间复杂度为O(n^2),考虑优化。
    忽略min,将该式展开,并让内层循环变量j对应的状态位于方程左边,得到f[j]=-land[i].x*land[j+1].y+f[i]
    队首需要维护的条件:
    当外层循环为i时,对于决策点jk(j<k),若需要保证j的决策比k的决策更优,则f[j]+land[i].x*land[j+1].y<f[k]+land[i].x*land[k+1].y
    分离参数(分离原则为将jk有关的项作为分式放在左边,并保持j和k的相对位置不变)得\frac{f[j]-f[k]}{land[j+1].y-land[k+1].y}<-land[i].x(注意这里计算时,land[j+1].y-land[k+1].y为负数,不等号方向会改变)
    因此,为了保证队首存储着最优决策,需要保证队首相邻两点斜率<-land[i].x
    队尾需要维护的条件:
    对应的dp数组可理解为处于第一象限的点集(land[x].y,f[x])。(因为排序的原因,这里的横坐标单调减,故队首为横坐标最大的点)
    最终关系式为小于号,就需要维护一个上凸包,即保证凸包上相邻两点斜率单调递减。
    根据本文开始的规则,题目满足上凸包中,-land[i].x递减,所以用单调队列维护。

    上凸包
    代码如下
    /*
    
    */
    #define method_1
    #ifdef method_1
    /*
    
    */
    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cmath>
    #include<set>
    #include<map>
    #include<queue>
    #include<stack>
    #include<vector>
    #include<cstring>
    #include<cstdlib>
    #include<iomanip>
    #include<ctime>
    #include<string>
    #include<bitset>
    #define D(x) cout<<#x<<" = "<<x<<"  "
    #define E cout<<endl
    using namespace std;
    typedef long long ll;
    typedef pair<int,int>pii;
    const int maxn=50000+5;
    const ll INF=0x3f3f3f3f3f3f3f3fll;
    int n;
    ll f[maxn];
    struct node{
        ll l,r;
        bool operator<(const node& h)const{return l<h.l||(l==h.l&&r<h.r);}
    }square[maxn],land[maxn];
    double d(int x,int y){ //计算x到y的直线斜率,x在y的左侧
        return ((double)f[y]-f[x])/(land[y+1].r-land[x+1].r);
    }
    void pre(){
        sort(square+1,square+n+1);
        int cnt=0;
        square[n+1].l=-INF,square[n+1].r=-INF;
        for(int i=1;i<=n;i++){
    //        if(square[i].l<=square[i+1].l&&square[i].r<=square[i+1].r) continue;
            while(cnt&&square[i].r>=land[cnt].r) cnt--; //用这种方式去除完全包含于其他矩形的矩形
            land[++cnt].l=square[i].l,land[cnt].r=square[i].r;
        }
        n=cnt;
        /*
        D(cnt);E;
        for(int i=1;i<=n;i++){
            D(land[i].l);D(land[i].r);E;
        }
        */
    }
    int q[maxn];
    int head=1,tail=0;
    void dp(){
        f[0]=0;
        q[++tail]=0;
        for(int i=1;i<=n;i++){
            while(head<tail&&d(q[head],q[head+1])>(double)(-land[i].l)) head++;
            f[i]=f[q[head]]+land[i].l*land[q[head]+1].r;
            while(head<tail&&d(q[tail-1],q[tail])<=d(q[tail],i)) tail--;
            q[++tail]=i;
        }
        printf("%lld",f[n]);
    }
    int main() {
        ios::sync_with_stdio(false);
        //freopen("BZOJ1597.in","r",stdin);
        scanf("%d",&n);
        for(int i=1;i<=n;i++) scanf("%lld%lld",&square[i].l,&square[i].r);
        pre();
        dp();
        return 0;
    }
    #endif
    #ifdef method_2
    /*
    
    */
    
    #endif
    #ifdef method_3
    /*
    
    */
    
    #endif
    

    BZOJ4518
    题意
    给定长度为n(n<=3000)的序列,需要将其分为m(m<=3000)段,使得这m段长度的方差v最小,求最小方差v*m^2。(可以证明,v*m^2是一个整数)
    题解
    首先推导方差的公式。(设a_i表示第i段的长度)
    v*m^2=\frac {\sum_{i=1}^{m}(a_i-\bar{a})^2}{m}*m^2
    因为\bar{a} =\frac { \sum_{i=1}^{m}a_i}{m}
    v*m^2=\sum_{i=1}^{m}(m*a_{i}^{2}-2*a_i*\sum_{i=1}^{m}a_i+\frac{(\sum_{i=1}^{m}a_i)^{2}}{m})
    将求和符号展开后,得到
    v*m^2=m*\sum_{i=1}^{m}(a_{i}^{2})-(\sum_{i=1}^{m}a_i)^2
    其中(\sum_{i=1}^{m}a_i)^2为定值,因此只需要考虑\sum_{i=1}^{m}(a_{i}^{2})的最小值。
    状态定义:f[i,j]表示前i个数字分为j段的最小\sum_{k=1}^{j}(a_{k}^{2})
    目标:m*f[n,m]-(\sum_{i=1}^{m}a_i)^2
    边界:f[0,0]=0,其他为INF
    转移方程:f[i,j]=min(f[k,j-1]+(a_{k+1}+...+a_{i})^{2})
    预处理前缀和s_i=a_1+...+a_i后,可在O(1)时间内计算(a_{k+1}+...+a_{i})^{2}
    故总时间复杂度为O(n^2*m)
    考虑优化时间复杂度,即对于状态i,j,要高效找到前继的最优状态k,j-1
    队首需要维护的条件:
    当外层循环为i,j时,对于决策点kl(k<l),若需要保证k的决策比l的决策更优,则f[k,j-1]+(s_i-s_{k})^{2}<f[l,j-1]+(s_i-s_{l})^{2}
    分离参数(将k,l有关项作为分式(这个分式就是斜率)放在左侧,i有关项放在右侧)得\frac { (f[k,j-1]+s_{k}^{2})-(f[l,j-1]+s_{l}^{2})}{s_k-s_l}>2*s_i(注意s_k-s_l<0,故推导时不等号符号会改变)
    因此,为了保证队首存储着最优决策,需要保证队首相邻两点斜率>2*s_i
    队尾需要维护的条件:
    对应的dp数组可理解为处于第一象限的点集(s_x,f[x,y]+s_{x}^{2})
    最终关系式为大于号,就需要维护一个下凸包,即保证凸包上相邻两点斜率单调递增。
    根据本文开始的规则,题目满足下凸包中,2*s_i递增,所以用单调队列维护。

    下凸包
    PS:代码中的状态定义和题解中的状态定义,第一维和第二维的含义交换了。
    题解链接 https://www.luogu.com.cn/problemnew/solution/P4072
    代码如下
    /*
    
    */
    #define method_1
    #ifdef method_1
    /*
    
    */
    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cmath>
    #include<set>
    #include<map>
    #include<queue>
    #include<stack>
    #include<vector>
    #include<cstring>
    #include<cstdlib>
    #include<iomanip>
    #include<ctime>
    #include<string>
    #include<bitset>
    #define D(x) cout<<#x<<" = "<<x<<"  "
    #define E cout<<endl
    using namespace std;
    typedef long long ll;
    typedef pair<int,int>pii;
    const int maxn=3000+5;
    const int INF=0x3f3f3f3f;
    int n,m,s[maxn];
    int f[maxn][maxn]; //f[i,j]表示处理到第i天,已经走了前j段的最小平方和
    int head,tail,q[maxn];
    double d(int i,int x,int y){
        return ((double)f[i][x]-f[i][y]+s[x]*s[x]-s[y]*s[y])/(s[x]-s[y]);
    }
    void init(){
        head=1,tail=0;
    }
    void dp(){
        init();
        memset(f,INF,sizeof(f));
        for(int i=0;i<=n;i++) f[1][i]=s[i]*s[i];
        for(int i=2;i<=m;i++){
            init();
            q[++tail]=0;
            for(int j=1;j<=n;j++){
                while(head<tail&&d(i-1,q[head],q[head+1])<2*s[j]) head++;
                f[i][j]=f[i-1][q[head]]+(s[j]-s[q[head]])*(s[j]-s[q[head]]);
    //            D(i);D(j);D(q[head]);D(f[i-1][q[head]]);D(f[i][j]);E;
                while(head<tail&&d(i-1,q[tail-1],q[tail])>d(i-1,q[tail],j)) tail--;
                q[++tail]=j;
            }
        }
    }
    int main() {
        ios::sync_with_stdio(false);
    //  freopen("BZOJ4518.in","r",stdin);
        cin>>n>>m;
        int x;
        for(int i=1;i<=n;i++){
            cin>>x;
            s[i]=s[i-1]+x;
        }
        dp();
        cout<<(ll)m*f[m][n]-s[n]*s[n];
        return 0;
    }
    #endif
    #ifdef method_2
    /*
    
    */
    
    #endif
    #ifdef method_3
    /*
    
    */
    
    #endif
    

    BZOJ3675
    题意
    长度为n(n<=100000)的非负整数序列,可以切割k(k<=200)次,每次得分为切割出的新的两个序列的元素和乘积,求最大得分。
    题解
    首先,注意到该题的得分与切割顺序无关。
    证明:
    考虑将一块abc分为a,b,c三块。
    方法一:先分成a,bc。答案为a(b+c)+bc=ab+ac+bc
    方法二:先分成ab,c。答案为c(a+b)+ab=ab+ac+bc
    即最终得分仅与切割点有关。
    状态定义:f[i,j]表示前i个数字,切割j次的最大得分
    目标:f[n,k]
    边界:f[0,0]=0
    转移方程:f[i,j]=max(f[k,j-1]+s_k*(s_i-s_k))(s[]为序列的前缀和)
    目前时间复杂度O(n^2*k),空间复杂度O(n*k),都需要优化。
    优化空间复杂度可使用滚动数组(因为f[,j]状态仅仅依赖于f[,j-1]状态,所以可以滚动数组优化第二维),
    考虑优化时间复杂度,即对于状态i,j,要高效找到前继的最优状态k,j-1
    队首需要维护的条件:
    当外层循环为i,j时,对于决策点kl(k<l),若需要保证k的决策比l的决策更优,则f[k,j-1]+s_k*(s_i-s_k)>f[l,j-1]+s_l*(s_i-s_l)
    分离参数(将k,l有关项作为分式(这个分式就是斜率)放在左侧,i有关项放在右侧)得\frac{(f[k,j-1]-s_{k}^{2})-(f[l,j-1]-s_{l}^{2})}{s_k-s_l}<-s_i
    因此,为了保证队首存储着最优决策,需要保证队首相邻两点斜率<-s_i(注意这里s_k可能等于s_l所以需要特判)
    队尾需要维护的条件:
    对应的dp数组可理解为点集(s_x,f[x,y]-s_{x}^{2})
    最终关系式为小于号,就需要维护一个上凸包,即保证凸包上相邻两点斜率单调递减。
    根据本文开始的规则,题目满足上凸包中,-s_i递减,所以用单调队列维护。

    上凸包
    PS:根据状态定义,每次由f[,j-1]f[,j]递推,所以外层循环变量j,内层循环变量i
    题解链接 https://www.luogu.com.cn/problemnew/solution/P3648
    代码如下
    /*
    
    */
    #define method_1
    #ifdef method_1
    /*
    
    */
    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cmath>
    #include<set>
    #include<map>
    #include<queue>
    #include<stack>
    #include<vector>
    #include<cstring>
    #include<cstdlib>
    #include<iomanip>
    #include<ctime>
    #include<string>
    #include<bitset>
    #define D(x) cout<<#x<<" = "<<x<<"  "
    #define E cout<<endl
    using namespace std;
    typedef long long ll;
    typedef pair<int,int>pii;
    const int maxn=1e5+5;
    const int maxk=200+5;
    const double INF=1e18;
    int n,k;
    ll s[maxn];
    ll f[maxn][2];
    int head,tail,q[maxn];
    double d(int i,int x,int y){
        if(s[x]==s[y]) return INF;
        return ((double)f[x][i&1]-s[x]*s[x]-(f[y][i&1]-s[y]*s[y]))/(s[x]-s[y]);
    }
    void init(){
        head=1,tail=0;
    }
    void dp(){
        init();
        for(int j=1;j<=k;j++){
            init();
            q[++tail]=0;
            for(int i=1;i<=n;i++){
                while(head<tail&&d(j-1,q[head],q[head+1])>-s[i]) head++;
                f[i][j&1]=f[q[head]][j-1&1]+s[q[head]]*(s[i]-s[q[head]]);
                while(head<tail&&d(j-1,q[tail-1],q[tail])<d(j-1,q[tail],i)) tail--;
                q[++tail]=i;
            }
        }
    }
    int main() {
    //  ios::sync_with_stdio(false);
        //freopen("BZOJ3675.in","r",stdin);
        scanf("%d%d",&n,&k);
        ll x;
        for(int i=1;i<=n;i++){
            scanf("%lld",&x);
            s[i]=s[i-1]+x;
        }
        dp();
        cout<<f[n][k&1];
        return 0;
    }
    #endif
    #ifdef method_2
    /*
    
    */
    
    #endif
    #ifdef method_3
    /*
    
    */
    
    #endif
    

    BZOJ4709
    题意
    长度为n的正整数序列s[],每次可以从序列的一端选择一个区间,并选择一个常数s_0。则该区间的贡献为区间内s_0的个数t^2*s_0,求总贡献的最大值。
    题解
    存在一个据说很显然的结论:每次选取的区间左右端点值必然相同,并且该值即为即将为区间选定的常数s_0
    这个结论的证明不难,因为确定左端点的情况下,右端点的左右移动,都不会使得该区间的答案更优。
    预处理出c[i]表示s[i]是相同大小的数中的第几个后,即可开始dp。
    状态定义:f[i]表示前i个数的最大贡献
    目标:f[n]
    边界:f[0]=0
    转移方程:f[i]=max(f[j-1]+s[i]*(c[i]-c[j]+1)^2),s[i]==s[j]
    当前时间复杂度为O(n^2),考虑优化,即对于状态j,要高效找到前继的最优状态k
    队首需要维护的条件:
    当外层循环为i时,对于决策点jk(j<k),若需要保证j的决策比k的决策更优,则f[j-1]+s[i]*(c[i]-c[j]+1)^2>f[k-1]+s[i]*(c[i]-c[k]+1)^2
    分离参数(将j,k有关项作为分式(这个分式就是斜率)放在左侧,i有关项放在右侧)得\frac {(f[j-1]+s[j]*c[j]*c[j]-2*s[j]*c[j])-(f[k-1]+s[k]*c[k]*c[k]-2*s[k]*c[k])}{c[j]-c[k]}<2*s_i*c_i(化简时须利用s[i]=s[j]=s[k],并考虑c[j]<c[k]
    因此,为了保证队首存储着最优决策,需要保证队首相邻两点斜率<2s_ic_i
    队尾需要维护的条件:
    对应的dp数组可理解为点集(c[x],f[x-1]+s[x]*c[x]*c[x]-2*c[x]*s[x])
    最终关系式为小于号,就需要维护一个上凸包,即保证凸包上相邻两点斜率单调递减。
    根据本文开始的规则,题目满足上凸包中,2*s_i*c_i递增,所以用单调栈维护。
    (同时还需将上述分析中的队首条件改换为队尾条件,并且转换不等号方向(这是因为原先推导队首条件时,假设j<kjk更优,而到了单调栈中,应假设j>kjk更优))

    上凸包
    PS:
    因为所有转移均在s_i=s_j这样的数字之间发生,所以实现中需要对每组相同的s_i维护一个单调栈。
    另外,因为是单调栈不是单调队列,所以最优解会在尾端取到。因此dp时对应的四条语句有了顺序变化。
    1 准备放入i,把不满足条件的栈顶弹出。
    2 i进栈。
    3 更新目标斜率,把不满足目标斜率的栈顶弹出。
    4 根据栈顶求f[i]
    题解链接 https://www.luogu.com.cn/problemnew/solution/P5504
    代码如下
    /*
    
    */
    #define method_1
    #ifdef method_1
    /*
    
    */
    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cmath>
    #include<set>
    #include<map>
    #include<queue>
    #include<stack>
    #include<vector>
    #include<cstring>
    #include<cstdlib>
    #include<iomanip>
    #include<ctime>
    #include<string>
    #include<bitset>
    #define D(x) cout<<#x<<" = "<<x<<"  "
    #define E cout<<endl
    using namespace std;
    typedef long long ll;
    typedef pair<int,int>pii;
    const int maxn=100000+5;
    const int maxs=10000+5;
    const int INF=0x3f3f3f3f;
    int n;
    ll s[maxn],c[maxn];
    int tot[maxs];
    ll f[maxn];
    vector<int>st[maxs];
    ll X(int i){
        return c[i];
    }
    ll Y(int i){
        return f[i-1]+s[i]*c[i]*c[i]-2*c[i]*s[i];
    }
    ll calc(int i,int j){
        return f[j-1]+s[i]*(c[i]-c[j]+1)*(c[i]-c[j]+1);
    }
    double d(int i,int j){
        return ((double)Y(i)-Y(j))/(X(i)-X(j));
    }
    #define t1 st[t][st[t].size()-1]
    #define t2 st[t][st[t].size()-2]
    void dp(){
        for(int i=1;i<=n;i++){
            int t=s[i];
            while(st[t].size()>=2&&d(i,t1)>=d(t1,t2)) st[t].pop_back();
            st[t].push_back(i);
            while(st[t].size()>=2&&d(t1,t2)<=2*c[i]*s[i]) st[t].pop_back();
            f[i]=calc(i,t1);
        }
    }
    int main() {
        ios::sync_with_stdio(false);
        //freopen("BZOJ4709.in","r",stdin);
        scanf("%d",&n);
        for(int i=1;i<=n;i++){
            scanf("%lld",&s[i]);
            tot[s[i]]++;
            c[i]=tot[s[i]];
        }
        dp();
        printf("%lld",f[n]);
        return 0;
    }
    #endif
    #ifdef method_2
    /*
    
    */
    
    #endif
    #ifdef method_3
    /*
    
    */
    
    #endif
    

    相关文章

      网友评论

        本文标题:DP训练——斜率优化DP

        本文链接:https://www.haomeiwen.com/subject/jbwdqhtx.html