动态规划:将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解。
判断一个问题能否使用DP解决:能将大问题拆成几个小问题,且满足无后效性、最优子结构性质
DP的核心思想:尽量缩小可能解空间。
暴力做法是枚举所有的可能解,这是最大的可能解空间。
DP是枚举有希望成为答案的解。这个空间比暴力的小得多。
参考阮行止的回答
例1、青蛙跳台阶问题
题目链接
思想
对该问题进行抽象,n个阶梯,每个阶梯都代表一个位置,然后将这个n个位置相连(只能用长度为1和2的线)
这个问题就转化成了从 位置0 到 位置10 有几种不同的路可以走?
递推关系:dp[n] = dp[n-1] + dp[n-2];
比如7到10有几种走法=8到10的走法+9到10的走法
递推结果可以由下面这个数组说明
参考牛岱的回答
代码
class Solution {
public:
int numWays(int n) {
vector<int> dp(n+1,1);
for(int i=2;i<=n;i++){
dp[i]=(dp[i-1]+dp[i-2])%1000000007;
}
return dp[n];
}
};
例2、最长上升子序列
题目链接
思想
方法1, O(n2)做法
参考
代码
对应方法1
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> dp(nums.size());
for(int i=0;i<nums.size();i++){
dp[i]=1;
for(int j=0;j<i;j++){
if(nums[j]<nums[i]&&dp[j]+1>dp[i])
dp[i]=dp[j]+1;
}
}
//这里我调用逆序函数,返回首部不晓得为啥不行
// sort(dp.rbegin(),dp.rend());
// return dp[0];
int res=0;
for(int i=0;i<dp.size();i++){
if(res<dp[i])
res=dp[i];
}
return res;
}
};
二分查找解法
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
//二分查找解法
vector<int> top=nums;
// 牌堆数初始化为 0
int piles = 0;
for (int i = 0; i < nums.size(); i++) {
// 要处理的扑克牌
int poker = nums[i];
int left = 0, right = piles;
while (left < right) {
int mid = (left + right) / 2;
if (top[mid] > poker) {
right = mid;
}
else if (top[mid] < poker) {
left = mid + 1;
}
else {
right = mid;
}
}
// 没找到合适的牌堆, 新建⼀堆
if (left == piles) piles++;
// 把这张牌放到牌堆顶
top[left] = poker;
}
// 牌堆数就是 LIS ⻓度
return piles;
}
};
对上诉二分查找代码进行分析
序列为2 5 3 4 1 7 6
下图为i=0时刚进入循环
进while循环前
i=1
例3、剪绳子
题目链接
思想
参考出处
动态规划5步走
1. 判题题意是否为找出一个问题的最优解
2. 从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题
3. 从下往上分析问题 ,找出这些问题之间的关联(状态转移方程)
4. 讨论底层的边界问题
5. 解决问题(通常使用数组进行迭代求出最优解)
由5步走可知
-
判题题意是否为找出一个问题的最优解 看到字眼是“可能的最大乘积是多少”,由此判断是求最优解问题,可以用动态规划解决;
-
从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题。 题目中举了个例子:当绳子的长度为8时,我们把它剪成长度分别为2,3,3的三段,此时得到的最大乘积是18;我们可以从这里开始突破,把长度为8绳子的最大乘积分解为数个子问题,长度为8我们可以把它看成长度为1和7的绳子的和,或者长度为2和6的绳子的和,或者长度为3和5的绳子的和and so on! 到这里,相信大家已经看到一丝真理了吧?
-
从下往上分析问题 ,找出这些问题之间的关联(状态转移方程) 在第二点时,我们已经从上到下分析问题了,现在我们要从下往上分析问题了。分析可知, f(8)的值就是f(1)*f(7),f(2)*f(6),f(3)*f(5),f(4)*f(4)它们之中的最大值,即f(8) =
Max{f(1)*f(7),f(2)*f(6),f(3)*f(5),f(4)*f(4)}
只要知道f(1)到f(7)的值就能求出f(8);对于f(7),只要知道f(1)到f(6)的值就能求出f(7);对于f(6),只要知道f(1)到f(5)的值就能求出f(6);以些类推,我们只要知道前几个边界的值,就能一步步迭代出后续的结果!
状态转移方程: f(n)=Max{f(n-i)*f(i)} i={1,2,3,…,n/2} -
讨论底层的边界问题 底层的边界问题说的就是最小的前几个数值的f(n)的值,本题中就是f(0)、f(1)、f(2)、f(3)的值 。
对于f(0),长度为0的绳子,没办法剪,没有意义
对于f(1),长度为1的绳子,没办法剪,设为1
对于f(2),长度为2的绳子,只有一种剪法,剪成两段长度为1的绳子,但剪后的乘积为1,比自身更小;如果不是求自身的值,要求乘积最大值的话就没必要剪。
对于f(3),长度为3的绳子,只有一种剪法,剪成两段长度为1和2的绳子,但剪后的乘积为2,比自身更小;如果不是求自身的值,要求乘积最大值的话也没必要剪。
代码
Java版本
public static int cutting(int n) {
//长度小于等等于1没办法剪
if(n <= 1)
return 0;
//对于f(2),长度为2的绳子,只有一种剪法,剪成两段长度为1的绳子,剪后的乘积为1
if(n == 2)
return 1;
//对于f(3),长度为3的绳子,只有一种剪法,剪成两段长度为1和2的绳子,但剪后的乘积为2
if(n == 3)
return 2;
int max = 0;
//数组用于存储绳子乘积最大值
int value[] = new int[n + 1];
value[0] = 0;
value[1] = 1;
//剪后的乘积为1,比自身更小;如果不是求自身的值,要求乘积最大值的话就没必要剪
value[2] = 2;
//剪后的乘积为2,比自身更小;如果不是求自身的值,要求乘积最大值的话也没必要剪
value[3] = 3;
//从f(4)开始迭代
for(int i = 4;i <= n; i++) {
max = 0;
for(int j = 1;j <= i/2; j++) {
int val = value[j] * value[i - j];
max = val > max ? val : max;
}
value[i] = max;
}
max = value[n];
return max;
}
C++版本
class Solution {
public:
int cuttingRope(int n) {
vector<int> dp(n+1,0);
dp[0]=1;
for(int i=1;i<=(n+1)/2;i++){
for(int j=i;j<=n;j++){
dp[j]=max(dp[j],dp[j-i]*i);
}
}
return dp[n];
}
};
下面是上诉的c++代码的执行过程
北大OJ
PAT
codeup
C++的常用函数