dp优化入门学习

20 篇文章 0 订阅
4 篇文章 0 订阅

单调队列优化dp

对于某个状态,可由一段连续区间转移,用单调队列维护实现O(1)转移

遗留题 P5858 「SWTR-03」Golden Sword
暴力dp d p i j dp_{i j} dpij 前i个,当前炼金锅有 j j j个物品的最大耐久度
j j j k ∈ [ j − 1 , j + s − 1 ] k \in[j-1,j+s-1] k[j1,j+s1] 转移 用单调队列优化枚举 k k k

P2254 [NOI2005] 瑰丽华尔兹
枚举每个时间段 k k k d p k , i , j dp_{k,i,j} dpk,i,j表示前 k k k 个时间段,当前位置为 ( i , j ) (i,j) (i,j)时滑动的最大值 ,对于每个时间段 确定了 ( i , j ) (i,j) (i,j)由哪段区间转移,如果中途有障碍,则无法转移至之后滑动到的点(队列清空),对于之前的位置单调队列维护最佳转移点

P2569 [SCOI2010]股票交易
d p i , j dp_{i,j} dpi,j i i i 天,手里还有 j j j 张股票,最多能赚的钱
枚举 i i i j j j,对于第i天,可以买卖股票,也可以什么都不干
什么都不干:直接由前一天转移 d p i , j = d p i − 1 , j dp_{i,j}=dp_{i-1,j} dpi,j=dpi1,j
买卖:在 [ j − w , j − 1 ] [j-w,j-1] [jw,j1] w w w 天,对于 j j j 来说无效
所以从 i − w − 1 i-w-1 iw1 这一天转移

枚举 j j j ,然后对于枚举 k k k(买卖前有 k k k 张股票),可以使用单调队列维护 k k k 所在区间最优转移点

P2627 [USACO11OPEN]Mowing the Lawn G
最朴素做法: d p i , j dp_{i,j} dpi,j表示前 i i i 个数,连续选了 j j j 个数的最大和
i i i 个数有选或不选两种决策 时间复杂度 O(NK),但转移的时候不是由一段连续的区间转移

显然可以进行更优的状态设计,实际就是在这 n n n 个数中,选出几个数(和尽可能小),相邻两数的距离不超过 k + 1 k+1 k+1 ,那么枚举到第 i i i 个数时,能转移的状态区间范围可以缩小,且是一段连续的区间,能使用单调队列优化
d p i = m i n ( d p j + a i ) , j ∈ [ i − k − 1 , i − 1 ] dp_i=min(dp_j+a_i),j\in[i-k-1,i-1] dpi=min(dpj+ai),j[ik1,i1]

2022南京 B
看到有修改,还有必建点的限制,不妨先丢到这两个额外的限制。
最简单版本:无修改,无必建点,然后就变成了一个很板的1D1Ddp,用单调队列优化
考虑加上必建点的限制:我们发现,如果我当前队列中要加进的点是必建点,那么在之后的转移,要保证这个点必选,队列中的所有点一定要全部弹出
考虑再加上修改的:先考虑最简单的,如果我当前修改的点是必建点,那么我只需要在原有答案上直接修改即可;但如果当前修改点不是必建点,我们考虑动态规划转移的一个贡献本质,假设当前修改点是 x x x , x x x 这个点最多只会对 [ x + 1 , x + k ] [x+1,x+k] [x+1,x+k] 产生直接的贡献,而 [ x + k , n ] [x+k,n] [x+k,n] 我们只会存在间接的贡献,也就是说,我们更新完 [ x + k , n ] [x+k,n] [x+k,n] 这些点后,后续我们的最小花费 g g g 是固定的(倒着dp预处理即可),那么对于更新后的 i ∈ [ x + 1 , x + k ] i \in [x+1,x+k] i[x+1,x+k] a n s = m i n ( a n s , f [ i ] + g [ i ] − a [ i ] ) ans=min(ans,f[i]+g[i]-a[i]) ans=min(ans,f[i]+g[i]a[i])

