算法题中的动态规划
首先了解下背包问题,动态规划的一个实例就是解决背包问题。
wiki:背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。
组合优化:就是在一个有限的对象集中找到最优对象的一类课题,组合优化的问题特征是可行解的集是离散或者可以简化到离散,目标是找到最优解。
条件:有n种物品,物品j的重量为wj,价格为pj。我们假定所有物品的重量和价格都是非负的。背包所能承受的最大重量为W。
- 0-1背包问题:限定每种物体只能选择0个或1个
- 有界背包问题:限定物品j最多只能选择bj个
- 无界背包问题:不限定每种物品的数量
动态规划#
通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法
适用情况#
- 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
- 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
- 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
实例#
斐波那契数列
用文字来说,斐波那契数列就是从0和1开始,之后的斐波那契数列就是由之前两数相加而得出(简单的说就是递归的方式定义)。
function fib(n)
if n = 0 or n = 1
return n
return fib(n − 1) + fib(n − 2)
这就是以递归的方式计算第n个值,但是这种递归的方式拆分开就会发现做了重复的计算,每一个n(n>1)最后都会变成f(0)和f(1)相加的结果,这种算法的运算时间是以指数级增长的。采用的动态规划优化的方式就是将前n个已经算出的数保存在数组中,在后面的计算中直接应用前面的结果,从而避免了重复计算。算法的运算时间变为O(n)。
array map [0...n] = { 0 => 0, 1 => 1 }
fib(n)
if(map m does not contain key n)
m[n] := fib(n − 1) + fib(n − 2)
return m[n]
背包问题
-
无界背包
我们假定重量都是正数(wj > 0)。在总重量不超过W的前提下,我们希望总价格最高。对于Y ≤ W,我们将在总重量不超过Y的前提下,总价格所能达到的最高值定义为A(Y)。A(W)即为问题的答案。
显然,A(Y)满足:
- A(0) = 0
- A(Y) = max { A(Y - 1), { pj + A(Y - wj) | wj ≤ Y } }
其中,pj为第j种物品的价格。关于第二个公式的一个解释:总重量为Y时背包的最高价值可能有两种情况,第一种是该重量无法被完全填满,这对应于表达式A(Y - 1)。第二种是刚好填满,这对应于一个包含一系列刚好填满的可能性的集合,其中的可能性是指当最后放进包中的物品恰好是重量为wj的物品时背包填满并达到最高价值。而这时的背包价值等于重量为wj物品的价值和当没有放入该物品时背包的最高价值之和。故归纳为表达式pj + A(Y - wj)。最后把所有上述情况中背包价值的最大值求出就得到了A(Y)的值。
复杂度分析:如果总重量为0,总价值也为0。然后依次计算A(0), A(1), ..., A(W),并把每一步骤的结果存入表中供后续步骤使用,完成这些步骤后A(W)即为最终结果。由于每次计算A(Y)都需要检查n种物品,并且需要计算W个A(Y)值,因此动态规划解法的时间复杂度为O(nW)
-
0-1背包问题
同样的前提:假定w1, ..., wn和W都是正整数。我们将在总重量不超过Y的前提下,前j种物品的总价格所能达到的最高值定义为A(j, Y)。
A(j, Y)的递推关系为:
- A(0, Y) = 0
- 如果wj > Y, A(j, Y) = A(j - 1, Y)
- 如果wj ≤ Y, A(j, Y) = max { A(j - 1, Y), pj + A(j - 1, Y - wj)}
通过计算A(n, W)即得到最终结果。为提高算法性能,我们把先前计算的结果存入表中。因此算法需要的时间和空间都为O(nW),通过对算法的改进,空间的消耗可以降至O(W)。
简单的面试题#
题目:有1分2分5分的硬币组成1角,共有多少种组合?
-
第一种解法(也是最直观的解法):暴力枚举法,就是直接定义循环累加或者递归
void main(){ int n = 0; // 5分硬币最多有i个 for (int i=0; i<3; i++) { // 2分硬币最多有10-5*i个 for (int j=0; j<=(10-5*i)/2; j++) { // 1分硬币的个数 for (int k=0; k<= 10 - 5*i - 2*j; k++) { if (10 == 5*i + 2*j +k) { n++; printf("5分:%d个,2分:%d个,1分:%d个",i,j,k); } } } } printf("所有组合有%d种",n);
int fun(s,n){ int count =0; int a[3] = { 1,2,5 }; if(n>2){ if(s == 0) return 1; else return 0; }else{ for(int i=0;s>=i*a[n];i++){ count += fun(s-i*a[n],n+1); } } return count; } int main(){ printf("所有组合有%d种",fun(10,0)); return 0; }
递归的方法我也没有理解。
-
第二种解法:
简单分析,不难看出这就是个无界背包问题,同时也是Fibonacci的动态规划解法。
int coinCombinations(int coins[], int coinKinds, int sum) { vector<vector<int> > dp(coinKinds + 1, vector<int>(sum+1,0)); for (int i = 0; i <= coinKinds; ++i) //递推初始条件 { dp[i][0] = 1; } for (int i = 1; i <= coinKinds; ++i) { for (int j = 1; j <= sum; ++j) { dp[i][j] = 0; //j / coins[i-1]表示能取的该硬币的最大数量。 for (int k = 0; k <= j / coins[i-1]; ++k) //i-1是因为coins是从0开始算起的。 { dp[i][j] += dp[i-1][j - k * coins[i-1]]; } } } return dp[coinKinds][sum]; }
dp[i][sum] = 用前i种硬币构成sum 的所有组合数。
状态转移方程:
dp[i][sum] = dp[i-1][sum - 0*Vm] + dp[i-1][sum - 1*Vm] + dp[i-1][sum - 2*Vm] + … + dp[i-1][sum - K*Vm]; 即前i中硬币构成sum的组合数是由前i-1中硬币构成sum中减去第i种硬币使用次数乘以币值的差的组合数累加。其中K = sum / Vm,dp[i][0] = 1 where sum= 0 ,dp[0][sum] = 0 where i=0
其实本题就是Fibonacci问题,对应1分2分5分的硬币三种,组合成1角,共有多少种组合? 这个问题就是:
f(n) = f(n-1) + f(n-2) + f(n-5) .其中f(0) = 1 , f(1) = 1, f(2) = 2, f(3) = 2, f(4) = 3.
还有一种类似无界背包问题解决的算法(我没有理解):
int main(){ int weight[] =[1,2,5]; int dp[0]=1; for(int i=0;i<3;i++){ for(int j=weight[i];j<10;j++){ dp[j] += dp[j-weight[i]]; } } pritf("所有组合有%d种",dp[10]); return 0; }
作者:EGBDFACE
出处:https://www.cnblogs.com/EGBDFACE/p/16271443.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南