动态规划法

动态规划法

将算式的计算结果记录在内存中,需要时直接调用结果,从而避免无用的重复计算

  • 求最优解的数学思路
  • 组合优化
  • 图像解析

涉及问题

  • 动规基础
  • 背包问题
  • 打家劫舍
  • 股票问题
  • 子序列问题

动态规划解题五步曲

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 打印dp数组
WA三问
  • 这道题目我举例推导状态转移公式了么?
  • 我打印dp数组的日志了么?
  • 打印出来了dp数组和我想的一样么?

#斐波那契数列

  1. 确定dp[i]含义 : dp[i]表示第i个斐波那契值为dp[i]
  2. 递推公式 : dp[i] = dp[i - 1] + dp[i - 2]
  3. dp数组初始化 : dp[0] = 1, dp[1] = 1
  4. 遍历顺序 : 从前向后遍历
  5. 打印dp数组 : 用来debug
一般方法
fibonacci(n){
	if(n == 0 || n == 1) return 1;
	return fibonacci(n - 2) + fibonacci(n - 1);
}
上述算法虽然能求出斐波那契数列的第n项,但复杂度方面尚有缺陷 例: 求f(5)时必须求出f(4)和f(3), 求f(4)时还要再调用一次f(3)
通过记忆化递归生成斐波那契数列
makeFibonacci(){
	F[0] = 1;
	F[1] = 1;
	for(int i = 2; i <= n; ++i){
		F[i] = F[i - 1] + F[i - 2];
	}
}

空间优化

class Solution {
public:
	int fib(int N) {
		if (N <= 1) return N;
		int dp[2];
		dp[0] = 0;
		dp[1] = 1;
		for (int i = 2; i <= N; i++) {
		int sum = dp[0] + dp[1];
		dp[0] = dp[1];
		dp[1] = sum;
		}
		return dp[1];
	}
};

综上所述, 将小规模局部问题的解存储在内存中, 等到计算大问题的解时直接拿来有效利用, 这就是动态规划的基本思路

#爬楼梯

  1. dp[i]表示达到第i阶有dp[i]种方法
  2. dp[i] = dp[i - 1] + dp[i - 2]
  3. 初始化dp[0]<- 没有意义,不考虑 dp[1]= 1, dp[2] = 2
  4. 遍历顺序: 从前向后

实则就是

#最长公共子序列

image.png

讲解

Xi = {x1, x2, … , xi}
Yi = {y1, y2, … , yi}
长度分别是m, n
求Xm与Yn的最长子序列

两种情况:

  • x m = y n 时 , 在 X m − 1 与 Y n − 1 的最长子序列后面加上 x m ( = y n ) 就是 X m 与 Y n 的最长子序列 x_m = y_n时, 在X_{m - 1}与Y_{n - 1}的最长子序列后面加上x_m(=y_n)就是X_m与Y_n的最长子序列 xm=yn,Xm1Yn1的最长子序列后面加上xm(=yn)就是XmYn的最长子序列
  • x m ≠ y n 时 , X m − 1 与 Y n 和 X m 与 Y n − 1 的最长子序列中更长的一方就是 X m 与 Y n 的最长子序列 x_m \neq y_n时, X_{m-1}与Y_n和X_m与Y_{n-1}的最长子序列中更长的一方就是X_m与Y_n的最长子序列 xm=yn,Xm1YnXmYn1的最长子序列中更长的一方就是XmYn的最长子序列

image.png

#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>

using namespace std;
const int N = 1010;

string X, Y;
int c[1000][1000] = {0};


int lcs(string X, string Y){
	int m = X.size();
	int n = Y.size();
	int maxl = 0;
	memset(c, 0, sizeof(dp));
	// 从下标为1开始,在字串前面加一个空格
	X = ' ' + X;
	Y = ' ' + Y;
	for(int i = 1; i <= m; ++i){
		for(int j = 1; j <= n; ++j){
			if(X[i] == Y[j]){
				c[i][j] = c[i - 1][j - 1] + 1;
			}
			else{
				c[i][j] = max(c[i - 1][j], c[i][j - 1]);
			}
			maxl = max(maxl, c[i][j]);
		}
	}
	return maxl;
}