#include <bits/stdc++.h>
#define ll long long
#define ull unsigned long long
void yrzr(){
    int n,k;
    std::cin>>n>>k;
    n++;
    std::vector<int> a(n+1);
    for (int i=1;i<n;i++){
        std::cin>>a[i];
    }
    std::vector<int> must(n+1);
    must[0]=must[n]=1;
    for (int i=1;i<n;i++){
        char ch;
        std::cin>>ch;
        must[i]=ch-'0';
    }

    std::deque<int> q;
    q.push_back(0);
    std::vector<ll> f(n+1);
    for (int i=1;i<=n;i++){
        while (!q.empty()&&i-q.front()>k){
            q.pop_front();
        }
        f[i]=f[q.front()]+a[i];
        // std::cout<<i<<" "<<f[i]<<"\n";

        while (!q.empty()&&(must[i]==1||f[i]<f[q.back()])){
            q.pop_back();
        }
        q.push_back(i);
    }

    while (!q.empty()){
        q.pop_back();
    }

    std::vector<ll> g(n+1);
    
    q.push_back(n);
    for (int i=n-1;i>=0;i--){
        while (!q.empty()&&q.front()-i>k){
            q.pop_front();
        }
        g[i]=g[q.front()]+a[i];

        while (!q.empty()&&(must[i]==1||g[i]<g[q.back()])){
            q.pop_back();
        }
        q.push_back(i);
    }
    for (int i=0;i<=n;i++){
        g[i]-=a[i];
        // std::cout<<i<<" "<<f[i]+g[i]<<"\n";
    }

    int m;
    std::cin>>m;
    while (m--){
        int p,v;
        std::cin>>p>>v;
        int use=a[p];
        a[p]=v;

        std::vector<ll> temp(k+5);

        while (!q.empty()){
            q.pop_back();
        }
        for (int i=std::max(0,p-k);i<=p-1;i++){
            while (!q.empty()&&(must[i]==1||f[i]<f[q.back()])){
                q.pop_back();
            }
            q.push_back(i);
        }


        // std::cout<<"\n";
        for (int i=p;i<=std::min(p+k+1,n);i++){
            temp[i-p]=f[i];
            while (!q.empty()&&i-q.front()>k){
                q.pop_front();
            }
            f[i]=f[q.front()]+a[i];
            // std::cout<<f[i]<<" ";

            while(!q.empty()&&(must[i]==1||f[i]<f[q.back()])){
                q.pop_back();
            }
            q.push_back(i);
        }

        ll ans=1e18;
        for (int i=p;i<=std::min(n,p+k+1);i++){
            ans=std::min(ans,f[i]+g[i]);
        }
        // std::cout<<"\n";
        std::cout<<ans<<"\n";

        for (int i=p;i<=std::min(n,p+k+1);i++){
            f[i]=temp[i-p];
        }
        a[p]=use;
    }
}
int main(){
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);

    int T=1;
    std::cin>>T;
    while (T--){
        yrzr();
    }
    return 0;
}

斜率优化dp(单调一次函数)

核心就是构造一个一次函数, y y y只与转移点有关, x x x只与一次转移点有关, k k k为x前系数,即斜率,剩下 b b b d p i dp_i dpi及其它杂项
由求 d p i dp_i dpi 的最小 / / /大 转换成求截距的最小(维护下凸壳)/大(维护上凸壳),维护时横坐标需由小到大,舍弃不可能选取的转移点,然后使直线与凸包相切,得到最优转移点
斜率变化单调时,找最优点可以用单调栈或单调队列优化至O(1)
斜率变化不单调时,找最优点用二分

P2365 任务安排
入门斜率优化
P3195 [HNOI2008]玩具装箱
经典分区间,按题意转移,整理式子斜率优化

P4072 [SDOI2016]征途
同样分区间问题,由方差的公式,考虑每多一个区间,对答案产生的贡献转移方程,难点在于式子的表达转换与如何选择合适的 d p dp dp含义,最后套一个斜率优化

P2120 [ZJOI2007] 仓库建设 超级hack数据 末尾的点可能没有货物,不需要建立仓库(即不需要对其划分区间)
规定物品只能往编号更大的地方运,转换为分区间,区间内的货物都送往区间右端点的问题。对于区间的内的每个店单独到右端点的距离,整体考虑转换为前缀和解决。整理式子,套斜率优化,找到最优的上一个区间右端点进行转移

P3628 [APIO2010] 特别行动队
区间划分板子,由题意易得转移方程,整理后套斜率优化

