01背包,多重背包,打家劫舍, dp模板

01背包问题

有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

dp做题的步骤
1.确定dp数组 以及 下标的含义
2.确定 递推公式
3.dp数组如何初始化(很重要的问题)
4.确定遍历的顺序,遍历顺序很重要,你要确保你用到的前面的数据已经求出来了
调不出来的时候,可以打印dp数组
总的来说,知道了递推公式只是第一步,要认真的考虑初始化和遍历的问题。

01背包的二维板子(物品只能放一次)

#include <bits/stdc++.h>
using namespace std;

int n, bagweight;
void solve()
{
	vector<int>weight(n, 0);
	vector<int>value(n, 0);

	for (int i = 0; i < n; i++)
		cin >> weight[i];
	for (int j = 0; j < n; j++)
		cin >> value[j];
	//dp[i][j]
	vector<vector<int>>dp(n, vector<int>(bagweight + 1, 0));

	/*初始化,需要用到dp[i - 1]的值
		j < weight[0]已经在上方被初始化为0
		j >= weight[0]的值就初始化为value[0]*/
	for (int j = weight[0]; j <= bagweight; j++)
		dp[0][j] = value[0];
	for (int i = 1; i < n; i++)
	{//遍历所有的物品
		for (int j = 0; j <= bagweight; j++)
		{//遍历背包容量
			if (j < weight[i])dp[i][j] = dp[i - 1][j];
			else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
		}
	}
	cout << dp[n - 1][bagweight];
}
int main()
{
	cin >> n >> bagweight;
	solve();
	return 0;
}

一维dp数组(滚动数组)
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]
dp[j]=max(dp[j], dp[j - weight[i]] + value[i])

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。dp[i][j]是由dp[i-1][]推出来的,所以我们要保证dp[j]更新时,dp[j-weight[i]]依旧是dp[i-1]时的数据。这样才能保证每个物品放一次
for (int i = 0; i < n; i++)
for (int j = bagweight; j >= weight[i]; j–)
{//倒序遍历
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}

#include <bits/stdc++.h>
using namespace std;

int n, bagweight;
int  solve()
{
	vector<int>weight(n, 0);
	vector<int>value(n, 0);

	for (int i = 0; i < n; i++)
		cin >> weight[i];
	for (int i = 0; i < n; i++)
		cin >> value[i];

	vector<int>dp(bagweight+1, 0);
	for (int i = 0; i < n; i++)
		for (int j = bagweight; j >= weight[i]; j--)
			dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
	return  dp[bagweight];
}


int main()
{
	cin >> n >> bagweight;
	cout<<solve();
	
	return 0;
}

分割等和问题 力扣

背包能否装满

背包的体积为sum / 2
背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
背包如果正好装满,说明找到了总和为 sum / 2 的子集。
背包中每一个元素是不可重复放入。
以上分析完,我们就可以套用01背包,来解决这个问题了。
1.dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。
如果背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了
2.dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
3.从dp[j]的定义来看,首先dp[0]一定是0。(初始值设置很重要)
如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。
这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了。important,我经常犯这种初始化错误
本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。

class Solution {
public:
    bool canPartition(vector<int>& nums) {

        int sum=0;
        for (int i=0;i<nums.size();i++)
            sum+=nums[i];
            if (sum%2){
                return false;
            }
            int tar=sum/2;
        vector<int>dp(tar+1,0);
        for (int i=0;i<nums.size();i++)
        for (int j=tar;j>=nums[i];j--)
        {
            dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
        }
        if (dp[tar]==tar)return true;
        else return false;
    }
};

最后一块石头的重量 力扣
其实这道题 还有一个 小小 的 问题转化,题目中原意是任选出两块石头进行碰撞,我们将两块石头的碰撞,转化成两个集合之间的碰撞。这也是一个很常见的思维方式和技巧。
这道题和上面那一道,高度重合。石头的总质量为sum,因为要返回最后一块石头的最小可能重量,所以我们尽可能的将石头分成两堆质量尽可能相等,他们之间的差就是答案。可以转换成容量为sum/2的背包最多放下多少价值的01背包问题。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum=0;
        for (int i=0;i<stones.size();i++)
        sum+=stones[i];