int main(){
	int n;
	cin >> n;
	while(n--){
		cin >> X >> Y;
		cout << lcs(X, Y) << endl;
	}
	return 0;
}

#矩阵链乘法

计算 n 个矩阵的乘积 M 1 M 2 M 3 . . . M n 时进行最少次标量相乘的最少次数 计算n个矩阵的乘积M_1M_2M_3...M_n时进行最少次标量相乘的最少次数 计算n个矩阵的乘积M1M2M3...Mn时进行最少次标量相乘的最少次数

#include<iostream>
#include<algorithm>

using namespace std;
static const int N = 100;

int main(){
	int n;
	int p[N];
	int m[N][N];
	cin >> n;
	for(int i = 1; i <= n; ++i){
		cin >> p[i - 1] >> p[i];
	}
	for(int i = 1; i <= n; ++i) m[i][i] = 0;
	for(int l = 2; l <= n; ++l){
		for(int i = 1; i <= n - 1 + 1; ++i){
			int j = i + l - 1;
			m[i][j] = (l << 21);
			for(int k = i; k <= j - 1; ++k){
				m[i][j] = min(m[i][j], m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j]);
			}
		}
	}

	cout << m[1][n] << endl;
	return 0;
}

背包九讲

0-1背包

有n件物品最多能背重量为w的背包.第i件物品的重量为weight[i],得到的价值为value[i].每件物品只能用一次,求解有容量为k的背包能装的最大价值是多少?

#include<iostream>
#include<vector>
#include<string>
using namespace std;
int main(){
	int M, n;
	cin >> M >> n;
	vector<int>  dp(M + 10, 0), c(n + 10), w(n + 10);
	// dp[j] 表示容量为 j 的背包的最大价值为 dp[j]
	for(int i = 1; i <= n; ++i){
		cin >> w[i] >> c[i];
	}
	for(int i = 1; i <= n; ++i){ // 遍历物品
		for(int j = M; j >= w[i]; --j){ // 遍历背包, 为什么要降序, 因为每个值都是由自己和左边推出来的, 左边先变化叻, 右边就乱水了
			dp[j] = max(dp[j - w[i]] + c[i], dp[j]);
			/*
				考虑第 i 个物品在放和不放的最大值就是 dp[j]
				如果放, 就是减去当前背包的容量后剩下的容量的最大价值 + 当前这个物品的价值 dp[j - weight[i]] + cost[i]
				如果不放, 最大价值就是当前背包容量的最大价值 dp[j]
				能进来此循环的就一定能放, 因为遍历的是这个背包的总容积到当前物品的容积
				抹去 [我放难道不比不放价值高吗?] 的想法, 如果你放, 那么你剩下的容量的最大价值就不一定 > 当前容量的最大价值了
				举个例子(假设你有一个 6 的背包, 有三个物品, 下面分别是第几个物品, 其容量, 其价值)
				i=1   2  15
				i=2   3  20
				i=3   6  30
				模拟数组如下(直接跳去 i=2 时遍历结束后的数组)
				index 0  1  2  3  4  5  6
				dp   0  0  15 20 20 35 35
				接下来开始考虑第三个物品
				开始遍历, 当背包容量为 6 时
				放 -- 因为第三个物品价值 6 , 所以就是你背包为 0 的时候的最大价值(0) + 30
			          如果第三个物品容量为 4 , 所以就是你背包为 2 的时候的最大价值(15) + 30
			 	不放 -- 考虑前 i 个物品的最大价值 35
			   
			 	以上案例可以看出 你放不一定就比不放价值高 因为放的话 前面的物品会被舍弃(数值上的舍弃)
			 	所以很巧妙的就能算出 背包容积为 M 的最大价值了
			*/

		}
	}
	cout << dp[M] << endl;
	return 0;
}

可以转换成0-1背包的题目↓↓↓

分割等和子集

给定一个只包含正整数的非空数组.是否可以将这个数组分割成两个子集,使得两个子集的元素和相等
注意: 每个数组中的元素不会超过100,数组的大小不会超过200

例子:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成[1, 5, 5]和[11]

解题思路

