斜率优化

  假设一个动态规划问题的状态表示为dp[i][j][k]……,通常最外层的几重循环分别是对i,j,k……等进行遍历,然后接下来几重循环(通常只有一重)遍历对dp[i][j][k]……这个状态的分解,并取出其中的最优解。斜率优化直观上的实现方式就是把各种分解方式映射为平面上的点集,一种决策方式对应一条经过点集中一个点的直线,这条直线的斜率是一个跟状态相关的常数(不同的状态斜率可能不同),而直线截距的则是该状态以该种决策方式分解得到的dp值,显然,这种情况下想得到截距最小的点不用遍历整个点集,而只要从点集的下凸包中寻找就可以了。因为已经计算完的状态会加入这个点集之中,所以斜率优化的主要工作就是维护这个下凸包。

  如果状态转移方程形式为:dp[i]=min{a[i]*b[j]+c[i]+d[j]},可以将其改写成:c[i]+d[j]=-a[i]*b[j]+dp[i],将其看成一条直线,c[i]+d[j]是函数值;-a[i]是斜率;b[j]是自变量;dp[i]是截距。

  那么对于状态dp[i],其分解方式构成的点集为{(b[j],c[i]+d[j])},每次选择的最优解位于该点集的下凸包上。

  对于下凸包的维护分两种情况:

  1)点的横坐标依次递增:这种情况下只需维护一个队列。设队列末尾的两个元素为k1和k2,即将加入点集的点为k,若k1k2的斜率大于k2k的斜率,那么此时应当删除k2,重复此步骤直到不满足条件为止,然后将k加到队列末尾(因为k横坐标最大,所以它一定是下凸包中的一员)。

  2)点的横坐标不满足递增:使用Splay或CDQ分治。

  对于已维护好的下凸包,最优的点的选择也分为两种情况:

  1)随着状态的改变,斜率依次递增:显然这种情况下最优的点一定是不断向右移动的,当满足当前状态的斜率大于队首两个元素之间的斜率时,删除队首元素,重复此步骤直到不满足条件为止,然后每次直接选择队首元素即可。

  2)随着状态的改变,斜率不满足递增:二分法确定最优点(设斜率为a,最优点为k,则应满足k-1k之间的斜率小于a,kk+1之间的斜率大于a)

  例题:

#pragma comment(linker, "/STACK:102400000,102400000")
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<vector>
#include<algorithm>
#include<iostream>
#include<map>
#include<queue>
#include<stack>
#include<string>
#include<functional>
#include<math.h>
//#include<bits/stdc++.h>
using namespace std;
typedef long long lint;
typedef vector<int> VI;
typedef pair<int, int> PII;
typedef queue<int> QI;


void makedata() {
    freopen("input.txt", "w", stdout);
    fclose(stdout);
}

int a[1500];
int dp[1500][1500], cost[1500], sum[1500], q[1500];

bool popHead(int k1, int k2, int i, int j) {
    return sum[i] * (sum[k2] - sum[k1]) >
           (dp[k2][j - 1] - cost[k2] + sum[k2] * sum[k2]) - (dp[k1][j - 1] - cost[k1] + sum[k1] * sum[k1]);
}
bool popTail(int k1, int k2, int k, int j) {
    return ((dp[k2][j - 1] - cost[k2] + sum[k2] * sum[k2]) - (dp[k1][j - 1] - cost[k1] + sum[k1] * sum[k1])) * (sum[k] - sum[k2]) >
           ((dp[k][j - 1] - cost[k] + sum[k] * sum[k]) - (dp[k2][j - 1] - cost[k2] + sum[k2] * sum[k2])) * (sum[k2] - sum[k1]);
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("input.txt", "r", stdin);
#endif
    //makedata();
    std::ios::sync_with_stdio(0), cin.tie(0);
    int n, m;
    while (cin >> n >> m) {
        if (n + m == 0) break;
        m++;
        for (int i = 1; i <= n; i++) cin >> a[i];
        sum[1] = a[1];
        for (int i = 2; i <= n; i++) sum[i] = sum[i - 1] + a[i];
        cost[1] = 0;
        for (int i = 2; i <= n; i++) cost[i] = cost[i - 1] + a[i] * sum[i - 1];
        memset(dp, 0x3F, sizeof(dp));
        for (int i = 1; i <= n; i++) dp[i][i] = 0, dp[i][1] = cost[i];
        for (int j = 2; j <= n; j++) {
            int h = 0, t = 0;
            q[t++] = j - 1;
            q[t++] = j;
            for (int i = j + 1; i <= n; i++) {
                /*for (int k = j - 1; k < i; k++) {
                    dp[i][j] = min(dp[i][j], dp[k][j - 1] + cost[i] - cost[k] - sum[k] * (sum[i] - sum[k]));
                }*/
                while (h + 1 < t && popHead(q[h], q[h + 1], i, j)) h++;
                int k = q[h];
                dp[i][j] = dp[k][j - 1] + cost[i] - cost[k] - sum[k] * (sum[i] - sum[k]);
                while (h + 1 < t && popTail(q[t - 2], q[t - 1], i, j)) t--;
                q[t++] = i;
            }
        }
        cout << dp[n][m] << endl;
    }
    return 0;
}

HDU2829:斜率和横坐标均单调递增