dp斜率优化的学习

可供参考学习博客: link

斜率单调,二元组横坐标单调

hdu 3507: link
  
  很容易得到 dp 的表达式: d p [ i ] = m i n ( d p [ i ] ,   d p [ j ] + ( s u m [ i ] − s u m [ j ] ) 2 + M )   ( j < i ) dp[i] = min(dp[i], \ dp[j] + (sum[i] - sum[j])^2 + M) \ (j < i) dp[i]=min(dp[i], dp[j]+(sum[i]sum[j])2+M) (ji)
  
  可以化成: d p [ i ] = − 2 s u m [ i ] ∗ s u m [ j ] + d p [ j ] + s u m 2 [ j ] + s u m 2 [ i ] + M dp[i] = -2sum[i] * sum[j] + dp[j] + sum^2[j] + sum^2[i] + M dp[i]=2sum[i]sum[j]+dp[j]+sum2[j]+sum2[i]+M,对于 i i i 来说, s u m [ i ] , M sum[i], M sum[i],M 是一个常数
  若是把 ( s u m [ j ] ,   d p [ j ] + s u m 2 [ j ] ) (sum[j], \ dp[j] + sum^2[j]) (sum[j], dp[j]+sum2[j]) 放到一个坐标系中,即 x = s u m [ j ] ,   y = d p [ j ] + s u m 2 [ j ] ,   c = s u m 2 [ i ] + M x = sum[j], \ y = dp[j] + sum^2[j], \ c = sum^2[i] + M x=sum[j], y=dp[j]+sum2[j], c=sum2[i]+M

  可以得到 y = 2 s u m [ i ]   x   +   d p [ i ]   +   c y = 2sum[i] \ x \ + \ dp[i] \ + \ c y=2sum[i] x + dp[i] + c,由线性规划知识,为使得纵截距越小,应该维护一个下凸包(斜率增大)

  由于斜率是单增的,所以可以用单调队列维护凸包,每次只要保证队列头的斜率大于 2 s u m [ i ] 2sum[i] 2sum[i];并且所加入的点横坐标单增,维护凸包只要对队尾操作即可。

#include <bits/stdc++.h>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 5e5 + 10;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
//const double pi = acos(-1.0);
const double eps = 1e-11;

struct point
{
    ll x, y;
    point(ll k1, ll k2): x{k1}, y{k2} {}
    point() {}
}q[maxn];

int n, m, tmp, L, R;
ll a[maxn], sum[maxn], dp[maxn];

void Pop(ll k)        //维护一个下凸包,斜率单增
{
    while(R >= L + 1 && (q[L+1].y - q[L].y) <= k * (q[L+1].x - q[L].x))
        L++;
}


void Push(point p)              //维护一个下凸包
{
    ll x1, x2, y1, y2;
    while(R >= L + 1)
    {
        x1 = p.x - q[R - 1].x;
        y1 = p.y - q[R - 1].y;
        x2 = p.x - q[R].x;
        y2 = p.y - q[R].y;
        if(x1 * y2 - x2 * y1 > 0)
            break;
        R--;
    }
    q[++R] = p;
}

int main()
{
    while(~scanf("%d %d", &n, &m))
    {
        for (int i = 1; i <= n; i++)  scanf("%lld", &a[i]);
        for (int i = 1; i <= n; i++)  sum[i] = sum[i-1] + a[i];
        q[1].x = q[1].y = 0;
        L = R = 1;
        for (int i = 1; i <= n; i++)
        {
            Pop(2 * sum[i]);
            dp[i] = -2 * sum[i] * q[L].x + q[L].y + sum[i] * sum[i] + m;
            Push(point(sum[i], dp[i] + sum[i] * sum[i]));
        }
        printf("%lld\n", dp[n]);
    }
}

P4360:link
  
  首先可以算出只有山下锯木厂的费用,现在我们计算加了两个锯木厂之后节省了多少费用,则要求减少费用最大。
  
  记 s u m [ i ] sum[i] sum[i] [ 1 , i ] [1, i] [1,i] 这些树的重量, d i s t [ i ] dist[i] dist[i] 为第 i i i 棵树到达山下的距离。若 d p [ i ] dp[i] dp[i] 表示在 i i i 建立第二个锯木厂的最大节省费用(还有一个在 i i i 的上面),则有:
  