转换成0-1背包问题

  1. 将整个数组里面的数求和
  2. 把这个数/2, 如果不能整除直接false
  3. 把/2这个数当作背包的容量
  4. 现在问题转换成能不能从这些物品当作选到值为容量的0-1背包问题

最后一块石头的重量

有一堆石头, 每块石头的重量都是正整数
每一回合, 从中选出任意两块石头, 然后将它们一起击碎.假设石头的重量分别为x和y, 且x<=y那么:
如果x == y, 那么两块石头完全粉碎;
如果x != y, 那么重量x的石头会完全粉碎,留下重量y-x的新石头
最后最多只会剩下一块石头. 返回此石头的最小可能重量. 如果没有石头剩下,返回零

例子:
输入: [2, 7, 4, 1, 8, 1]
输出: 1
解释:

  • 组合2和4,得到2,所以数组转化为[2,7,1,8,1]
  • 组合7和8,得到1,所以数组转化为[2,1,1,1]
  • 组合2和1,得到1,所以数组转化为[1,1,1]
  • 组合1和1,得到0,所以数组转化为[1],这就是最有值.
解题思路

不要被例子的解释迷惑,例子解释很多时候会带偏我们的思考方向,拿来理解题意就行
这题也是要转化成0-1背包问题

  1. 将数组分成尽可能大的两堆, 因为这样把这两堆相撞后答案才会尽可能小
  2. 上面的解决方法就是数组全部元素总和/2, 一堆数值为 总和/2 另一堆为 总和 - 总和/2 (注意这里的 总和/2 是向下取整, 所以后者始终>=前者)
  3. 现在问题变成装满容量为 总和/2 的背包的最大值是多少
  4. 最后返回的答案是 (sum - dp[target]) - dp[target]

目标和

给定一个非负整数, a 1 , a 2 , . . . , a n a_1, a_2, ..., a_n a1,a2,...,an 和一个目标数S.现在你有两个符号+和-.对于数组中的任意一个整数,可以从+或-中选择一个符号添加在前面.
返回可以使最终数组的和为S的所有方案数.

例子:
输入: nums:[1,1,1,1,1], S: 3
输出: 5
解释:

  • -1+1+1+1+1 = 3

  • +1-1+1+1+1 = 3

  • +1+1-1+1+1 = 3

  • +1+1+1-1+1 = 3

  • +1+1+1+1-1 = 3
    一共有5种方法让目标和为3

    解题思路

    依旧是转化成0-1背包问题解决

    1. 能将数组分成两个组合(left, right)使得left + right = sum, left - right = S就是一个组合
    2. 转化一下公式得到 left = (S + sum) / 2
    3. 现在问题就变成了装满容量为 left 的背包有几种方法
    4. 如果 (S + sum) / 2 有余数表明没有方案可以凑成 S
    5. 注意这个不是求最大价值了,是求方案数,和爬楼梯联立起来了
      动态转移方程为: dp[j] += dp[j - nums[i]]

一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n
请你找出并返回 strs 的最大子集的大小, 该子集中最多有 m 个1和 n 个0
如果x的所有元素也是y的元素,集合x是集合y的子集

例子:
输入: strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出: 2
解释: 最多有5个0和3个1的最大子集是{“10”,“0001”,“1”,“0”},因此答案为4

解题思路

转化成二维背包的0-1背包问题,两个维度分别是m和n,值就是最大子集数
注意这题不一定要恰好m个0和n个1, 小于等于是可以的

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int dp[110][110] = {0};
        for(auto str : strs){
            int zero = 0;
            int one = 0;
            for(auto c : str){
                if(c == '0') zero++;
                else one++;
            }
            for(int i = m; i >= zero; --i){
                for(int j = n; j >= one; --j){
                    dp[i][j] = max(dp[i][j], dp[i - zero][j - one] + 1);
                }
            }
        }
        return dp[m][n];
    }
};

完全背包

和0-1背包一样,只是这次可以选无数次同样的物品

可以转换成几种类型的问法↓

经典型 n件物品内拿出重量不超过w的物品, 最大价值v是多少

dp数组含义:装至当前物品到容量为j的背包时的最大价值为dp[j]
动态转移方程:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

方案型 n件物品内拿出价值总和为v的物品, 有几种拿法