//向下取整
        int tar=sum/2;
        vector<int>dp(tar+1,0);
        for (int i=0;i<stones.size();i++)
        for (int j=tar;j>=stones[i];j--)
        dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);

return sum-2*dp[tar];
    }
};

目标和 力扣

给你一个非负整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 。返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

这道题和上面那道题类似。我们只能对数组内的数字进行两种操作。所以我们可以转换成两个集合之间的操作 。这是一中很重要的思想啊!!
本题要如何使表达式结果为target,

既然为target,那么就一定有 left组合 - right组合 = target。

left + right = sum,而sum是固定的。right = sum - left

公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2 。(遇到除法,我们要警惕,向下取整的问题)

target是固定的,sum是固定的,left就可以求出来。

此时问题就是在集合nums中找出和为left的组合。
是不是很神奇!nice!

此时问题就转化为,装满容量为left的背包,有几种方法。

其实这就是一个组合问题了。
确定dp数组以及下标的含义
1.dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
2.确定递推公式
有哪些来源可以推出dp[j]呢?

只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包
那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

所以求组合类问题的公式,都是类似这种:
dp[j] += dp[j - nums[i]]
3.dp数组如何初始化
从递推公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。
4.nums放在外循环,target在内循环,且内循环倒序。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
       int sum=0;
        for(int i=0;i<nums.size();i++)
            sum+=nums[i];
        if ((sum+target)%2)return 0;
        if (abs(target)>sum)return 0;
        int bagsize=(sum+target)/2;
        vector<int>dp(bagsize+1,0);
        dp[0]=1;
        for (int i=0;i<nums.size();i++)
        for (int j=bagsize;j>=nums[i];j--)
        dp[j]+=dp[j-nums[i]];

        return dp[bagsize];

    }
};

下面还是一道 ,转化成 两个集合 的问题。(遇到题,一定要肯花时间去思考,发现问题的本质,不要被题面所迷惑!不要着急敲代码)
洛谷说来这道题我上次寒假的时候就 做过了。但是当时 是copy的题解。如今再来看,才感觉有点懂得。说来也惭愧,现在才懂啊!我是杂鱼!!
大致题意:给出你 四科 ,每一科题数和所花费的时间。他的左右两个大脑可以同时计算
2道不同的题目,但是仅限于同一科。因此,kkksc03 必须一科一科的复习。问复习的最短时间。
分析:显然我们要利用 每一科题目左右大脑可以同时计算来最小化时间,并且只能一科一科的处理。所以只能最小化 单独的一科所用的时间,然后四科的时间累加起来。现在的问题就是 怎样 最小化,单独一科 的时间,
左右两脑可以同时处理,所以问题就转换成了 一个集合,分成两个尽可能相等 的集合(和上上道题类似,背包容量是这一科总时间的一半,向下取整就可以)。那么这一科所花费的时间就是这两个集合中 花费时间 较大的那一个。最后四科累加起来,就是答案了!

#include <bits/stdc++.h>
using namespace std;

int result;
int main()
{
	vector<int>len(5, 0);
	for (int i = 1; i <= 4; i++)
		cin >> len[i];
	
	for (int i = 1; i <= 4; i++)
	{
		vector<int>ve(len[i] + 1, 0);
		int t = 0;
		for (int j = 1; j <= len[i]; j++)
		{
			cin >> ve[j];
			t += ve[j];
		}
		int bagweight=t /2;//背包容量
		vector<int>dp(bagweight+1, 0);
		for (int i = 1; i < ve.size(); i++)
			for (int j = bagweight ; j >= ve[i]; j--)
				dp[j] = max(dp[j], dp[j - ve[i]] + ve[i]);
		result += max(t - dp[bagweight], dp[bagweight]);

	}
	cout << result << "\n";
	return 0;
}

完全背包问题

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。

#include <bits/stdc++.h>
using namespace std;

int  main() {
	//遍历物品,遍历背包容量
	//每个物品只能添加一次
	for (int i=0; i<weight.size(); i++)
		for (int j=bagweight; j>=weight[i]; j--) {
			dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
		}
	//每个物品可以添加多次
	for (int i=0; i<weight.size(); i++)
		for (int j=weight[i]; j<=bagweight; j++) {
			dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
		}
	return 0;
}

