动态规划刷题ing

##动态规划##

最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

暴力法

int max_fun(int a,int b){    
    return a>b?a:b;
}
int sumOfArray(int* nums,int left,int right){    
    int sum=0;    
    for(int i=left;i<=right;i++){        
        sum+=nums[i];    
    }   
    return sum;
}
int maxSubArray(int* nums, int numsSize){   
    int res=INT_MIN;    
    for(int i=0;i<numsSize;i++)    { 
        for(int j=0;j<=i;j++){     
            int sum= sumOfArray(nums,j,i);        
            res=max_fun(sum,res);      
        }   
    }    
    return res;
}

动态规划

我们把目光落到动态规划上面来,首先需要把这个问题分解成最优子问题来解。最主要的思路就是将上面的45个组合进行
,分解成数量较少的几个子问题。在这里我们一共有9个数字,顺理成章的我们把组合分解成9个小组的组合。

第一个子组合是以第一个数字结尾的连续序列,也就是 [-2],最大值-2

第二个子组合是以第二个数字结尾的连续序列,也就是 [-2,1], [1],最大值1

第三个子组合是以第三个数字结尾的连续序列,也就是 [-2,1,3], [1,3], [3],最大值4

以此类推。。。

如果我们能够得到每一个子组合的最优解,也就是子序列的最大值,整体的最大值就可以通过比较这9个子组合的最大值来得到了。现在我们找到了最优子问题,重叠子问题在哪呢?那就得细心比较一下每个子问题。

从第二个子组合和第三个子组合可以看到,组合 3 只是在组合 2 的基础上每一个数组后面添加第 3 个数字,也就是数字 3,然后增加一个只有第三个数字的数组 [3] 。这样两个组合之间的关系就出现了,可是我们不关心这个序列是怎么生成的,只是关心最大值之间的关系。

下面我们看组合 3 的组成,我们将子组合 3 分成两种情况:
继承子组合二得到的序列,也就是[-2,1,3], [1,3] (最大值 1 = 第二个组合的最大值 + 第三个数字)
单独第三个数字的序列,也就是[3] (最大值 2 = 第三个数字)
如果 第二个序列的最大值 大于0,那么最大值 1 就比最大值 2 要大,反之最大值 2 较大。这样,我们就通过第二个组合的最大值和第三个数字,就得到了第三个组合的最大值。因为第二个组合的结果被重复用到了,所以符合这个重叠子问题的定义。通俗来讲这个问题就变成了,第 i 个子组合的最大值可以通过第i-1个子组合的最大值和第 i 个数字获得,如果第 i-1 个子组合的最大值没法给第 i 个数字带来正增益,我们就抛弃掉前面的子组合,自己就是最大的了。

步骤一、定义状态 -> 定义数组元素的含义
定义 dp[i] 为以 i 结尾子串的最大值
步骤二、状态转移方程 -> 找出数组元素间的关系式

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7mfDofjO-1595485719099)(C:\Users\86178\Desktop\力扣\图片\2020-04-06_112339.png)]

步骤三、初始化 -> 找出初始条件
dp[0] = nums[0];
步骤四、状态压缩 -> 优化数组空间
每次状态的更新只依赖于前一个状态,就是说 dp[i] 的更新只取决于 dp[i-1] , 我们只用一个存储空间保存上一次的状态即可。
步骤五、选出结果
有的题目结果是 dp[i] 。
本题结果是 dp[0]…dp[i] 中最大值

标准动态规划

int max_fun(int a,int b){
    return a>b?a:b;
}
int maxSubArray(int* nums, int numsSize){
    int dp[numsSize];
    dp[0]=nums[0];
    for(int i=1;i<numsSize;i++){
        if(dp[i-1]>0)
            dp[i]=dp[i-1]+nums[i];
        else
            dp[i]=nums[i];
    }
    int res=dp[0];
    for (int j = 0; j < numsSize; ++j) {
        res=max_fun(res,dp[j]);
    }
    return res;
}

优化一点的动态规划

根据转移方程可知,每次状态的更新只依赖于前一个状态,就是说 dp[i] 的更新只取决于 dp[i-1] , 我们只用一个存储空间保存上一次的状态即可

int max_fun(int a,int b){
    return a>b?a:b;
}
int maxSubArray(int* nums, int numsSize){
    int subMax=nums[0];
    int max=nums[0];
    for(int i=1;i<numsSize;i++){
        if(subMax>0)
            subMax=subMax+nums[i];
        else
            subMax=nums[i];
        max=max_fun(max,subMax);
    }
    return max;
}

分治法

分治法是将整个数组切分成几个小组,然后每个小组再切分成几个更小的小组,一直到不能继续切分也就是只剩一个数字为止。每个小组会计算出最优值,汇报给上一级的小组,一级一级汇报,上级拿到下级的汇报找到最大值,得到最终的结果。和归并排序的算法类似,先切分,再合并结果。

这个问题中的关键就是如何切分这些组合才能使每个小组之间不会有重复的组合(有重复的组合意味着有重复的计算量),这个问题应该困扰了不少的同学,我在学习理解的时候也花了不少时间。

首先是切分分组方法,就这个案例中的例子来,我们有一个数组 [-2,1,-3,4,-1,2,1,-5,4] ,一共有 9 个元素,我们 center=(start + end) / 2 这个原则,得到中间元素的索引为 4 ,也就是 -1,拆分成三个组合:

​ [-2,1,-3,4,-1]以及它的子序列(在-1左边的并且包含它的为一组)
​ [2,1,-5,4]以及它的子序列(在-1右边不包含它的为一组)
​ 任何包含-1以及它右边元素2的序列为一组(换言之就是包含左边序列的最右边元素以及右边序列最左边元素的序列,比如 [4,-1,2,1],这样就保证这个组合里面的任何序列都不会和上面两个重复)
以上的三个组合内的序列没有任何的重复的部分,而且一起构成所有子序列的全集,计算出这个三个子集合的最大值,然后取其中的最大值,就是这个问题的答案了。

然而前两个子组合可以用递归来解决,一个函数就搞定,第三个跨中心的组合应该怎么计算最大值呢?

答案就是先计算左边序列里面的包含最右边元素的子序列的最大值,也就是从左边序列的最右边元素向左一个一个累加起来,找出累加过程中每次累加的最大值,就是左边序列的最大值。

同理找出右边序列的最大值,就得到了右边子序列的最大值。左右两边的最大值相加,就是包含这两个元素的子序列的最大值。

在计算过程中,累加和比较的过程是关键操作,一个长度为 n 的数组在递归的每一层都会进行 n 次操作,分治法的递归层级在 logNlogN 级别,所以整体的时间复杂度是 O(nlogn)O(nlogn),在时间效率上不如动态规划的 O(n)O(n) 复杂度。

分治法的思路是这样的,其实也是分类讨论。

连续子序列的最大和主要由这三部分子区间里元素的最大和得到:

第 1 部分:子区间 [left, mid];
第 2 部分:子区间 [mid + 1, right];
第 3 部分:包含子区间[mid , mid + 1]的子区间,即 nums[mid] 与nums[mid + 1]一定会被选取。
对它们三者求最大值即可

int max_fun(int a, int b){
    return a>b?a:b;
}
int maxSubArrayDivideWithBorder(int *nums, int start, int end) {
    if(start==end)
        return nums[start]; // 只有一个元素,也就是递归的结束情况
    int center=(start+end)/2;
    int left=maxSubArrayDivideWithBorder(nums,start,center);//计算左侧序列的最大值
    int right=maxSubArrayDivideWithBorder(nums,center+1,end);//计算右侧序列的最大值

    //计算横跨两个子序列的最大值
    //从左边序列的最右边元素向左一个一个累加起来,找出累加过程中每次累加的最大值,就是左边序列的最大值。
    int leftCrossMax=INT_MIN;
    int leftCrossSum=0;
    for(int i=center;i>=start;i--)
    {
        leftCrossSum+=nums[i];
        leftCrossMax=max_fun(leftCrossMax,leftCrossSum);
    }
    //从右边序列的最左边元素向右一个一个累加起来,找出累加过程中每次累加的最大值,就是右边序列的最大值。
    int rightCrossMax=INT_MIN;
    int rightCrossSum=0;
    for (int j = center+1; j <=end ; ++j) {
        rightCrossSum+=nums[j];
        rightCrossMax=max_fun(rightCrossMax,rightCrossSum);
    }
    //计算横跨两个子序列的最大值
    int crossMax=leftCrossMax+rightCrossMax;
    return max_fun(crossMax,max_fun(left,right));
}
int maxSubArray(int* nums, int numsSize){
    return maxSubArrayDivideWithBorder(nums,0,numsSize);
}

多数元素

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

输入: [3,2,3]
输出: 3

哈希

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        unordered_map<int, int> counts;
        int majority = 0, cnt = 0;
        for (int num: nums) {
            ++counts[num];
            if (counts[num] > cnt) {
                majority = num;
                cnt = counts[num];
            }
        }
        return majority;
    }
};

分治法

思路

​ 如果数 a 是数组 nums 的众数,如果我们将 nums 分成两部分,那么 a 必定是至少一部分的众数。

解决

​ 我们使用经典的分治算法递归求解,直到所有的子问题都是长度为 1 的数组。长度为 1 的子数组中唯一的数显然是众数,直接返回即可。如果回溯后某区间的长度大于 1,我们必须将左右子区间的值合并。如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。否则,我们需要比较两个众数在整个区间内出现的次数来决定该区间的众数

int countInDivide(int *nums, int key,int start, int end){
    int count=0;
    for (int i = start; i <=end ; ++i) {
        if (nums[i]==key)
            count++;
    }
    return count;
}
int majorityElementDivide(int *nums, int start, int end) {
    if(start==end)
        return nums[end];
    int mid=(end-start)/2+start;
    int left=majorityElementDivide(nums,start,mid);
    int right=majorityElementDivide(nums,mid+1,end);

    if(left==right)
        return left;
    int leftCount=countInDivide(nums,left,start,end);
    int rightCount=countInDivide(nums,right,start,end);
    if(leftCount==rightCount)
        return -1;
    else
        return leftCount>rightCount?leftCount:rightCount;
}
int majorityElement(int* nums, int numsSize){
    return majorityElementDivide(nums, 0, numsSize - 1);
}

投票法

如果我们把众数记为 +1+1,把其他数记为 -1−1,将它们全部加起来,显然和大于 0,从结果本身我们可以看出众数比其他数多

  1. 众数抵消掉非众数,
  2. 非众数抵消非众数。 因为众数数量大于一半,所以剩下的一定是众数
int majorityElement(int* nums, int numsSize){
    int s=1;
    int maj=nums[0];
    for(int i=1;i<numsSize;i++){
        if(maj==nums[i])
            s++;
        else
            s--;
        if(s==0)
            maj=nums[i+1];
    }
    return maj;
}

随机化

因为超过众数在数组中超过一半,我们随机挑选一个下标,检查它是否是众数,如果是就返回,否则继续随机挑选。

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        while (true) {
            int candidate = nums[rand() % nums.size()];
            int count = 0;
            for (int num : nums)
                if (num == candidate)
                    ++count;
            if (count > nums.size() / 2)
                return candidate;
        }
        return -1;
    }
};

爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

**注意:**给定 n 是一个正整数。

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1+ 12.  2
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1+ 1+ 12.  1+ 23.  2+ 1

暴力法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o7bMzP3u-1595485719101)(C:\Users\86178\Desktop\力扣\图片\IMG_20200406_213136.jpg)]

int climbRecursion(int start, int end) {
    //if (start > end)
     //   return 0;
    if (start == end)
        return 1;
    if ((end - start) < 2)      //官方给的思路是第一个if,我改成了这个if 这样的话叶子节点会少,
        return climbRecursion(start + 1, end);//但是由于树形递归 复杂度为2^n,没有从根本上解决问题
    return climbRecursion(start + 1, end) + climbRecursion(start + 2, end);
}

int climbStairs(int n) {
    return climbRecursion(0, n);
}

暴力法的优化

借助数组存储了重复结点,复杂度是n,相当于遍历了一次所有的可行解

int climbRecursion(int start, int end,int *memo) {
    if (start > end)
        return 0;
    if (start == end)
        return 1;
    if(memo[start]>0)
        return memo[start];
    memo[start]=climbRecursion(start + 1, end,memo) + climbRecursion(start + 2, end,memo);
    return memo[start];
}

int climbStairs(int n) {
    int memo[n];
    for (int i = 0; i < n; ++i) {
        memo[i]=0;
    }
    return climbRecursion(0, n,memo);
}

动态规划

标准动态规划

第 i 阶可以由以下两种方法得到:

在第 (i-1) 阶后向上爬一阶。

在第 (i-2)阶后向上爬 2 阶。

所以到达第 i 阶的方法总数就是到第 (i-1) 阶和第 (i-2)阶的方法数之和。

令 dp[i] 表示能到达第 ii 阶的方法总数:

dp[i]=dp[i-1]+dp[i-2]

int climbStairs(int n) {
    if(n==1)
        return 1;
    int dp[n+1];
    dp[1]=1;
    dp[2]=2;
    for (int i = 3; i <=n; ++i) {
        dp[i]=dp[i-1]+dp[i-2];
    }
    return dp[n];
}

优化一点的动态规划

因为其实不需要存这么多数据,只要2个就行了

int climbStairs(int n) {
    if(n==1)
        return 1;
    int first=1,second=2,third;
    for (int i = 3; i <=n ; ++i) {
        third=first+second;
        first=second;
        second=third;
    }
    return third;
}

买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

动态规划

写的有点死板,直接就 dp[i]=max{dp[i-1],nowPrice-minPrice}上了,但是几乎和暴力法一样,到了n^2