d p [ i ] = m a x ( d p [ i ] ,   s u m [ j ] ∗ d i s t [ j ] + ( s u m [ i ] − d i s t [ j ] ) ∗ d i s t [ i ] )   = m a x ( d p [ i ] ,   − d i s t [ i ] ∗ s u m [ j ] + s u m [ j ] ∗ d i s t [ j ] + s u m [ i ] ∗ d i s t [ i ] ) dp[i] = max(dp[i], \ sum[j] * dist[j] + (sum[i] - dist[j]) * dist[i]) \ = max(dp[i], \ -dist[i] * sum[j] + sum[j] * dist[j] + sum[i] * dist[i]) dp[i]=max(dp[i], sum[j]dist[j]+(sum[i]dist[j])dist[i]) =max(dp[i], dist[i]sum[j]+sum[j]dist[j]+sum[i]dist[i])
  
  若是把 ( s u m [ j ] ,   d i s t [ j ] ∗ s u m [ j ] ) (sum[j], \ dist[j]*sum[j]) (sum[j], dist[j]sum[j]) 放到一个坐标系中,即 x = s u m [ j ] ,   y = d i s t [ j ] ∗ s u m [ j ] ,   c = s u m [ i ] ∗ d i s t [ i ] x = sum[j], \ y = dist[j]*sum[j], \ c = sum[i] * dist[i] x=sum[j], y=dist[j]sum[j], c=sum[i]dist[i]

  可以得到 y = d i s t [ i ]   x   +   d p [ i ]   −   c y = dist[i] \ x \ + \ dp[i] \ - \ c y=dist[i] x + dp[i]  c,由线性规划知识,为使得纵截距越大,应该维护一个上凸包(斜率减小)

  由于斜率是单建的,所以可以用单调队列维护凸包,每次只要保证队列头的斜率小于 d i s t [ i ] dist[i] dist[i];并且所加入的点横坐标单增,维护凸包只要对队尾操作即可。

#include <bits/stdc++.h>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 5e4 + 10;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
//const double pi = acos(-1.0);
const double eps = 1e-11;

struct point
{
    ll x, y;
    point(ll k1, ll k2): x{k1}, y{k2} {}
    point() {}
}q[maxn];

int n, L, R;
ll d[maxn], sum[maxn], ans, res;

void Pop(ll k)
{
    while(R >= L + 1 && (q[L+1].y - q[L].y) >= k * (q[L+1].x - q[L].x))
        L++;
}


void Push(point p)
{
    ll x1, x2, y1, y2;
    while(R >= L + 1)
    {
        x1 = p.x - q[R - 1].x;
        y1 = p.y - q[R - 1].y;
        x2 = p.x - q[R].x;
        y2 = p.y - q[R].y;
        if(x1 * y2 - x2 * y1 < 0)
            break;
        R--;
    }
    q[++R] = p;
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)  scanf("%lld %lld", &sum[i], &d[i]);
    for (int i = 2; i <= n; i++)  sum[i] += sum[i-1];
    for (int i = 1; i <= n; i++)  res += sum[i] * d[i];
    for (int i = n - 1; i >= 1; i--)  d[i] += d[i+1];
    q[1].x = q[1].y = 0;
    L = R = 1;
    for (int i = 1; i <= n; i++)
    {
        Pop(d[i]);
        ans = max(ans, sum[i] * d[i] - q[L].x * d[i] + q[L].y);
        Push(point(sum[i], sum[i] * d[i]));
    }
    printf("%lld\n", res - ans);
}

斜率非单调,二元组横坐标非单调

   这种条件的做法需要CDQ分治或者平衡树,摘自:link
