目录
总目录:
【学习笔记】玩转算法面试-- Leetcode真题分门别类讲解
九、动态规划问题
经典动态规划问题:斐波那契数列;
记忆化搜索:递归的基础上,添加记忆化;自上而下解决问题。
动态规划:自下而上的解决问题。层层递推。
从上而下思考问题,从下而上解决问题。
两种动归:
- 一种是求最优解类,典型问题是背包问题;「最优子结构」
- 另一种就是计数类,它们都存在一定的递推性质。
将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。
1、参考练习题
70. 爬楼梯
递归思路:第n个台阶 = n-1个台阶跨一步 +n-2个台阶跨两步;
120. 三角形最小路径和
dfs ;超时;
记忆化搜索 ;记录最小路径。
动态规划;倒数第二层开始,加他左右孩子较小的一个。代码如下:尽量不去修改原数组。
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
vector<int> temp(triangle.back());
for(int i=triangle.size()-2;i>=0;i--)
for(int j=0;j<triangle[i].size();j++)
temp[j] = min(temp[j],temp[j+1])+triangle[i][j];
return temp[0];
}
};
64. 最小路径和
和上一题动归思想一致。
343. 整数拆分 同剑指offer 剪绳子
通过求子问题的最优解获得原问题的最优解。
记忆化搜索;可以AC。
动态规划;dp = 拆分成 这两部分直接乘 或者 继续往下拆。取其最大值。
class Solution {
public:
int integerBreak(int n) {
// memo[i] 表示将数字i分割(至少分割成两部分)后得到的最大乘积
vector<int> memo(n + 1, -1);
memo[1] = 1;
for(int i = 2 ; i <= n ; i ++)
// 求解memo[i]
for(int j = 1 ; j <= i - 1 ; j ++)
memo[i] = max(memo[i], max(j * (i - j), j * memo[i - j]));
return memo[n];
}
};
dfs +贪心;3和4为最优解。
279. 完全平方数 //***
状态转移方程为:
dp[i] = MIN(dp[i - j * j] + 1)
dp i 表示的是 i 需要多少个平方数。枚举 j 即可。j*j是一个数,所以加一,只需要求 i - j * j 的个数最小值即可。
91. 解码方法
经典动归题;分为两个数组能组成一个字母和只能拆开组成字母。
62. 不同路径 63. 不同路径 II
经典动归题;简单题;
2、记忆化搜索与暴力穷举思想的重要性
198. 打家劫舍
暴力穷举:返回max(抢下一家,当前家钱+抢下下一家的钱)超时;但是思想很重要。
其实也全部遍历到了,对于每个i都有抢与不抢。这个dfs 的定义是考虑偷取index的房子,而不是一定偷取这个房子。
int dfs(vector<int>& nums, int index)
{
if (index >= nums.size()) return 0;
return max(dfs(nums,index + 1), nums[index] + dfs(nums,index + 2));
}
int rob(vector<int>& nums) {
if (nums.size() < 2) return nums.back();
return dfs(nums, 0);
}
根据穷举,加上记忆化搜索;就能过了;
int dfs(vector<int>& nums,vector<int>& memory, int index)
{
if (index >= nums.size()) return 0;
if( memory[index] != -1) return memory[index];
memory[index] = max(dfs(nums,memory,index + 1), nums[index] + dfs(nums,memory,index + 2));
return memory[index];
}
int rob(vector<int>& nums) {
if (nums.size() < 2) return nums.back();
vector<int> memory(nums.size(),-1);
return dfs(nums,memory, 0);
}
dp;就是记忆化搜索的反向版。
dp[i] = max(nums[i]+dp[i-2],dp[i-1]);
213. 打家劫舍 II
核心在于抢第一家与否,如果能理解上面的题的穷举;这道题也很好穷举。这是加上了一种不偷第一家的情况,然后偷第一家,就不能偷最后一家;虽然超时,但是这个思想很重要
int try_rob(vector<int>& nums,int left,int right)
{
if(left>right) return 0;
return max(try_rob(nums,left+1,right),nums[left]+try_rob(nums,left+2,right));
}
int rob(vector<int>& nums) {
if(nums.size() <2) return nums.back();
return max(try_rob(nums,1,nums.size()-1),try_rob(nums,0,nums.size()-2));
}
根据穷举,加上记忆化搜索即可;发现需要使用两个memory,一个作为可能抢了第一家的,一个作为没抢的。
int try_rob(vector<int>& nums,vector<int>& memory,int left,int right)
{
if(left>right) return 0;
if(memory[left] != -1) return memory[left];
memory[left] = max(try_rob(nums,memory,left+1,right),nums[left]+try_rob(nums,memory,left+2,right));
return memory[left];
}
int rob(vector<int>& nums) {
if(nums.size() <2) return nums.back();
vector<int> memory1(nums.size(),-1),memory2(nums.size(),-1);
return max(try_rob(nums,memory1,1,nums.size()-1),try_rob(nums,memory2,0,nums.size()-2));
}
dp;根据上面记忆化搜索,很容易想到,要用两个dp数组。然后取最大值即可。
int rob(vector<int>& nums) {
if(nums.size()<2) return nums.back();
vector<int>dp1(nums.size()),dp2(nums.size());
dp1[0] = nums[0];
dp1[1] = max(nums[0],nums[1]);
for(int i=2;i<nums.size()-1;i++)
dp1[i] = max(dp1[i-1],nums[i]+dp1[i-2]);
dp2[0] = 0;
dp2[1] = nums[1];
for(int i=2;i<nums.size();i++)
dp2[i] = max(dp2[i-1],nums[i]+dp2[i-2]);
return max(dp1[dp1.size()-2],dp2.back());
}
这有个问题要想清楚: 难道在前 i 间的最高金额dp[i] 情况下,第 i 间一定被偷了吗?假设没有被偷,那 i+1 间的最大值应该也可能是 dp[i+1] = dp[i] + num吧?其实这种假设的情况可以被省略,这是因为:
- 假设第 i 间没有被偷,那么此时 dp[i] = dp[i-1],此时 dp[i+1] = dp[i] + num = dp[i-1] + num,即可以将 两种情况合并为一种情况 考虑;
- 假设第 i 间被偷,那么此时 dp[i+1] = dp[i] + num不可取 ,因为偷了第 i 间就不能偷第 i+1间。
也就是说这些情况其实都被考虑进去了,这个一定要想明白
337. 打家劫舍 III
核心还是和上面一样,抢与不抢。重要的还是穷举思想
暴力穷举;虽然会超时,但是有了这个基础,就AC了;
class Solution {
public:
int try_rob(TreeNode* root)//试图抢这个节点
{
if (!root) return 0;
int temp1 = try_rob(root->left) + try_rob(root->right);
int temp2 = root->val;
if (root->left) temp2 += try_rob(root->left->left) + try_rob(root->left->right);
if (root->right) temp2 += try_rob(root->right->left) + try_rob(root->right->right);
return max(temp1, temp2);
}
int rob(TreeNode* root) {
return try_rob(root);
}
};
然后在穷举基础上,加上记忆化就好。
class Solution {
public:
unordered_map<TreeNode*,int> memory;
int try_rob(TreeNode* root)
{
if (!root) return 0;
if( memory.find(root)!= memory.end())
return memory[root];
int temp1 = try_rob(root->left) + try_rob(root->right);
int temp2 = root->val;
if (root->left) temp2 += try_rob(root->left->left) + try_rob(root->left->right);
if (root->right) temp2 += try_rob(root->right->left) + try_rob(root->right->right);
memory[root] = max(temp1, temp2);
return memory[root];
}
int rob(TreeNode* root) {
return try_rob(root);
}
};
dp;从下往上做,只需要记录子节点有没有抢,返回的也是抢了的和没抢 的结果。
可以直接返回两个元素,第一个代表的是偷了这个节点,第二个代表的是没偷这个节点。
- 当前节点抢:就加上子节点没抢的两个;
- 当前节点不抢:取:子节点没抢的两个、子节点没抢了的两个、左节点抢了+右节点没抢、左节点没抢+右节点抢了的最大值;即,返回子节点最大的即可;
class Solution {
public:
//first 偷了当前节点,second 没偷
pair<int, int> try_rob(TreeNode* root)
{
if (!root) return make_pair(0, 0);
pair<int, int> temp_left = try_rob(root->left);
pair<int, int> temp_right = try_rob(root->right);
int temp1 = root->val;
temp1 += temp_left.second;
temp1 += temp_right.second;
int temp2 = max(temp_left.first, temp_left.second)+max(temp_right.first, temp_right.second);
return make_pair(temp1, temp2);
}
int rob(TreeNode* root) {
pair<int, int> temp = try_rob(root);
return max(temp.first, temp.second);
}
};
309. 最佳买卖股票时机含冷冻期 //***
暴力递归、dfs: (超时,但是很重要)
只需要考虑当天卖了,直接跳到后天继续遍历;
当前天的最大收益为:(状态为是否有股票在身上)
- 有股票:今天卖了,跳到后天继续dfs;
- 有股票:继续保持,跳到明天继续dfs;
- 无股票:今天买,跳到明天继续dfs;
- 无股票:无操作,跳到明天继续dfs;
定义递归函数含义:对当天的操作,has 代表当天是否持有股票;
class Solution {
public:
int dfs(vector<int>& prices,int index,bool has)
{
if(index>=prices.size() ) return 0;
int a=0;
if(has)
{
a = max(
dfs(prices,index+2,0)+prices[index],
dfs(prices,index+1,1)
);
}
else
{
a = max(
dfs(prices,index+1,1)-prices[index],
dfs(prices,index+1,0)
);
}
return a;
}
int maxProfit(vector<int>& prices) {
return dfs(prices,0,0);
}
};
记忆化搜索: 加上记忆化就能过
定义二维数组,代表持有与不持有的当天最大收益;
class Solution {
public:
vector<vector<int>> memory;
int dfs(vector<int>& prices,int index,bool has)
{
if(index>=prices.size() ) return 0;
int a=0;
if(has)
{
if(memory[index][1] != -1)
return memory[index][1];
a = max(
dfs(prices,index+2,0)+prices[index],
dfs(prices,index+1,1)
);
memory[index][1] = a;
}
else
{
if(memory[index][0] != -1)
return memory[index][0];
a = max(
dfs(prices,index+1,1)-prices[index],
dfs(prices,index+1,0)
);
memory[index][0] = a;
}
return a;
}
int maxProfit(vector<int>& prices) {
memory = vector<vector<int>> (prices.size(),vector<int>(2,-1));
return dfs(prices,0,0);
}
};
dp : dp 代表当前为止,最大收益;
三种状态:分别为持有股票【1】、不持有并刚卖了【2】、不持有【3】;
- 依赖于: (【3】-当天股票价 、【1】)中的最大值;
- 只依赖于【1】 +当天股票价;
- 依赖于: (【2】、【3】)中的最大值;
最终返回【2】 【3】 中最大值;
class Solution {
public:
int maxProfit(vector<int>& prices) {
//has //sold //haven't
vector<vector<int>> dp(prices.size(),vector<int>(3));
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[0][2] = 0;
for(int i=1;i<prices.size();i++)
{
dp[i][0] = max(dp[i-1][0],dp[i-1][2] -prices[i]);
dp[i][1] = dp[i-1][0]+prices[i];
dp[i][2] = max(dp[i-1][1],dp[i-1][2]);
}
return max(dp.back()[1],dp.back()[2]);
}
};
3、0-1 背包问题
背包容量 C(capacity),有n个不同物品编号0~n-1;每件物品重量W[i] ,价值V[i] ;
问:向背包中盛放哪些物品,使之不超过背包容积基础上,物品总价值最大。
- 暴力:O(n*2^n);可以用dfs 找到重叠子问题、最优子结构->记忆化搜索、动态规划;
- 贪心算法:按性价比来选择;存在最后浪费空间的问题;贪心算法无法解决此问题;
递归函数定义:考虑将n个物品放入容量为C的背包,使其价值最大。
F(i ,c):取下面两种最大值;
- 不放这个物品,向后dfs;
- 放这个物品,减去容量后向后dfs,返回值加上这个物品价值;
记忆化搜索:
class Knapsack0_1 {
private:
vector<vector<int>> memory;
int best_value(const vector<int> &w, const vector<int> &v, int index, int c)
{
if(index < 0 || c <= 0) return 0;
if (memory[index][c] != -1) return memory[index][c];
int res = best_value(w, v, index - 1, c);
if (c >= w[index])
res = max(res, v[index] + best_value(w, v, index - 1, c - w[index]));
memory[index][c] = res;
return res;
}
public:
int knapsack0_1(const vector<int> &w, const vector<int> &v, int c)
{
int n = w.size();
memory = vector<vector<int>>(n, vector<int>(c + 1, -1));
return best_value(w, v, n - 1, c);
}
};
动态规划:
class Knapsack0_1 {
private:
public:
int knapsack0_1(const vector<int> &w, const vector<int> &v, int c)
{
assert(w.size() ==v.size());
int n = w.size();
if (n == 0) return 0;
vector<vector<int>> memory = vector<vector<int>>(n, vector<int>(c + 1, 0));
for (int j = 1; j < c + 1; j++) //capacity
if (j >= w[0]) //背包中能放入该物品。
memory[0][j] = v[0];
//枚举只考虑元素i时,容积为j时的最优结构
for (int i = 1; i < n; i++)
{
for (int j = 1; j < c + 1; j++) //capacity
{
int temp = memory[i - 1][j];
if (j >= w[i]) //当前容积>= 当前物体的大小,考虑:加入i元素 + 除了i元素意外空间的最优解
temp = max(temp, v[i] + memory[i - 1][j - w[i]]);//放入元素i后,剩余空间为j - w[i]
memory[i][j] = temp;
}
}
return memory.back().back();
}
};
void main()
{
vector<int> w = { 1,2,3}, v = {6,10,12};
Knapsack0_1 k1;
cout<<k1.knapsack0_1(w,v,5);
}
根据转移方程知道:包含当前元素最优解,只与包含上一个元素的最优解有关。
时间 O(n*c) 无法优化;
空间可由 O(n*c) 优化到 O( 2c ) = O( c );还可优化到O(c)只要一行。
优化为2*c的空间:里面使用的时候,都对索引%2 即可。
优化为c的空间:里面使用的时候,都是使用的当前的和之前的,从后往前更新就会不冲突。直接更新当前节点。 同时,当不能往前更新了,可以剪枝提前终止;
class Knapsack0_1 {
private:
public:
int knapsack0_1(const vector<int> &w, const vector<int> &v, int c)
{
assert(w.size() ==v.size());
int n = w.size();
if (n == 0) return 0;
vector<int> memory(c + 1, 0);
for (int j = 1; j < c + 1; j++) //capacity
if (j >= w[0]) //背包中能放入该物品。
memory[j] = v[0];
//枚举只考虑元素i时,容积为j时的最优结构
for (int i = 1; i < n; i++)
{
for (int j = c; j >=0; j--) //capacity
{
if (j >= w[i]) //当前容积>= 当前物体的大小,考虑:加入i元素 + 除了i元素意外空间的最优解
memory[j] = max(memory[j], v[i] + memory[j - w[i]]);//放入元素i后,剩余空间为j - w[i]
else
break;
}
}
return memory.back();
}
};
void main()
{
vector<int> w = { 1,2,3}, v = {6,10,12};
Knapsack0_1 k1;
cout<<k1.knapsack0_1(w,v,5);
}
4、0-1 背包问题- 变种
完全背包问题: 每个物品可以无限的使用;
- 每个物品能取到的个数是有最大值的。将每个物品扩充至i个(i为这个物品能放进多少个);又转换成为了有限的物品;
- 和上面的一样,只是扩充的是这个物品的2倍4倍8倍(大小和价值同样扩大)这样也能实现所有的组合;
多重背包问题: 每个物品有num[i]个; 和上述1. 一致;
多维费用背包问题: 每个物体既有体积又有重量,背包也有这两种维度;
动态规划中设立的状态多了一个;
动态规划数组变成三维;
物品之间约束问题: 物品间相互排斥、相互依赖;
5、0-1 背包问题- 例题
416. 分割等和子集 //**
典型背包问题:即装满sum/2的背包;时间复杂度O(nsum/2) = o(nsum)
暴力dfs: 过了36/116;
记忆化搜索: 过了77/116; 加剪枝(一个满足了,另外的不用跑了) ;过了,二维数组相较于哈希套哈希,时间更快,但是空间更大。
class Solution {
public:
vector<vector<int>> my_hash;
bool fill_(vector<int>& nums, int index, int coloum)
{
if (coloum < 0 || index >= nums.size()) return false;
if (coloum == 0) return true;
if(my_hash[index][coloum]!=-1)
return my_hash[index][coloum];
bool temp;
if (nums[index] <= coloum)
temp = fill_(nums, index + 1, coloum - nums[index])||fill_(nums, index + 1, coloum);
else
temp = fill_(nums, index + 1, coloum);
my_hash[index][coloum] = temp;
return temp;
}
bool canPartition(vector<int>& nums) {
int sum = 0;
for (auto &it : nums)
sum += it;
if (sum % 2 != 0) return false;
sum >>=1;
my_hash = vector<vector<int>>(nums.size(),vector<int>(sum+1,-1));
return fill_(nums, 0, sum);
}
};
动态规划: 定义dp [n] 为背包大小为n 的背包,最满能装多少。
对nums 进行遍历,每次新加一个数字,看能不能将背包装的更满。用的是背包问题中优化了空间的结构。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (auto &it : nums)
sum += it;
if (sum % 2 != 0) return false;
sum >>=1;
vector<int> my_hash(sum+1,0);
for(int i=sum;i>=0&& nums[0]<=i ;i--)
my_hash[i] = nums[0];
for(int i=1;i<nums.size();i++)
{
for(int j=sum;j>=nums[i];j--)
{
if(my_hash[j-nums[i]]+nums[i]>my_hash[j])
my_hash[j] = my_hash[j-nums[i]]+nums[i];
}
if(my_hash.back() ==sum) return true;
}
return false;
}
};
亦或者定义dp[I] 为大小为i的背包是否能被填满;
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (auto &it : nums)
sum += it;
if (sum % 2 != 0) return false;
sum >>=1;
vector<bool> dp(sum+1);
for(int i=sum;i>=0 ;i--)
dp[i] = nums[0] ==i;
for(int i=1;i<nums.size();i++)
for(int j=sum;j>=nums[i];j--)
dp[j] = dp[j] | dp[j-nums[i]];
return dp.back();
}
};
322. 零钱兑换 //*组合问题?
暴力穷举: 过了 31 / 188
记忆化搜索: 过了;注意memory 中初始化0,如果当前路径不可取,同样memory改为-1;否则还是过不了
class Solution {
public:
int res = INT_MAX;
vector<int> momery;
int dfs(vector<int>& coins, int amount)
{
if (amount < 0) return-1;
if (amount == 0)
return 0;
if(momery[amount]!= 0) return momery[amount];
int temp = 0,temp_res = INT_MAX;
for (int i = 0; i < coins.size(); i++)
{
temp =dfs(coins, amount - coins[i]);
if(temp !=-1)
temp_res = min(temp_res,temp+1);
}
momery[amount-1] = temp_res==INT_MAX ? -1:temp_res;
return momery[amount-1];
}
int coinChange(vector<int>& coins, int amount) {
if (amount == 0) return 0;
momery = vector<int>(amount+1,0);
return dfs(coins, amount);
}
};
动态规划: dp[i] 为总额为i时,最少的硬币组合
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1,-1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
int temp = INT_MAX;
for (int j = 0; j < coins.size(); j++)
{
if (coins[j] <= i && dp[i - coins[j]] >= 0)
{
temp = min(temp, dp[i - coins[j]] + 1);
}
}
if (temp != INT_MAX)
dp[i] = temp;
}
return dp.back();
}
};
377. 组合总和 Ⅳ
相较前两个简单些;
暴力dfs: 超时;但是思想蛮重要
class Solution {
public:
int result;
void dfs(vector<int>& nums, int target)
{
if(target<0) return ;
if(target ==0)
{
result++;
return ;
}
for(auto & it:nums)
dfs(nums,target-it);
}
int combinationSum4(vector<int>& nums, int target) {
result = 0;
dfs(nums,target);
return result;
}
};
记忆化搜索: 过了。已经可以百分百了;组合问题?
注意:temp 算出来是0也得覆盖memory;代表的是当前target无法被充满;否则还是超时,相当于少剪枝了;
class Solution {
public:
vector<int> memory;
int dfs(vector<int>& nums, int target)
{
if(target<0) return 0;
if(target ==0) return 1;
if(memory[target]!=-1) return memory[target];
int temp =0;
for(auto & it:nums)
{
if(it<=target)
temp+=dfs(nums,target-it);
}
memory[target] =temp;
return temp;
}
int combinationSum4(vector<int>& nums, int target) {
sort(nums.rbegin(),nums.rend());
memory = vector<int>(target+1,-1);
return dfs(nums,target);
}
};
动态规划: 也能百分百;
上述memory中存的是当前target 能被充满的次数;
dp[I] 代表的是taget ==i 的时候,最多能被填充多少种组合;
注意:中间过程会溢出;题目只保证了最后结果不溢出,很可恶!
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int>dp(target+1,-1);
dp[0] =1;
for(int i=1;i<target+1;i++)
{
unsigned long long temp = 0;
for(int j=0;j<nums.size();j++)
{
if(nums[j]<=i&& dp[i-nums[j]]!=-1)
temp+=dp[i-nums[j]];
}
dp[i] = temp;
}
return dp.back();
}
};
474. 一和零 //***
记忆化搜索:
刚开始是在for里写着,还是超时;
class Solution {
public:
vector<vector<vector<int>>> memo3;
int dfs(vector<pair<int, int>> &str_num, int m, int n, int index)
{
if (index >= str_num.size() )
return 0;
if(memo3[index][m][n]!=-1) return memo3[index][m][n];
int temp = 0;
// for (int i = index; i < str_num.size(); i++)
if (m - str_num[index].first >= 0 && n - str_num[index].second >= 0)
temp = max(dfs(str_num, m, n, index + 1), 1+dfs(str_num, m - str_num[index].first, n - str_num[index].second, index + 1));
else
temp = max(temp, dfs(str_num, m, n, index + 1));
memo3[index][m][n] =temp;
return temp;
}
int findMaxForm(vector<string>& strs, int m, int n) {
vector<pair<int, int>> str_num(strs.size(), { 0,0 });
for (int i = 0; i < strs.size(); i++)
for (auto &chr : strs[i])
if (chr == '0')
str_num[i].first++;
else
str_num[i].second++;
memo3 = vector<vector<vector<int>>>(str_num.size(),vector<vector<int>>(m+1,vector<int>(n+1,-1)));
return dfs(str_num, m, n, 0);
}
};
动态规划: 可优化为二维
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<pair<int, int>> str_num(strs.size(), { 0,0 });
for (int i = 0; i < strs.size(); i++)
for (auto &chr : strs[i])
if (chr == '0')
str_num[i].first++;
else
str_num[i].second++;
vector<vector<vector<int>>> dp(str_num.size() + 1, vector<vector<int>>(m + 1, vector<int>(n + 1, 0)));
for (int k = 1; k < str_num.size() + 1; k++)
{
for (int i = 0; i <= m; i++)
{
for (int j = 0; j <=n; j++)
{
dp[k][i][j] = dp[k-1][i][j];
if (i >= str_num[k-1].first&& j >= str_num[k-1].second)
{
dp[k][i][j] = max(dp[k][i][j], 1+dp[k-1][i - str_num[k-1].first][j - str_num[k-1].second]);
// cout<<dp[k][i][j]<<endl;
}
}
}
}
return dp.back().back().back();
}
};
139. 单词拆分 较上面的简单
完全背包问题;写dfs 和记忆化搜索只是用个思路;
暴力dfs: 暴力搜索vec 中能不能匹配剩余的;过了 35 / 44
class Solution {
public:
bool _match(string &a,string &b,int index_a)
{
for(int i=0;i<b.size();i++)
if(a[index_a++]!=b[i]) return false;
return true;
}
bool dfs(string &s, vector<string>& wordDict,int index)
{
if(index>=s.size()) return true;
bool temp = false;
for(int i=0;i<wordDict.size();i++)
if(_match(s,wordDict[i],index))
{
temp = dfs(s,wordDict,index+wordDict[i].size());
if(temp)
break;
}
return temp;
}
bool wordBreak(string s, vector<string>& wordDict) {
return dfs(s,wordDict,0);
}
};
记忆化搜索: 发现状态只与当前索引之后能不能匹配有关; 已经能百分百了
class Solution {
public:
vector<int> memo;
bool _match(string &a,string &b,int index_a)
{
for(int i=0;i<b.size();i++)
if(a[index_a++]!=b[i]) return false;
return true;
}
bool dfs(string &s, vector<string>& wordDict,int index)
{
if(index>=s.size()) return true;
if(memo[index] !=-1) return memo[index];
bool temp = false;
for(int i=0;i<wordDict.size();i++)
if(_match(s,wordDict[i],index))
{
temp = dfs(s,wordDict,index+wordDict[i].size());
if(temp)
break;
}
memo[index] = temp;
return temp;
}
bool wordBreak(string s, vector<string>& wordDict) {
memo = vector<int>(s.size()+1,-1);
return dfs(s,wordDict,0);
}
};
动态规划:
dp [i] 定义为s字符串从0~i 能不能匹配;
只有dp[j] 匹配了,且 j~ i 这段匹配了,dp[I] 才能匹配;
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> my_hash(wordDict.begin(),wordDict.end());
vector<bool> dp(s.size()+1,false);
dp[0] = true;
for(int i=1;i<s.size()+1;i++)
for(int j=i -1;j>=0;j--)
{
string temp = s.substr(j,i-j);
if(dp[j] &&my_hash.find(temp)!=my_hash.end())
{
dp[i] = true;
break;
}
}
return dp.back();
}
};
494. 目标和 较简单/ //***动归不好想
暴力dfs: 这个题很意外,暴力直接过了。。。给爷整蒙了。
class Solution {
public:
int dfs(vector<int>& nums, int target,int index)
{
if(index >=nums.size())
if(target ==0) return 1;
else return 0;
int temp = 0;
temp += dfs(nums,target+nums[index],index+1);
temp += dfs(nums,target-nums[index],index+1);
return temp;
}
int findTargetSumWays(vector<int>& nums, int target) {
return dfs(nums,target,0);
}
};
记忆化搜索: memory 因为会产生负数target ,要用hash。
class Solution {
public:
unordered_map<int,vector<int>> memo;
int dfs(vector<int>& nums, int target,int index)
{
if(index >=nums.size())
{
if(target ==0) return 1;
else return 0;
}
if(memo.find(target)!= memo.end()&& memo[target][index]!=-1) return memo[target][index];
int temp = 0;
temp += dfs(nums,target+nums[index],index+1);
temp += dfs(nums,target-nums[index],index+1);
if(memo.find(target)== memo.end())
memo[target] = vector<int>(nums.size()+1,-1);
memo[target][index] =temp;
return temp;
}
int findTargetSumWays(vector<int>& nums, int target) {
return dfs(nums,target,0);
}
};
动归:
dp[i][j] :数组中前i个元素组成j 的方案个数;
所有的和 =sum;加起来==target 的前面是符号的和 = neg;于是有sum-neg-neg = target;
于是就是求数组中元素组合成为neg,的背包问题;预处理部分较多;
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(auto & it:nums)
sum+=it;
if((sum-target)%2!=0||(sum-target)<0) return 0;
int newt = (sum-target)/2;
vector<vector<int>> dp (nums.size()+1,vector<int>(newt+1,0));
dp[0][0] = 1;
//i个元素中,组成j 的个数
for(int i=1;i<=nums.size();i++)
{
for(int j=0;j<=newt;j++)
{
dp[i][j] = dp[i-1][j];
if(nums[i-1]<=j)
dp[i][j]+= dp[i-1][j-nums[i-1]];
}
}
return dp.back().back();
}
};
6、 最长上升子序列
300. 最长递增子序列
memo 记忆的是以index 开头的最长递增子序列;
然后调用他的那个index肯定是严格小于他的;于是只需要一维记忆;动归一样;
376. 摆动序列
7、 最长上升子序列 -例题
300. 最长递增子序列 //***
暴力dfs: 过22 / 54 时间复杂度O(n*2^n)
class Solution {
public:
int dfs(vector<int>& nums, int index)
{
if (index >= nums.size()) return 0;
int temp = 0;
for (int i = index + 1; i < nums.size(); i++)
if (nums[i] > nums[index])
temp = max(temp, dfs(nums, i ) + 1);
return temp;
}
int lengthOfLIS(vector<int>& nums) {
int res = 0;
for (int i = 0; i < nums.size(); i++)
res = max(dfs(nums, i) + 1, res);
return res;
}
};
记忆化搜索: 过了
memo 记忆的是以index 开头的最长递增子序列;
然后调用他的那个index肯定是严格小于他的;于是只需要一维记忆;动归一样;
class Solution {
public:
vector<int> memo;
int dfs(vector<int>& nums, int index)
{
if (index >= nums.size()) return 0;
if(memo[index]!=-1) return memo[index];
int temp = 0;
for (int i = index + 1; i < nums.size(); i++)
if (nums[i] > nums[index])
temp = max(temp, dfs(nums, i ) + 1);
memo[index] =temp;
return temp;
}
int lengthOfLIS(vector<int>& nums) {
int res = 0;
memo =vector<int>(nums.size()+1,-1);
for (int i = 0; i < nums.size(); i++)
res = max(dfs(nums, i) + 1, res);
return res;
}
};
动态规划: O(n2)
dp[i] 表示 以i结尾的最长上升子数列长度; 这样好分析问题;
于是就简单懂了;相当于记忆化搜索memo 的反向;
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int >dp(nums.size(),1);
int _max = 1;
for(int i=1;i<nums.size();i++)
{
int temp = dp[i];
for(int j=i-1;j>=0;j--)
if(nums[j]<nums[i])
temp = max(temp,dp[j]+1);
dp[i] = temp;
_max = max(temp,_max);
}
return _max;
}
};
动归+二分: 这个解法不再要求之列 O(n*logn)这个问题的最优解;
考虑:tail数组中装的就是最长的递增子序列,对于每个数,都考虑插进去;
- 如果这个数能插进去,说明这个数比tail中一个数字小,插进去使得后面出现更长的子序列概率加大;
- 如果这个数插不进去,只能放在最后,说明这个数比暂时最长子序列的数都大;最长子序列随之增大;在这个时候将记录的最长结果加1;
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int >tail(nums.size(),0);
int res = 0;
tail[res] =nums[0];
//[i,res]
for(int k=1;k<nums.size();k++)
{
auto & num = nums[k];
if(num>tail[res])
tail[++res] =num;
else
{
int i=0,j=res;
while(i<j)
{
int mid = i+(j-i)/2;
if(tail[mid]<num)
i = mid+1;
else
j=mid;
}
tail[i] = num;
}
}
return res+1;
}
};
376. 摆动序列
暴力: 过了8 / 26复杂度 O(n*2^n)
class Solution {
public:
int dfs(vector<int>& nums, int index, bool up)
{
if (index >= nums.size()) return 0;
int temp = 0;
if (up)
{
for (int i = index + 1; i < nums.size(); i++)
if (nums[i] > nums[index])
temp = max(temp, dfs(nums, i, false) + 1);
}
else
{
for (int i = index + 1; i < nums.size(); i++)
if (nums[i] < nums[index])
temp = max(temp, dfs(nums, i, true) + 1);
}
return temp;
}
int wiggleMaxLength(vector<int>& nums) {
int res = 0;
for(int i=0;i<nums.size();i++)
res = max(res, dfs(nums, 0, true) + 1);
for(int i=0;i<nums.size();i++)
res = max(res,dfs(nums,i,false)+1);
return res;
}
};
记忆化搜索: 能过;百分之六击败
记忆的是当前index 和上升还是下降的最大值;
class Solution {
public:
vector<pair<int,int>> memo;
int dfs(vector<int>& nums, int index, bool up)
{
if (index >= nums.size()) return 0;
int temp = 0;
if (up)
{
if(memo[index].first!=-1) return memo[index].first;
for (int i = index + 1; i < nums.size(); i++)
if (nums[i] > nums[index])
temp = max(temp, dfs(nums, i, false) + 1);
memo[index].first =temp;
}
else
{
if(memo[index].second!=-1) return memo[index].second;
for (int i = index + 1; i < nums.size(); i++)
if (nums[i] < nums[index])
temp = max(temp, dfs(nums, i, true) + 1);
memo[index].second =temp;
}
return temp;
}
int wiggleMaxLength(vector<int>& nums) {
int res = 0;
memo = vector<pair<int,int>>(nums.size(),{-1,-1});
for(int i=0;i<nums.size();i++)
res = max(res, dfs(nums, 0, true) + 1);
for(int i=0;i<nums.size();i++)
res = max(res,dfs(nums,i,false)+1);
return res;
}
};
动态规划:
up[i]:数组 nums[0…i] 中的最长上升摆动序列
down[i]: 数组 nums[0…i] 中的最长下降摆动序列
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
vector<pair<int,int>> dp(nums.size(),{1,1});
for(int i=1;i<nums.size();i++)
{
if(nums[i]>nums[i-1])
dp[i] = {dp[i-1].second+1,dp[i-1].second};
else if(nums[i]<nums[i-1])
dp[i] = {dp[i-1].first,dp[i-1].first+1};
else
dp[i] =dp[i-1];
}
return max(dp.back().second,dp.back().first);
}
};
摆幅 +贪心 : 这个题的特殊解法,O(n)
因为是摆动数组,直接将其全部计算拍动,然后按照>0 <0 摆动计算下去;
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
for(int i=nums.size()-1;i>0;i--)
nums[i] = nums[i]-nums[i-1];
int res1 =1;
bool up = false;
for(int i=1;i<nums.size();i++)
if((up&&nums[i]>0 )||(!up &&nums[i]<0))
{
res1++;
up = !up;
}
int res2 =1;
up = true;
for(int i=1;i<nums.size();i++)
if((up&&nums[i]>0 )||(!up &&nums[i]<0))
{
res2++;
up = !up;
}
return max(res2,res1);
}
};
354. 俄罗斯套娃信封问题
暴力dfs: 能过 61 /84
class Solution {
public:
int dfs(vector<vector<int>>& nums,int index)
{
int temp = 0;
for(int i=0;i<nums.size();i++)
if(nums[index][0]>nums[i][0] && nums[index][1]>nums[i][1] )
temp = max(temp,dfs(nums,i)+1);
return temp;
}
int maxEnvelopes(vector<vector<int>>& envelopes) {
int temp=0;
for(int i=0;i<envelopes.size();i++)
temp = max(temp,dfs(envelopes,i)+1);
return temp;
}
};
记忆化搜索 能过 83/84
class Solution {
public:
vector<int> memo;
int dfs(vector<vector<int>>& nums,int index)
{
int temp = 0;
if(memo[index] !=-1) return memo[index];
for(int i=0;i<nums.size();i++)
if(nums[index][0]>nums[i][0] && nums[index][1]>nums[i][1] )
temp = max(temp,dfs(nums,i)+1);
memo[index] = temp;
return temp;
}
int maxEnvelopes(vector<vector<int>>& envelopes) {
memo = vector<int>(envelopes.size(),-1);
int temp=0;
for(int i=0;i<envelopes.size();i++)
temp = max(temp,dfs(envelopes,i)+1);
return temp;
}
};
另类想法 dp 能过 69/84
存每个能嵌套哪些,然后根据嵌套dfs算最大深度;
class Solution {
public:
vector<vector<int>> nums;
int dfs(int index)
{
if (nums[index].empty())
return 0;
int temp = 0;
for (int i = 0; i < nums[index].size(); i++)
temp = max(temp, dfs(nums[index][i]) + 1);
return temp;
}
int maxEnvelopes(vector<vector<int>>& envelopes) {
nums = vector<vector<int>>(envelopes.size());
vector<int> dp(envelopes.size(), 1);
for (int i = 0; i < envelopes.size(); i++)
for (int j = 0; j < envelopes.size(); j++)
if (envelopes[j][0] < envelopes[i][0] && envelopes[j][1] < envelopes[i][1])
nums[i].push_back(j);
int _max = 1;
for (int i = 0; i < envelopes.size(); i++)
{
if (!nums[i].empty())
dp[i] += dfs(i);
_max = max(_max, dp[i]);
}
return _max;
}
};
动归 过了
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
sort(envelopes.begin(),envelopes.end());
vector<int >dp(envelopes.size(),1);
int res =0;
for( int i=0;i<envelopes.size();i++)
{
for( int j=0;j<i;j++)
{
if(envelopes[i][0] > envelopes[j][0] &&envelopes[i][1] > envelopes[j][1])
dp[i] =max(dp[i],dp[j]+1);
}
res = max(dp[i],res);
}
return res;
}
};
8、 最长公共子序列 LCS
基因匹配问题等;
9、 动态规划找出具体解
- 反向查找这个解即可
- 动态规划求是否有解,回溯求出所有具体解;
1、子序列具体解
300. 最长递增子序列
只需要每个长度选一个即可;最后的出的就是其中一个序列
2、0 - 1 背包问题具体解
- 根据当前解的来源,判断当前物品有没有被最优放入包内;