int max_fun(int a, int b) {
    return a > b ? a : b;
}
int findMinPrice(int* prices,int end){
    int min=INT_MAX;
    for (int i = 0; i < end; ++i) {
        if(prices[i]<min)
        {
            min=prices[i];
        }
    }
    return min;
}
int maxProfit(int* prices, int pricesSize){
    int dp[pricesSize];
    for (int i = 0; i < pricesSize; ++i) {
        dp[i]=0;
    }
    int minPrice;
    dp[0]=0;
    for (int i = 1; i < pricesSize; ++i) {
        minPrice=findMinPrice(prices,i);
        dp[i]=max_fun(dp[i-1],prices[i]-minPrice);
    }
    int max=INT_MIN;
    for (int i = 0; i < pricesSize; ++i) {
        if(dp[i]>max)
        {
            max=dp[i];
        }
    }
    return max;
}

优化的动态规划

每次看到别人的代码我都好想哭啊

int maxProfit(int* prices, int pricesSize){
    if (prices<=1)
        return 0;
    int maxProfit=0;
    int minPrice=prices[0];
    for (int i = 1; i < pricesSize; ++i) {
        maxProfit=max_fun(maxProfit,prices[i]-minPrice);
        minPrice=min_fun(minPrice,prices[i]);
    }
    return maxProfit;
}

暴力法

国际惯例来个暴力法

int maxProfit(int* prices, int pricesSize){
    int maxProfit=0;
    for (int i = 0; i < pricesSize; ++i) {
        for (int j = i; j >0 ; --j) {
            if(maxProfit<(prices[i]-prices[j]))
                maxProfit=prices[i]-prices[j];
        }
    }
    return maxProfit;
}

优化的暴力法

还是大佬牛皮,其实不需要一个数组,一个变量存放最大利润就可以了

一天或者买入或者卖出,有点贪心的意思,我也说不清楚啊

官方:我们来假设自己来购买股票。随着时间的推移,每天我们都可以选择出售股票与否。那么,假设在第 i 天,如果我们要在今天卖股票,那么我们能赚多少钱呢?

显然,如果我们真的在买卖股票,我们肯定会想:如果我是在历史最低点买的股票就好了!太好了,在题目中,我们只要用一个变量记录一个历史最低价格 minprice,我们就可以假设自己的股票是在那天买的。那么我们在第 i 天卖出股票能得到的利润就是 prices[i] - minprice。

因此,我们只需要遍历价格数组一遍,记录历史最低点,然后在每一天考虑这么一个问题:如果我是在历史最低点买进的,那么我今天卖出能赚多少钱?当考虑完所有天数之时,我们就得到了最好的答案。

int maxProfit(int* prices, int pricesSize){
    int minPrice=INT_MAX;
    int maxProfit=0;
    for (int i = 0; i < pricesSize; ++i) {
        if(prices[i]<minPrice)
            minPrice=prices[i];
        else{
            if((prices[i]-minPrice)>maxProfit)
                maxProfit=prices[i]-minPrice;
        }
    }
    return maxProfit;
}

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

思路
对于连续的 n 栋房子:H1,H2,H3…HnH 1 ,H 2 ,H 3 …H n ,小偷挑选要偷的房子,且不能偷相邻的两栋房子,方案无非两种:
方案一:挑选的房子中包含最后一栋;
方案二:挑选的房子中不包含最后一栋;
获得的最大收益的最终方案,一定在这两种方案中产生,用代码表述就是:
最优结果 = Math.max(方案一最优结果,方案二最优结果)

动态规划

int max_fun(int a,int b){
    return a>b?a:b;
}
int min_fun(int a,int b){
    return a>b?b:a;
}

int rob(int* nums, int numsSize){
    if(numsSize==0)
        return 0;
    if(numsSize==1)
        return nums[0];
    int dp[numsSize];
    int maxRob=INT_MIN;
    dp[0]=nums[0];
    dp[1]=max_fun(nums[0],nums[1]);
    for (int i = 2; i < numsSize; ++i) {
        dp[i]=max_fun(dp[i-2]+nums[i],dp[i-1]);
        if(dp[i]>maxRob)
            maxRob=dp[i];
    }
    return maxRob;
}

优化的动态规划

dp[i-1]=max_fun(dp[i-3]+nums[i-1],dp[i-2]);

dp[i]=max_fun(dp[i-2]+nums[i],dp[i-1]);

直接用num1

public int rob(int[] num) {
    int num1 = 0;
    int num2 = 0;
    for (int x : num) {
        int temp = num2;
        num2 = Math.max(num1 + x, currMax);
        num1 = temp;
    }
    return currMax;
}

二刷的时候(状态转移的思路)

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.empty())
            return 0;
        int dp[nums.size()][2];//0是不偷 1是偷
        dp[0][1]=nums[0];
        dp[0][0]=0;
        for(int i=1;i<nums.size();i++){
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]);//当前不偷的状态从之前偷或者不偷的状态转移而来
            dp[i][1]=dp[i-1][0]+nums[i];//当前偷的状态仅仅从之前不偷的状态转移而来
        }
        return max(dp[nums.size()-1][0],dp[nums.size()-1][1]);
    }
};

二刷的时候(状态转移 滑动窗口)

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.empty())
            return 0;
        int a=0;//保存前一次的不偷状态
        int b=nums[0];//保存前一次的偷的状态
        int temp;
        for (int i = 1; i <nums.size() ; ++i) {
            temp=max(a,b); //当前不偷的状态从之前偷或者不偷的状态转移而来
            b=a+nums[i];//当前偷的状态仅仅从之前不偷的状态转移而来
            a=temp;
        }
        return max(a,b);
    }
};

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

状态转移 滑动窗口

class Solution {
public:
    int handleRob(vector<int>& nums,int start,int end){
        int a=0;//保存前一次的不偷状态
        int b=nums[start];//保存前一次的偷的状态
        int temp;
        for (int i = start+1; i <end ; ++i) {
            temp=max(a,b); //当前不偷的状态从之前偷或者不偷的状态转移而来
            b=a+nums[i];//当前偷的状态仅仅从之前不偷的状态转移而来
            a=temp;
        }
        return max(a,b);
    }
    int rob(vector<int>& nums) {
        if(nums.empty())
            return 0;
        if(nums.size()==1)
            return nums[0];
        int res1=handleRob(nums,0,nums.size()-1);
        int res2=handleRob(nums,1,nums.size());
        return max(res1,res2);
    }
};

740. 删除与获得点数

给定一个整数数组 nums ,你可以对它进行一些操作。

每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除每个等于 nums[i] - 1 或 nums[i] + 1 的元素。

开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

示例 1:

输入: nums = [3, 4, 2]
输出: 6
解释:
删除 4 来获得 4 个点数,因此 3 也被删除。
之后,删除 2 来获得 2 个点数。总共获得 6 个点数。
示例 2:

输入: nums = [2, 2, 3, 3, 3, 4]
输出: 9
解释:
删除 3 来获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。

