目录
【动态规划的条件】
(1)最优子结构:原问题的最优解是通过子问题的最优解得到的;
(2)无后效性:前面状态的决策不会限制到后面状态的决策;
(3)重复子问题:一个子问题可以被重复利用到多个父亲状态中。
【关键】舍空间而取时间
【数字金字塔】
【题目描述】
给定一个n层的金字塔,求一条从最高点到底层任意点的路径使得路径经过的数字之和最大。(每一步可以走到左下方的点也可以到达右下方的点)
【思路】
【将思路转换为代码】
for(int i=1;i<=n;i++) { for(int j=1;j<=i;j++) { if(j==1) f[i][j]=f[i-1][j]+a[i][j];//表示在对角线上的元素 else if(i==j) f[i][j]=f[i-1][j-1]+a[i][j];//表示在最左侧一列的元素 else f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];//表示在除了上述两种情况以外 //的其他位置元素 } }
【代码优化——条件合并】
for(int i=1;i<=n;i++) for(int j=1;j<=i;j++) f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j]; //之所以可以合并三种情况,是因为金字塔中的数字都为正整数, //那么在i==j或j==1这两种情况下,max函数不会选择那个不可以调用的位置
【复杂度分析】
时间复杂度:O(
)(双层for循环)
空间复杂度:O(
)(开设了二维数组)
【最大子段和】
给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。
b[0]=a[0]; for(int i=1;i<n;i++) { if(b[i-1]>0)//如果上一段子段和>0,说明对整体的子段和相加有益处,需要保留上一段子段和 b[i]=b[i-1]+a[i]; else//否则就将子段和重新计算,将b[i]置为a[i] b[i]=a[i]; }
【0-1背包问题】
【题目描述】给定n个物品,每个物体有个体
和一个价值
。现有一个容量为V的背包,请问如何选择物品装入背包,使得获得的总价值最大?
【思路】
通过讨论每个物品放与不放,连接前 i -1 个物品的状态和前 i 个物品状态之间的关系,最终结果就是两种选择下,收益的更大值。
我们维护一个二维状态 f [ i , j ], 来表示前 i 个物品,放到体积为 j 的背包里
可以得到:f [ i , j ] = max( f [ i − 1, j ] , f [ i −1, j − v [ i ] ] + p [ i ] )
【0-1背包问题动态规划的四要素】
(1)状态:一个二维状态 f [ i , j ], 来表示前 i 个物品,放到体积为 j 的背包里
(2)转移方程:
f[i][j]=f[i-1][j];//表示装不下第i个物品 f[i][j]=max(f[i-1][j],f[i-1,j-v[i]]+p[i]);
(3)初始状态:
f[0][j]=0;//表示一个物品都没放,价值为0
(4)转移方向:保证 i 从小到大增大,等式右边的状态比等式左边先算出来。
完整代码如下:
#include<iostream> #include<algorithm> #define N 1002 using namespace std; int n, V, v[N], p[N],f[N][N]; int main() { cin >> n >> V; for (int i = 1; i <= n; i++) cin >> v[i] >> p[i]; for (int i = 1; i <= n; i++) { for (int j = 1; j <= V; j++) { if (j < v[i]) f[i][j] = f[i - 1][j]; else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + p[i]); } } cout << f[n][V]<<endl; return 0; }
【复杂度分析】
空间复杂度:O(nV)使用了二维数组 f [ n ][ V ]
时间复杂度:O(nV)双层for循环
【缺点分析】
因为这个算法与物品个数,背包容量有关,假如物品个数很多,物品体积也非常大的时候,空间复杂度会急剧增加。
【算法优化1——滚动数组优化】
【基本思想】
在动态规划中,有时候内存空间会比较紧张,所以我们需要一些技巧来优化内存开销,下面提出一种优化方式为“滚动数组优化”,其基本思想类似于“踩石头过河”。
而在此题中,当我们在计算第 i 行时,只需保留第 i -1 行,可以把前 i - 2 行的内存空间释放掉,那么也就是说每一次计算只需要两行的数据。那么我们可以只利用 f [ 2 ][ V ]来记录数组的状态。奇数行填入状态f [ 1 ][ j ]中,偶数行填入状态f [ 0 ][ j ]中。
代码修改如下:
int f[2][V]; for(int i=1;i<=n;i++) for(int j=1;j<=V;j++) if(j<v[i]) f[i&1][j]=f[(i-1)&1][j]; else f[i&1][j]=max(f[(i-1)&1][j],f[(i-1)&1][j-v[i]]+p[i]); cout<<f[n&1][V]<<endl;
【注意】
(1)奇数的二进制表示的最低位为“1”,偶数的最低位为“0”,可以利用 i & 1 来取 i 的奇偶性:
i & 1 = 1 ( i 为奇数)
i & 1 = 0( i 为偶数)
(2)利用 i & 1 来取 i 的奇偶性,为什么不用 i % 2 呢?因为位运算的优先级最低,但是运算速度却最高,用 i & 1来判断奇偶性比用 i % 2 要高4倍,当循环的次数非常大时,位运算是非常有效率的。
【复杂度分析】
空间复杂度降为O(2V)
【算法优化2——优化到一维数组】
【基本思路】
//j<V时 if (j < v[i]) f[i][j] = f[i - 1][j]; else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + p[i]);
根据上面代码可以看出来,当 j < v [ i ]时,f [ i ][ j ] = f [ i - 1 ][ j ],那么如果我们将 j 从大到小进行枚举,当 j 从 V 变化到 v[ i ]的过程中,一直记录的是:
f [ i ][ j ] = max(f [ i - 1 ][ j ], f [ i - 1 ][ j - v [ i ] ] + p [ i ] )
当 j < v [ i ] 时,一直都有f [ i ][ j ] = f [ i - 1 ][ j ],那么如果映射到一维数组的话,相当于没有变化。
所以我们维护一个一维数组f [ j ],当 j < v [ i ]时,f [ j ]记录的就是f [ i - 1, j ],当 j > v [ i ]时,f [ j ]记录的就是f [ i ,j ]。
采用代码如下:
int f[N];//维护一个一维数组 for(int i=1;i<=n;i++) for(int j=V;j>=v[i];j--)//j从V开始枚举到v[i],1~v[i]的状态都是一样的,f[i,j]=f[i-1,j] //可以用v[i]的状态来代表v[i]之前的状态 f[j]=max(f[j],f[j-v[i]]+p[i]); cout<<f[V]<<endl;
【复杂度分析】
空间复杂度降为O(V)
【青蛙跳台阶——动态规划解法】
【题目描述】
一只青蛙一次可以跳 1 级台阶,也可以跳 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
这道题目我曾经在一篇文章中用递归和递推的方法阐述过,在这里将用动态规划的思想阐述一下。之前文章放在下面啦,有需要可以参考呀!
(1)青蛙若是想跳到第 n 个台阶,那么最后一步只有两种跳法,跳一步或者跳两步,那么分别有
f ( n -1 )和 f ( n -2 )种跳法,所以一共有 f ( n)= f ( n-1)+ f ( n-2)种跳法;
(2)初始条件为:f (0)=1
f (1)=1;
f (2)=2;
(3)定义一维数组dp,dp [ i ]代表青蛙跳到第 i 个台阶时,需要的步数,dp[ i ]=dp[ i-1]+dp[ i-2];
采用代码段如下:
int dp[N];
for(int i=2;i<=n;i++)
dp[i]=dp[i-1]+dp[i-2];
cout<<dp[n]<<endl;
动态规划问题就先聊到这里啦,以后学到更多的话还会继续po到博客上的!
撒花完结!