目录
变体1: 一个数组,分为两个子数组,使得它们的和相等(动态规划)
7.1 给定一个正整数n,拆成至少两个数的和=n,让这些数的乘积最大
扩展1:给定一个数组,找出能构成差为1的最长的等差数列的元素
动态规划几个链接:
1、经典算法之动态规划(Dynamic Programming)_~青萍之末~的博客-CSDN博客_动态规划算法经典案例
2、动态规划算法经典案例_uestclr的博客-CSDN博客_动态规划算法
3、动态规划经典问题总结_内推小仙女的博客-CSDN博客_动态规划问题
动态规划算法是从暴力搜索算法优化过来的,如果我们不清楚暴力搜索的过程,就难以理解动态规划的实现,当我们了解了动态规划算法的基本原理的文字概述,实现条件之后,这时可能并不是太理解这种思想,去面对实际问题的时候也是无从下手,这个时候我们不能停留在文字层面上,而应该去学习经典动态规划算法的实现,然后倒回来看这些概念,便会恍然大悟。
动态规划算法的难点在于 从实际问题中抽象出动态规划表dp,dp一般是一个数组,可能是一维的也可能是二维的,也可能是其他的数据结构。
动态规划的关键点:
1、最优化原理,也就是最有子结构性质。这指的是一个最优化策略具有这样的性质,无论过去状态和决策如何,对前面的决策所形成的状态而言,余下的决策必须构成最优策略,简单来说就是一个最优化策略的子策略总是最优的,如果一个问题满足最优化原理,就称其有最优子结构性质。
2、无后效性,指的是某个状态下的决策的收益,只与状态和决策相关,与达到该状态的方式无关。
3、子问题的重叠性,动态规划将原来指数级的暴力搜索算法改进到了具有多项式时间复杂度的算法,其中的关键在于解决了荣誉,重复计算的问题,这是动态规划算法的根本目的。
4、总体来说,动态规划算法就是一系列以空间换取时间的算法。
1、上台阶问题(斐波那契数列)
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……
第一项和第二项是1,之后的每一项为之前两项的和
有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。
分析:动态规划的实现的关键在于能不能准确合理的用动态规划表来抽象出 实际问题。在这个问题上,我们让f(n)表示走上n级台阶的方法数。
那么当n为1时,f(n) = 1,n为2时,f(n) =2。就是说当台阶只有一级的时候,方法数是一种,台阶有两级的时候,方法数为2。那么当我们要走上n级台阶,必然是从n-1级台阶迈一步或者是从n-2级台阶迈两步,所以到达n级台阶的方法数必然是到达n-1级台阶的方法数加上到达n-2级台阶的方法数之和。即f(n) = f(n-1)+f(n-2),我们用dp[n]来表示动态规划表,dp[i],i>0,i<=n,表示到达i级台阶的方法数。
以上是动态规划的分析,下面的递归算法的分析:
上第1级: 1种上法
上第2级: 2种上法
上第3级: 3种上法 3 = 2 + 1
上第4级: 5种上法 5 = 3 + 2
上第5级: 8种上法 8 = 5 + 3
...
1,2,3,5,8, ...
从第3级楼梯开始,每级楼梯的上法等于之前两级楼梯上法的和。
由此构成斐波那契数列,登上第20级台阶种类数即为该数列第二十项的数值,经计算为10946种
//递归方法
int foo(int n)
{
if (n == 1)
{
return 1;
}
if (n == 2)
{
return 2;
}
if (n > 2)
{
return foo(n-1) + foo(n-2);
}
}
//循环方式求解
int Fibloop(int n)
{
if (n == 1)
{
return 1;
}
if (n == 2)
{
return 2;
}
int f1 = 1;
int f2 = 2;
int vale_n = 0; //第n个数的值
//因为斐波那契数列是从0和1开始,并在第三个数的时候才开始有规律
for (int i = 3; i <= n; i++)
{
vale_n = f1 + f2;
f1 = f2;
f2 = vale_n;
}
return vale_n;
}
2、最小(大)路径和
给定一个矩阵m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的m如下,那么路径1,3,1,0,6,1,0就是最小路径和,返回12。
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0
分析思路1:对于这个题目,假设矩阵是m行n列的矩阵,那么我们用dp[m][n]来抽象这个问题,dp是一个二维数组存放最短路径,其中dp[i][j]表示的是从原点(左上角)到 (i,j)位置的最短路径。我们首先计算第一行和第一列,直接累加即可,那么对于其他位置,要么是从它左边的位置达到,要么是从上边的位置达到,我们取左边和上边的较小值,然后加上当前的路径值,就是达到当前点的最短路径。然后从左到右,从上到下依次计算即可。
简洁分析思路2:
此题属于动态规划类题目,我们可以用一个dp二维数组存放最短路径,dp[i][j]就是左上角(0,0)到位置 (i,j) 的最短路径,我们要求的是dp[n][m]的最小值,我们只需要从dp[i-1][j],dp[i][j-1]这两个中选出最小者再加上dp[n][m]自己本身的路径就可以了。然后递推
int min(int a,int b)
{
return a<b?a:b;
}
void short(int[][] arr, int m,int n)
{
int[][] dp = new int[m][n];
for(i=0;i<=m-1;i++)
{
for(j=0;j<=n-1;j++)
{
if(i==0 && j==0)
dp[i][j]=arr[i][j];
if(i==0 && j!=0) //第一行边界
dp[i][j]= dp[i][j-1]+arr[i][j];
if(i!=0 && j==0) //第一列边界
dp[i][j]= dp[i-1][j]+arr[i][j];
if(i!=0 && j!=0) //其他
dp[i][j]=arr[i][j]+min(dp[i-1][j],dp[i][j-1]);
}
}
}
3、最长递增子序列
分析1:
给定数组arr,返回arr的最长递增子序列的长度,比如arr=[2,1,5,3,6,4,8,9,7],最长递增子序列为[1,3,4,8,9]返回其长度为5.
设L(i)表示L中以ai为末元素的最长递增子序列的长度。则有如下的递推方程:
这个递推方程的意思是,在求以ai为末元素的最长递增子序列时,找到所有序号在L前面且小于ai的元素aj,即j<i且aj<ai。如果这样的元素存在,那么对所有aj,都有一个以aj为末元素的最长递增子序列的长度L(j),把其中最大的L(j)选出来,由于 aj<ai 那么L(i) 就等于最大的 L(j) 加上1,即以ai为末元素的最长递增子序列,等于以使L(j)最大的那个aj为末元素的递增子序列最末再加上ai;如果这样的元素不存在,即ai前面的元素aj都比ai大,即 aj > ai,那么ai自身构成一个长度为1的以ai为末元素的递增子序列,L(i)=1。
直接用DP求解,算法如下:时间复杂度为O(N^2)
①最优子问题
设lis[i] 表示索引为 [0...i] 上的数组上的最长递增子序列。初始时,lis[i]=1,注意,在DP中,初始值是很重要的,它是整个算法运行正确的关键。而初始值 则可以 通过 画一个小的示例来 确定。
这个前提是 j < i
从 0 到 j 遍历,j 的取值范围为:0,1...i-1, 遍历过程中更新 lis[i] 的值
当 arr[i] > arr[j],lis[i] =max( lis[j]+1 , lis[i] )
当 arr[i] < arr[j],lis[i] = 1;其中,j 的取值范围为:0,1...i-1
如果要输出这个最长的子序列:
例如给定的数组为{5,6,7,1,2,8},则 L(0)=1, L(1)=2, L(2)=3, L(3)=1, L(4)=2, L(5)=4。所以该数组最长递增子序列长度为4,序列为{5,6,7,8}。
如果要输出这个最大序列,可以考虑 从最大的L 的位置,开始逐个递减,例如最大是 L(5)=4,则它肯定是从 L(i)=3,并且要满足 arr[i] < arr[5] 得到的,因此逐个向前搜索满足这个条件的就可以了
void Lis(int[] arr)
{
int n= arr.length();
// 用于存放dp(i)值,dp[i]表示到第a[i]个数时,最长上升子序列的长度(注:dp[]的最后一个元素肯定是
// 最大的长度);
int[] dp = new int[n];
for(int i = 0; i < arr.length(); i++) {
//dp[]每一个元素的最小值为1,极端情况是,a[]是倒叙排列,那么dp[]={1}
dp[i] = 1;
}
for(int i = 0; i < arr.length(); i++)
{
for (int j = 0; j < i; j++)
{
//此时 j<i && arr[j] < arr[i],所以此时取max(dp[j]+1, dp[i]);
if (arr[j] < arr[i])
{
dp[i] = max(dp[j]+1, dp[i]); //两者取其大的数,dp[i]是实时更新的
}
}
//然后遍历找到最大的就好
}
变体0:俄罗斯套娃信封问题
变体1:最长连续递增子序列
思路:
- 给定一个未经排序的整数数组,找到最长且连续的的递增序列。
- dp[i]表示以i位置结尾,即nums[i]值结尾的,最长连续递增序列的长度
- 想要求dp[i] 只需要关注 nums[i] 与 nums[i - 1]的对比
- 当nums[i] > nums[i - 1],可以和nums[i-1]拼接起来, dp[i] = dp[i - 1] + 1;
- 当nums[i] <=nums[i - 1] nums[i]自身形成一个最长连续递增序列,长度为1
int findLengthOfLCIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
int[] dp = new int[n];
int max_len = 1;
for (int i = 1; i < n; i++) {
//初始化,后面dp[i]会更新
dp[i] = 1;
if (nums[i] > nums[i - 1]) {
dp[i] = dp[i - 1] + 1;
max_len= max(max_len, dp[i]); //直接找出最大
}
}
return max_len;
}
变体2:最大连续子序列和
参考:动态规划——最大连续子序列和 - Just_for_Myself - 博客园
多种解法:https://blog.csdn.net/Sruggle/article/details/109277644
剑指 Offer 42. 连续子数组的最大和:力扣
int findMax(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
dp[0] = A[0]; // 边界
for(i=1; i<n; ++i) {
// 状态转移方程
dp[i] = max(A[i], dp[i-1] + A[i]);
}
// 求最大连续子序列和
int max = dp[0];
for(i=1; i<n; ++i) {
if(dp[i] > max) {
max = dp[i];
}
}
return max;
}
变体2扩展1:连续子数组的最大乘积
参考:
leetcode之连续子数组的最大乘积_国家级睡觉型选手的博客-CSDN博客_连续子数组的乘积
变体2扩展2:最大子矩阵和
给定一个M*N 的矩阵,求一个子矩阵,使子矩阵的和最大
参考:力扣
首先我们将二维数组进行降维求解,也就是建立一个一维数组sum[]来存储该矩阵的若干连续行的纵向累加和,我们先来举个栗子:
对于矩阵:
1, 2, 3, 5
-1, 2,-4,-3
0,-1, 7, 6
那么对于sum[]的所有可能情况有6种:
[1,2,3,5] 仅首行纵向累加
[0,4,-1,2] 第一行和第二行
[0,3,6,8] 所有的三行
[-1,1,3,3] 后两行
[-1,2,-4,-3] 第二行
[0,-1,7,6] 最后一行
我们分别对sum[]的这六种情况,求一维数组的子数组最大累加和求解的问题。
public int[] getMaxMatrix(int[][] matrix) {
int max=Integer.MIN_VALUE;
int dp=0,start=0;
int[] ans=new int[4];// 保存最大子矩阵的左上角和右下角的行列坐标
int[] sum=null; //纵向累加数组
for(int i=0;i<matrix.length;i++) { //以i为上边,从上而下扫描
sum=new int[matrix[0].length]; //每次更换子矩形上边,就要清空b,重新计算每列的和
for(int j=i;j<matrix.length;j++) { //子矩阵的下边,从i到N-1,不断增加子矩阵的高
dp=0;
start=0;
for(int k=0;k<sum.length;k++) {
//我们只是不断增加其高,也就是下移矩阵下边,所有这个矩阵每列的和只需要加上新加的那一行的元素
sum[k]+=matrix[j][k];
//以下代码是求一维数组最大连续子数组的和
dp+=sum[k];
if(dp<sum[k]){
dp=sum[k];
start=k; // 暂时保存其左上角,列开始位置·
}
if(max < dp) {
ans[0]=i;
ans[1]=start;
ans[2]=j;
ans[3]=k;
max=dp;
}
}
}
}
return ans;
}
变体3:找数组中最长和为0连续子序列
题目:
找到一个数字序列中和为零的最长序,求出这个子序列的长度。
例如:
{100,1,2,-3,2,-3,1,2,1},其中从和为零的最长序列为{1,2,-3,2,-3,1},一个6个元素,所以长度为6。
要求:
时间复杂度O(N)
思路:
因为时间复杂度有要求,所以不能用常规暴力法来实现。
1、申请一个数组 sum,大小为 data.size()+1,即比原来的数组大1.
2、令sum[0]=0; sum[i+1]=sum[i]+num[i] 。就是将原数组data中,第i项数据之前的数据相加,保存到sum中。
为什么令sum[0]=0。若某一个前缀和为 0,说明该项之前(包含该项在内)的子数组之和为 0 ;两个前缀和如果相等,则说明两者相差的数据之和为 0;所以本文为了消除前缀和为0的单独考虑,就把sum[0]=0; i从0开始,sum[i+1]=sum[i]+num[i]
。这样要是出现0就可以出现两个0.
3、找出sum中相同元素的最远距离。这个最远距离就表示和为零的最长子序列的长度。
原理: 对一个数列来说,如果它的其中一个子序列和为零,那么一直在对数列求和的过程中,加上这整个子序列不会改变和的大小。
void MaxSubSumof0(vector<int>num)
{
int maxlen = 0;
int maxi; //左侧位置
int maxj; //右侧位置
vector<int>sum(num.size() + 1, 0);
for (int i = 0; i < num.size(); i++)
sum[i + 1] = sum[i] + num[i];
for (int i = 0; i < sum.size(); i++)
{
for (int j = sum.size()-1; j >= i; j--)
{
if (sum[i] == sum[j])
{
if (maxlen < j - i)
{
maxlen = j - i;
maxi = i;
maxj = j;
}
break;//这次循环是最大的,中间有重复的忽略
}
}
}
}
//a为待求解的数组,res为求解的和为0的最长字串
int longestnum(vector<int> num, vector<int>&res){
vector<int> sumvec{0}; //和数组,第一个元素为0
int sum = 0;
for (int c : num){
sum += c;
sumvec.push_back(sum);
}
unordered_map<int, int> summap;
int l = 0, r = -1, len = 0;
for (int i = 0; i < sumvec.size(); i++){
auto it = summap.find(sumvec[i]);
if (it != summap.end()){ //元素出现过,找到了和为0的子数列
if (i - it->second > len){
len = i - it->second; //更新长度
l = it->second; //更新左侧位置
r = i -1; //更新右侧位置
}
}
else
summap[sumvec[i]] = i; //元素没有出现过,插入新元素
}
}
变体4:找两个数,使得后面一个数减前一个数的差最大
在数组中,数字减去它右边的数字得到一个数对之差,求所有数对之差的最大值
1)这个题目最直接的想法就是穷举,时间代价是O(n^2),即检查每个元素与其右边元素差值的最大值。复杂度太高
2)现在知道被减数 是 i 左子数组中的最大值,可以这样做,顺序遍历数组a,找到当前下标为i的元素左子数组的最大值,用此最大值减去当前a[i]元素,记做当前数对差值,再与之前记录的数对差值作比较判断是否进行更新,
//容器从两个元素开始,以此往后取第三个、第四个……元素
//构造最大差值元素
int max_diff(int[] arr)
{
if(arr.size()<=1)
cout<<"长度必须是大于2"<<endl;
//下面的过程时元素大于等于二个时
//先定义最大差值,留着更新
int MaxDiff=arr[1]-arr[0];
//前两个元素的 最小值
int MIN=min(arr[0],arr[1]);
//从第三个元素开始
for(int i=2; i< arr.length, i++)
{
//若这个元素和前面元素的差值比MaxDiff大,更新 MaxDiff
if (arr[i] - MIN > MaxDiff)
{
MaxDiff = arr[i] - MIN;
}
//若这个元素比前面容器的元素min小,更新min
if (arr[i] < MIN){
MIN = arr[i];
}
}
return MaxDiff;
}
4、求解两个字符串的最长公共子序列
给定两个字符串str1和str2,返回两个字符串的最长公共子序列,例如:str1="1A2C3D4B56",str2="B1D23CA45B6A", "123456"和"12C4B6"都是最长公共子序列,返回哪一个都行。
分析:本题是非常经典的动态规划问题,假设str1的长度为M,str2的长度为N,则生成M*N的二维数组dp,dp[i][j]的含义是str1[0..i]与str2[0..j]的最长公共子序列的长度。
dp值的求法如下:
dp[i][j]的值必然和dp[i-1][j],dp[i][j-1],dp[i-1][j-1]相关,结合下面的代码来看,我们实际上是从第1行和第1列开始计算的,而把第0行和第0列都初始化为0,这是为了后面的取最大值在代码实现上的方便,dp[i][j]取三者之间的最大值。
比较好的分析:
这是一个动态规划的题目。对于可用动态规划求解的问题,一般有两个特征:①最优子结构;②重叠子问题
①最优子结构
设 X=(x1,x2,.....xn) 和 Y={y1,y2,.....ym} 是两个序列,将 X 和 Y 的最长公共子序列记为LCS(X,Y)
找出LCS(X,Y)就是一个最优化问题。因为,我们需要找到X 和 Y中最长的那个公共子序列。而要找X 和 Y的LCS,首先考虑X的最后一个元素和Y的最后一个元素。
1)如果 xn=ym,即X的最后一个元素与Y的最后一个元素相同,这说明该元素一定位于公共子序列中。因此,现在只需要找:LCS(Xn-1,Ym-1)
LCS(Xn-1,Ym-1)就是原问题的一个子问题。为什么叫子问题?因为它的规模比原问题小。(小一个元素也是小嘛....)
为什么是最优的子问题?因为我们要找的是Xn-1 和 Ym-1 的最长公共子序列啊。。。最长的!!!换句话说,就是最优的那个。(这里的最优就是最长的意思)
2)如果xn != ym,这下要麻烦一点,因为它产生了两个子问题:LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1)
因为序列X 和 序列Y 的最后一个元素不相等嘛,那说明最后一个元素不可能是最长公共子序列中的元素嘛。(都不相等了,怎么公共嘛)。
LCS(Xn-1,Ym)表示:最长公共序列可以在(x1,x2,....x(n-1)) 和 (y1,y2,...yn)中找。
LCS(Xn,Ym-1)表示:最长公共序列可以在(x1,x2,....xn) 和 (y1,y2,...y(n-1))中找。
求解上面两个子问题,得到的公共子序列谁最长,那谁就是 LCS(X,Y)。用数学表示就是:
LCS=max{LCS(Xn-1,Ym),LCS(Xn,Ym-1)}
由于条件 1) 和 2) 考虑到了所有可能的情况。因此,我们成功地把原问题 转化 成了 三个规模更小的子问题。
int findLCS(string A, int n, string B, int m)
{
// n表示字符串A的长度,m表示字符串B的长度
int[][] dp = new int[n+1][m+1] ;
for(int row = 0; row < n+1; row++)
dp[row][0] = 0;
for(int column = 0; column < m+1; column++)
dp[0][column] = 0;
/*
//比较方便的做法是,在定义的同时赋值,所有数组元素均为0,
//因为后面的循环中dp元素是循环更新的
int dp[n+1][m+1] = {0};
*/
/*
注意!注意!注意!此处的解释比较重要:
我们创建的数组dp是[n+1][m+1]维的。而且把第一行和第一列都置为0
dp[i][j]表示的是从A,B数组的第一个元素(id号是0)开始,到数组A的第i个元素(其实是A[i-1])
和数组B的第j个元素(其实是B[j-1])的最大公共子序列的长度
这样就不难理解下面的代码
*/
for (int i = 1; i < n + 1; i++)
{
for (int j = 1; j< m + 1; j++)
{
if (A[i-1]==B[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[n][m];
}
//另一种比较简洁的写法
int findLCS(string A, int n, string B, int m)
{
int[][] dp = new int[n+1][m+1] ;
for (int i = 0;i < n + 1;i++)
{
for (int j = 0; j<m + 1;j++)
{
if(i == 0 || j == 0)
dp[i][j] = 0;
else if (A[i-1]==B[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[n][m];
}
扩展1:求解两个字符串的最长(连续)公共子串
参考:从优化到再优化,最长公共子串 - Ider - 博客园
给定两个字符串,求出它们之间最长的相同子字符串的长度。
注意相同子串必须是连续的
//最长公共子序列
public static int lcs(String str1, String str2) {
int len1 = str1.length();
int len2 = str2.length();
int c[][] = new int[len1+1][len2+1];
for (int i = 0; i <= len1; i++) {
for( int j = 0; j <= len2; j++) {
if(i == 0 || j == 0) {
c[i][j] = 0;
} else if (str1.charAt(i-1) == str2.charAt(j-1)) {
c[i][j] = c[i-1][j-1] + 1;
} else {
c[i][j] = max(c[i - 1][j], c[i][j - 1]);
}
}
}
return c[len1][len2];
}
//最长公共子串
public static int lcs(String str1, String str2) {
int len1 = str1.length();
int len2 = str2.length();
int result = 0; //记录最长公共子串长度
int c[][] = new int[len1+1][len2+1];
for (int i = 0; i <= len1; i++) {
for( int j = 0; j <= len2; j++) {
if(i == 0 || j == 0) {
c[i][j] = 0;
} else if (str1.charAt(i-1) == str2.charAt(j-1)) {
c[i][j] = c[i-1][j-1] + 1;
result = max(c[i][j], result);
} else {
c[i][j] = 0;
}
}
}
return result;
}
暴力解法 – 所得即所求
对于该问题,直观的思路就是问题要求什么就找出什么。要子串,就找子串;要相同,就比较每个字符;要最长就记录最长。所以很容易就可以想到如下的解法。
int longestCommonSubstring_n3(const string& str1, const string& str2)
{
size_t size1 = str1.size();
size_t size2 = str2.size();
if (size1 == 0 || size2 == 0) return 0;
// the start position of substring in original string
int start1 = -1;
int start2 = -1;
// the longest length of common substring
int longest = 0;
// record how many comparisons the solution did;
// it can be used to know which algorithm is better
int comparisons = 0;
for (int i = 0; i < size1; ++i)
{
for (int j = 0; j < size2; ++j)
{
// find longest length of prefix
int length = 0;
int m = i; //记录str1的位置
int n = j; //记录str2的位置
while(m < size1 && n < size2)
{
++comparisons;
if (str1[m] != str2[n])
break;
++length;
++m;
++n;
}
if (longest < length)
{
longest = length;
start1 = i;
start2 = j;
}
}
}
}
动态规划法 – 空间换时间
有了一个解决问题的方法是一件很不错的事情了,但是拿着上边的解法回答面试题肯定不会得到许可,面试官还是会问有没有更好的解法呢?不过上述解法虽然不是最优的,但是依然可以从中找到一个改进的线索。不难发现在子串比较中有很多次重复的比较。
比如再比较以i和j分别为起始点字符串时,有可能会进行i+1和j+1以及i+2和j+2位置的字符的比较;而在比较i+1和j+1分别为起始点字符串时,这些字符又会被比较一次了。也就是说该问题有非常相似的子问题,而子问题之间又有重叠,这就给动态规划法的应该提供了契机。
暴力解法是从字符串开端开始找寻,现在换个思维考虑以字符结尾的子串来利用动态规划法。
假设两个字符串分别为s和t,s[i]和t[j]分别表示其第i和第j个字符(字符顺序从0开始),再令L[i, j]表示以s[i]和s[j]为结尾的相同子串的最大长度。应该不难递推出L[i, j]和L[i+1,j+1]之间的关系,因为两者其实只差s[i+1]和t[j+1]这一对字符。若s[i+1]和t[j+1]不同,那么L[i+1, j+1]自然应该是0,因为任何以它们为结尾的子串都不可能完全相同;而如果s[i+1]和t[j+1]相同,那么就只要在以s[i]和t[j]结尾的最长相同子串之后分别添上这两个字符即可,这样就可以让长度增加一位。合并上述两种情况,也就得到L[i+1,j+1]=(s[i]==t[j]?L[i,j]+1:0)这样的关系。
最后就是要小心的就是临界位置:如若两个字符串中任何一个是空串,那么最长公共子串的长度只能是0;当i为0时,L[0,j]应该是等于L[-1,j-1]再加上s[0]和t[j]提供的值,但L[-1,j-1]本是无效,但可以视s[-1]是空字符也就变成了前面一种临界情况,这样就可知L[-1,j-1]==0,所以L[0,j]=(s[0]==t[j]?1:0)。对于j为0也是一样的,同样可得L[i,0]=(s[i]==t[0]?1:0)。
最后的算法代码如下:
//dp[i][j]:串(x1,x2,...,xi)与串(y1,y2,...,yj),
//d[i][j]表示这两个串结与最长公共子串结尾相同时,最长公共子串的长度
//状态转移方程如下:
//若i=0或j=0,则dp[i][j] = 0
//否则:
// 若A[i]==B[j],则dp[i][j] = dp[i-1][j-1] + 1
// 若A[i]!=B[j],则dp[i][j] = 0
//用于打印的函数,后面才用到
void print_substring(string str, int end, int length)
{
int start = end - length + 1;
for(int k=start;k<=end;k++)
cout << str[k];
cout << endl;
}
int main()
{
string A,B;
cin >> A >> B;
int x = A.length();
int y = B.length();
A = " " + A;//特殊处理一下,便于编程
B = " " + B;
//回忆一下dp[][]的含义?
int **dp = new int* [x+1];
int i,j;
for(i=0;i<=x;i++)
{
dp[i] = new int[y+1];
for(j=0;j<=y;j++)
dp[i][j] = 0;
}
//下面计算dp[i][j]的值并记录最大值
int max_length = 0;
for(i=1;i<=x;i++)
for(j=1;j<=y;j++)
if(A[i]==B[j])
{
dp[i][j] = dp[i-1][j-1] + 1;
if(dp[i][j]>max_length)
max_length = dp[i][j];
}
else
dp[i][j] = 0;
//LCS的长度已经知道了,下面是根据这个最大长度和dp[][]的值,
//找到对应的 LCS具体子串, 注意:可能有多个
int const arr_length = (x>y?x:y) + 1;
int end_A[arr_length]; //记录LCS在字符串A中结束的位置
int num_max_length = 0; //记录LCS的个数
for(i=1;i<=x;i++)
for(j=1;j<=y;j++)
if(dp[i][j] == max_length)
end_A[num_max_length++] = i;
cout << "the length of LCS(substring) is : " << max_length << endl << " nums: " << num_max_length << endl << "they are (it is): " << endl;
for(int k=0;k<num_max_length;k++) //输出每个具体的子串
print_substring(A, end_A[k], max_length);
return 0;
}
int longestCommonSubstring_n2_n2(const string& str1, const string& str2)
{
size_t size1 = str1.size();
size_t size2 = str2.size();
if (size1 == 0 || size2 == 0) return 0;
//vector<vector<int> > table(size1, vector<int>(size2, 0));
// the start position of substring in original string
int[][] table = new int[size1][size2]
int start1 = -1;
int start2 = -1;
// the longest length of common substring
int longest = 0;
// record how many comparisons the solution did;
// it can be used to know which algorithm is better
int comparisons = 0;
for (int j = 0; j < size2; ++j)
{
++comparisons;
table[0][j] = (str1[0] == str2[j] ? 1 :0);
}
for (int i = 1; i < size1; ++i)
{
++comparisons;
table[i][0] = (str1[i] == str2[0] ? 1 :0);
for (int j = 1; j < size2; ++j)
{
++comparisons;
if (str1[i] == str2[j])
{
table[i][j] = table[i-1][j-1]+1;
}
}
}
for (int i = 0; i < size1; ++i)
{
for (int j = 0; j < size2; ++j)
{
if (longest < table[i][j])
{
longest = table[i][j];
start1 = i-longest+1;
start2 = j-longest+1;
}
}
}
#ifdef IDER_DEBUG
cout<< "(first, second, comparisions) = ("
<< start1 << ", " << start2 << ", " << comparisons
<< ")" << endl;
#endif
return longest;
}
5、0-1背包问题
变体1: 一个数组,分为两个子数组,使得它们的和相等(动态规划)
变体2:变体1的变体:给一组数字添加正负号使他们的和为0
变体的解答参考:
Leetcode 题解 - 动态规划-0-1 背包(2):划分数组为和相等的两部分_酷记麻辣油的博客-CSDN博客_将数组分成等和的两个部分 背包问题
https://blog.csdn.net/qq_40861091/article/details/100890008
背包问题,动态规划经典问题,一个背包有额定的承重重量,有N件物品,每件物品都有自己的价值,记录在数组V中,也都有自己的重量,记录在数组W中,每件物品只能选择要装入还是不装入背包,要求在不超过背包承重的前提下,选出的物品总价值最大。
分析:假设物品编号从1到n,一件一件的考虑是否加入背包,假设dp[x][y]表示前x件物品,不超过重量y的时候的最大价值,枚举一下第x件物品的情况:
情况1:如果选择了第x件物品,则前x-1件物品得到的重量不能超过y-w[x]。
情况2:如果不选择第x件物品,则前x-1件物品得到的重量不超过y。
所以dp[x][y]可能等于dp[x-1][y],也就是不取第x件物品的时候,价值和之前一样,也可能是dp[x-1][y-w[x]]+v[x],也就是拿第x件物品的时候,当然会获得第x件物品的价值。两种可能的选择中,应该选择价值较大的那个,也就是:
dp[x][y] = max{dp[x-1][y],dp[x-1][y-w[x]]+v[x]}
dp[x-1][y-w[x]]+v[x] 表示:当取第X件物品时,为了取得最大值,当然是其他物品的总重量 <= [y-w[x]] ,即不取第X个物品时求得最大值dp[x-1][y-w[x]], 取上X物品的价值是 dp[x-1][y-w[x]]+v[x]
因此,对于dp矩阵来说,行数是物品的数量n,列数是背包的重量w,从左到右,从上到下,依次计算出dp值即可。
状态定义:
本质上就是从i
个物品中选择一定数量的物品在一定空间限制的前提下,求这些物品的最大总价值,我们可以定义一个二维数组dp[i][j]
,这个数组的值就表示从前i
件物品进行选择,在不超过容量j
的前提下所满足最大的物品总价值。(注:此处的第i
件物品对应与数组下标i
)
确定初始状态:
当只有一个物品时,如果该物品的体积v
不大于背包容量j
,则初始值dp[0][j]=v
,否则dp[0][j]=0
。
状态转移方程:
对于第i件物品,设它的所占容量为v[i],价值为w[i],我们可以选择该物品也可以不选择该物品,如果不选择该物品则dp[i][j]=dp[i−1][j],如果选择该物品有两种情况:
- 背包剩余空间不够了,那么此时就无法选择该物品,dp[i][j]=dp[i−1][j]
- 背包剩余空间充足,那么此时的物品总价值为 dp[i][j]=dp[i−1][j−v[i]]+w[i]
综上,转移方程为 dp[i][j]=max(dp[i−1][j],dp[i−1][j−v[i]]+w[i])
public int bagTest(int[] weight,int[] value,int bagWeight){
int n=weight.length;
int[][] dp=new int[n][bagWeight+1];
for(int j=weight[0];j<=bagWeight;j++){
// dp[0][j]=value[0];
dp[0][j] = j >= weight[0] ? value[0] : 0;
}
for(int i=1;i<n;i++){
for(int j=0;j<=bagWeight;j++){
if(j - weight[i]>= 0){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[n-1][bagWeight];
}
/**
*
* @param N 物品数
* @param C 背包容量
* @param v 每件的体积
* @param w 每件物品的价值
* @return 最大价值
*/
public int zoKnapsack(int N, int C, int[] v, int[] w) {
//0-1背包朴素
int[][] dp = new int[N][C+1];
//初始化
for (int j = 0; j <= C; j++) {
dp[0][j] = j >= v[0] ? w[0] : 0;
}
//处理剩余元素
for (int i = 1; i < N; i++) {
for (int j = 0; j <= C; j++) {
//不选
int x = dp[i-1][j];
//选
int y = j >= v[i] ? dp[i-1][j-v[i]] + w[i] : 0;
//取两者中的最大值
dp[i][j] = Math.max(x, y);
}
}
return dp[N-1][C];
}
int findLcs()
{
/*物品数量*/
int n = 4;
/*背包承重*/
int cap = 10;
int v[4] = {42,12,40,25};
int w[4] = {7,3,4,5};
/*二维动态规划表*/
//vector<int> p(cap+1,0);
//vector<vector<int>> dp(n+1,p);
int dp[n+1][cap+1] = {0};
for (int i = 1; i<n+1;i++){/*枚举物品*/
for (int j = 1; j<cap+1;j++){/*枚举重量*/
/*判断枚举的重量和当前选择的物品重量的关系
如果枚举的和总量大于等于选择物品,则需要判断是否选择当前物品*/
//因为我们是从i=1,j=1开始,所以第i个物体在w中的位置是w[i-1]
if (j-w[i-1]>=0)
dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i]);
else
/*如果枚举的重量还没有当前选择物品的重量大,那就只能是不取当前物品*/
dp[i][j] = dp[i-1][j];
}
}
return dp[n][cap];
}
6、硬币找零问题
leetcode322:力扣
我们有面值为1元3元5元的硬币若干枚,如何用最少的硬币凑够11元?
分析:
1 求问题的最优解:最小的硬币数
2 是否有子问题:f(n)表示的最少硬币数是是上一次拿时候的硬币数最少。
注意:f(n)是n元的最小硬币数,最后一次可拿的硬币数为1,3,5 则下一步
的最小硬币数为 f(n-vi) 它的状态变更不是按元数的,是按照上次拿的硬币钱目
3 状态转移方程为 f(n)= min(f(n-vi)+1)
4 边界问题(找到最后一个重复的问题) 这里
f(1)=1 ,f(2)=f(1)+f(1)=2 f(3)=min(1,f(2)+1)
f(4)=f(3)+1 f(5)=1
5 从上往下分析问题,从下往上解决问题。
"""递归方法
我们有面值为1元3元5元的硬币若干枚,如何用最少的硬币凑够11元?
1 求问题的最优解:最小的硬币数
2 是否有子问题:coin(n)表示的最少硬币数是上一次拿时候的硬币数最少。
注意:coin(n)是n元的最小硬币数,最后一次可拿的硬币数为1,3,5 则下一步
的最小硬币数为 coin(n-coin_money[i]) 它的状态变更不是按元数的,是按照上次拿的硬币钱目
3 状态转移方程为 coin(n)= min(coin(n - coin_money[i]) + 1)
4 边界问题(找到最后一个重复的问题) 这里
coin(1)=1 ,coin(2)=coin(1)+coin(1)=2 coin(3)=min(1,coin(2)+1)
coin(4)=coin(3)+1 coin(5)=1
5 从上往下分析问题,从下往上解决问题。
"""
//递归的方法
int Findmin(n)
{
if (n == 1) #把所有的边界问题找到
return 1
if (n == 2)
return 2
if (n == 3)
return 1
if (n == 4)
return 2
if (n == 5)
return 1;
int coin_money[3] = [1,3,5];
int min_num = n; //在n > 5 的前提下,最后的结果肯定小于n
for (int j=6;j<=n;j++)
{
for(int i=0;i< 3 ;i ++)
{
// 最后一次先拿1,剩下的在前面的方法已经有了,剩下的为n-coin_money[1]
// 最后一次拿3,剩下的前面有了
// 最后一次拿5,剩下的取前面的结果
// 维护一个min_count,是三者最小的数
int count = Findmin(j-coin_money[i])+1; // 采用了递归的思想 这里是从上到下,
if(min_num > count) //复杂度比较高
min_num = count;
}
}
return min_num;
}
动态规划方法:
/*动态规划方法
假设dp[i]表示拼凑出i元所需的最少硬币,我们要求的就是dp[m]。
很明显dp[0] = 0,这是边界条件。
假如我们已经知道了dp[0]到dp[i-1],那么求dp[i]就是遍历可能的硬币规格,
求如果dp[i]加入的硬币是这个规格的,那么之前可能是由哪一个dp[x]添加的这枚硬币的结果,
并且要求的是dp[x]中最小的,因为我们所求的dp[i]是dp[x]+1.
*/
int findmincoin(int num)
{
const MIN = 9999;
int coins_value = [1,2,5];
int len = coins_value.length();
int[] dp = new int[num + 1];
dp[0] = 0; //边界条件
for(int i=1;i<=num;i++)
{
dp[i] = MIN; //初始化一个最大值,后面进行更新
for(int j=0;coins_value[j]<=i && j< len;j++)
{//遍历每种规格的硬币
dp[i] = min(dp[i-coins_value[j]]+1,dp[i]);
}
}
return dp[num];
}
7、数组分成m段求各段和的最大值 最小
链接:LeetCode 410 —— 力扣
题目描述
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
示例:
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
二分查找的思路:
本题二分法的思路很有意思,首先我们想子数组的最大和 范围一定是 在数组元素中的最大值 到 数组所有元素的和 之间。
有了上下界,我们就可以用二分的方式在其中遍历。 遍历的过程是,我们从上下界的中间出发,定一个最大和的值 mid,(mid就是我们要求的最大和的最小值),然后判断在mid值下数组是否能够被分为m个部分。判断方法:对数组进行划分,从左往右,加和恰好大于mid时,表示当前这个num不能再往本组加了,一加就超过了,所以一组划分好了,我们用temp跟踪当前的数组和,既然这个num不属于当前一组,一定是下一组,于是temp = num。在temp没大于mid前,只管累加即可。
统计出当前mid下可以划分多少组后,就可以和m比较,如果划分的组数大于m,表示mid值太小了,将下边界left值变大(left=mid+1),否则划分的组数没到m,表示mid大了,将上边界right下移 (right=mid)。
二分的骨架是一样的,重点是这个思路以及在mid的统治下统计有多少组。
class Solution {
public:
// 判断 数组能否分成m 个组。
bool check(vector<int>& nums, int x, int m) {
long long sum = 0;
int cnout = 1;
for (int i = 0; i < nums.size(); i++) {
if (sum + nums[i] > x) {
cnout ++;
sum = nums[i];
} else {
sum += nums[i];
}
}
return cnout <= m;
}
int splitArray(vector<int>& nums, int m) {
long long left = 0, right = 0;
// 求出数组元素最大值左边界(left),数组和右边界(right)
for (int i = 0; i < nums.size(); i++) {
right += nums[i];
if (left < nums[i]) {
left = nums[i];
}
}
while (left < right) {
long long mid = (left + right) >> 1;
if (check(nums, mid, m)) {
right = mid; // 我们计算的是 小于mid 的数组和,说有右边界变小应该为 right = mid
} else {
left = mid + 1;
}
}
return left;
}
};
参考链接: 参考1--二分+dp分析的比较详细
参考2:详解数组分段和最大值最小问题(最小m段和问题) - 鱼与鱼 - 博客园
7.1 给定一个正整数n,拆成至少两个数的和=n,让这些数的乘积最大
参考:1、【Leetcode刷题Python】343. 整数拆分_Better Bench的博客-CSDN博客_python整数拆分
8、 盛最多水的容器
LeetCode:力扣
思路二:双撞指针,设立两个指针,一个从头一个从尾,相向而行遍历数组,每次舍弃较短边
要想让容纳的水最多,就得让宽和高的乘积最大,宽度肯定是取首尾的时候最大,但是此时的高就未必了。
如果用暴力法肯定是可以的,但是某些用例会超时,怎么能用 时间复杂度解决这题呢?
以往我们在滑动窗口的题目中,两个指针都是从同一端开始向另一端移动,而本题我们可以让两个指针分别放在两端,这样可以至少保证,目前的宽度是最长的。
那如何移动指针能让面积更大呢?显然,不管哪个首尾往中间移动,宽度都会减少,而高度取决于两个高的最小值,我们应该往中间移动低的一段。这样,最小高度值有可能会变高,面积也就会增大了。
保持如此移动指针,保持面积最大值不停更新,直到两个指针相遇,我们就能得到最后的结果了。
由于整个过程只遍历了一次数组,因此时间复杂度为O(n),其中n为数组height的长度。而使用空间就是几个变量,故空间复杂度是O(1)。
int maxArea(int[] height) {
// 对撞指针
int l = 0;
int r = height.size()-1;
int max_val = 0;
while (l < r) {
h = min(height[l], height[r])
area_val = h * (right - left)
max_val = max(max_val, area_val);
if (height[l] < height[r])
l++;
else
r--;
}
return max_val;
}
9、接雨水
动态规划方法,必须会:力扣
现在主要解释一下上面链接中的双指针法:
先明确几个变量:
leftMax:左边的最大值,它是从左往右遍历找到的
rightMax:右边的最大值,它是从右往左遍历找到的
left:从左往右处理的当前下标
right:从右往左处理的当前下标
定理一:在某个位置i处,它能存的水,取决于它左右两边的最大值中较小的一个。
定理二:当我们从左往右处理到left下标时,左边的最大值leftMax对它而言是可信的,但rightMax对它而言是不可信的。(见下图,由于中间状况未知,对于left下标而言,rightMax未必就是它右边最大的值)
定理三:当我们从右往左处理到right下标时,右边的最大值rightMax对它而言是可信的,但leftMax对它而言是不可信的。
对于位置left而言,它左边最大值一定是leftMax,右边最大值一定 "大于等于"rightMax,这时候,如果leftMax<rightMax成立,那么它就知道自己能存多少水了(取决较小的一个)。无论右边将来会不会出现更大的rightMax,都不影响这个结果。 所以当leftMax<rightMax时,我们就希望去处理left下标,反之,我们希望去处理right下标。
rightMax
leftMax __
__ | |
| |__ __?????????????????????? | |
__| |__| __| |__
left right
10、柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
方法一 左右两边分别使用单调栈
首先从左往右对数组进行遍历,借助单调栈求出了每根柱子的左边界,随后从右往左对数组进行遍历,借助单调栈求出了每根柱子的右边界
考察单调栈:当前 i 位置能组成最大矩形,是以 height[i] 为高能组成的矩形。因此从当前 i 位置向左右找到 第一个 height[index] < height[i] 的 left_index 和 right_index,他们之间即为能组成的最大矩形,宽度为 right_index - left_index - 1(注: 这里 -1 自己画图自己理解)
说明:left, right 存储的是数组下标,left 边界是 -1, right 的边界是 n, 数组的下标是取值范围是[0, n-1]。具体可画图理解
参考链接:力扣
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
// left, right 存储的是下标
vector<int> left(n), right(n);
// 数组初始化
// int left[n] = {-1};
stack<int> index_stack;
for (int i = 0; i < n; ++i) {
while (!index_stack.empty() && heights[index_stack.top()] >= heights[i]) {
index_stack.pop();
}
left[i] = (index_stack.empty() ? -1 : index_stack.top());
index_stack.push(i);
}
index_stack = stack<int>();
for (int i = n - 1; i >= 0; --i) {
while (!index_stack.empty() && heights[index_stack.top()] >= heights[i]) {
index_stack.pop();
}
right[i] = (index_stack.empty() ? n : index_stack.top());
index_stack.push(i);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
ans = max(ans, (right[i] - left[i] - 1) * heights[i]);
}
return ans;
}
方法二:单调栈 + 常数优化
解法1:力扣
解法2:上述方法一的延伸这个只看方法二就可以:力扣
10.1 扩展1:0 1矩阵求只包含1的最大矩形面积
参考:LeetCode 85. 最大矩形
参考答案: 力扣
10.2 扩展1:0 1矩阵求只包含1的最大矩形面积
参考:LeetCode 221. 最大正方形
11、求一组数中最长的等差数列的长度
扩展1:给定一个数组,找出能构成差为1的最长的等差数列的元素
题目描述
题目:给定一个数组求出数组最长等差数列的长度。
举例:3,8,4,5,6,2
输出:5。
思路
典型的动态规划问题,但是注意此题的动态规划不能简单用一个dp数组就可以得出长度,应该在用一个表示最长长度的变量来随时更新为每一次遍历的最长长度。
思想为:dp[i][diff]表示第i个数,等差为diff的长度。
dp[i][diff] = dp[j][diff] + 1,j < i
/**
* 求最长等差数列的长度
* 运用动态规划
* 1.首先对数组array进行排序
* 2.数组长度为length = array.length - 1,然后差最大为最大值减去最小值为diff
* 3.进行dp[length][diff + 1]
* 则dp[i][diff] 表示 第i个数 等差为 diff的个数
* 显然有 dp[i][diff] = dp[j][diff] + 1,其中j < i
*/
public static int calArithmeticSequence(int[] nums){
int length = nums.length;
if(length == 0) return 0;
Arrays.sort(nums); // 先排序,从小到大
/**
* 最大等差
*/
int diffMax = nums[length - 1] - nums[0];
/**
* dp数组保存每次遍历的结果
*/
int[][] dp = new int[length][diffMax + 1];
/**
* 因为任何单个数,等差无论多少,长度初始化都为1
*/
for(int i = 0; i < length; i++)
for(int j = 0; j <= diffMax; j++)
dp[i][j] = 1;
int longestLength = 1;
for(int i = 1; i < length; i++)
/**
* 依次考察i之前的数,对于每个j与i的差值temp
* 都要对dp[i][temp] = dp[j][temp] + 1
* 然后在看看当前的长度是否大于max,若大于max,则更新max值。
*/
for(int j = i - 1; j >= 0; j--){
int temp = nums[i] - nums[j];
dp[i][temp] = dp[j][temp] + 1;
longestLength = Math.max(longestLength, dp[i][temp]);
}
return longestLength;
}
12、丑数
求第n个丑数--coding_ytusdc的博客-CSDN博客
13、打家劫舍问题
官方题解: 力扣
进阶问题:力扣
14、最小编辑距离
LeetCode:力扣