dp数组含义:容量为j的背包有dp[j]种方法
动态转移方程:dp[j] += dp[j - nums[i]]

  • 组合型
    物品栏内的物品拿出价值总和为n的物品,有几种拿法(组合)
    遍历顺序:先遍历物品,再遍历背包
  • 排列型
    物品栏内的物品拿出价值总和为n的物品,有几种拿法(排列)
    遍历顺序:先遍历背包,再遍历物品
不贪型 n件物品内拿出重量总和为w的物品, 最少只要拿几件

dp数组含义:容量为j的背包拿的物品的最少数量为dp[j]
动态转移方程:dp[j] += dp[j - nums[i]]

可以转换成完全背包的题目↓↓↓

leetcode-518 零钱兑换II

leetcode-103 零钱兑换

leetcode-104 组合总和IV

leetcode-279 完全平方数


买卖股票最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

题目分析:
只能买一次

dp数组含义(二维, 会写了直接删掉变成一维):

  • dp[i][0]表示遍历到当前持有股票的高大金额
  • dp[i][1]表示遍历到当前不持有股票的最大金额

动态转移方程:

  • dp[i][0] = max(dp[i - 1][0]), -prices[i]) 当天持有股票的最大金额是 前一天持有的最大金额 或者 我当天买(因为只能买一次)
  • dp[i][1] = max(dp[i - 1][1]), dp[i - 1][0] + prices[i]) 当天不持有股票的最大金额是 前一天不持有的最大金额 或者 前一天持有的最大金额 + 当天金额(我当天卖)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        vector<vector<int>> dp(len, vector<int>(2, 0)); // dp[i][0]表示当前持有股票最大金额 dp[i][1] 表示当前不持有股票最大金额
        dp[0][0] = -prices[0];
        for(int i = 1; i < len; ++i){
            dp[i][0] = max(-prices[i], dp[i - 1][0]);
            dp[i][1] = max(dp[i - 1][0] + prices[i], dp[i - 1][1]);
        }
        return dp[len - 1][1];
    }
};

买卖股票的最佳时机II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润

题目分析:
可以购买多次(注意上下的差别)

dp数组含义:(和上面一题一样)

  • dp[i][0] 表示遍历到当天持有股票的最大金额
  • dp[i][1] 表示遍历到当天不持有股票的最大金额

动态转移方程:

  • dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]) 当天持有的最大金额是 前一天持有的最大金额前一天不持有的最大金额 - 当天金额(当天买)(这样就表示了可以买多次)
  • dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]) 当天不持有的最大金额是 前一天不持有的最大金额前一天持有的最大金额 + 当天金额(当天卖)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        vector<vector<int>> dp(30010, vector<int>(2, 0));
        dp[0][0] = -prices[0];
        for(int i = 1; i < prices.size(); ++i){
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
        }
        return max(dp[prices.size() - 1][0], dp[prices.size() - 1][1]);
    }
};

买卖股票的最佳时机III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 **两笔 **交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

题目分析:
至多两次

dp数组的含义:

  • dp[i][0] 表示遍历到当天第一次持有的最大金额
  • dp[i][1] 表示遍历到当天第一次不持有的最大金额
  • dp[i][2] 表示遍历到当天第二次持有的最大金额
  • dp[i][3] 表示遍历到当天第二次不持有的最大金额

