动态规划/leetcode/直接推导递推公式

通过编写动态规划系列题目,拓宽思路 

583. Delete Operation for Two Strings

针对两个字符串删除一定的字符使这两个字符相同,求最少的删除数。
可以转换为求最长公共子序列问题。求出来之后两个字符串的长度和减去最长子序列的两倍即可。
对最长公共子序列动态规划的理解,应该先从自定向下的递归方法理解。
参考链接提供所有方法:https://leetcode.com/problems/delete-operation-for-two-strings/solution/
里面首先介绍递归的方法,然后介绍在递归的基础上加存储数组的方法,然后介绍动态规划的方法,并介绍了一种不转换为最长子序列的动态规划方法。通常的压缩方法也介绍了。
下面是最长子序列的二维动态规划方法。
class Solution {
public:
    int minDistance(string word1, string word2) {
        vector<vector<int>> dp(word1.size()+1,vector<int>(word2.size()+1,0));//采用这种扩容的方法,可以防止一个数组为空的情况,而且可以把边界情况放到一个循环中讨论
        for(int i=0;i<=word1.size();i++)
            for(int j=0;j<=word2.size();j++)
            {
                if(i==0||j==0) continue;
                if(word1[i-1]==word2[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 word1.size()+word2.size()-2*dp[word1.size()][word2.size()];
        
    }
};


413 Arithmetic slices 等差数列切片 子数组(连续子序列)

题意:寻找一个数组的连续子数组,使得这个子数组是等差数列。请求出这种子数组的个数。

我的思路

(1)n方复杂度,首先求累加和数组,然后根据等差数列求和充要条件,判断序列是否是等差数列,但是解雇不对

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& A) {
        
        int n=A.size();
        if(n<=2) return 0;
        vector<int> sum(n+1,0);
        //求累加和数组
        for(int i=1;i<=n;i++)
        sum[i]=sum[i-1]+A[i-1];
        int count=0;
        for(int i=3;i<=n;i++)
        {
            for(int k=3;i-k>=0;k++)
            {
                int cursum=sum[i]-sum[i-k];
                if((cursum-k*A[i-k])%(k*(k-1)/2)==0)
                count++;
            }
        }
          return count;  
    }
};
(2)动态规划方法

设dp[i]表示到第i1个时,前面构成的数列个数,

如果 A[i-1]-A[i-2]==A[i-2]-A[i-3]  dp[i]=dp[i-1]+1+(len-2)+1,这里的len是在前面至少是3个数的时候的长度3,如果前面不构成等差序列,len=0,公式变为dp[i]=dp[i-1]+1

如果A[i-1]-A[i-2]!=A[i-2]-A[i-3]  ,dp[i]=dp[i-1] 此时把len变成0.

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& A) { 
        int n=A.size();
        vector<int> dp(n+1,0);
        int len=0;
        for(int i=3;i<=n;i++)
        {
            if(A[i-1]-A[i-2]==A[i-2]-A[i-3])
            {
               if(len==0)//说明之前的三个不是这种序列,从现在开始重新构成3个数的等差序列
                {
                    dp[i]=dp[i-1]+1;
                   len=3;//在结尾改变len
                }
            else//说明当前等差数列的长度在递增过程
            {
                dp[i]=dp[i-1]+len-1;
                len++;//len自增后是当前这一片的最大长度
            }
            }
            else
            {
                dp[i]=dp[i-1];
                len=0;//
            }
        }
        return dp[n];
    }
};

可以把dp压缩成 一个变量:

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& A) { 
        int n=A.size();
        int dp=0;
        int len=0;
        for(int i=3;i<=n;i++)
        {
            if(A[i-1]-A[i-2]==A[i-2]-A[i-3])
            {
               if(len==0)
                {
                    dp=dp+1;
                   len=3;
                }
            else
            {
                dp=dp+len-1;
                len++;
            }
            }
            else
            {
                dp=dp;
                len=0;
            }
        }
        return dp;
    }
};


参考方法:

(1)dp[i]表示以i结尾的等差数列个数,如果A[i] - A[i -1] == A[i - 1] - A[i - 2],dp[i]= dp[i -1] +1;否则dp[i]=0

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