这里很好理解,我们先来回想一下01背包的时候的遍历,为了避免一个物品被重复放入,我们采用背包容量倒序遍历,现在每个物品可以被多次放入,因此我们直接就正序遍历即可.纯的完全背包两层for的顺序可以颠倒。
零钱兑换 力扣

纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!

在这里插入图片描述
确定dp数组以及下标的含义
dp[j]:凑成总金额j的货币组合数为dp[j]
确定递推公式
dp[j] 就是所有的dp[j - coins[i]](考虑coins[i]的情况)相加。
所以递推公式:dp[j] += dp[j - coins[i]];
在这里插入图片描述
如果求组合数就是外层for循环遍历物品,内层for遍历背包。

如果求排列数就是外层for遍历背包,内层for循环遍历物品。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int>dp(amount+1,0);
        dp[0]=1;
        for (int i=0;i<coins.size();i++)
        for (int j=coins[i];j<=amount;j++)
        {
            dp[j]+=dp[j-coins[i]];
        }
        return dp[amount];
    }
};

组合总数 力扣 排列的数量

l零钱兑换 力扣

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 0; i < coins.size(); i++) { // 遍历物品
            for (int j = coins[i]; j <= amount; j++) { // 遍历背包
                if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过
                    dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
                }
            }
        }
        if (dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
};

01背包和完全背包,
不可能考察板子题,所以我们要将问题进行转化。很多时候,物品的重量和价值是一种东西。我个人认为 这些用背包解决的问题,一般都有一个约束条件,可能是和不超多少。这个也就是背包的容量,有的时候这个容量很明显,有的时候要自己进行转化。去找背包中最大价值是多少,或者是装满背包的方法种数,然后再去辨别物品是否之只能放一次,决定使用01背包还是完全背包。很多乍一看和被背包毫不相关的问题,经过问题的转化,都可以用背包解决。最主要的还是 要 抽象出来问题。需要练啊
练!多思考!

打家劫舍问题

打家劫舍 力扣
1确定dp数组(dp table)以及下标的含义
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。

2确定递推公式
决定dp[i]的因素就是第i房间偷还是不偷。

如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。

如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,(注意这里是考虑,并不是一定要偷i-1房,)

然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);

3dp数组如何初始化
从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]

从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
4.确定遍历顺序
dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历

class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        vector<int> dp(nums.size());
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for (int i = 2; i < nums.size(); i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[nums.size() - 1];
    }
};

打家劫舍2 成环 力扣
这道题再上道题的基础上,将数组变成了 环状的。这样会改变首尾的相邻状态。
也就是 首 和 尾 元素 不能同时考虑。我们可以跑两遍上面的程序 取最大值。考虑首元素不考虑尾元素 ,和不考虑首元素考虑尾元素。(也许这就是数学上化曲为直的思想,世界的尽头是数学)哈哈^ _ ^

买卖股票问题

买卖股票1
知道题可以直接贪心,0(n)的复杂度。但是为了给下面的题做铺垫,我们这里说一下dp的做法。感觉dp第一步,怎样设计dp的含义。对dp状态的设计,就是要保证它能够覆盖所有的情况,其实对于dfs,bfs搜索说也是如此。进行状态的转移的时候,都要保持这样的一个原则。
1.确定dp数组(dp table)以及下标的含义
dp[i][0] 表示第i天持有股票所得最多现金
其实一开始现金是0,那么加入第i天买入股票现金就是 -prices[i], 这是一个负数。
dp[i][1] 表示第i天不持有股票所得最多现金
注意这里说的是“持有”,“持有”不代表就是当天“买入”!也有可能是昨天就买入了,今天保持持有的状态
2确定递推公式
如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]
那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);

如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来

第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]
同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
3.dp数组如何初始化
由递推公式 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[0][0]和dp[0][1]推导出来。

那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] -= prices[0];

dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0;
股票买卖2和上面那一道类似。
这里dp[i][0]代表的是第i天不持有股票

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len=prices.size();
        vector<vector<int>>dp(len,vector<int>(2,0));
        dp[0][0]=0;dp[0][1]=-prices[0];
        for (int i=1;i<len;i++)
        {
            dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]);
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]);
        }
return max(dp[len-1][1],dp[len-1][0]);
    }
};

