斜率dp模板

维护凸包。

/*
 我们假设k<j<i。如果在j的时候决策要比在k的时候决策好,那么也是就是dp[j]+M+(sum[i]-sum[j])^2<dp[k]+M+(sum[i]-sum[k])^2。(因为是最小花费嘛,所以优就是小于)
 
 两边移项一下,得到:(dp[j]+num[j]^2-(dp[k]+num[k]^2))/(2*(num[j]-num[k]))<sum[i]。我们把dp[j]-num[j]^2看做是yj,把2*num[j]看成是xj。
 
 那么不就是yj-yk/xj-xk<sum[i]么?   左边是不是斜率的表示?
 
 那么yj-yk/xj-xk<sum[i]说明了什么呢?  我们前面是不是假设j的决策比k的决策要好才得到这个表示的? 如果是的话,那么就说明g[j,k]=yj-jk/xj-xk<sum[i]代表这j的决策比k的决策要更优。
 
 
 
 关键的来了:现在从左到右,还是设k<j<i,如果g[i,j]<g[j,k],那么j点便永远不可能成为最优解,可以直接将它踢出我们的最优解集。为什么呢?
 
 我们假设g[i,j]<sum[i],那么就是说i点要比j点优,排除j点。
 
 如果g[i,j]>=sum[i],那么j点此时是比i点要更优,但是同时g[j,k]>g[i,j]>sum[i]。这说明还有k点会比j点更优,同样排除j点。
 
 排除多余的点,这便是一种优化!
 
 
 
 接下来看看如何找最优解。
 
 设k<j<i。
 
 由于我们排除了g[i,j]<=g[j,k]的情况,所以整个有效点集呈现一种上凸性质,即k j的斜率要大于j i的斜率。
 
 
 
 这样,从左到右,斜率之间就是单调递减的了。当我们的最优解取得在j点的时候,那么k点不可能再取得比j点更优的解了,于是k点也可以排除。换句话说,j点之前的点全部不可能再比j点更优了,可以全部从解集中排除。
 
 
 
 于是对于这题我们对于斜率优化做法可以总结如下:
 
 1,用一个单调队列来维护解集。
 
 2,假设队列中从头到尾已经有元素a b c。那么当d要入队的时候,我们维护队列的上凸性质,即如果g[d,c]<g[c,b],那么就将c点删除。直到找到g[d,x]>=g[x,y]为止,并将d点加入在该位置中。
 
 3,求解时候,从队头开始,如果已有元素a b c,当i点要求解时,如果g[b,a]<sum[i],那么说明b点比a点更优,a点可以排除,于是a出队。最后dp[i]=getDp(q[head])。
 
*/
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
typedef int LL;
LL a[500005];
LL dp[500005];
int q[500005];
LL s[500005];
LL get1(int n,int m){
    return dp[n]-dp[m]+s[n]*s[n]-s[m]*s[m];
}
LL get2(int n,int m){
    return s[n]-s[m];
}
int main()
{
    int i,n,m,tear,rear;
    while(scanf("%d%d",&n,&m)!=EOF){
        s[0]=0;
        dp[0]=0;
        for(i=1;i<=n;i++){
            scanf("%d",&a[i]);
            s[i]=s[i-1]+a[i];
        }
        rear=0;
        tear=0;
        q[tear++]=0;
        for(i=1;i<=n;i++){
            while(rear+1<tear){
                if(get1(q[rear+1],q[rear])<=2*s[i]*get2(q[rear+1],q[rear])) rear++;
                else break;
            }
            dp[i]=dp[q[rear]]+(s[i]-s[q[rear]])*(s[i]-s[q[rear]])+m;
            while(tear>rear+1){
                if(get1(i,q[tear-1])*get2(q[tear-1],q[tear-2])<=get1(q[tear-1],q[tear-2])*get2(i,q[tear-1])) tear--;
                else break;
            }
            q[tear++]=i;
        }
        printf("%d\n",dp[n]);
    }
}