上述dp可以用一个变量代替,因为每次只涉及当前值和前面的值。

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& A) {
        int res = 0, cur = 0;
        for (int i = 2; i < A.size(); ++i) {
            if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {
                cur += 1;
                res += cur;
            } else {
                cur = 0;
            }
        }
        return res;
    }
};
(2)使用公式

[1,2,3,4]含有3个长度至少为3的算数切片,我们再来看[1,2,3,4,5]有多少个呢:
len = 3: [1,2,3], [2,3,4], [3,4,5]
len = 4: [1,2,3,4], [2,3,4,5]
len = 5: [1,2,3,4,5]
那么我们可以找出递推式,长度为n的等差数列中含有长度至少为3的算数切片的个数为(n-1)(n-2)/2,那么题目就变成了找原数组中等差数列的长度,然后带入公式去算个数即可
下面的代码count=1时表示长度为3.
public class Solution {
    public int numberOfArithmeticSlices(int[] A) {
        int count = 0;
        int sum = 0;
        for (int i = 2; i < A.length; i++) {
            if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {//如果满足,长度自增,1的时候说明长度是3,2的时候说明长度是4
                count++;
            } else {
                sum += (count + 1) * (count) / 2;//连续数列结束,把之前的数加上
                count = 0;//并把count变成0
            }
        }//注意这种方法不会有重复,它实际分别找到数组中成片出现的最长数列,每个这样的数列不可能相连
        return sum += count * (count + 1) / 2;//在长度自增过程中,并没有把最后算的数列个数加到总和中去,最后需要做补充操作。当最后不构成数列时,count为0
    }
}
同样的一个实现: 只是len和count的取值不同。len为3时,表示长度为3 这使得公式与推导公式完全一样,但是最后的时候,可能len为2,不构成数列,这个时候需要加一个判断

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& A) {
        int res = 0, len = 2, n = A.size();
        for (int i = 2; i < n; ++i) {
            if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {
                ++len;
            } else {
                if (len > 2) res += (len - 1) * (len - 2) * 0.5;
                len = 2;
            }
        }
        if (len > 2) res += (len - 1) * (len - 2) * 0.5;
        return res;
    }
};

446. Arithmetic Slices II - Subsequence 等差数列切片 子序列(可以不连续)


646 maxmum length of pair chain  pair的最大链

给定n组数,每组的第一个数小于第二个。如果两个数组,第一个组的大值小于第二个组的小值,那么构成一个链。题目要求返回最大的链的长度。



343 integer break  整数切分乘积最大

一个数可以分成几个数的和,这些数相乘的最大值是多少?


357 Count Numbers with Unique Digits 

给定一个数n,在0 ≤ x < 10n中找所有的数的没有相同数字的数量

486 Predict the Winner   纸牌博弈问题

题目:有一个整型数组A,代表数值不同的纸牌排成一条线。玩家a和玩家b依次拿走每张纸牌,规定玩家a先拿,玩家b后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家a和玩家b都绝顶聪明,他们总会采用最优策略。请返回最后获胜者的分数。给定纸牌序列A及序列的大小n,请返回最后分数较高者得分数(相同则返回任意一个分数)。(获得的牌数相加)

思路:

递归推导:

