目录
动态规划适合解决多阶段决策最优解模型问题
动态规划的三个特征:
1.最优子结构
2.无后效性
3.重复子问题
动态规划解题思路:
1.状态转移表法
回溯算法实现-定义状态-画递归树-找重复子问题-画状态转移表-根据递推关系填表
0-1背包问题:我们有一个背包,背包总的承载重量是Wkg。现在我们有n个物品,每个物品的重量不等,并且不可分割。我们现在期望选择几件物品,装载到背包中。在不超过背包所能装载重量的前提下,如何让背包中物品的总重量最大?
我们用一个二维数组states[n][w+1],来记录n个物品放入载重w公斤背包的状态。
第0个(下标从0开始编号)物品的重量是2,要么装入背包,要么不装入背包,决策完之后,会对应背包的两种状态,背包中物品的总重量是0或者2。我们用states[0][0]=true和states[0][2]=true来表示这两种状态。
第1个物品的重量也是2,基于之前的背包状态,在这个物品决策完之后,不同的状态有3个,背包中物品总重量分别是0(0+0),2(0+2 or 2+0),4(2+2)。我们用states[1][0]=true,states[1][2]=true,states[1][4]=true来表示这三种状态。
以此类推,直到考察完所有的物品后,整个states状态数组就都计算好了。我把整个计算的过程画了出来,你可以看看。图中0表示false,1表示true。我们只需要在最后一层,找一个值为true的最接近w(这里是9)的值,就是背包中物品总重量的最大值。
根据上面的状态转移表推导出递推关系,写出动态规划代码
weight:物品重量,n:物品个数,w:背包可承载重量
public int knapsack(int[] weight, int n, int w) {
boolean[][] states = new boolean[n][w+1]; // 默认值false
states[0][0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
if (weight[0] <= w) {
states[0][weight[0]] = true;
}
for (int i = 1; i < n; ++i) { // 动态规划状态转移
for (int j = 0; j <= w; ++j) {// 不把第i个物品放入背包
if (states[i-1][j] == true) states[i][j] = states[i-1][j];
}
for (int j = 0; j <= w-weight[i]; ++j) {//把第i个物品放入背包
if (states[i-1][j]==true) states[i][j+weight[i]] = true;
}
}
for (int i = w; i >= 0; --i) { // 输出结果
if (states[n-1][i] == true) return i;
}
return 0;
}
2. 状态转移方程法
找最优子结构-写状态转移方程-将状态转移方程翻译成代码
3. LeetCode题解
3.1 LeetCode 509. 斐波那契数
例如斐波那契数列数列,我们可以很容易知道他的状态转移方程是 f(n)=f(n-1)+f(n-2)。递归的实现如下:
int fib(int n) {
if(n<=1)
return n;
return fib(n-1)+fib(n-2);
}
由于计算f(n)需要先计算f(n-1)和f(n-2),但计算f(n-1)又需要计算f(n-2)和f(n-3);计算f(n-2)需要计算f(n-3)和f(n-4)。这个递归推导计算中有大量重复计算,时间复杂度是O(n!)。
重复子问题正是动态规划要解决的重复子问题,很适合用动态规划解决。动态规划实现:
int fib(int n) {
if(n<=1)
return n;
vector<int> dp(n+1);
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
此实现使用了动态规划,问题最优解包含了子问题的最优解(最优子结构),每一步计算都只依赖上一步(无后效性),没有重复计算(解决重复子问题)。这个算法的时间复杂度是O(n),不过空间复杂度也是O(n)。一般动态规划问题都可以写出一个O(n)大小的状态转移方程,不过大部分问题都可以转换为O(1)的空间,因为我们只需要上一步的状态和当前的状态。
int fib(int n) {
if(n<=1)
return n;
int a1 = 0, a2 = 1;
int res = 0;
for(int i = 2; i <= n; i++) {
res = a1+a2;
a1 = a2;
a2 = res;
}
return res;
}
上面这个算法使用动态规划实现了O(n)的时间复杂度,空间复杂度O(1)。
3.2 LeetCode 70. 爬楼梯
int climbStairs(int n) {
if(n<=2)
return n;
int a1 = 1, a2 = 2;
int res = 0;
for(int i = 3; i <= n; i++) {
res = a1+a2;
a1 = a2;
a2 = res;
}
return res;
}
归纳总结后发现状态转移方程也是f(n) = f(n-1)+f(n-2),只是要注意初始状态f(1)=1,f(2)=2
3.3 LeetCode 198. 打家劫舍
int rob(vector<int>& nums) {
if(nums.size() == 1)
return nums[0];
if(nums.size() == 2)
return max(nums[0], nums[1]);
int a1 = nums[0];
int a2 = max(nums[0], nums[1]);
int an = 0;
for (int i = 2; i < nums.size(); ++i) {
an = max(a1+nums[i],a2);
a1 = a2;
a2 = an;
}
return an;
}
状态转移方程 f(n) = max(f(n-2)+num[n], f(n-1))
3.4 LeetCode 53. 最大子序和
int maxSubArray(vector<int>& nums) {
int maxCur = nums[0], maxRes = nums[0];
for (int i = 1; i < nums.size(); ++i) {
maxCur = max(maxCur+nums[i], nums[i]);
maxRes = max(maxRes, maxCur);
}
return maxRes;
}
状态转移方程 f(n) = max(f(n)+num[i], num[i]);
3.5 LeetCode 152. 乘积最大子数组
int maxProduct(vector<int>& nums) {
int maxCur = nums[0];
int minCur = nums[0];
int maxRes = nums[0];
for(int i = 1; i < nums.size(); i++){
int mx = maxCur, mn = minCur;
maxCur = max(mx*nums[i], max(nums[i], mn*nums[i]));
minCur = min(mn*nums[i], min(nums[i], mx*nums[i]));
maxRes = max(maxCur, maxRes);
}
return maxRes;
}
3.6 LeetCode 1014. 最佳观光组合
int maxScoreSightseeingPair(vector<int>& values) {
int maxSum = values[0], maxRes = 0;
for (int i = 1; i < values.size(); ++i) {
maxRes = max(maxRes, maxSum+values[i]-i);
maxSum = max(maxSum, values[i]+i);
}
return maxRes;
}
3.7 LeetCode 55. 跳跃游戏
bool canJump(vector<int>& nums) {
if(nums.size()==1)
return true;
int maxIndex{};
for (int i = 0; i < nums.size()-1;i++) {
maxIndex = max(maxIndex, i+nums[i]);
if(maxIndex < i+1)
return false;
}
return true;
}
状态转移方程 f(n) = max(f(n), i+num[i]);