动态规划

/*
 * 核心思路在对问题的转化 题干中的删除前后的数 转换为是否删除当前的数
 * 如果你选择删除第i个数 那么最优结果就是第i-1个数的最优解
 * 如果你选择不删除第i个数 那么最优结果就是第i-2个数的最优解+当前位置的数乘他的个数
 * */
class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        if(nums.empty())
            return 0;
        int length=0;
        for(int x:nums)
            length=max(x,length);
        vector<int> value(length+1,0),dp(length+1,0);
        for(int x:nums)
            value[x]++;
        //打家劫舍的标准做法
        dp[1]=value[1];
        for (int i = 2; i <=length; ++i) {
            dp[i]=max(dp[i-1],dp[i-2]+value[i]*i);
        }
        return dp[length];
    }
};

除数博弈

爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。

最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:

​ 选出任一 x,满足 0 < x < N 且 N % x == 0 。
​ 用 N - x 替换黑板上的数字 N 。
如果玩家无法执行这些操作,就会输掉游戏。

只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 false。假设两个玩家都以最佳状态参与游戏。

示例 1:

输入:2
输出:true
解释:爱丽丝选择 1,鲍勃无法进行操作。

示例 2:

输入:3
输出:false
解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。

菜鸡数学

我想的是直接找这样的数有几个,显然奇数个爱丽丝赢了,偶数个输了

void division(int N,int * num){
    if(N==1){
        return 0;
    }
    int i;
    for (i=1;i<N ; ++i) {
        if(N%i ==0){
            (*num)++;
            break;
        }
    }
    division(N-i,num);
}
int divisorGame(int N){
    int num=0;
    division(N,&num);
    return (num%2)?1:0;
}

真实数学

规则:Alice先手
观察:谁先从2的基础减去1谁胜

1.若N为奇数,则可以整除的为奇数。若可以整除,Alice先手减去奇数,得到偶数,则Bob只需每次减一直到2,Bob胜;Alice为奇数不能整除,则需每次减1,Bob先得到2,Bob胜。所以奇数的话Alice输。
2.若N为偶数,则其可以整除的为奇数或偶数。为保证胜利,Alice只需每次减一先得到2即可。如果Alice减去1得到奇数,由规则 1 可知,奇数的话先手会输(此时Bob先手)。所以偶数的话Alice会赢。

class Solution {
    public boolean divisorGame(int N) {
        return N % 2 == 0;
    }
}

动态规划

算法的解释依旧是按照自己的思路来的,但是这个dp我还是没想到啊 害

#define bool int;
bool divisorGame(int N){
    if (N==1)
        return 0;
    if(N==2)
        return 1;
    int dp[N+1];
    dp[1]=0;//找到o个,偶数
    dp[2]=1; //找到1个,奇数
    for (int i = 3; i < N; ++i) {
        dp[i]=0;
        for (int j = 1; j < i; ++j) {
            if(dp[i-j]==0 && i/j==0){ //按照规则来,但是需要使得dp[i-j]能找到偶数个,然后加上当前这个,就是奇数个
                dp[i]=1;
                break;
            }
        }
    }
    return dp[N];
}

使用最小花费爬楼梯

我满以为这是一题简简单单的动态规划 没想到也不简单 呜呜呜呜

数组的每个索引做为一个阶梯,第 i个阶梯对应着一个非负数的体力花费值 costi

每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。

您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。

输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。

不断优化的动态规划

F(n) = Min(F(n+1)+cost[n],F(n+2)+cost[n])

从这个楼梯向上爬,看第一个开始好还是第二个开始好

plan1

先简单递归

int min_fun(int a,int b){
    return a>b?b:a;
}
int size;
int calcMinCostClimbingStairs(int* cost,int n){
    if(n>=size)
        return 0;
    return min_fun(calcMinCostClimbingStairs(cost,n+1)+cost[n],calcMinCostClimbingStairs(cost,n+2)+cost[n]);
}

int minCostClimbingStairs(int* cost, int costSize){
    size=costSize;
    int res=min_fun(calcMinCostClimbingStairs(cost,0),calcMinCostClimbingStairs(cost,1));
    return res;
}

plan2

dp数组

看的我脑壳疼 以后再说


买卖股票的最佳时机II

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

暴力回溯

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-niPYPBZA-1595485719103)(C:\Users\86178\Desktop\力扣\图片\2020-04-10_215431.png)]

华丽丽的超时了 看完大佬的思路,本来想能不能加个剪枝函数,结果貌似真的得完全回溯

思路就是一路到底,慢慢回来再到底,不断试探

int res;
int size;
//level是当前得层数,status是状态 0不持有股票 1持有,profit当前收益
void dfs(int* prices,int level,int status,int profit){
    if(level==size) {
        res = max_fun(res, profit);
        return;
    }
    dfs(prices,level+1,status,profit);
    if(status==0){
        status=1;
        dfs(prices,level+1,status,profit-prices[level]);
    }else{
        status=0;
        dfs(prices,level+1,status,profit+prices[level]);
    }
}
int maxProfit(int* prices, int pricesSize){
    if(pricesSize<2)
        return 0;
    res=0;
    size=pricesSize;
    int profit=0;
    dfs(prices,0,0,profit);
    return res;
}

贪心算法

其实用贪心算法就很简单,只需要考虑昨天卖今天卖赚不赚就好了,没有分析好题目啊

int maxProfit(int* prices, int pricesSize){
    if(pricesSize<2)
        return 0;
    int res=0;
    for (int i = 1; i < pricesSize; ++i) {
        res+=max_fun(prices[i]-prices[i-1],0);
    }
    return res;
}

动态规划

大佬牛皮!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VXqr0Coo-1595485719105)(C:\Users\86178\Desktop\力扣\图片\2020-04-10_222534.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yDLcwrfj-1595485719107)(C:\Users\86178\Desktop\力扣\图片\2020-04-10_222551.png)]

int maxProfit(int* prices, int pricesSize){
    if(pricesSize<2)
        return 0;
    int dp[pricesSize][2];
    dp[0][0]=0;
    dp[0][1]=-prices[0];
    for (int i = 1; i < pricesSize; ++i) {
        dp[i][0]=max_fun(dp[i-1][0],dp[i-1][1]+prices[i]);//保持上回合没买的状态或者上回合买了这回合卖掉,使得手上没股票,0
        dp[i][1] = max_fun(dp[i - 1][1], dp[i - 1][0] - prices[i]);//保持上回合买了的状态或者上回合没买,这回合买了,使得手上又股票,1
    }
    return dp[pricesSize-1][0];//倒数第二回合肯定不买的
}

滑窗

int maxProfit(int* prices, int pricesSize){
    if(pricesSize<2)
        return 0;
    int cash = 0;
    int hold = -prices[0];

    int preCash = cash;
    int preHold = hold;
    for (int i = 1; i < pricesSize; i++) {
        cash = max_fun(preCash, preHold + prices[i]);
        hold = max_fun(preHold, preCash - prices[i]);

        preCash = cash;
        preHold = hold;
    }
    return cash;
}