<think>嗯,用户之前问过C++模板,现在又询问动态规划的常见模板。我需要先确定用户的需求是什么。可能用户正在学习算法,或者准备面试,需要掌握动态规划的实现方法。动态规划是一个比较复杂的主题,常见的问题类型有很多,比如背包问题、路径问题、字符串处理等。用户可能需要一些典型的模板来帮助理解和应用。 首先,我应该回顾用户之前的提问历史。用户之前得到了关于C++各种模板的回答,包括基础算法、数据结构、经典算法等。现在用户可能希望深入动态规划这一特定领域。需要确保提供的模板结构清晰,涵盖不同类型的问题,并且有代码示例和注意事项。 接下来,用户可能对动态规划的基本概念有所了解,但需要具体的实现模板。我需要分门别类地介绍常见的动态规划问题类型,比如线性DP、背包问题、区间DP、状态压缩DP等。每个类别下给出典型的例子,比如斐波那契数列、最长递增子序列、0-1背包等,并提供C++的代码模板。 同时,要注意代码的正确性和最佳实践,比如使用vector而不是原生数组,避免不必要的内存开销。还要提到初始化和状态转移方程的注意事项,因为这是动态规划的关键部分。此外,用户可能需要了解如何优化空间复杂度,比如滚动数组的技巧。 另外,用户可能在实际应用中遇到问题,比如如何定义状态,如何推导状态转移方程,或者在处理边界条件时的错误。因此,在注意事项部分需要强调这些点,帮助用户避免常见错误。 最后,应该提供一些学习建议,比如推荐练习题目,或者如何逐步掌握动态规划。这样用户不仅获得模板,还能知道如何应用和进一步学习。需要确保回答结构清晰,代码示例正确,并且语言通俗易懂,避免使用过于专业的术语,让不同层次的学习者都能理解。</think>以下是为您整理的C++动态规划常见模板分类及详解(附典型例题代码实现): --- ### **一、基础线性DP模板** #### 1. 斐波那契数列(一维DP) ```cpp int fib(int n) { if(n <= 1) return n; vector<int> dp(n+1); // 使用vector自动初始化 dp[0] = 0; dp[1] = 1; for(int i=2; i<=n; ++i) { dp[i] = dp[i-1] + dp[i-2]; // 状态转移方程 } return dp[n]; } // 优化空间版(滚动数组) int fib_opt(int n) { if(n <=1) return n; int prev = 0, curr = 1; for(int i=2; i<=n; ++i){ int sum = prev + curr; prev = curr; curr = sum; } return curr; } ``` #### 2. 最长递增子序列(LIS) ```cpp int lengthOfLIS(vector<int>& nums) { vector<int> dp(nums.size(), 1); // 初始化为1 int max_len = 1; for(int i=1; i<nums.size(); ++i){ for(int j=0; j<i; ++j){ if(nums[i] > nums[j]){ dp[i] = max(dp[i], dp[j]+1); // 状态转移 } } max_len = max(max_len, dp[i]); } return max_len; } ``` --- ### **二、背包问题模板** #### 1. 0-1背包(二维DP) ```cpp int knapsack(vector<int>& weights, vector<int>& values, int capacity) { int n = weights.size(); vector<vector<int>> dp(n+1, vector<int>(capacity+1, 0)); for(int i=1; i<=n; ++i){ for(int w=1; w<=capacity; ++w){ if(w < weights[i-1]){ dp[i][w] = dp[i-1][w]; // 无法放入 } else { dp[i][w] = max( // 决策:放或不放 dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1] ); } } } return dp[n][capacity]; } // 优化空间版(一维数组倒序遍历) int knapsack_opt(vector<int>& weights, vector<int>& values, int capacity) { vector<int> dp(capacity+1, 0); for(int i=0; i<weights.size(); ++i){ for(int w=capacity; w>=weights[i]; --w){ // 必须倒序 dp[w] = max(dp[w], dp[w-weights[i]] + values[i]); } } return dp[capacity]; } ``` #### 2. 完全背包(一维DP正序遍历) ```cpp int completeKnapsack(vector<int>& weights, vector<int>& values, int capacity) { vector<int> dp(capacity+1, 0); for(int i=0; i<weights.size(); ++i){ for(int w=weights[i]; w<=capacity; ++w){ // 正序遍历 dp[w] = max(dp[w], dp[w-weights[i]] + values[i]); } } return dp[capacity]; } ``` --- ### **三、区间DP模板** #### 矩阵链乘法(区间分割) ```cpp int matrixChainMultiplication(vector<int>& dims) { int n = dims.size()-1; // 矩阵个数 vector<vector<int>> dp(n, vector<int>(n, 0)); for(int len=2; len<=n; ++len){ // 区间长度 for(int i=0; i<=n-len; ++i){ // 起始点 int j = i + len - 1; // 结束点 dp[i][j] = INT_MAX; for(int k=i; k<j; ++k){ // 分割点 int cost = dp[i][k] + dp[k+1][j] + dims[i]*dims[k+1]*dims[j+1]; dp[i][j] = min(dp[i][j], cost); } } } return dp[0][n-1]; } ``` --- ### **四、状态压缩DP模板** #### 旅行商问题(TSP) ```cpp int tsp(vector<vector<int>>& dist) { int n = dist.size(); vector<vector<int>> dp(1<<n, vector<int>(n, INT_MAX/2)); dp[1][0] = 0; // 初始状态:只访问过0号城市 for(int mask=1; mask<(1<<n); ++mask){ for(int u=0; u<n; ++u){ if(!(mask & (1<<u))) continue; // u不在当前状态 for(int v=0; v<n; ++v){ if(mask & (1<<v)) continue; // v已在当前状态 int new_mask = mask | (1<<v); dp[new_mask][v] = min(dp[new_mask][v], dp[mask][u] + dist[u][v]); } } } int final_mask = (1<<n)-1; int res = INT_MAX; for(int u=1; u<n; ++u){ res = min(res, dp[final_mask][u] + dist[u][0]); } return res; } ``` --- ### **五、DP优化技巧** 1. **滚动数组优化**: ```cpp // 原二维数组 vector<vector<int>> dp(n, vector<int>(m)); // 优化为一维 vector<int> dp(m); ``` 2. **斜率优化**(单调队列): ```cpp deque<int> q; // 存储决策点 for(int i=0; i<n; ++i){ while(!q.empty() && check(q.front(), i)) q.pop_front(); dp[i] = calculate(q.front()); while(!q.empty() && better(i, q.back())) q.pop_back(); q.push_back(i); } ``` --- ### **六、注意事项** 1. **四步法解题**: - 定义状态(明确dp数组含义) - 确定状态转移方程 - 初始化边界条件 - 确定遍历顺序 2. **常见错误**: - 数组越界(特别注意索引从0还是1开始) - 未正确初始化(如最小值问题忘记初始化为极大值) - 状态转移条件不完整 3. **调试技巧**: ```cpp // 打印DP表 for(auto& row : dp){ for(int x : row) cout << setw(3) << x; cout << endl; } ``` --- ### **七、经典练习题** 1. 简单:爬楼梯(70)、打家劫舍(198) 2. 中等:最长公共子序列(1143)、零钱兑换(322) 3. 困难:编辑距离(72)、正则表达式匹配(10) 建议从基础模板入手,逐步理解状态设计和转移方程的本质,再挑战复杂变形问题。每个模板类型建议完成3-5道相似题型巩固理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值