【C++】【学习笔记】【动态规划问题详解与例题】记忆化搜索与暴力穷举思想 ;0-1 背包问题;子序列问题;


总目录:

【学习笔记】玩转算法面试-- Leetcode真题分门别类讲解

九、动态规划问题

经典动态规划问题:斐波那契数列;

记忆化搜索:递归的基础上,添加记忆化;自上而下解决问题。

动态规划:自下而上的解决问题。层层递推。

从上而下思考问题,从下而上解决问题。

两种动归:

  1. 一种是求最优解类,典型问题是背包问题;「最优子结构」
  2. 另一种就是计数类,它们都存在一定的递推性质。

将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。

在这里插入图片描述

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吧?其实这种假设的情况可以被省略,这是因为:

  1. 假设第 i 间没有被偷,那么此时 dp[i] = dp[i-1],此时 dp[i+1] = dp[i] + num = dp[i-1] + num,即可以将 两种情况合并为一种情况 考虑;
  2. 假设第 i 间被偷,那么此时 dp[i+1] = dp[i] + num不可取 ,因为偷了第 i 间就不能偷第 i+1间。

也就是说这些情况其实都被考虑进去了,这个一定要想明白

来自:
https://leetcode-cn.com/problems/house-robber-ii/solution/213-da-jia-jie-she-iidong-tai-gui-hua-jie-gou-hua-/

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;从下往上做,只需要记录子节点有没有抢,返回的也是抢了的和没抢 的结果。

可以直接返回两个元素,第一个代表的是偷了这个节点,第二个代表的是没偷这个节点。

  1. 当前节点抢:就加上子节点没抢的两个;
  2. 当前节点不抢:取:子节点没抢的两个、子节点没抢了的两个、左节点抢了+右节点没抢、左节点没抢+右节点抢了的最大值;即,返回子节点最大的即可;
	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: (超时,但是很重要)

只需要考虑当天卖了,直接跳到后天继续遍历;

当前天的最大收益为:(状态为是否有股票在身上)

  1. 有股票:今天卖了,跳到后天继续dfs;
  2. 有股票:继续保持,跳到明天继续dfs;
  3. 无股票:今天买,跳到明天继续dfs;
  4. 无股票:无操作,跳到明天继续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】;

  1. 依赖于: (【3】-当天股票价 、【1】)中的最大值;
  2. 只依赖于【1】 +当天股票价;
  3. 依赖于: (【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] ;
问:向背包中盛放哪些物品,使之不超过背包容积基础上,物品总价值最大。

  1. 暴力:O(n*2^n);可以用dfs 找到重叠子问题、最优子结构->记忆化搜索、动态规划;
  2. 贪心算法:按性价比来选择;存在最后浪费空间的问题;贪心算法无法解决此问题;

递归函数定义:考虑将n个物品放入容量为C的背包,使其价值最大。

F(i ,c):取下面两种最大值;

  1. 不放这个物品,向后dfs;
  2. 放这个物品,减去容量后向后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 背包问题- 变种

完全背包问题: 每个物品可以无限的使用;

  1. 每个物品能取到的个数是有最大值的。将每个物品扩充至i个(i为这个物品能放进多少个);又转换成为了有限的物品;
  2. 和上面的一样,只是扩充的是这个物品的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)这个问题的最优解;

题解:https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-dong-tai-gui-hua-2/

考虑:tail数组中装的就是最长的递增子序列,对于每个数,都考虑插进去;

  1. 如果这个数能插进去,说明这个数比tail中一个数字小,插进去使得后面出现更长的子序列概率加大;
  2. 如果这个数插不进去,只能放在最后,说明这个数比暂时最长子序列的数都大;最长子序列随之增大;在这个时候将记录的最长结果加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 背包问题具体解

  • 根据当前解的来源,判断当前物品有没有被最优放入包内;
    在这里插入图片描述

参考

liuyubobo的课