买卖股票3
这个规定了交易的次数。最多进行两场交易。
这个主要还是设计状态。设计完状态,递推公式也就水到渠成了
1确定dp数组以及下标的含义
一天一共就有五个状态,
没有操作
第一次持有股票
第一次不持有股票
第二次持有股票
第二次不持有股票
dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。
最后的结果:如果第一次卖出已经是最大值了,那么我们可以在当天立刻买入再立刻卖出。所以dp[4][4]已经包含了dp[4][2]的情况。也就是说第二次卖出手里所剩的钱一定是最多的。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len=prices.size();
        vector<vector<int>>dp(len,vector<int>(5,0));
        dp[0][1]=-prices[0];dp[0][2]=0;dp[0][3]=-prices[0];
        dp[0][4]=0;
        for (int i=1;i<len;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]);
            dp[i][4]=max(dp[i-1][4],dp[i-1][3]+prices[i]);
        }
        return dp[len-1][4];
    }
};

买卖股票4这道题仍然是限制交易次数的。只不过相比上一道题限制两次,这道题限制k次。我们仍然是沿袭上面的思路。只不过,显然的我们要对dp数组的第二位进行扩展,否则不能代表所有的状态。

1.确定dp数组以及下标的含义
使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j]
j的状态表示为:

0 表示不操作
1 第一次买入
2 第一次卖出
3 第二次买入
4 第二次卖出

除了0以外,偶数就是卖出,奇数就是买入。

题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。j最大的下标是2*k
2。递推公式,类比上面的那道题

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int len=prices.size();
        vector<vector<int>>dp(len,vector<int>(2*k+1,0));

        for (int j=1;j<2*k;j+=2)
        dp[0][j]=-prices[0];
        for (int i=1;i<len;i++)
        {
            for (int j=0;j<2*k-1;j+=2)
            {
                dp[i][j+1]=max(dp[i-1][j+1],dp[i-1][j]-prices[i]);
                dp[i][j+2]=max(dp[i-1][j+2],dp[i-1][j+1]+prices[i]);
            }
        }
       return dp[len-1][2*k];
    }
};

买卖股票问题含冷冻期

子序列问题

最长递增子序列
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序
1.dp[i]的定义
dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
2.状态转移方程
位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。
3.dp[i]的初始化
每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.
4.确定遍历顺序
dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。
j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if(nums.size()==1)return 1;
        vector<int>dp(nums.size(),1);
        int result=0;
        for (int i=1;i<nums.size();i++)
        {   for (int j=0;j<i;j++)
                 if(nums[i]>nums[j]) dp[i]=max(dp[i],dp[j]+1);
            result=max(result,dp[i]);
        }
        return result;
    }

};

最长连续递增子序列
1.确定dp数组(dp table)以及下标的含义
dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]。
2.确定递推公式
如果 nums[i] > nums[i - 1]
dp[i] = dp[i - 1] + 1;
因为本题要求连续递增子序列,所以就只要比较nums[i]与nums[i - 1],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。
既然不用j了,那么也不用两层for循环,本题一层for循环就行,比较nums[i] 和 nums[i - 1]。
这道题目也可以用贪心来做,也就是遇到nums[i] > nums[i - 1]的情况,count就++,否则count为1,记录count的最大值就可以了。
概括来说:不连续递增子序列的跟前0-i 个状态有关,连续递增的子序列只跟前一个状态有关
最长重复子数组
注意题目中说的子数组,其实就是连续子序列。(连续的)
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
1.确定dp数组(dp table)以及下标的含义
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 )
那dp[0][0]是什么含义呢?总不能是以下标-1为结尾的A数组吧。
其实dp[i][j]的定义也就决定着,我们在遍历dp[i][j]的时候i 和 j都要从1开始。
2.确定递推公式
根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。
即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
根据递推公式可以看出,遍历i 和 j 要从1开始!
3.dp数组如何初始化
根据dp[i][j]的定义,dp[i][0] 和dp[0][j]其实都是没有意义的!

但dp[i][0] 和dp[0][j]要初始值,因为 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1;

所以dp[i][0] 和dp[0][j]初始化为0。

举个例子A[0]如果和B[0]相同的话,dp[1][1] = dp[0][0] + 1,只有dp[0][0]初始为0,正好符合递推公式逐步累加起来。
4.确定遍历顺序
外层for循环遍历A,内层for循环遍历B。或者反过来,都可以

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

