leetcode 494 目标和
leetcode 322 零钱兑换
leetcode 518 零钱兑换II
算法解决问题的核心实际上还是穷举。问题的关键在于如何聪明、有效率地进行进行穷举,这是我们构建合适算法的目标。
动态规划和回溯算法看起来有挺多共同之处,都涉及到了【递归】和【做选择】,那么他们之间区分在哪里呢?以及这两者之间是否能够转化?
通常来讲,我们使用回溯算法去遍历的时候,就是在使用暴力穷举的方法,当数据量很庞大的时候,显而易见地就会使算法效率变得很低。所以,我们通常会引入“剪枝”的思想来进一步优化,即在遍历的时候,我们往往能通过提前预判筛查掉一些肯定不可能的情况,来降低计算量的大小。这样一种回溯的结构,实际上就是在对树进行深度遍历的过程。不管是二叉树,还是N叉树也好,关键在于在【选择列表】中做选择。下面给出回溯算法的框架,就能很体会这个场景。(来自labuladong,文末附链接。)
仔细一想,这当中实际上有不少节点都被重复运算了,但是计算机是没有记忆的,所以这其中我们消耗了不少精力在计算之前已经出现的结果上。所以我们提出了使用【动态规划】来解决这种“重复子问题”的情况。
通过leetcode题来看一下具体情况。
这道题一拿到手,你就想肯定能用回溯求解。对于每个数我有两种【选择】,“+”或者“-”,针对每种选择我们可以得到对应【选择】下的状态,也就是当前的算术结果,当算术结果等于目标和时,我们就得到了一种解法。浅显易懂,暴力也比没做出来好。
代码如下:
这里我们使用target==0来作为判断,可以减少用res值去记录当前的算术结果,所以之间把剩余的值放进去继续递归。
void backTrack(int i,vector& nums,long target){
if(i == nums.size()){
if(target == 0)
res++; //得到一种解法
return; //结束的地方
}
backTrack(i+1,nums,target-nums[i]); //选择加号
backTrack(i+1,nums,target+nums[i]); //选择减号
}
你开始着手优化算法了。既然之间回溯存在了重复子问题的状况。则使用动态规划来解决,一般可以使用备忘录和“迭代填表”法。
既然我们刚刚说有结果被重复的被计算?那么我们就可以把子问题记录起来,当同一个子问题再次出现的时候,就可以查询“备忘录”直接取出结果。这就是它的思想。
代码如下:
map memo;
dp代表当前状态下的方案数
int dp(vector& nums, int i,long target){
//退出状态
if(i==nums.size()){
if(target==0) return 1;
return 0;
}
string key = to_string(i) + "," + to_string(target);
//查询备忘录
if(memo.find(key)!=memo.end()){
return memo.find(key)->second;
}
//迭代计算
long result = dp(nums,i+1,target-nums[i])+dp(nums,i+1,target+nums[i]);
//记入备忘录
memo.insert(pair(key,result));
return result;
}
实际上上面使用备忘录的方法仍然还不够理想。当数据量很大时,查询也消耗了许多时间。最好的情况是我们能找到状态转换之间的关系,用填表的形式,用已知的状态来求解当前未知的状态。因此状态转移方程对动态规划是至关重要的。
我们可以把目标和的这个问题转化为一个子集划分问题。对于每个数我们要从两个选择“+”和“-”进行选择,因此nums中的所有数被划分为两个子集A和B,分别代表分配“+”和“-”的数。存在如下关系:
sum(A) - sum(B) = target
sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums)
所以 sum(A) = (target + sum(nums)) / 2,问题转换成nums中可以找出几个子集A,使得A的元素和为(target + sum(nums)) / 2。
这里翻译一下就是,子集A中的元素代表背包中选中的物品。而A的元素和就相当于背包的容量大小((target + sum(nums)) / 2),而有几个子集A,就是找出符合条件(选中物品总和等于背包容量)的方案数。
那就直接按照背包问题的情况处理了。
int subset(vector&nums,int target){
int N = nums.size();
int sum = 0;
for(int i = 0;i> dp(nums.size()+1, vector(total+1,0));
dp[0][0]=1;
for(int i =1 ;i<=nums.size();i++){
for(int j = 0;j<=total;j++){
if(j==0) dp[i][0] = 1;
if(j>=nums[i-1]){
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
}else
dp[i][j] = dp[i-1][j];
}
}
return dp[N][total];
}
其实还可以进一步优化,使用【状态压缩】的方法,将dp的二维数组转换成一维数组。因为当前的选择状态只与上一个状态有关,我们可以只存储上一个状态即可。
涉及到最值问题,实际上只需要思考状态方程的关键。
class Solution {
public:
int coinChange(vector& coins, int amount) {
vector dp(amount+1,amount+1); //金额为i需要dp[i]硬币
dp[0] = 0;
for(int i = 0;i
本质和目标和问题是一样的,直接给出代码。
class Solution {
public:
int change(int amount, vector& coins) {
//base line
vector> dp(coins.size()+1,vector(amount+1,0));
dp[0][0]=1;
for(int i = 1;i<=coins.size();i++){
for(int j=0;j<=amount;j++){
if(j==0) dp[i][0] = 1;
//if(coins[i]=coins[i-1]) dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]];
else dp[i][j]=dp[i-1][j];
}
}
return dp[coins.size()][amount];
}
};
做动态规划题目,想清楚三点。
https://labuladong.gitbook.io/algo/dong-tai-gui-hua-xi-lie/targetsum