分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

注意:

你可以假设胃口值为正。
一个小朋友最多只能拥有一块饼干。

输入: [1,2,3], [1,1]
输出: 1
解释: 
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。


输入: [1,2], [1,2,3]
输出: 2
解释: 
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.

菜鸡贪心算法

int findContentChildren(int* g, int gSize, int* s, int sSize){
    quickSort(g,0,gSize-1);
    quickSort(s,0,sSize-1);
    int flagIndex=-1;
    int res=0;
    for (int i = 0; i < gSize; ++i) {
        for (int j = flagIndex+1; j < sSize; ++j) {
            if(s[j]>=g[i]){
                flagIndex=j;
                res++;
                break;
            }
        }
    }
    return res;
}

双指针

因为已经有序了,只要找满足的就好了,思路和我想得一样的,就是没想到双指针 比我的快多了

int findContentChildren(int* g, int gSize, int* s, int sSize){
    quickSort(g,0,gSize-1);
    quickSort(s,0,sSize-1);
    int res=0;
    int i=0,j=0;
    while(i<gSize && j<sSize){
        if(g[i]<=s[j]){
            res++;
            i++;
            j++;
        } else{
            j++;
        }
    }
    return res;
}

划分为k个相等子集

​ 给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。

输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。

暴力回溯

根据题意 就是找k个aver 就想到回溯 然而没剪枝 有点慢

int aver=0;
int visited[17];
int end;
int dfs(int* nums, int numsSize,int nowLen,int nowGet, int pos){
    if(pos>=numsSize)
        return 0;
    if(nowGet==end)
        return 1;
    for (int i = pos; i < numsSize; ++i) {
        if(visited[i]!=1){
            if(nums[i]+nowLen==aver){
                visited[i]=1;
                if(dfs(nums,numsSize,0,nowGet+1,0))
                    return 1;
                visited[i]=0;
            }else if(nums[i]+nowLen<aver){
                visited[i]=1;
                if(dfs(nums,numsSize,nums[i]+nowLen,nowGet,i+1))
                    return 1;
                visited[i]=0;
            }
        }
    }
    return 0;
}
int canPartitionKSubsets(int* nums, int numsSize, int k){
    for (int i = 0; i < numsSize; ++i)
        aver+=nums[i];
    int sum=aver;
    aver=aver/k;
    end=k;
    memset(visited, 0, sizeof(visited));
    for (int i = 0; i < numsSize; ++i){
        if(nums[i]>aver)
            return 0;
    }
    if(aver*k!=sum)
        return 0;
    return dfs(nums,numsSize,0,0,0);
}

优化的回溯

int aver=0;
int visited[17];
int end;
int dfs(int* nums, int numsSize,int nowLen,int nowGet, int pos){
    if(pos>=numsSize)
        return 0;
    if(nowGet==end)
        return 1;
    for (int i = pos; i < numsSize; ++i) {
        if(visited[i]!=1){
            if(nums[i]+nowLen==aver){
                visited[i]=1;
                /*最后一个参数的含义为,我们从第一个元素开始查找,如果第一个元素符合条件,我们就从第二个元素开始找,以此类推,当我们第n个子集符合条件时,我们就从第n + 1个元素开始查找,nowGet + 1个元素所对应的下标为nowGet*/
                if(dfs(nums,numsSize,0,nowGet+1,nowGet))
                    return 1;
                visited[i]=0;
            }else if(nums[i]+nowLen<aver){
                visited[i]=1;
                if(dfs(nums,numsSize,nums[i]+nowLen,nowGet,i+1))
                    return 1;
                visited[i]=0;
                for (; i + 1 < numsSize && nums[i] == nums[i + 1]; i++);//当该元素不符合条件,后面的与他一样的元素肯定不符合,可以跳过
            }
        }
    }
    return 0;
}
int canPartitionKSubsets(int* nums, int numsSize, int k){
    for (int i = 0; i < numsSize; ++i)
        aver+=nums[i];
    int sum=aver;
    aver=aver/k;
    end=k;
    memset(visited, 0, sizeof(visited));
    for (int i = 0; i < numsSize; ++i){
        if(nums[i]>aver)
            return 0;
    }
    if(aver*k!=sum)
        return 0;
    return dfs(nums,numsSize,0,0,0);
}

完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

输入: n = 12
输出: 3 
解释: 12 = 4 + 4 + 4.
输入: n = 13
输出: 2
解释: 13 = 4 + 9.

动态规划

状态方程 dp[i] = MIN( i , dp[i - j * j] + 1)

int min_fun(int a,int b){
    return a>b?b:a;
}
int numSquares(int n){
    int dp[n+1] ;
    memset(dp,0, sizeof(dp));
    for (int i = 1; i <=n; ++i) {
        dp[i]=i;
        for (int j = 1; i-j*j>=0; ++j) {
            dp[i]=min_fun(dp[i],dp[i-j*j]+1);
        }
    }
    return dp[n];
}

贪心枚举

count从1到n进行枚举 判断n能否被分为count份

int square_nums[99];
int i;
int is_divided_by(int n, int count){
    if(count==1)
    {
        for (int j = 1; j < i; ++j) {
            if(square_nums[j]==n)
                return 1;
        }
        return 0;
    }
    for (int k = 1; k <i ; ++k) {
        if(is_divided_by(n-square_nums[k],count-1)){
            return 1;
        }
    }
    return 0;
}
int numSquares(int n){
    for (i = 1; i*i <= n; ++i) {
        square_nums[i]=i*i;
    }
    int count=1;
    for (; count <=n; ++count) {
        if(is_divided_by(n,count))
            return count;
    }
    return count;
}

最长等差数列

给定一个整数数组 A,返回 A 中最长等差子序列的长度。

回想一下,A 的子序列是列表 A[i_1], A[i_2], …, A[i_k] 其中 0 <= i_1 < i_2 < … < i_k <= A.length - 1。并且如果 B[i+1] - B[i]( 0 <= i < B.length - 1) 的值都相同,那么序列 B 是等差的。

输入:[3,6,9,12]
输出:4
解释: 
整个数组是公差为 3 的等差数列。
输入:[9,4,7,2,10]
输出:3
解释:
最长的等差子序列是 [4,7,10]

暴力法

前两次循环,找到两个有前后关系的元素,得到等差数列的公差,第三个循环找满足公差的数组

class Solution {
public:
    int longestArithSeqLength(vector<int>& A) {
        int len=A.size();
        int res=0;
        int diffStep;
        int diff;
        int currentRes;
        for (int i = 0; i < len; ++i) {
            for (int j = i+1; j < len; ++j) {
                currentRes=2;
                diffStep=A[i]-A[j];
                diff=diffStep;
                for (int k = j+1; k < len; ++k) {
                    if(A[j]-A[k]==diff){
                        ++currentRes;
                        diff+=diffStep;
                    }
                }
                res=max(res,currentRes);
            }
        }
        return res;
    }
};