在这里插入图片描述
P4655: link

  若设 d p [ i ] dp[i] dp[i] 表示处理完前 i i i 个柱子的最小费用, s u m [ i ] sum[i] sum[i] 表示前 i i i 个柱子的拆除费用。

  转移为 d p [ i ] = m i n ( d p [ i ] ,   d p [ j ] + s u m [ i − 1 ] − s u m [ j ] + ( h [ i ] − h [ j ] ) 2 ) = − 2 h [ i ] ∗ h [ j ] + ( d p [ j ] − s u m [ j ] + h 2 [ j ] ) + s u m [ i − 1 ] + h 2 [ i ] dp[i] = min(dp[i], \ dp[j] + sum[i-1] - sum[j] + (h[i] - h[j])^2) = -2h[i] * h[j] + (dp[j] - sum[j] + h^2[j]) + sum[i-1] + h^2[i] dp[i]=min(dp[i], dp[j]+sum[i1]sum[j]+(h[i]h[j])2)=2h[i]h[j]+(dp[j]sum[j]+h2[j])+sum[i1]+h2[i]

  若是把 ( h [ j ] ,   d p [ j ] − s u m [ j ] + h 2 [ j ] ) (h[j], \ dp[j] - sum[j] + h^2[j]) (h[j], dp[j]sum[j]+h2[j]) 放到一个坐标系中,即 x = h [ j ] ,   y = d p [ j ] − s u m [ j ] + h 2 [ j ] ,   c = s u m [ i − 1 ] + h 2 [ i ] x =h[j], \ y = dp[j] - sum[j] + h^2[j], \ c = sum[i-1] + h^2[i] x=h[j], y=dp[j]sum[j]+h2[j], c=sum[i1]+h2[i]

  可以得到 y = 2 h [ i ]   x   +   d p [ i ]   −   c y = 2h[i] \ x \ + \ dp[i] \ - \ c y=2h[i] x + dp[i]  c,由线性规划知识,为使得纵截距越小,应该维护一个下凸包(斜率增大)

  但是这个斜率 2 h [ i ] 2h[i] 2h[i] 不单调,横坐标也不单调,所以需要 CDQ 分治来维护,具体细节可以见代码:

#include <bits/stdc++.h>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 1e5 + 10;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
//const double pi = acos(-1.0);
const double eps = 1e-11;

struct point             //来存决策点
{
    ll x, y;
    point(ll k1, ll k2): x{k1}, y{k2} {}
    point() {}
    bool operator<(const point &m)
    {
        return x ^ m.x ? x < m.x : y < m.y;
    }
}a[maxn], q[maxn];

struct node           //来存斜率
{
    int id;
    ll slope;
    bool operator<(const node &m)
    {
        return slope < m.slope;
    }
}b[maxn], tmpb[maxn];

int n;
ll h[maxn], dp[maxn], sum[maxn];

ll getx(int x)
{
    return h[x];
}

ll gety(int x)
{
    return dp[x] - sum[x] + h[x] * h[x];
}

void CDQ(int l, int r)
{
    if(l == r)        //当这个结点dp值已经被更新完之后,则它可以作为一个决策点,加入a数组
    {
        a[l].x = getx(l);
        a[r].y = gety(r);
        return;
    }
	
	//根据[L, mid]决策点来建立凸壳
    int mid = (l + r) / 2, L = 1, R = 0;
    ll x1, y1, x2, y2;
    CDQ(l, mid);
    sort(a + l, a + 1 + mid);    //将决策点按照横坐标排序(下凸包横坐标相同时,取纵坐标小的)
    q[++R] = a[l];
    for (int i = l; i <= mid; i++)
    {
        if(a[i].x == q[L].x)  continue;
        while(L < R)
        {
            x1 = a[i].x - q[R - 1].x;   y1 = a[i].y - q[R - 1].y;
            x2 = a[i].x - q[R].x;       y2 = a[i].y - q[R].y;
            if(x1 * y2 - x2 * y1 > 0)        //维护凸壳
                break;
            R--;
        }
        q[++R] = a[i];
    }
	
	//用左半部分的决策点更新[mid + 1, r]的dp值
    for (int i = mid + 1; i <= r; i++)  tmpb[i] = b[i];   
    sort(tmpb + 1 + mid, tmpb + 1 + r);     //根据斜率排序
    for (int i = mid + 1; i <= r; i++)
    {
        while(L < R && (q[L+1].y - q[L].y) <= tmpb[i].slope * (q[L+1].x - q[L].x))   
            L++;
        dp[tmpb[i].id] = min(dp[tmpb[i].id], - tmpb[i].slope * q[L].x + q[L].y + sum[tmpb[i].id - 1] + h[tmpb[i].id] * h[tmpb[i].id]);
    }
    CDQ(mid + 1, r);
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)  scanf("%lld", &h[i]);
    for (int i = 1; i <= n; i++)  scanf("%lld", &sum[i]);
    for (int i = 1; i <= n; i++)  sum[i] += sum[i-1];
    for (int i = 2; i <= n; i++)  dp[i] = dp[i-1] + (h[i] - h[i-1]) * (h[i] - h[i-1]);
    for (int i = 1; i <= n; i++)  b[i].id = i, b[i].slope = 2 * h[i];       //提前处理好每个点的斜率
    
    CDQ(1, n);
    printf("%lld\n", dp[n]);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值