最长公共子序列
和上道题,区别在于这里不要求是连续的了,但要有相对顺序
1.确定dp数组(dp table)以及下标的含义
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
2.确定递推公式
主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同
如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。
即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

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]);
}

3.dp数组如何初始化
先看看dp[i][0]应该是多少呢?
test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;
同理dp[0][j]也是0。
4.确定遍历顺序
从递推公式,可以看出,有三个方向可以推出dp[i][j],
dp[i-1][j-1] dp[i-1][j] dp[i][j-1]

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
        for (int i = 1; i <= text1.size(); i++) {
            for (int j = 1; j <= text2.size(); 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[text1.size()][text2.size()];
    }
};

子序列的应用
不想交的线
我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。

现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。
以这种方法绘制线条,并返回我们可以绘制的最大连线数。
绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且直线不能相交!

直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!

编辑距离

判断子序列 力扣
这道题可以 使用 双指针 o(len(t))的时间复杂度
也许需要注意的是 空字符串是 任何串的子序列。

class Solution {
public:
    bool isSubsequence(string s, string t) {
        if (s.size()==0)return true;
        int sp=0;int tp=0;
        while(tp<t.size())
        {
            if (t[tp]==s[sp])
            {   
                tp++;sp++;
                 if (sp==s.size())
                    return true;
                if (tp==t.size())
                     return false;
            }
            else {
                tp++;
                if (tp==t.size())
                    return false;
            }
        }
        return false;
    }
    
};

下面是动规的做法
1.确定dp数组(dp table)以及下标的含义
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。
2.确定递推公式
在确定递推公式的时候,首先要考虑如下两种操作,整理如下:
if (s[i - 1] == t[j - 1])
t中找到了一个字符在s中也出现了
if (s[i - 1] != t[j - 1])
相当于t要删除元素,继续匹配
if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;
if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1];
和 最长公共子序列 (opens new window)的递推公式基本那就是一样的,区别就是 本题 如果删元素一定是字符串t,而最长公共子序列 是两个字符串都可以删元素。
3.dp数组如何初始化
从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],所以dp[0][0]和dp[i][0]是一定要初始化的。
在定义dp[i][j]含义的时候以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。
因为这样的定义在dp二维矩阵中可以留出初始化的区间,相当于将初始化的代码 ,压缩到 dp递推的过程里了

编辑距离的入门题目(毕竟这里只是涉及到减法)

class Solution {
public:
    bool isSubsequence(string s, string t) {

        vector<vector<int>>dp(s.size()+1,vector<int>(t.size()+1,0));
        
        for (int i=1;i<=s.size();i++)
        for (int j=1;j<=t.size();j++)
        {
            if (s[i-1]==t[j-1])
            dp[i][j]=dp[i-1][j-1]+1;
            else dp[i][j]=dp[i][j-1];
        }
        if (dp[s.size()][t.size()]==s.size())return true;
        else return false;
    }
};

不同的子序列 力扣
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数
1.确定dp数组(dp table)以及下标的含义
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
2.确定递推公式
这一类问题,基本是要分析两种情况

s[i - 1] 与 t[j - 1]相等
s[i - 1] 与 t[j - 1] 不相等
当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。

一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1][j-1]。

一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。
当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j]

所以递推公式为:dp[i][j] = dp[i - 1][j];
3.dp数组如何初始化
从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j] 是从上方和左上方推导而来,如图:,那么 dp[i][0] 和dp[0][j]是一定要初始化的。
dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。

那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。

再来看dp[0][j],dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。

那么dp[0][j]一定都是0,s如论如何也变成不了t。

最后就要看一个特殊位置了,即:dp[0][0] 应该是多少。

dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。
这里开 long long 都会爆范围,只能开uint64_t

class Solution {
public:
    int numDistinct(string s, string t) {
        vector<vector<uint64_t >>dp(s.size()+1,vector<uint64_t >(t.size()+1,0));
        for (int i=1;i<=t.size();i++)
        dp[0][i]=0;
        for (int i=1;i<=s.size();i++)
        dp[i][0]=1;
        dp[0][0]=1;

        for (int i=1;i<=s.size();i++)
            for(int j=1;j<=t.size();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];
            }
            const long long  mod=1e9+7;
        return dp[s.size()][t.size()]%mod;
    }
};

两字字符串的删除操作 力扣

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值