动态转移方程:

  • dp[i][0] = max(dp[i - 1][0], -prices[i]) 当天第一次持有股票的最大金额是 前一天持有的最大金额 或者 我当天买(因为只能买一次)
  • dp[i][1] = max(dp[i- 1][1], dp[i - 1][0] + prices[i]) 当天第一次不持有股票的最大金额是 前一天不持有的最大金额 或者 前一天持有的最大金额 + 当天金额(我当天卖)
  • dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i]) 当天第二次持有股票的最大金额是 前一天持有的最大金额 或者 我第一次不持有的`最大金额 + 当天金额(我当天卖)
  • dp[i][3] = max(dp[i- 1][3], dp[i - 1][2] + prices[i]) 当天第二次不持有股票的最大金额是 前一天不持有的最大金额 或者 前一天持有的最大金额 + 当天金额(我当天卖)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        // dp[i][0] dp[i][1] dp[i][2] dp[i][3] 分别表示第一次持有,第一次不持有,第二次持有,第二次不持有的最大金额
        vector<vector<int>> dp(len, vector<int>(4, 0));
        dp[0][0] = -prices[0];
        dp[0][2] = dp[0][0];
        for(int i = 1; i < len; ++i){
            dp[i][0] = max(dp[i - 1][0], -prices[i]);
            dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
        }
        return dp[len - 1][3];
    }
};

买卖股票的最佳时机IV

给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

题目分析:
至多k次(上面是手动弄两次, 其实只要加一层循环就行)

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int len = prices.size();
        // 我这里的dp[1]才表示第一次持有, 第二次持有是dp[3] ...
        // 我这里的dp[2]才表示第一次不持有, ...
        // 为了动态转移方便写,我的dp[0] = 0,拿来凑的
        vector<int> dp(2 * k + 1, 0);
        for(int i = 1; i <= 2 * k; i += 2){
            dp[i] = -prices[0];
        }
        for(int i = 1; i < len; ++i){
            for(int j = 1; j <= 2 * k; ++j){
                if(j % 2 == 1){
                    dp[j] = max(dp[j], dp[j - 1] - prices[i]);
                } else{
                    dp[j] = max(dp[j], dp[j - 1] + prices[i]);
                }
            }
        }
        return dp[2 * k];
    }
};

买卖股票的最佳时机含冷冻期

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        // dp[0] 表示持有股票的最大金额 // dp[1] 表示不在冷冻期内不持有股票的最大金额
        // dp[2] 表示当天卖出股票的最大金额 // dp[3] 表示冷冻期的最大金额
        // 实际上就是把不持有股票拆成三个状态以区分冷冻期
        vector<vector<int>> dp(len, vector<int>(4, 0));
        dp[0][0] = -prices[0];
        for(int i = 1; i < len; ++i){
            dp[i][0] = max({dp[i - 1][0], dp[i - 1][1] - prices[i], dp[i - 1][3] - prices[i]});
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
            dp[i][2] = dp[i - 1][0] + prices[i];
            dp[i][3] = dp[i - 1][2];
        }
        return max({dp[len - 1][1], dp[len - 1][2], dp[len - 1][3]});
    }
};

最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
**子序列 **是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

题目分析:

dp数组含义:

  • dp[i] 表示以 nums[i] 为结尾的最长子序列长度

动态转移方程:

  • dp[i] = max(dp[i], dp[j] + 1)
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int len = nums.size();
        int maxn = 1; // 每个元素都是一个子序列
        vector<int> dp(len, 1);
        for(int i = 1; i < len; ++i){
            for(int j = 0; j < i; ++j){
                if(nums[i] > nums[j]){
                    dp[i] = max(dp[i], dp[j] + 1);
                }
                maxn = max(maxn, dp[i]);
            }
        }
        return maxn;
    }
};

最长重复子序列

给两个整数数组 nums1nums2 ,返回两个数组中 公共的 、长度最长的子数组的长度 。

题目分析:
连续的公共子序列长度

dp数组含义:

  • dp[i][j] 表示以 nums1[i - 1] 及 num2[i - 2] 为结尾的最长重复子序列长度

动态转移方程:

  • dp[i][j] = dp[i - 1][j - 1] + 1
class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int len1 = nums1.size();
        int len2 = nums2.size();
        vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
        int res = 0;
        for(int i = 1; i <= len1; ++i){
            for(int j = 1; j <= len2; ++j){
                if(nums1[i - 1] == nums2[j - 1]){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                if(dp[i][j] > res) res = dp[i][j];
            }
        }
        return res;
    }
};

最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
    两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

题目分析:
不需要连续的公共子序列长度

dp数组含义:

  • dp[i][j] 表示遍历到 nums1[i - 1] 及 nums[j - 1] 的最长公共子序列长度

动态转移方程:

  • dp[i][j] = dp[i - 1][j - 1] + 1 (当nums1[i] == nums2[j])
  • dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) (当 nums1[i] != nums2[j])
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int len1 = text1.length();
        int len2 = text2.length();
        vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
        for(int i = 1; i <= len1; ++i){
            for(int j = 1; j <= len2; ++j){
                if(text1[i - 1] == text2[j - 1]){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else{
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[len1][len2];
    }
};

不同的子序列

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE""ABCDE" 的一个子序列,而 "AEC" 不是)
题目数据保证答案符合 32 位带符号整数范围。

题目分析:

dp数组含义:

  • dp[i][j] 表示 nums1(0 ~ i-1) 里面可以组成多少个 nums2(0 ~ j-1) 这样的子序列

动态转移方程:

  • dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] (当nums[i-1] == [j-1])
  • dp[i][j] = dp[i - 1][j] (当 nums[i-1] != nums[j-1])

Snipaste_2023-12-18_12-13-01

class Solution {
public:
    int numDistinct(string s, string t) {
        int len1 = s.length();
        int len2 = t.length();
        // dp[i][j] 表示 s(0 ~ i-1) 可以组成多少个 t(0 ~ j-1)
        vector<vector<unsigned long>> dp(len1 + 1, vector<unsigned long>(len2 + 1, 0));
        for(int i = 0; i <= len1; ++i){
            dp[i][0] = 1;
        }
        for(int i = 1; i <= len1; ++i){
            for(int j = 1; j <= len2; ++j){
                if(s[i - 1] == t[j - 1]){
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                }
                else{
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[len1][len2];
    }
};

状态压缩

class Solution {
public:
    int numDistinct(string s, string t) {
        int len1 = s.length();
        int len2 = t.length();
        vector<unsigned long> dp(len2 + 1, 0);
        dp[0] = 1;
        for(int i = 1; i <= len1; ++i){
            for(int j = len2; j >= 1; --j){
                if(s[i - 1] == t[j - 1]){
                    dp[j] += dp[j - 1];
                }
            }
        }
        return dp[len2];
    }
};

编辑距离

设A和B是两个字符串。我们要用最少的字符操作次数,将字符串A转换为字符串B。这里所说的字符操作共有三种:
1、删除一个字符;
2、插入一个字符;
3、将一个字符改为另一个字符。
对任意的两个字符串A和B,计算出将字符串A变换为字符串B所用的最少字符操作次数。

题目分析:

dp数组含义:

  • dp[i][j] 表示遍历到text1[i - 1]及text2[j - 1]的最少操作次数

动态转移方程:

  • dp[i][j] = dp[i - 1][j - 1] (当text1[i - 1] == text2[j - 1])
  • dp[i][j] = min({dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]}) + 1 (else)
#include<iostream>
#include<vector>
#include<string>
#include<algorithm>

using namespace std;

signed main(){
	string text1;
	string text2;
	cin >> text1 >> text2;
	int len1 = text1.length();
	int len2 = text2.length();
	// dp[i][j] 表示遍历到text1[i - 1]及text2[j - 1]的最少操作次数
	vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
	for(int i = 1; i <= len1; ++i){
		dp[i][0] = i;
	}
	for(int j = 1; j <= len2; ++j){
		dp[0][j] = j;
	}
	for(int i = 1; i <= len1; ++i){
		for(int j = 1; j <= len2; ++j){
			if(text1[i - 1] == text2[j - 1]){
				dp[i][j] = dp[i - 1][j - 1];
			}
			else{
				dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;
			}
		}
	}
	cout << dp[len1][len2] << endl;
}

区间dp

遍历顺序

15810
269
37
4

石子合并问题(排)

在一个操场上一排地摆放着N堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。
计算出将N堆石子合并成一堆的最小得分。

题目分析:

dp数组含义:

  • dp[i][j] 表示从区间i到j合成的最小代价

初始化:
dp[i][i] = 0 表示自己和自己合并的代价是零(不能合并)

遍历顺序:

  1. 遍历区间长度 len(从2开始到数组长度)(因为1被我们初始化了)
  2. 每个区间起点 i, 从而间接得到终点 j = i + len - 1
  3. 遍历切割位置,使得dp[i][j]具体是在哪里合并得到的最小

动态转移方程:
sum[j] - sum[i - 1] 表示石子在区间i到j的累加

  • dp[i][j] = min(dp[i][j], dp[i][k - 1] + dp[k][j] + sum[j] - sum[i - 1])
#include<iostream>
#include<vector>

using namespace std;

signed main(){
	int n;
	cin >> n;
	vector<int> sum(n + 1, 0);
	for(int i = 1; i <= n; ++i){
		cin >> sum[i];
			sum[i] += sum[i - 1];
	}
	vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0x3f3f3f3f));
	for(int i = 1; i <= n; ++i){
		dp[i][i] = 0;
	}
	for(int len = 2; len <= n; ++len){
		for(int i = 1; i + len - 1 <= n; ++i){
			int j = i + len - 1;
			for(int k = i + 1; k <= j; ++k){
				dp[i][j] = min(dp[i][j], dp[i][k - 1] + dp[k][j] + sum[j] - sum[i - 1]);
			}
		}
	}
	cout << dp[1][n] << endl;
}

石子合并问题(环)

问题描述: 在一个圆形操场的四周摆放着n堆石子. 现在要将石子有次序地合并成一堆. 规定每次只能选相邻的2堆石子合并成一堆, 并将新的一堆石子数记为该次合并的得分. 试设计一个算法, 计算出将n堆石子合并成一堆的最小得分和最大得分.
算法设计: 对于给定n堆石子, 计算合并成一堆的最小得分和最大得分.
数据输入: 第1行是正整数n, 1<=n<=100, 表示有n堆石子. 第2行有n个数, 分别表示n堆石子的个数.
结果输出: 第1行是最小得分, 第2行是最大得分.

题目分析:
将环形转换为直线
通过将数量变为 2n来转换成直线问题。 比如数组a【1,2,3】,但是环形的要求是1也可以和3连上,所以我们可以把数组a当成 【1,2,3,1,2,3】。这样,我们就可以算出 【2,3,1】的,【3,1,2】的。

#include<iostream>
#include<vector>

using namespace std;

signed main(){
	int n;
	cin >> n;
	int resmin = 0x3f3f3f3f; // 因为是环, 用来记录最大的合成代价
	int resmax = 0; // 因为是环, 用来记录最小的合成代价
	vector<int> stone_sum(2 * n + 1, 0); // 用来记录石头累加的总和, 方便我直接获得一个区间内的总和
	for(int i = 1; i <= n; ++i){
		cin >> stone_sum[i]; // 读入数据
		stone_sum[i + n] = stone_sum[i]; // 复制一份在后面
	}
	for(int i = 2; i <= 2 * n; ++i){
		stone_sum[i] += stone_sum[i - 1]; // 累加操作
	}
	// dpmax[i][j] 表示从区间i到j合成的最大代价
	// dpmin[i][j] 表示从区间i到j合成的最小代价
	vector<vector<int>> dpmax(2 * n + 1, vector<int>(2 * n + 1, 0));
	vector<vector<int>> dpmin(2 * n + 1, vector<int>(2 * n + 1, 0x3f3f3f3f));
	for(int i = 1; i <= 2 * n; ++i){
		dpmin[i][i] = 0; // 自己和自己不能合并, 所以合并代价为0
	}
	for(int len = 2; len <= 2 * n; ++len){ // 遍历区间长度
		for(int i = 1; i + len - 1 <= 2 * n; ++i){ // 
			int j = i + len - 1;
			for(int k = i + 1; k <= j; ++k){ // 遍历分割k 从i到k-1这段 与 k到j这段进行合并 每个当中拿 最小/最大 的
				dpmax[i][j] = max(dpmax[i][j], dpmax[i][k - 1] + dpmax[k][j] + stone_sum[j] - stone_sum[i - 1]);
				dpmin[i][j] = min(dpmin[i][j], dpmin[i][k - 1] + dpmin[k][j] + stone_sum[j] - stone_sum[i - 1]);
			}
		}
	}
	// 处理环的合并
	for(int i = 1; i <= n; ++i){
		if(dpmax[i][n + i - 1] > resmax){
			resmax = dpmax[i][n + i - 1];
		}
		if(dpmin[i][n + i - 1] < resmin){
			resmin = dpmin[i][n + i - 1];
		}
	}
	// 输出
	cout << resmin << endl << resmax << endl;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值