动态规划

两重循环,一个遍历所有元素,一个遍历当前元素之前的元素 做差得到diff

若是减数存在含以diff为为公差的数列,则减数和被减数与之前元素继续组成等差数列,被减数继续维持加1的等差数列

若不存在,则减数和被减数组成等差数列,数列元素为2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Q60Qbmm-1595485719109)(C:\Users\86178\Desktop\力扣\图片\2020-05-02_160331.png)]

class Solution {
public:
    int longestArithSeqLength(vector<int>& A) {
        int len=A.size();
        if(len==0)
            return 0;
        int res=1;
        vector<unordered_map<int,int>> dp(len);
        for (int i = 0; i < len; ++i) {
            for (int j = 0; j < i; ++j) {
                int diff=A[i]-A[j];
                if(dp[j][diff])
                    dp[i][diff]=dp[j][diff]+1;
                else
                    dp[i][diff]=2;
                res=max(res,dp[i][diff]);
            }
        }
        return res;
    }
};

多边形三角剖分的最低分

给定 N,想象一个凸 N 边多边形,其顶点按顺时针顺序依次标记为 A[0], A[i], …, A[N-1]。

假设您将多边形剖分为 N-2 个三角形。对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 N-2 个三角形的值之和。

返回多边形进行三角剖分后可以得到的最低分。

输入:[1,2,3]
输出:6
解释:多边形已经三角化,唯一三角形的分数为 6
输入:[3,7,4,5]
输出:144
解释:有两种三角剖分,可能得分分别为:3*7*5 + 4*5*7 = 245,或 3*4*5 + 3*4*7 = 144。最低分数为 144

经典区间dp

​ 解题模板

memset(dp,0,sizeof(dp));
//初始dp数组
for(int len=2;len<=n;len++){
    //枚举区间长度
    for(int i=1;i<n;++i){//枚举区间的起点
        int j=i+len-1;//根据起点和长度得出终点
        if(j>n) break;//符合条件的终点
        for(int k=i;k<=j;++k)//枚举最优分割点
            //状态转移方程
        }
}
class Solution {
public:
    int minScoreTriangulation(vector<int>& A) {
        const int n=A.size();
        int dp[n][n];
        memset(dp,0,sizeof(dp));
        for (int len = 2; len <= n; ++len) {    //枚举长度,从2开始
            for (int left = 0; left < n; ++left) {     //枚举左端点
                int right=left+len;
                if(right >= A.size()) break;
                dp[left][right]=INT_MAX;    // dp[left][right] 代表left~right区间形成的环的最小得分值
                for (int i = left+1; i < right; ++i) {  //枚举区间内的所有的点(不包括端点)),将环分割成左右两部分
                    dp[left][right]=min(dp[left][right],dp[left][i]+dp[i][right]+A[left]*A[right]*A[i]);
                }
            }
        }
        return dp[0][n-1];
    }
};

出界的路径数

给定一个 m × n 的网格和一个球。球的起始坐标为 (i,j) ,你可以将球移到相邻的单元格内,或者往上、下、左、右四个方向上移动使球穿过网格边界。但是,你最多可以移动 N 次。找出可以将球移出边界的路径数量。答案可能非常大,返回 结果 mod 109 + 7 的值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tb9Z9W5O-1595485719110)(C:\Users\86178\Desktop\力扣\图片\2020-05-08_222115.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Laf7eywB-1595485719111)(C:\Users\86178\Desktop\力扣\图片\2020-05-08_222100.png)]

dp从外界向内部求解

思路:dp[i][j][k]表示从(i,j)出发经过k步出界的路径总数,等价于从外界某点出发经过k步到达(i,j)的路径总数
	显然就有转移方程
	dp[i][j][k]=dp[i−1][j][k−1]+dp[i+1][j][k−1]+dp[i][j−1][k−1]+dp[i][j+1][k−1]

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WurvbKni-1595485719112)(C:\Users\86178\Desktop\力扣\图片\2020-05-08_222220.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-STQobtaR-1595485719113)(C:\Users\86178\Desktop\力扣\图片\2020-05-08_222234.png)]

int findPaths(int m, int n, int N, int i, int j) {
        int MOD = 1000000007;
        if (N == 0) { return 0; }

        vector<vector<vector<unsigned long long int>>> dp(m + 2, vector<vector<unsigned long long int>>(n + 2, vector<unsigned long long int>(N + 1, 0)));
        for (int i = 0; i <= m + 1; i++) {
            dp[i][0][0] = 1;
            dp[i][n + 1][0] = 1;
        }
        for (int i = 0; i <= n + 1; i++) {
            dp[0][i][0] = 1;
            dp[m + 1][i][0] = 1;
        }
        for (int k = 1; k <= N; k++) {
            for (int i = 1; i <= m; i++) {
                for (int j = 1; j <= n; j++) {
                    dp[i][j][k] = (dp[i - 1][j][k - 1] + dp[i + 1][j][k - 1] + dp[i][j - 1][k - 1] + dp[i][j + 1][k - 1]) % MOD;
                }
            }
        }
        int sum = 0;
        for (int k = 1; k <= N; k++) {
            sum = (sum + dp[i + 1][j + 1][k]) % MOD;
        }
        return sum;
    }

最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

输入: "cbbd"
输出: "bb"

暴力法

 string longestPalindrome(string s) {
        string temp="";
        string res=s.substr(0,1);
        for (int i = 0; i < s.size(); ++i) {
            for (int j = i; j <s.size() ; ++j) {
                temp+=s[j];
                string tem=temp;
                reverse(tem.begin(),tem.end());
                if(temp==tem)
                    res=tem.size()>res.size()?tem:res;
            }
            temp="";
        }
        return res;
    }

动态规划

思路就是先找基础的回文,单个字符或者连着的两个字符肯定是回文,然后接下来就是 再基础回文的两端看是不是相同字符 是则继续 不是则换一个

string longestPalindrome(string s) {
        int len=s.size();
        if(len==1 || len==0)
            return s;
        int start=0;//回文串起始位置
        int max=1;//回文串最大长度
        vector<vector<int>> dp(len,vector<int>(len,0));
        for (int i = 0; i < len; ++i) {
            dp[i][i]=1;//单个字符是回文串
            if(i<len-1&&s[i]==s[i+1])
            {
                dp[i][i+1]=1;
                max=2;
                start=i;
            }
        }
        for (int i = len-3; i>=0; --i) {
            for (int j = i+2; j <len ; ++j) {
                if(s[i]==s[j]){
                    dp[i][j]=dp[i+1][j-1];
                    if(dp[i][j] && j-i+1>max){
                        max=j-i+1;
                        start=i;
                    }
                }else{
                    dp[i][j]=0;
                }
            }
        }
        return s.substr(start,max);
    }

最长公共子序列

给定两个字符串,输出最长子序列