P5504 [JSOI2011] 柠檬
贪心的想,分出的区间若确定使用的贝壳大小 x x x,左右端点必须为 x x x 大小的贝壳,易证:由于区间个数无限制,如果不是,可以缩小当前区间,使其它大小贝壳分到其它区间产生贡献

d p i dp_{i} dpi代表当前区间右端点为 i i i,区间选择 s i s_i si 大小的贝壳 所得到的最大答案 ,枚举前一个状态 j j j 时:
第一步优化:存储 s j = = s i s_j==s_i sj==si的点
第二步优化:在存储的这些点的基础上用多个贝壳大小不同单调栈维护最佳相同贝壳大小的转移点。

CF 311B Cats Transport
将问题转换一下:将每只猫在某个位置出现的时间转换为在1这个点出现的时间,也就是预处理人走到某只猫位置 所耗费的时间
最后对每只猫在1这个点出现的时间进行排序
问题转换为分区间,一个区间对应一个铲屎官,可将区间右端点看做铲屎官出发时间,对答案产生的贡献即每个区间的数与右端点数的差:
∑ j i ( t i − t j ) \sum_j^i (t_i-t_j) ji(titj) 类似P2120 [ZJOI2007] 仓库建设 前缀和预处理
然后套斜率优化找到最优的前一个区间的右端点

CF631E Product Sum
d p i dp_{i} dpi表示 i i i数字移到某个位置后,序列的最大价值。
分类讨论 i i i移到 [ 1 , i − 1 ] [1,i-1] [1,i1] [ i + 1 , n ] [i+1,n] [i+1,n],通过前缀和,枚举转移点 j j j O ( 1 ) O(1) O(1)转移,复杂度为 O ( n 2 ) O(n^2) O(n2)。拆开式子发现可以进行斜率优化, Y = s u m i Y=sum_i Y=sumi X = i X=i X=i K = a i K=a_i K=ai,维护下凸壳,因为 a i a_i ai不一定单调所以用二分寻找最优转移点
当然也可以先对 a i a_i ai基数排序(因为 i i i对选取 j j j的范围没有影响),然后直接单调队列优化,复杂度 O ( n ) O(n) O(n)

CF1179D Fedor Runs for President
树上斜优
新添加一条边产生的贡献就是经过这条边的路径条数,首先分析哪些点对可以经过这条路径,比如当前增加一条路径1-2,那么3-5能经过,3-4不能经过在这里插入图片描述
所以贡献式子为 ∑ s x ∗ ( n − s x ) 2 \sum {s_x*(n-s_x)} \over 2 2sx(nsx) s x s_x sx即以 x x x为一个根,不向环上扩展的子树大小,化简式子:
n 2 − ∑ s x 2 2 n^2-\sum {s_x}^2 \over 2 2n2sx2
因此我们只需要找到 s x 2 s_x^2 sx2的最小值即可
d p u dp_u dpu为在 u u u子树内,选取一个儿子的一条链,这条链的 ∑ s x 2 \sum {s_x}^2 sx2最小值
分类更新答案 ∑ s x 2 \sum {s_x}^2 sx2
1. u u u为新增路径端点,那么 ∑ s x 2 \sum {s_x}^2 sx2 就为 m i n ( d p v + ( n − s z v ) 2 ) min(dp_v+(n-sz_v)^2) min(dpv+(nszv)2)
2. u u u为新增路径的经过点,那么 ∑ s x 2 \sum {s_x}^2 sx2 就为
m i n ( d p v 1 + d p v 2 + ( n − s z v 1 − s z v 2 ) 2 ) min(dp_{v_1}+dp_{v_2}+(n-sz_{v_1}-sz_{v_2})^2) min(dpv1+dpv2+(nszv1szv2)2)

d p u dp_u dpu的转移也很好写,它的本质就是转移一条链,初始化 d p u = s z u 2 dp_u=sz_u^2 dpu=szu2,然后在选择儿子时取最小值 d p u = m i n ( d p v + ( s z u − s z v ) 2 ) dp_u=min(dp_v+(sz_u-sz_v)^2) dpu=min(dpv+(szuszv)2)

对于2转移,需要O(n^2),拆开转移式,发现可以进行斜率优化,需要注意的是,要让 e u e_{u} eu数组的儿子以 s z v sz_v szv从小到大排序

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值