//递归推导
  class Solution {
public:
    bool PredictTheWinner(vector<int>& nums) {
        int sum=accumulate(nums.begin(),nums.end());
        int me=p(nums,0,nums.size()-1);
        if(sum-me>=me) return false;
        else
            return true;
    }
    //递归推导
    int p1(vector<int>& nums,int i,int j)//i表示牌的最左边,j表示牌的最右边
    {
        //可以取i,也可以取j
        //如果只剩一张牌,取走这个牌
        if(i==j) return nums[i];
        //如果剩两张牌,因为奇偶性不同最后剩得牌不一样
        if(i=j-1) return max(nums[i],nums[j]);
        int sum=0;
        //如果我取走i,给对手的牌是i-1到j,对手的可以取走i-2或者j,他会选择剩下的牌加和小的那种情况
        //所以我取走i的话,对手给我留下下面两种牌堆
        //p(nums,i+2,j),p(nums,i+1,j-1);
        //他他妈的一定会留较小的那个:min(p(nums,i+2,j),p(nums,i+1,j-1))
        //同样道理,我也可以取走j
        //留给对手的牌堆是:i到j-1.他留给我的牌堆是:min(p(nums,i+1,j-1),p(nums,i,j-2))
        //但是现在决定权在我这里,所以我会选择:
        return max(nums[i]+min(p(nums,i+2,j-1),p(nums,i+1,j-1)),nums[j]+min(p(nums,i+1,j-1),p(nums,i,j-2)));
    }
};
改成动态规划
class Solution {
public:
    bool PredictTheWinner(vector<int>& nums) {
    int sum=std::accumulate(nums.begin(),nums.end(),0);
    int n=nums.size();
        if(n==1) return true;
    vector<vector<int>> dp(n,vector<int>(n,0));
    for(int i=n-1;i>=0;i--)
    {
        for(int j=i;j<n;j++)
        {
            if(i==j) dp[i][j]=nums[i];
            else if(i+1==j) dp[i][j]=max(nums[i],nums[j]);
            else
            {
                dp[i][j]=max(nums[i]+min(dp[i+2][j],dp[i+1][j-1]),nums[j]+min(dp[i+1][j-1],dp[i][j-2]));
            }     
        }
    }
    if(sum-dp[0][n-1]<=dp[0][n-1]) return true;
        else
            return false;
    }
};

改成一维数组:由于依赖左下方的反斜对角线,因此需要从左上至右下沿反斜对角线遍历

标准答案:

他的递归思路不太一样https://leetcode.com/problems/predict-the-winner/solution/


464Can I Win  

给定数的范围,每次两个人轮流拿出一个数,这些数加起来大于目标数的时候的那个人赢,问第一个拿数的人能赢吗?假设两个人都灰常聪明

375Guess Number Higher or Lower II 

猜数,猜多少花多少,问最少带多少钱能猜对?

392 Is Subsequence   判断一个字符串是否是另一个字符串的子序列

494 target Sum 

这道题给了我们一个数组,和一个目标值,让我们给数组中每个数字加上正号或负号,然后求和要和目标值相等,求有多少中不同的情况。


650 2 Keys Keyboard   

文本里只有一个字符A,每次的操作只能是复制粘贴当前所有数字或者再粘贴一遍,求给定字符长度所需要的最小操作次数

通过推导找规律,发现dp[i]:if(k*j==i) dp[i]=min(dp[k]+dp[j]) ,就是说一个数的操作次数等于它相乘因子的操作数之和, 取sqrt找到最相近的两个因数相加即可。

class Solution {
public:
    int minSteps(int n) {
        //dp[i]:if(k*j==i) dp[i]=min(dp[k]+dp[j]) 
        if(n<=1) return 0;
        vector<int> dp(n+1,0);
        dp[1]=0;
        for(int i=2;i<=n;i++)
        {
            dp[i]=i;
            int factor=sqrt(i);
            for(int j=factor;j>0;j--)
            {
                if(i%j==0) {
                    dp[i]=dp[j]+dp[i/j];
                    break;
                }
            }
        }
    return dp[n];
    }
        
};

另外一种解法:

由于2 * 2 = 2 + 2,2 * 3 > 2 + 3,4 * 4 > 4 + 4 ,而之前的那种方法,得到的两个因子,每个因子可以分别分解。这个题目相当于将一个数分解成多个因子的加和。比如30->5+6,6->2+3,就是2+3+5.所以可以有递归实现:

class Solution {
public:
    int minSteps(int n) {
        if (n == 1) return 0;
        for (int i = 2; i < n; i++)
            if (n % i == 0) return i + minSteps(n / i);
        return n;
    }
};
这个递归实现可以改成迭代实现:

 public int minSteps(int n) {
        int s = 0;
        for (int d = 2; d <= n; d++) {
            while (n % d == 0) {
                s += d;
                n /= d;
            }
        }
        return s;
    }
这样复杂度达到logn

377 Combination Sum IV  

给定一个集合,每个数都不同,给定一个目标,在集合中选择数(可重复)的加和是这个目标值,问有多少种(次序不同的算不同种)?