思路:dp[i][j]=dp[i-1][j-1]	s1[i]==s2[j]
	 dp[i][j]=dp[i][j-1]	dp[i][j-1]>dp[i-1][j]
	 dp[i][j-1]=dp[i][j]	dp[i-1][j]>dp[i][j-1]

动态规划

源程序:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <memory>
using namespace std;


void findSubsequence(string s1,string s2){
    int m=s1.length();
    int n=s2.length();
    string res;
    int dp[m+1][n+1];
    int tag[m+1][n+1];
    memset(dp,0, sizeof(dp));
    memset(tag,0, sizeof(tag));
    for(int i=0;i<m;i++)
    {
        for(int j=0;j<n;j++)
        {
            if(s1[i]==s2[j])
            {
                dp[i+1][j+1]=dp[i][j]+1;
                tag[i+1][j+1]=1;
            }else if(dp[i][j+1]>=dp[i+1][j])
            {
                dp[i+1][j+1]=dp[i][j+1];
                tag[i+1][j+1]=2;
            }else
            {
                dp[i+1][j+1]=dp[i+1][j];
                tag[i+1][j+1]=3;
            }
        }
    }
    for(int i = m,j = n;i >= 0 && j >= 0; )
    {
        if(tag[i][j] == 1)
        {
            i--;
            j--;
            res+=s1[i];
        }
        else if(tag[i][j] == 2)
            i--;
        else
            j--;
    }
    reverse(res.begin(),res.end());
    cout<<"最长子序列为:"<<res<<"\t长度为:"<<dp[m][n];
}
int main(){
    string a1,a2;
    cout<<"请输入序列1和序列2\n";
    cin>>a1>>a2;
    findSubsequence(a1,a2);
}

523. 连续的子数组和

给定一个包含 非负数 的数组和一个目标 整数 k,编写一个函数来判断该数组是否含有连续的子数组,其大小至少为 2,且总和为 k 的倍数,即总和为 n*k,其中 n 也是一个整数。

输入:[23,2,4,6,7], k = 6
输出:True
解释:[2,4] 是一个大小为 2 的子数组,并且和为 6。

输入:[23,2,6,4,7], k = 6
输出:True
解释:[23,2,6,4,7]是大小为 5 的子数组,并且和为 42。

暴力法

测试用例太刁钻了 几个0就得特殊判断了

class Solution {
public:
    bool checkSubarraySum(vector<int>& nums, int k) {
        int flag=0;
        int count=0;
        if(k==0)
            flag=1;
        for (int i = 0; i < nums.size(); ++i) {
            if(i<nums.size()-1 && nums[i]==0 && nums[i+1]==0 && flag)
                return true;
            int sum=nums[i];
            for (int j = i+1; j < nums.size(); ++j) {
                sum+=nums[j];
                if(!flag && sum%k==0)
                    return true;
            }
        }
        return false;
    }
};

官方暴力法 我好菜啊

class Solution {
public:
    bool checkSubarraySum(vector<int>& nums, int k) {
        for (int i = 0; i < nums.size()-1; ++i) {
            int sum=nums[i];
            for (int j = i+1; j < nums.size(); ++j) {
                sum+=nums[j];
                if(sum==k | (k!=0 && sum%k==0))
                    return true;
            }
        }
        return false;
    }
};

前缀和

碰到好多次了 一直没想到用欸

class Solution {
public:
    bool checkSubarraySum(vector<int>& nums, int k) {
        int sum[nums.size()];
        sum[0]=nums[0];
        for (int i = 1; i < nums.size(); ++i) {
            sum[i]=sum[i-1]+nums[i];
        }
        for (int i = 0; i < nums.size()-1; ++i) {
            for (int j = i+1; j < nums.size(); ++j) {
                int sumn=sum[j]-sum[i]+nums[j];
                if(sumn==k | (k!=0 && sumn%k==0))
                    return true;
            }
        }
        return false;
    }
};

hash加速

这个hash的思路很巧妙 用hash表来存放前缀和 但是将前缀和对k取余

那么当出现相同的key值得时候 就代表 存在一个i和j 0i和0j的前缀和都是k的倍数 那么 i~j的部分也理应是k的倍数 只要j-i>1就可了

class Solution {
public:
    bool checkSubarraySum(vector<int>& nums, int k) {
        int sum=0;
        unordered_map<int,int> map;
        map[0]=-1; // 防止类似 0 0 0 的输入结果异常
        for (int i = 0; i < nums.size(); ++i) {
            sum+=nums[i];
            if(k!=0)
                sum=sum%k;
            if(map.find(sum)!=map.end()){
                if(i-map[sum]>1){
                    return true;
                }
            }else{
                map[sum]=i;
            }
        }
        return false;
    }
};

96. 不同的二叉搜索树

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

输入: 3
输出: 5
思路:
	dp[n]:长度为n的序列的不同的二叉搜索树的个数
	辅助函数G(i,n):以i为根节点的不同二叉搜索树的个数(1<=i<=n)
易得dp[n]=(1~n) G(i,n)
    接着分析G(i,n)怎么得到
    序列 123456G(3,6)其实就是求 左子树12的个数和右子树456的个数 就是dp[2]和dp[3]G(3,6)=dp[2]+dp[3]
    G(i,n)=dp[i-1]+[n-i];
dp[n]=(1~n) dp[i-1]+[n-i] 得到了转移函数
class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n+1,0);
        dp[0]=1;
        dp[1]=1;
        for (int i = 2; i <=n; ++i) {
            for (int j = 1; j <=i ; ++j) {
                dp[i]+=dp[j-1]*dp[i-j];
            }
        }
        return dp[n];
    }
};

背包问题大总结

01背包问题

参考链接:https://leetcode-cn.com/problems/coin-lcci/solution/bei-bao-jiu-jiang-ge-ren-yi-jian-da-jia-fen-xiang-/

dp[i][j]表示将前i个物品放入体积为j的背包的所能获得的最大价值
c[i]为i物品的体积,w[i]为i物品的价值
m为物品总个数,n为背包总体积
分析:1 不放入i物品,则dp[i][j]=dp[i-1][j]
	 2 放入i物品,则dp[i][j]=dp[i-1][j-c[i]]
	 res=max(dp[0][0-n])
#include <iostream>
#include <algorithm>
#include <cstring>
#include <memory>

using namespace std;

const int N=1010;

int n,m;
int dp[N][N];
int v[N],w[N];

int main(){
    cin>>n>>m;
    for (int i = 1; i <=n ; ++i) cin>>v[i]>>w[i];
    memset(dp,0, sizeof(dp));
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= m; ++j) {
            dp[i][j]=dp[i-1][j];
            if(j>=v[i])
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
        }
    }
    int res=0;
    for (int i = 0; i < n; ++i) res=max(res,dp[n][i]);
     cout<<res;
}
优化之后的代码

f[v] 表示物品体积之和小于等于 v 时的最大价值(初始化全为0)

res=f[m],

