C++ 动态规划:一维动态规划,背包问题,区间动态规划
C++ 动态规划
动态规划的定义
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。动态规划是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。简单来说动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。
动态规划的基本思想
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算。
一维动态规划
示例:
【题目描述】
一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法
要想跳到第 10 级台阶,要么是先跳到第 9 级,然后再跳 1 级台阶上去;要么是先
跳到第 8 级,然后一次迈 2 级台阶上去。同理,要想跳到第 9 级台阶,要么是先跳到第 8 级,然后再跳 1 级台阶上去;要么是先跳到第 7 级,然后一次迈 2 级台阶上去。要想跳到第 8 级台阶,要么是先跳到第 7 级,然后再跳 1 级台阶上去;要么是先跳到第 6 级,然后一次迈 2 级台阶上去。
假设跳到第 n 级台阶的跳数我们定义为 f(n),很显然就可以得出以下公式:
F(10) = f(9)+f(8)
f (9) = f(8) + f(7)
f (8) = f(7) + f(6)
...
f(3) = f(2) + f(1)
即通用公式为: f(n) = f(n-1) + f(n-2)
那 f(2) 或者 f(1) 等于多少呢?
当只有 2 级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一
级。即 f(2) = 2;当只有 1 级台阶时,只有一种跳法,即 f(1)= 1;
这样就可以用递归算法解决因此,青蛙跳阶,递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),就是指数级别的,爆炸增长的,如果 n 比较大的话,超时很正常的了。
回过头来,你仔细观察这颗递归树,你会发现存在大量重复计算,比如 f(8)被计算
了两次,f(7)被重复计算了 3 次...所以这个递归算法低效的原因,就是存在大量的重复
计算!
既然存在大量重复计算,那么我们可以先把计算好的答案存下来,即造一个备忘录,等到下次需要的话,先去备忘录查一下,如果有,就直接取就好了,备忘录没有才开始计算,那就可以省去重新重复计算的耗时啦!这就是带备忘录的解法。
使用动态规划解法如下:
///关键代码:
int dp[10005];
dp[1]=1;
dp[2]=2;
int numWays(int n) {
for (int i = 3; i <= n; i++) {
dp[i] = (dp[i-1] + dp[i-2]); //状态转移方程
}
return dp[n];
}
f(n-1)和 f(n-2) 称为 f(n) 的最优子结构
f(n)= f(n-1)+f(n-2) 就称为状态转移方程
f(1) = 1, f(2) = 2 是边界
比如 f(10)= f(9)+f(8),f(9) = f(8) + f(7) ,f(8)就是重叠子问题
动态规划的解题思路如下:
1.穷举分析
2.确定边界
3.找出规律,确定最优子结构
4.写出状态转移方程
背包类型动态规划
背包问题的定义:
背包问题(Knapsack problem)是一种组合优化的 NP 完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过 W 的前提下,总价值是否能达到 V?它是在 1978 年由 Merkle 和 Hellman 提出的,定义如下:我们有 n 种物品,物品 j 的重量为 wj,价格为 pj。我们假定所有物品的重量和价格都是非负的。背包所能承受的最大重量为 W。如果限定每种物品只能选择 0 个或 1 个,则问题称为 0-1 背包问题。
背包问题的原理:
动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
背包问题的分类
背包问题可以总结为三类:01 背包问题、完全背包问题以及分组背包问题。
01 背包问题:每个元素最多取 1 次。具体来讲:一共有 N 件物品,第 i(i 从 1 开
始)件物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能
够装入背包的最大价值是多少?
完全背包问题:每个元素可以取多次。具体来讲:完全背包与 01 背包不同就是每种
物品可以有无限多个:一共有 N 种物品,每种物品有无限多个,第 i(i 从 1 开始)种
物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能够装入
背包的最大价值是多少?
分组背包问题:有多个背包,需要对每个背包放入物品,每个背包的处理情况与完全
背包完全相同。
在完全背包问题当中根据是否需要考虑排列组合问题(是否考虑物品顺序),可分为
两种情况,我们可以通过内外循环的调换来处理排列组合问题,如果题目不是排列组合问
题,则这两种方法都可以使用(推荐使用组合来解决)
而每个背包问题要求的也是不同的,按照所求问题分类,又可以分为以下几种:
1、最值问题:要求最大值/最小值
2、存在问题:是否存在…………,满足…………
3、组合问题:求所有满足……的排列组合
解题模板:
1.01 背包问题
【题目描述】
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
【输入格式】
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
【输出格式】
输出一个整数,表示最大价值。
【思路解析】
「01 背包」是指给定物品价值与体积(对应了「给定价值与成本」),在规定容量下
(对应了「限定决策规则」)如何使得所选物品的总价值最大。
1.01 背包是动态规划类问题。一般需要将主问题分解为多个子问题,然后记录下不
同子问题的解,最终才能推得最终问题的答案。
2.定义子问题:前 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值是多少?
3.我们用 f[i][v] 表示 前 i 件物 恰放入一个 容量为 v 的背包可以获得的 最大价值。
4.要求主问题 f[i][v] 就得先知道子问题 F[i-1][v]和 f[i-1][ v-v[i] ]是多少 。然后通过状
态转移方程:f[i][v]=max{ f[i-1][v] , f[i-1][ v-v[i] ]+c[i]] }
5.状态转移方程是整个背包问题的核心。解释:“前 i 个物品放入容量为 v 的背包中”
可以转化为“前 i-1 个物品已经放入了容量为 v 的背包中(即为 f[i-1][v])” 和 “第 i 个物品
放还是不放入背包中?”
如果第 i 个物品不放入背包,那么 f[i][v]=f[i-1][v]
如果第 i 个物品放入背包,那么 f[i][v]=f[i-1][ v-v[i] ] + c[i],此时这个式子里面只有
f[i-1][ v-v[i] ]未知,问题转化为“前 i-1 个物品放入了剩余容量为 v-v[i]的背包中能获
得最大价值是多少?
代码实现:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxN=1010;
int f[maxN][maxN]; //f[i][j]体积为 j 的背包在前 i 件物品中能装的最大价值
int v[maxN]; //物品体积
int w[maxN]; //物品价值
int main()
{
int n,m;
memset(f,0,sizeof(f));
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d %d",&v[i],&w[i]); //各物品价值
}
//计算所有子问题的答案。
for(int i = 1;i <= n; i++){
for(int j = 0;j <= m; j++){
f[i][j]=f[i-1][j]; //体积为 i-1 可以拿 f[i-1][j], 那体积比它大的 i 的至少也可以拿它那么多吧
if(j >= v[i]) // 防止 f[j-v[i]]越界
f[i][j] = max(f[i-1][j] , f[i-1][j - v[i]] + w[i]);
}
}
printf("%d\n",f[n][m]);
return 0;
}
2.完全背包
【题目描述】
有 n 种物品和一个容量为 c 的背包,每种物品都有无限件。
第 i 件物品的体积是 v[i],价值是 w[i]。
求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
【输入样例】
2 5
1 1
2 2
【输出样例】
5
解释:选一件物品 1,再选两件物品 2,可使价值最大 。
【思路解析】
我们可以直接将 01 背包的「状态定义」拿过来用:
dp[i][j]代表考虑前 i 件物品,放入一个容量为 j 的背包可以获得的最大价值。
由于每件物品可以被选择多次,因此对于某个 dp[i][j] 而言,其值应该为以下所有可
能方案中的最大值:
选择 0 件物品 i 的最大价值,即 dp[i-1][j]
选择 1 件物品 i 的最大价值,即 dp[i-1][j-v[i]]+w[i]
选择 2 件物品 i 的最大价值,即 dp[i-1][j-2v[i]]+2w[i]
...
选择 k 件物品 i 的最大价值,dp[i-1][j-kv[i]]+kw[i]
由此我们可以得出「状态转移方程」为:
dp[i][j] = max(dp[i-1][j],dp[i-1][j-2v[i]]+2w[i]), 0<k*v[i]≤j
代码实现
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxN=1010;
int f[maxN][maxN]; //f[i][j]体积为 j 的背包在前 i 件物品中能装的最大价值
int v[maxN]; //物品体积
int w[maxN]; //物品价值
int main()
{
int n,c;
memset(f,0,sizeof(f));
scanf("%d %d",&n,&c);
for(int i=0;i<n;i++){
scanf("%d %d",&v[i],&w[i]); //各物品价值
}
// 先预处理第一件物品
for (int j = 0; j <= c; j++) {
// 显然当只有一件物品的时候,在容量允许的情况下,能选多少件就选多少件
int maxK = j / v[0];
f[0][j] = maxK * w[0];
}
for (int i = 1; i < n; i++) {
for (int j = 0; j <= c; j++) {
// 不考虑第 i 件物品的情况(选择 0 件物品 i)
int x = f[i - 1][j];
// 考虑第 i 件物品的情况
int y = 0;
int k=1; //选择第 i 件物品的数量
while(j >= v[i] * k){
y = max(y, f[i - 1][j - k * v[i]] + k * w[i]);
k++;
}
f[i][j] = max(x, y);
}
}
printf("%d\n",f[n-1][c]);
return 0;
}
区间动态规划
区间动态规划定义:
区间类动态规划是线性动态规划的拓展,它在分阶段划分问题时,与阶段中元素出现
的顺序和由前一阶段的哪些元素合并而来有很大的关系。如状态 f[i][j],它表示以已合并的
次数为阶段、以区间的左端点 i 为状态,它的值取决于第 i 个元素和第 j 个元素断开的位置
k,即 f[i][k]+f[k+1][j]的值。这一类型的动态规划,阶段特征非常明显,求最优值时需要预
先设置阶段内的区间统计值,还要以动态规划的起始位置来判断。
区间类动态规划的特点:
合并:即将两个或多个部分进行整合,当然也可以反过来,也就是对一个问题分解成
两个或多个部分。
特征:对将问题分解成为两两合并的形式。
求解:对整个问题设最优值、枚举合并点,将问题分解成左右两个部分,最后合并左
右两个部分的最优值得到原问题的最优值。有点类似分治算法的解题思想。
区间类动态规划的典型应用有:石子合并、能量项链、凸多边形的划分等。
区间型 DP 中的区间通常指的就是字面意义上的区间,以一组数列为例,那么[i][j]表示
的就是 i 到 j 的这个区间。
区间型动态规划的递归关系,通常是呈现出去头去尾的特点,以下面的伪代码为例。
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) //区间[i][j]由去掉头(或者去掉尾)的[i + 1][j](dp[i][j - 1])推
出
基于此种特性,区间型 DP 的代码实现通常要对区间长度进行循环,即从区间长度短
的循环到区间长度长的,当求当前状态的值的时由于使用到的子状态的区间长度都小于当
前状态,因此都可以推出,非常的合理且符合直觉。
伪代码:
for (int i = 1; i <= len; i++) //循环区间长度
for (int j = 0; j <= n - len; j++) //循环做端点
for (int z = j + 1; z < n; z++) //循环右端点
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); //转移方程
例题解析:石子合并
【题目描述】
将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。规定每次只能选相邻的
两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:
选择一种合并石子的方案,使得做 n-1 次合并得分总和最大。
选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。
【输入格式】
输入第一行一个整数 n,表示有 n 堆石子。
第二行 n 个整数,表示每堆石子的数量。
【输出格式】
输出共两行:
第一行为合并得分总和最小值,
第二行为合并得分总和最大值。
【输入样例】
4
4 5 9 4
【输出样例】
43
54
数据范围与提示
对于 100%的数据,有 1≤n≤200。
【思路解析】
最初的第 l 堆石子和第 r 堆石子被合并成一堆,则说明 l~r 之间的每堆石子也已经被
合并,这样 l 和 r 才有可能相邻。因此,在任意时刻,任意一堆石子均可以用一个闭区间
[l,r]来描述,表示这堆石子是由最初的第 l~r 堆石子合并而成的,其重量为∑ai(l≤i≤r)。
另外,一定存在一个整数 k(l≤k<r),在这堆石子形成之前,先有第 l~k 堆石子(闭区间[l,k])
被合并成一堆,第 k+1~r 堆石子(闭区间[k+1,r])被合并成一堆,然后这两堆石子才合并成
[l,r]。
对应到动态规划中,就意味着两个长度较小的区间上的信息向一个更长的区间发生了
转移,划分点 k 就是转移的决策。自然地,应该把区间长度 len 作为 DP 的阶段。不过,区
间长度可以由左端点和右端点表示,即 len=r-l+1。本着动态规划“选择最小的能覆盖状态
空间的维度集合”的思想,我们可以只用左、右端点表示 DP 的状态。
设 sum[i]表示从第 l 堆到第 i 堆石子数总和 sum[i]=a[1]+a[2]…+a[i]。
Fmax[i][j]表示从第 i 堆石子合并到第 j 堆石子的最大的得分。
Fmin[i][j]表示从第 i 堆石子合并到第 j 堆石子的最小的得分。
则状态转移方程为:
Fmax[i][j]=max{Fmax[i][k]+Fmax[k+1][j]+sum[j]-sum[i-1]}(i≤k≤j-1).
Fmin[i][j]=min(Fmin[i][k]+Fmin[k+1][j]+sum[j]-sum[i-1])(i≤k≤j-1)
初始条件:Fmax[i][i]=0,Fmin[i][i]=INF。
时间复杂度为 O(n³)。
代码实现
#include <iostream>
using namespace std;
const int maxn=205,inf=0x7fffffff/2;
int f1[maxn][maxn],f2[maxn][maxn],s[maxn][maxn]={0};
int a[maxn],sum[maxn]={0},n,ans1,ans2;
void dp(){
for(int l=2;l<=n;l++){
for(int i=1;i<=2*n-l+1;i++){
int j=i+l-1;
f1[i][j]=inf;
f2[i][j]=0;
for(int k=i;k<j;k++){
f1[i][j]=min(f1[i][j],f1[i][k]+f1[k+1][j]);
f2[i][j]=max(f2[i][j],f2[i][k]+f2[k+1][j]);
}
f1[i][j]+=sum[j]-sum[i-1];
f2[i][j]+=sum[j]-sum[i-1];
}
}
ans1=inf,ans2=0;
for(int i=1;i<=n;i++)
ans1=min(ans1,f1[i][i+n-1]);
for(int i=1;i<=n;i++)
ans2=max(ans2,f2[i][i+n-1]);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
a[i+n]=a[i];
}
for(int i=1;i<=n*2;i++){
sum[i]=sum[i-1]+a[i];
f2[i][i]=0;
f1[i][i]=0;
}
dp();
printf("%d\n%d\n",ans1,ans2);
return 0;
}
以上便是这篇文章的所有内容,希望对你有帮助。~~
C++ 动态规划:一维动态规划,背包问题,区间动态规划的更多相关文章
- 【动态规划】简单背包问题II
问题 B: [动态规划]简单背包问题II 时间限制: 1 Sec 内存限制: 64 MB提交: 21 解决: 14[提交][状态][讨论版] 题目描述 张琪曼:“为什么背包一定要完全装满呢?尽可能 ...
- 01背包问题(动态规划)python实现
01背包问题(动态规划)python实现 在01背包问题中,在选择是否要把一个物品加到背包中.必须把该物品加进去的子问题的解与不取该物品的子问题的解进行比較,这样的方式形成的问题导致了很多重叠子问题, ...
- 【算法•日更•第十期】树型动态规划&区间动态规划:加分二叉树题解
废话不多说,直接上题: 1580:加分二叉树 时间限制: 1000 ms 内存限制: 524288 KB提交数: 121 通过数: 91 [题目描述] 原题来自:NOIP 20 ...
- 【算法•日更•第三十期】区间动态规划:洛谷P4170 [CQOI2007]涂色题解
废话不多说,直接上题: P4170 [CQOI2007]涂色 题目描述 假设你有一条长度为5的木版,初始时没有涂过任何颜色.你希望把它的5个单位长度分别涂上红.绿.蓝.绿.红色,用一个长度为5的字符 ...
- 动态规划专题 01背包问题详解 HDU 2546 饭卡
我以此题为例,详细分析01背包问题,希望该题能够为大家对01背包问题的理解有所帮助,对这篇博文有什么问题可以向我提问,一同进步^_^ 饭卡 Time Limit: 5000/1000 MS (Java ...
- 动态规划入门-01背包问题 - poj3624
2017-08-12 18:50:13 writer:pprp 对于最基础的动态规划01背包问题,都花了我好长时间去理解: poj3624是一个最基本的01背包问题: 题意:给你N个物品,给你一个容量 ...
- 动态规划:HDU-2542-0-1背包问题:饭卡
解题心得: 这题就是一个简单的0-1背包问题,只不过加了一系列的限制.可以想办法消去限制,直接转换成0-1背包问题的模板形式. 需要注意的几个点:首先对于剩余的5元钱的处理可以直接在总的钱数上将5减去 ...
- 动态规划:HDU-1203-0-1背包问题:I NEED A OFFER!
解题心得: 动态规划就是找到状态转移方程式,但是就本题0-1背包问题来说转移方程式很简单,几乎看模板就行了. 在本题来说WA了很多次,很郁闷,因为我记录v[i]的时候i是从0开始的,一些特殊数据就很尴 ...
- C++动态规划求解0-1背包问题
问题描述: 给定n种物品和一背包.物品i的重量是wi,其价值为vi,背包的容量为C.问:应该如何选择装入背包的物品,是的装入背包中物品的总价值最大? 细节须知: 暂无. 算法原理: a.最优子结构性质 ...
- 动态规划:HDU-2955-0-1背包问题:Robberies
解题心得: 这题涉及概率问题,所以要运用概率的知识进行解答.题目要求不被抓到的概率,但是给出的是被抓到的概率,所要用1减去后得到答案.最好使用double类型,避免精度问题导致WA. 先算出可以抢劫的 ...
随机推荐
- Markdown Support
Markdown 支持一览 Markdown 支持一览 身正不怕影子斜 我实在没有说过这样一句话 -- 鲁迅 古代文学史发展脉络 唐诗 宋词 元曲 冯·诺依曼结构 运算器 控制器 存储器 输入输出设备 ...
- 【SQLServer】max worker threads参数配置
查看和设置max worker threads USE master; //选中你想设置max worker threads的数据库.master表示在实例级别进行设置 GO EXEC sp_conf ...
- 使用Jumpserver堡垒机管理MySQL应用
第一步,添加系统用户 资产管理,系统用户,创建<数据库协议>MySQL 用户名和密码填写实际可以访问数据库的用户名和密码 第二步,添加应用 应用管理,数据库 主机和端口填写可以访问数据库的 ...
- 苹果手机安装郑好办手机app后给绿城通公交卡充值的步骤
1.苹果手机,需要带有NFC功能 苹果XS手机该功能默认是开启的,不用额外的其他操作 苹果11该功能需要设置开启才行.步骤:设置--通用--NFC,然后开启 2,公交卡 如下这种的公交卡可以充值: 如 ...
- 类似-Xms、-Xmn这些参数的含义
答: 堆内存分配: JVM初始分配的内存由-Xms指定,默认是物理内存的1/64 JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4 默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的 ...
- Elastic:Elastic Maps 基于位置的警报 - 7.10
文章转载自:https://elasticstack.blog.csdn.net/article/details/112535618
- SQLyog企业版
一.下载与安装 1.SQLyog的下载 链接:https://www.aliyundrive.com/s/11ZuFqB1z4E提取码:63fu 2.SQLyog的安装 双击下载程序,可以选择默认安装 ...
- C#-5 类和继承
一 类继承 继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一.继承呈现了面向对象程序设计的层次结构. 通过继承,我们能以一个已经存在的类为基础来定义一个新类.已存在的类称为基类或父类 ...
- .NET下数据库的负载均衡(有趣实验)(续)
.NET下数据库的负载均衡(有趣实验)这篇文章发表后,受到了众多读者的关注与好评,其中不乏元老级程序员. 读者来信中询问最多的一个问题是:它是否能支持"异种数据库"的负载均衡?? ...
- python的注释、变量
注释 注释是代码的解释型语言,不会影响代码执行,就是专门给程序员看的. 注释是很重要的代码组成部分! # 单行注释 ''' 多行注释 连续输入三个单引号 ''' """ ...