【学习笔记】玩转算法面试-- Leetcode真题分门别类讲解

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
买书问题 dp实现 题目:买书 有一书店引进了一套书,共有3卷,每卷书定价是60元,书店为了搞促销,推出一个活动,活动如下: 如果单独购买其中一卷,那么可以打9.5折。 如果同时购买两卷不同的,那么可以打9折。 如果同时购买三卷不同的,那么可以打8.5折。 如果小明希望购买第1卷x本,第2卷y本,第3卷z本,那么至少需要多少钱呢?(x、y、z为三个已知整数)。 1、过程为一次一次的购买,每一次购买也许只买一本(这有三种方案),或者买两本(这也有三种方案), 或者三本一起买(这有一种方案),最后直到买完所有需要的书。 2、最后一步我必然会在7种购买方案中选择一种,因此我要在7种购买方案中选择一个最佳情况。 3、子问题是,我选择了某个方案后,如何使得购买剩余的书能用最少的钱?并且这个选择不会使得剩余的书为负数 。母问题和子问题都是给定三卷书的购买量,求最少需要用的钱,所以有"子问题重叠",问题中三个购买量设置为参数, 分别为i、j、k。 4、的确符合。 5、边界是一次购买就可以买完所有的书,处理方式请读者自己考虑。 6、每次选择最多有7种方案,并且不会同时实施其中多种,因此方案的选择互不影响,所以有"子问题独立"。 7、我可以用minMoney[i][j][k]来保存购买第1卷i本,第2卷j本,第3卷k本时所需的最少金钱。 8、共有x * y * z个问题,每个问题面对7种选择,时间为:O( x * y * z * 7) = O( x * y* z )。 9、用函数MinMoney(i,j,k)来表示购买第1卷i本,第2卷j本,第3卷k本时所需的最少金钱,那么有: MinMoney(i,j,k)=min(s1,s2,s3,s4,s5,s6,s7),其中s1,s2,s3,s4,s5,s6,s7分别为对应的7种方案使用的最少金钱: s1 = 60 * 0.95 + MinMoney(i-1,j,k) s2 = 60 * 0.95 + MinMoney(i,j-1,k) s3 = 60 * 0.95 + MinMoney(i,j,k-1) s4 = (60 + 60) * 0.9 + MinMoney(i-1,j-1,k) s5 = (60 + 60) * 0.9 + MinMoney(i-1,j,k-1) s6 = (60 + 60) * 0.9 + MinMoney(i-1,j,k-1) s7 = (60 + 60 + 60) * 0.85 + MinMoney(i-1,j-1,k-1)
暴力法是一种简单但效率较低的求解0-1背包问题的方法。它通过穷举所有可能的组合来找到最优解。具体步骤如下: 1. 遍历所有可能的组合: - 对于每个物品,可以选择将其放入背包或不放入背包。 - 使用递归或循环来生成所有可能的组合。 2. 计算每个组合的总价值和总重量: - 对于每个组合,计算其总价值和总重量。 - 如果总重量超过背包的容量,则该组合无效。 3. 找到最优解: - 在所有有效的组合中,找到总价值最大的组合。 - 如果有多个组合具有相同的总价值,选择总重量最小的组合。 下面是一个使用暴力求解0-1背包问题的Python示例代码: ```python def brute_force_knapsack(weights, values, capacity): n = len(weights) max_value = 0 best_combination = [] # 生成所有可能的组合 for i in range(2**n): combination = [] total_weight = 0 total_value = 0 # 将物品放入或不放入背包 for j in range(n): if (i >> j) & 1: combination.append(j) total_weight += weights[j] total_value += values[j] # 检查组合是否有效 if total_weight <= capacity and total_value > max_value: max_value = total_value best_combination = combination return max_value, best_combination # 示例用法 weights = [2, 3, 4, 5] values = [3, 4, 5, 6] capacity = 8 max_value, best_combination = brute_force_knapsack(weights, values, capacity) print("Max value:", max_value) print("Best combination:", best_combination) ``` 这段代码使用了两个列表`weights`和`values`来表示物品的重量和价值,`capacity`表示背包的容量。函数`brute_force_knapsack`通过遍历所有可能的组合来求解0-1背包问题,并返回最优解的总价值和最优解的物品组合。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值