https://discuss.leetcode.com/topic/52302/1ms-java-dp-solution-with-detailed-explanation 本文讲解了自顶向下的递归和自底向上的动态规划

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        int n=nums.size();
        vector<int> com(target+1,0);
        com[0]=1;
        for(int i=1;i<=target;i++)
            for(int j=0;j<n;j++)
            {
                if(i-nums[j]>=0)
                    com[i]+=com[i-nums[j]];//组成i的最后一个元素可以是nums中的任何一个(只要不比总和大),所以com[i]是减去任何一个(并满足要求)的所有加和
                
            }
        return com[target];
    }
};


638 Shopping Offers 

打折促销组合,给定需要购买的商品类型和数量,求买这些东西花费的最少的钱


309 Best Time to Buy and Sell Stock with Cooldown  

可以任意的购买抛售股票,但是抛售之后的一天不能购买股票,求赚钱最多是多少?

 

416 Partition Equal Subset Sum把一个数组分成两组,问能不能两组的元素加和相等

376 Wiggle Subsequence  增减最长子序列

这个题看的题解
思路1:

设置up[i]表示到i位置时首差值为正的摆动子序列的最大长度,down[i]表示到i位置时首差值为负的摆动子序列的最大长度
对于每一个up[i]遍历之前的down[j],当满足nums[i]>nums[j]时,选择是否更新up[i]

对于每一个down[i]遍历之前的up[j],当满足nums[i] < nums[j]时,选择是否更新down[i]

代码如下:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n=nums.size();
        if(n<=1) return n;
        vector<int> up(n,1);//初始值是1
        vector<int> down(n,1);
        for(int i=1;i<n;i++)
            for(int j=0;j<i;j++)
            {
                if(nums[i]>nums[j])
                {
                    up[i]=max(up[i],down[j]+1);
                }
                else if(nums[i]<nums[j])
                {
                    down[i]=max(down[i],up[j]+1);
                }
            }
        return max(up[n-1],down[n-1]);
    }
};

思路2:

如果nums[i]>nums[i-1] 那么up[i]=down[i-1]+1,down[i]=down[i-1]

如果nums[i]>nums[i-1] 那么down[i]=up[i-1]+1,up[i]=up[i-1]

如果两者相等,down[i]=down[i-1] up[i]=up[i-1]

只需遍历一遍即可,注意down[0]和up[0]都是1

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n=nums.size();
        if(n<=1) return n;
        vector<int> up(n,0);//初始值是1
        vector<int> down(n,0);
        down[0]=1;
        up[0]=1;
        for(int i=1;i<n;i++)
        {
            if(nums[i]<nums[i-1] )
            {
           up[i]=down[i-1]+1;
                down[i]=down[i-1];
                
            }
            if(nums[i]>nums[i-1])
            {
                down[i]=up[i-1]+1;
                up[i]=up[i-1];
            }
            if(nums[i]==nums[i-1])
            {
                 down[i]=down[i-1];
                up[i]=up[i-1];
            }
        }
        return max(up[n-1],down[n-1]);
    }
};

贪心算法:

考虑增减曲线,我们在找点的时候,如果遇到连续升序或者连续降序的时候,一定而且只能选择这段的两个节点,当出现增减时,这些部分也包含进来。可以理解为,这个题目是将原来的节点做了一种“滤波处理”:节点的作用只描述趋势变化的增减特点,在一定时间内连续增加或减少时,我们只关心这一段的开始和结束,而忽略中间的过程。

public class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2)
            return nums.length;
        int prevdiff = nums[1] - nums[0];
        int count = prevdiff != 0 ? 2 : 1;
        for (int i = 2; i < nums.length; i++) {
            int diff = nums[i] - nums[i - 1];
            if ((diff > 0 && prevdiff <= 0) || (diff < 0 && prevdiff >= 0)) {
                count++;
                prevdiff = diff;
            }
        }
        return count;
    }
}


 
368 Largest Divisible Subset 求最大子集,子集中任意两个数有一个能被另一个整除

264 Ugly Number II   求第n个丑数



467 Unique Substrings in Wraparound String   


576 Out of Boundary Paths   


322 Coin Change    





  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法。 LeetCode动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值