如果要体积恰好为v时的最大值,就是初始化的时候dp[0]=0;其他都是负无穷

res=res=max(dp[0][0-n])

只要反向枚举就是了
#include <iostream>
#include <algorithm>
#include <cstring>
#include <memory>

using namespace std;

const int N=1010;

int n,m;
int dp[N];
int v[N],w[N];

int main(){
    cin>>n>>m;
    for (int i = 1; i <=n ; ++i) cin>>v[i]>>w[i];
    for (int i = 1; i <= n; ++i) {
        for (int j = m; j >=v[i]; --j) {
                dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    cout<<dp[m];
}

完全背包问题

每个物体可以用无限次

f[v] 表示物品体积之和小于等于 v 时的最大价值(初始化全为0)

res=f[m],

如果要体积恰好为v时的最大值,就是初始化的时候dp[0]=0;其他都是负无穷

res=res=max(dp[0][0-n])

只要正向枚举就是了
#include<iostream>
#include<algorithm>
#include <cstring>

using namespace std;

const int N=1010;

int n,m;
int dp[N];;
int v[N],w[N];

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    memset(dp,0, sizeof(dp));
    for(int i=0;i<=n;i++){
        // 一般同学的思路
        /*for (int i = 1; i <= n; ++i) {
        for (int j = m; j >=v[i]; --j) {
            for (int k = 0; k*v[i] <=j ; ++k) {
                dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
            }
        }
    }*/
        //up讲的
        for(int j=v[i];j<=m;j++){
            dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    cout<<dp[m];
}

多重背包问题

每个物品最多放s次

小范围暴力

数据范围
0<N,V≤100
0<vi,wi,si≤100

相当与01背包问题的扩展,01是每个物品只有选或者不选
方程就是
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
那么多重背包就可以写成
dp[j]=max(dp[j],dp[j-v[i]]+w[i],dp[j-2v[i]]+2w[i]+·····);
就是再加一个循环
#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N=101;
int dp[N];
int n,m;

int main(){
    cin>>n>>m;
    memset(dp,0, sizeof(dp));
    for(int i=0;i<n;i++){
        int v,w,s;
        cin>>v>>w>>s;
        for(int j=m;j>=v;j--){
            for(int k=1;k<=s && k*v<=j;k++){ //为什么k不是从0开始,因为k=1的时候,dp[j]=max(dp[j],dp[j-v[i]]+w[i]);就是01背包问题了,选或者不选
                dp[j]=max(dp[j],dp[j-k*v]+k*w);
            }
        }
    }
    cout<<dp[m];
}

大范围优化1

数据范围
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000

将多重背包转换为01背包
第i个物品的是v,m,s
那么将所有的s分为s个1,就相当于01背包问题了,每一份都是放或者不放
但是分成s份 复杂度太大了 再次优化就是 分成log2(s)取上界份
解释:7可以分为1 2 4
10可以分为 1 2 4 3
就是前面正常分 最后为了刚好就是剩下的组成一份
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>

using namespace std;
const int N=2010;

int n,m;
int dp[N];
struct Good{
 int v,w;
};

int main(){
 vector<Good> goods;
 cin>>n>>m;
 memset(dp,0, sizeof(dp));
 for (int i = 0; i < n; ++i) {
     int v,w,s;
     cin>>v>>w>>s;
     for (int j = 1; j <=s ; j*=2) {
         s-=j;
         goods.push_back({v*j,w*j});
     }
     if(s>0) goods.push_back({v*s,w*s});
 }
 for(auto good:goods){
     for (int j = m; j >=good.v ; --j) {
         dp[j]=max(dp[j],dp[j-good.v]+good.w);
     }
 }
 cout<<dp[m];
}

混合背包问题

物品一共有三类:

第一类物品只能用1次(01背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);

si=1 表示第 i 种物品只能用1次;
si=0 表示第 i 种物品可以用无限次;
si>0 表示第 i 种物品可以使用 si 次;

数据范围
0<N,V≤1000
0<vi,wi≤10001≤si≤1000
思路:按照问题种类分类,将多重背包转换为01背包,然后01背包和完全背包按照对应的方程解决
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>

using namespace std;

const int N=1010;
int dp[N];
int n,m;

struct Thing{
    int kind;
    int v,w;
};
vector<Thing> things;

int main(){

    cin>>n>>m;
    for(int i=0;i<n;i++){
        int v,w,s;
        cin>>v>>w>>s;
        if(s<0) things.push_back({-1,v,w});
        else if(s==0) things.push_back({0,v,w});
        else{
            for(int j=1;j<=s;j*=2){
                s-=j;
                things.push_back({-1,v*j,w*j});
            }
            if(s>0) things.push_back({-1,v*s,w*s});
        }
    }
    for(auto thing:things){
        if(thing.kind<0){
            for (int j = m; j >=thing.v ; --j) dp[j]=max(dp[j],dp[j-thing.v]+thing.w);
        }else{
            for (int j = thing.v; j <= m; ++j) dp[j]=max(dp[j],dp[j-thing.v]+thing.w);
        }
    }
    cout<<dp[m];
}

二维费用的01背包问题

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。

每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。

输入格式
第一行两个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

接下来有 N 行,每行三个整数 vi,mi,wi,用空格隔开,分别表示第 i 件物品的体积、重量和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N≤1000
0<V,M≤100
0<vi,mi≤100
0<wi≤1000

思路:f[i][j] 表示物品体积之和小于等于 i ,且重量小于j的最大价值(初始化全为0)
    就和01背包问题一样 就是多了一个维度
#include<iostream>
#include<algorithm>

using namespace std;

const int N=1010;
int n,v,m;
int dp[N][N];

int main(){
    cin>>n>>v>>m;
    for (int i = 0; i < n; ++i) {
        int a,b,c;
        cin>>a>>b>>c;
        for (int j = v; j >=a ; --j) {
            for (int k = m; k >=b ; --k) {
                dp[j][k]=max(dp[j][k],dp[j-a][k-b]+c);
            }
        }
    }
    cout<<dp[v][m];
}

分组背包问题

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。

接下来有 N 组数据:

每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100

思路:dp[i][j]表示前i组物品在容量最大为j的情况下的最大值(初始化全为0)
   	多重背包是分组背包的特殊情况,所以多重背包的通用解法适合分组背包
#include<iostream>
#include<algorithm>

using namespace std;

const int N=110;
int n,m;
int dp[N],v[N],w[N];

int main(){
    cin>>n>>m;
    for (int i = 0; i < n; ++i) {
        int s;
        cin>>s;
        for (int j = 0; j < s; ++j) cin>>v[j]>>w[j];
        for (int j = m; j >=0 ; --j) {
            for (int k = 0; k <s ; ++k) {
                if(j>=v[k]) //每个物品的体积不一定相同,所以要放到这里来,不能放到for里面
                    dp[j]=max(dp[j],dp[j-v[k]]+w[k]);
            }
        }
    }
    cout<<dp[m];
}

1

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值