文章目录

1、斐波那契数列

1.1 爬楼梯

70. Climbing Stairs(Easy)
You are climbing a stair case. It takes n steps to reach to the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

Note: Given n will be a positive integer.

Example 1:

Input: 2
Output: 2
Explanation: There are two ways to climb to the top.
1 step + 1 step
2 steps

Example 2:

Input: 3
Output: 3
Explanation: There are three ways to climb to the top.
1 step + 1 step + 1 step
1 step + 2 steps
2 steps + 1 step

走n阶楼梯的方法有两种,1、先走 1 级台阶,再走 n-1 级台阶;2、先走 2 级台阶,再走 n-2 级台阶
f[n] = f[n-1] + f[n-2]

class Solution {
public:
    int climbStairs(int n) {
        if(n<=1)
            return 1;
        int pre1=1,pre2=1;
        for(int i=1;i<n;i++)
        {
            int cur=pre1+pre2;
            pre2=pre1;
            pre1=cur;
        }
        return pre1;
    }
};

1.2 强盗抢劫

198. House Robber(Easy)
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.

Example 1:

Input: [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3). Total amount you can rob = 1 + 3 = 4.

**Example 2:

Input: [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1). Total amount you can rob = 2 + 9 + 1 = 12.

每个屋子都有一定的财产,相邻的两个房子不能都偷,最大可以偷多少财产。

问题分析:
每个房间财产为 nums[0]……nums[n-1]。

假设 0 至 x 间房获得的最大财产为 f(x)。
f(x) = max(f(x-1),f(x-2)+nums[x])
如果不拿房间 x 的财产,可以获得的总财产为 f(x-1),如果拿房间 x 的财产,可以获得的总财产为 f(x-2)+nums[x],二者取最大值就是 0-x 房间可以获得的最大财产。

class Solution {
public:
    int rob(vector<int>& nums) {
        int far=0,near=0;
        int n=nums.size();
        for(int i=0;i<n;i++)
        {
            int tmp=near;
            near=max(near,far+nums[i]);
            far=tmp;
        }
        return near;
    }
};

1.3 环形街道抢劫

213. House Robber II(Medium)
街道是环形的,街道上相邻两个房子不能抢,每个房子有一定的财宝数,求最大可以抢多少。
Example 1:

Input: [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.

Example 2:

Input: [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3). Total amount you can rob = 1 + 3 = 4.

问题分析
转换为非环形街道求解,求从第一个到倒数第二个房子的最大财宝数。再求第二个到倒数第一个房子的最大财宝数。取二者最大值。

class Solution {
public:
    int rob(vector<int>& nums) {
        int n=nums.size();
        if(n==0) return 0;
        if(n==1) return nums[0];
        return max(money(nums,0,n-2),money(nums,1,n-1));
    }
    int money(vector<int>nums,int left,int right)
    {
        int fast=nums[left],low=0;
        for(int i=left+1;i<=right;i++)
        {
            nums[i]=max(fast,low+nums[i]);
            low=fast;
            fast=nums[i];
        }
        return nums[right];
    }
};
2、矩阵路径

2.1 矩阵的最小的路径和

64. Minimum Path Sum(Medium)
Given a m × n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path.

Note: You can only move either down or right at any point in time.

Example:

Input:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
Output: 7
Explanation: Because the path 1→3→1→1→1 minimizes the sum.

找出矩阵的左上角到右下角的最小路径和,每次只能向右和向下移动。

问题分析

子问题:第 i 行第 j 列的元素到右下角的路径和。
边界条件
对于最后一行的每个元素到右下角路径和等于向右走和相加,最后一列的每个元素到右下角路径和等于向下走和相加。
递推公式
grid[i][j] = min( grid[i][j+1], grid[i+1][j] ) + grid[i][j];

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int row=grid.size();
        if(!row) return 0;
        int col=grid[0].size();
        if(!col) return 0;
        
        for(int i=col-2;i>=0;i--)	//最后一行每个元素到矩阵右下角最小路径和
            grid[row-1][i]=grid[row-1][i]+grid[row-1][i+1];
        for(int i=row-2;i>=0;i--)	//最后一列每个元素到矩阵右下角最小路径和
            grid[i][col-1]=grid[i][col-1]+grid[i+1][col-1];
        for(int i=row-2;i>=0;i--)	//对于每一个元素,到右下角,第一步只能向下或者向右
        {
            for(int j=col-2;j>=0;j--)
                grid[i][j]=min(grid[i][j+1],grid[i+1][j])+grid[i][j];
        }
        return grid[0][0];
    }
};

上面的方法是从矩阵的右下角向左上角求解,也可以从左上角向右下角求解。
问题分析

子问题:左上角到第 i 行第 j 列的元素的最小路径和。
边界条件
对于左上角到第一行的每个元素的最小路径和等于从左上角向右走和相加,左上角到第一列的每个元素的最小路径和等于左上角向下走和相加。
递推公式
grid[i][j]+=min(grid[i][j-1],grid[i-1][j]);

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int row=grid.size();
        if(row==0) return 0;
        int col=grid[0].size();
        if(col==0) return 0;
        for(int i=1;i<row;i++)		//左上角到第一列每个元素最小路径和
            grid[i][0]+=grid[i-1][0];
        for(int i=1;i<col;i++)		//左上角到第一行每个元素最小路径和
            grid[0][i]+=grid[0][i-1];
        for(int i=1;i<row;i++)
        {
            for(int j=1;j<col;j++)
                grid[i][j]+=min(grid[i][j-1],grid[i-1][j]);
        }
        return grid[row-1][col-1];
    }
};

2.2 矩阵的总路径数

62. Unique Paths(Medium)
A robot is located at the top-left corner of a m x n grid (marked ‘Start’ in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked ‘Finish’ in the diagram below).

How many possible unique paths are there?
Leetcode题解-算法-动态规划_leetcode题解
Above is a 7 x 3 grid. How many possible unique paths are there?

Note: m and n will be at most 100.

Example 1:

Input: m = 3, n = 2
Output: 3
Explanation:
From the top-left corner, there are a total of 3 ways to reach the bottom-right corner:

  1. Right -> Right -> Down
  2. Right -> Down -> Right
  3. Down -> Right -> Right

Example 2:

Input: m = 7, n = 3
Output: 28

问题分析:

子问题:从 i 行 j 列走到矩阵右下角多少种走法;
边界条件:在最后一行或者最后一列,只有一种走法;
递推公式:vec[i][j] = vec[i][j+1] + vec[i+1][j]; 表示从某一点到右下角的走法等于,先向右走一步的走法和先向下走一步走法之和。

class Solution {
public:
    int uniquePaths(int m, int n) {
        if(m==1||n==1)
            return 1;
        vector<vector<int>>vec(m,vector<int>(n,1));
        for(int i=m-2;i>=0;i--)
        {
            for(int j=n-2;j>=0;j--)
                vec[i][j]=vec[i][j+1]+vec[i+1][j];
        }
        return vec[0][0];
    }
};

这道题可以用公式求解,是组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,从 S 个位置中取出 D 个,有多少种方法,这个问题的解为 C(S, D)。

class Solution {
public:
    int uniquePaths(int m, int n) {
        int S=m+n-2;
        long D=m-1;
        double res=1;
        for(int i=1;i<=D;i++)
            res=res*(S-D+i)/i;
        return (int)res;
    }
};
3、数组区间

3.1 数组区间和

303. Range Sum Query - Immutable(Easy)
Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.

Example:

Given nums = [-2, 0, 3, -5, 2, -1]
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

会多次调用sumRange函数
问题分析
由于会多次调用sumRange函数,所以先将从第一个数到每个数的区间和求出来,以后相减就可以了。

class NumArray {
public:
    NumArray(vector<int>& nums) {
        int s=0;
        for(int i=0;i<nums.size();i++)
        {
            s=s+nums[i];
            sum.push_back(s);
        }
    }
    int sumRange(int i, int j) {
        if(i==0)
            return sum[j];
        else
            return sum[j]-sum[i-1];
    }
private:
    vector<int>vec;
    vector<int>sum;
};

3.2 数组中等差递增子区间的个数

413. Arithmetic Slices(Medium)
Example:

A = [1, 2, 3, 4]
return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself.

方法一:暴力搜索
以每个元素为起点,起点下标为 i,将终点坐标从 i+2 依次向后移动,查看以 i 为起点的所有等差区间,求和即可。

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

时间复杂度:O(n2)
空间复杂度:O(1)

方法二:动态规划
dp[i] 表示以 A[i] 结尾的等差区间的数量;

如果{ A[i-3],A[i-2],A[i-1] } 为等差区间,以 A[i-1] 结尾的等差区间数量为 dp[i-1]。并且 A[i]-A[i-1]==A[i-1]-A[i-2],得出以 A[i] 结尾的等差区间数量为 dp[i]=dp[i-1]+1。

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& A) {
        int n=A.size();
        int total=0;
        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;
        for(int i=0;i<n;i++)
            total+=dp[i];
        return total;
    }
};

时间复杂度:O(n)
空间复杂度:O(n)

动态规划的空间优化,计算 dp[i] 时每次只用到 dp[i-1],所有没有必要使用数组。

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

时间复杂度:O(n)
空间复杂度:O(1)

方法三:用公式计算
从前面的分析可以看出,当出现一个等差区间时,从数组中第三个数开始,以该数结尾的区间数分别为1,2,3,4……,所以可以直接用公式求解。

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& A) {
        int n=A.size();
        int total=0;
        int count=0;
        for(int i=2;i<n;i++)
        {
            if(A[i]-A[i-1]==A[i-1]-A[i-2])
                count++;
            else
            {
                total+=count*(count+1)/2;
                count=0;
            }
        }
        return total+count*(count+1)/2;
    }
};
4、分割整数

4.1 分割整数的最大乘积

343. Integer Break(Medium)
Given a positive integer n, break it into the sum of at least two positive integers and maximize the product of those integers. Return the maximum product you can get.

Example 1:

Input: 2
Output: 1
Explanation: 2 = 1 + 1, 1 × 1 = 1.

Example 2:

Input: 10
Output: 36
Explanation: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36.

Note: You may assume that n is not less than 2 and not larger than 58.

子问题:求和为 i 的整数分割后的最大乘积,用 dp[i] 表示
边界条件:dp[1]=1
递推公式:dp[i]=max(dp[i],max(dp[i-j]*j,(i-j)*j));

  • dp[i] 表示本次不分割的最大乘积
  • dp[i-j]*j 表示本次分割出一个 j,剩下的和为 i-j 再分割
  • (i-j)*j 表示本次分割出一个 j,剩下的不分割的最大乘积
class Solution {
public:
    int integerBreak(int n) {
        vector<int>dp(n+1,0);
        dp[1] = 1;
        for (int i = 2; i <= n; i++)
            for (int j = 1; j <= i - 1; j++)
                dp[i] = max(dp[i],max(dp[i-j]*j,(i-j)*j));
        return dp[n];
    }
};

4.2 将一个数分解为整数的平方和

279. Perfect Squares(Medium)
Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, …) which sum to n.

Example 1:

Input: n = 12
Output: 3
Explanation: 12 = 4 + 4 + 4.

Example 2:

Input: n = 13
Output: 2
Explanation: 13 = 4 + 9.

问题分析
子问题分解:将 i 分解为整数的平方,最少可以分解为几个数的平方,用 dp[i] 表示。
初始化:dp[i] = i,表示全部分解为 1 的和。
递推条件:dp[i] = min(dp[i], dp[i-j*j]+1) { j>=1 && j*j<=i }

求解过程为:

dp[0] = 0 
dp[1] = dp[0]+1 = 1
dp[2] = dp[1]+1 = 2
dp[3] = dp[2]+1 = 3
dp[4] = Min{ dp[4-1*1]+1, dp[4-2*2]+1 }
      = Min{ dp[3]+1, dp[0]+1 } 
      = 1
dp[5] = Min{ dp[5-1*1]+1, dp[5-2*2]+1 }
      = Min{ dp[4]+1, dp[1]+1 } 
      = 2

dp[13] = Min{ dp[13-1*1]+1, dp[13-2*2]+1, dp[13-3*3]+1 }
       = Min{ dp[12]+1, dp[9]+1, dp[4]+1 }
       = 2

dp[n] = Min{ dp[n - i*i] + 1 },  n - i*i >=0 && i >= 1
class Solution {
public:
    int numSquares(int n) {
        vector<int>dp(n+1,0);
        for(int i=0;i<=n;i++)
            dp[i]=i;
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j*j<=i;j++)
                dp[i]=min(dp[i],dp[i-j*j]+1);
        }
        return dp[n];
    }
};

4.3 分割整数构成字母字符串

91. Decode Ways(Medium)
A message containing letters from A-Z is being encoded to numbers using the following mapping:

‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26

Given a non-empty string containing only digits, determine the total number of ways to decode it.

Example 1:

Input: “12”
Output: 2
Explanation: It could be decoded as “AB” (1 2) or “L” (12).

Example 2:

Input: “226”
Output: 3
Explanation: It could be decoded as “BZ” (2 26), “VF” (22 6), or “BBF” (2 2 6).

问题分析
子问题:到第 i 个字符可以解码的总方法,用 dp[i] 表示。
边界条件:dp[0]=1,如果第一个字符为 ‘0’,dp[1]=0,否则为 1。
递推公式:
先看挑出一个字符:

  • 第 i 个字符不是 0(dp[i-1]!=0),一定可以挑出一个字符,dp[i]+=dp[i-1]

再看挑出两个字符:

  • 第 i-1 个字符是 0(dp[i-2] == 0),挑不出两个字符,进行下一轮判断
  • 第 i-1 个字符不是 0,看第 i-1,i 个字符组成的数字,如果小于等于26,表示可以挑出两个字符,dp[i]+=dp[i-2]

其他情况下无法挑出字符,为 0,不予改变。

class Solution {
public:
    int numDecodings(string s) {
        int n=s.size();
        vector<int>dp(n+1,0);
        dp[0]=1;
        dp[1]=(s[0]=='0'?0:1);
        for(int i=2;i<=n;i++) {
            if(s[i-1]!='0')
                dp[i]+=dp[i-1];
            if(s[i-2]=='0')
                continue;
            string tmp="";
            tmp=tmp+s[i-2]+s[i-1];
            if(tmp<="26")
                dp[i]+=dp[i-2];
        }
        return dp[n];
    }
};
5、最长上升子序列

5.1 最长上升子序列

300. Longest Increasing Subsequence (Medium)
Given an unsorted array of integers, find the length of longest increasing subsequence.

Example:

Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.

Note:
There may be more than one LIS combination, it is only necessary for you to return the length.
Your algorithm should run in O(n2) complexity.

Follow up: Could you improve it to O(n log n) time complexity?

问题分析:

方法一:动态规划
找子问题
设给定的序列为(a1 , a2 , …, aN),求以 ak(k=1, 2, 3…N)为终点的最长上升子序列的长度。

确定状态:
子问题只和数字的位置相关。因此序列中数的位置 k 就是“状态 ”,而状态 k 对应的“值”,就是以 ak 做为 “终点”的最长上升子序列度。状态一共有 N 个。

找出状态转移方程:
maxLen (k) 表示以 ak 做为“终点”的最长上升子序列的度,那么:

  • 初始状态:maxLen (1) = 1
  • maxLen (k) = max { maxLen (i):1<= i< k 且 ai< ak 且 k≠1 } + 1
    若找不到这样的 i,则 maxLen (k) = 1

maxLen (k) 的值,就是在 ak 左边,“终点”数值小于 ak,且长度最大的那个上升子序列的长度再加 1。

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

以上解法的时间复杂度为 O(N2),可以使用二分查找将时间复杂度降低为 O(NlogN)。

方法二:二分法
定义一个数组 a,其中 a[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于原数组肿每一个元素 x:

  • 如果它大于 a 数组所有的值,就把它添加到 a 后面,最长递增子序列长度加 1;
  • 如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x。

例如对于数组 [1, 8, 4, 12, 2],有:

   a              n          num
  [ ]             0           1
  [1]             1           8
  [1, 8]          2           4
  [1, 4]          2           12
  [1, 4, 12]      3           2
  [1 , 2, 12]     3          null
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int len=nums.size();
        int *a=(int *)malloc(len*sizeof(int));
        int n=0;
        for(int i=0;i<len;i++)
        {
            int index=Binary_search(a,n,nums[i]);
            a[index]=nums[i];
            if(index==n)
                n++;
        }
        return n;
    }
    
    int Binary_search(int a[],int n,int key)
    {
        int left=0,right=n;
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(a[mid]==key)
                return mid;
            else if(a[mid]>key)
                right=mid;
            else
                left=mid+1;
        }
        return left;
    }
};

5.2 数对可以组成的最长链

646. Maximum Length of Pair Chain(Medium)
You are given n pairs of numbers. In every pair, the first number is always smaller than the second number.

Now, we define a pair (c, d) can follow another pair (a, b) if and only if b < c. Chain of pairs can be formed in this fashion.

Given a set of pairs, find the length longest chain which can be formed. You needn’t use up all the given pairs. You can select pairs in any order.

Example 1:

Input: [[1,2], [2,3], [3,4]]
Output: 2
Explanation: The longest chain is [1,2] -> [3,4]

Note:
The number of given pairs will be in the range [1, 1000].

问题分析:

方法一:贪心
对所有的数对按照第二个数的大小进行排序。第一步选结束数字最小的那个数对。 然后,每步都选和上一个选中的数对不冲突且结束数字最小的那个数。

class Solution {
public:    
    int findLongestChain(vector<vector<int>>& pairs) {
    	//按数对中第二个数的大小进行排序
        sort(pairs.begin(), pairs.end(), [](const vector<int>& a, const vector<int>& b){ return a[1] < b[1]; });
        int len=pairs.size();
        int count=1;
        int end=pairs[0][1];
        for(int i=1;i<len;i++)
        {
            if(pairs[i][0]>end)
            {
                end=pairs[i][1];
                count++;
            }
        }
        return count;
    }
};

方法二:动态规划
对所有的数对按照第一个数的大小进行排序。子问题为数对 pairs[i](i=1, 2, 3…N)为终点的最长链。a[i] 表示已数对 pairs[i]为终点的链长度。

  • 初始状态:a[i] = 1
  • a[i] = max { a[i], a[j]+1} 0<= j< i 且 pairs[i][0]>pairs[j][1]

再寻找 a[i] 的最大值。

class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        sort(pairs.begin(),pairs.end(),[](vector<int>a,vector<int>b){return a[0]<b[0];});
        int len=pairs.size();
        vector<int>a(len,1);
        for(int i=1;i<len;i++) {
            for(int j=0;j<i;j++)
                if(pairs[i][0]>pairs[j][1])
                    a[i]=max(a[i],a[j]+1);
        }
        int ans=1;
        for(int c:a)
            ans=max(ans,c);
        return ans;
    }
};

5.3 最长摆动子序列

376. Wiggle Subsequence(Medium)
A sequence of numbers is called a wiggle sequence if the differences between successive numbers strictly alternate between positive and negative. The first difference (if one exists) may be either positive or negative. A sequence with fewer than two elements is trivially a wiggle sequence.

For example, [1,7,4,9,2,5] is a wiggle sequence because the differences (6,-3,5,-7,3) are alternately positive and negative. In contrast, [1,4,7,2,5] and [1,7,4,5,5] are not wiggle sequences, the first because its first two differences are positive and the second because its last difference is zero.

Given a sequence of integers, return the length of the longest subsequence that is a wiggle sequence. A subsequence is obtained by deleting some number of elements (eventually, also zero) from the original sequence, leaving the remaining elements in their original order.

Example 1:

Input: [1,7,4,9,2,5]
Output: 6
Explanation: The entire sequence is a wiggle sequence.

Example 2:

Input: [1,17,5,10,13,15,10,5,16,8]
Output: 7
Explanation: There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].

Example 3:

Input: [1,2,3,4,5,6,7,8,9]
Output: 2

Follow up:Can you do it in O(n) time?

摆动序列:连续数字之间的差异在正数和负数之间严格交替。
现在给定一个整数序列,求最长摆动子序列的长度,子序列通过原始序列删除一些元素,其余元素保持原始顺序得到。

方法一:动态规划
子问题:求以第 i 个元素结尾的最长摆动子序列,

  • up[i] 指的是到目前为止所获得的最长摆动子序列的长度,其中第 i 个元素是摆动子序列的最后一个元素并且以上升的摆动结束。
  • 类似地,down[i] 指的是到目前为止获得的最长摆动子序列的长度,其中第 i 个元素作为摆动子序列的最后一个元素并且以下降摆动结束。

边界条件:up[i] = down[i] = 1(每个元素自身都可以作为一个子序列)

递推公式:
up[i] = max(up[i], down[j]+1) (j=0, 1 ,……i-1)
down[i] = max(down[i], up[j]+1) (j=0, 1 ,……i-1)

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();
        if(n < 2) return n;
        vector<int>up(n, 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);
            }
        }
        int res=1;
        for(int i=0; i<n; i++)
            res = max(res, up[i]);
        for(int i=0; i<n; i++)
            res = max(res, down[i]);
        return res;
    }
};

时间复杂度:O(n2)
空间复杂度:O(n)

方法二:线性动态规划
子问题:前 i 个元素所组成的最长摆动子序列,

  • 到第 i 个元素,最后一个元素以上升的摆动结束的长度为 up[i]
  • 到第 i 个元素,最后一个元素以下降的摆动结束的长度为 down[i]

边界条件:
up[0] = down[0] = 1;

递推公式:

  • 如果 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];
  • 如果 nums[i] == nums[i-1],上摆下降序列都不变down[i] = down[i-1],up[i] = up[i-1]。
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();
        if(n < 2) return n;
        vector<int>up(n, 0);
        vector<int>down(n, 0);
        up[0] = down[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];
            }
            else if (nums[i] < nums[i-1]) {
                down[i] = up[i-1]+1;
                up[i] = up[i-1];
            }
            else {
                up[i] = up[i-1];
                down[i] = down[i-1];
            }
        }
        return max(up[n-1], down[n-1]);
    }
};

时间复杂度:O(n)
空间复杂度:O(n)

方法三:线性动态规划空间优化
由于每个 up[i],down[i],之和前一个up[i-1],down[i-1] 有关,所以可以不使用数组,直接使用两个数 up 和 down 即可

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

时间复杂度:O(n)
空间复杂度:O(1)

6、最长公共子序列 7、0-1背包

7.1 将数组分为和相等的两部分

416. Partition Equal Subset Sum(Medium)
Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Note:
Each of the array element will not exceed 100.
The array size will not exceed 200.

Example 1:

Input: [1, 5, 11, 5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].

Example 2:

Input: [1, 2, 3, 5]
Output: false
Explanation: The array cannot be partitioned into equal sum subsets.

方法一:
问题分解:dp[i][j]表示从前i个数字中取数字,和不超过 j,最大的和是多少。
边界条件:dp[0][j]=0; dp[i][0]=0;
递推公式:dp[i][j] = max( dp[i-1][j], dp[i-1][j-nums[i]]+nums[i] );

dp[i][j]表示前i个数字中取数子,背包容量为 j,可以得到的最大容量是多少,如果 dp[n][sum/2]==sum/2,即背包容量为 sum/2,从所有的数字中选,得到的最大和为 sum/2,说明可以将数组分为和相等的两份。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int n=nums.size();
        int sum=0;
        for(int c:nums)
            sum+=c;
        if(sum%2)
            return false;
        vector<int>dp(sum/2+1,0);
        for(int i=0;i<n;i++)
            for(int j=sum/2;j>=nums[i];j--)//背包剩余的容量要大于等于该数字
                dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
        return dp[sum/2]==sum/2;
    }
};

方法二:
问题分解:dp[i][j]表示从前 i 个数字中取数字,和为 j 是否可行,可行为 true,不可行为 false。
边界条件:dp[i][0]=true;
递推公式:dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];

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

7.2 为一组数指定正负号使其和为目标值

494. Target Sum(Medium)
You are given a list of non-negative integers, a1, a2, …, an, and a target, S. Now you have 2 symbols + and -. For each integer, you should choose one from + and - as its new symbol.

Find out how many ways to assign symbols to make sum of integers equal to target S.

Example 1:

Input: nums is [1, 1, 1, 1, 1], S is 3.
Output: 5
Explanation:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
There are 5 ways to assign symbols to make the sum of nums be target 3.

为一组数指定正负号,使得和为目标值,问一共有多少种方法。
方法一
将数分为正负两部分,分别为 P 和 N,
sum( P)-sum(N)=target
sum( P)+sum(N)+sum( P)-sum(N)=target+sum( P)-sum(N)
2*sum( P)=target+sum(nums)
问题变为从数组种任意选取数值,使得和为 (target+sum(nums))/2,一共有多少种选法。

问题分解:从前 i 个数字中任意选取,使得和为 j,一共有多少种选法,用 dp[i][j] 表示
边界条件:dp[i][0]=1,dp[0][j]=0 { j=1,2,3……}
递推公式:dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]]

空间优化,二维数组变为一维数组

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum=0;
        for(int c:nums)
            sum+=c;
        if(sum<S||(sum+S)%2==1)
            return 0;
        sum=(sum+S)/2;
        vector<int>dp(sum+1,0);
        dp[0]=1;
        for(int c:nums)
            for(int j=sum;j>=c;j--)
                dp[j]=dp[j]+dp[j-c];
        return dp[sum];
    }
};

方法二:递归

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum=0;
        return SumWays(nums,0,S);
    }
    int SumWays(vector<int>& nums, int begin, long S)
    {
        if(begin==nums.size())
            return S==0?1:0;
        return SumWays(nums,begin+1,S+nums[begin])+SumWays(nums,begin+1,S-nums[begin]);
    }
};

7.3 从字符串集合中选尽可能多的字符串并保证0和1个数不超过给定值

474. Ones and Zeroes(Medium)

Example 1:

Input: Array = {“10”, “0001”, “111001”, “1”, “0”}, m = 5, n = 3
Output: 4
Explanation: This are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are “10,”0001”,”1”,”0”

Example 2:

Input: Array = {“10”, “0”, “1”}, m = 1, n = 1
Output: 2
Explanation: You could form “10”, but then you’d have nothing left. Better form “0” and “1”.

问题分析:
可以理解为一个多维费用的 0-1 背包问题,只不过这里有两个背包,分别表示 0 的数量和 1 的数量。从集合中选取字符串,保证所有 0 的数量不大于 m,所有 1 的数量不大于 n,最多可以选多少个字符串。

问题分解:从集合中前 k 个字符串中字符,使得 0 的数量不大于 i,1 的数量不大于 j,最多可以选多少个字符串。用 dp[k][i][j] 表示。
边界条件:dp[k][0][j]=0; dp[k][i][0]=0;
递推公式:dp[k][i][j]=max( dp[k-1][i][j], dp[k-1][i-zero][j-one] ); // zero表示字符串 k 中 0 的个数,one 表示字符串 k 中 1 的个数。

空间优化,三维数组变为二维数组

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

7.4 用最少的硬币拼出总额

322. Coin Change(Medium)
You are given coins of different denominations and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.

Example 1:

Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1

Example 2:

Input: coins = [2], amount = 3
Output: -1

已知每一种硬币的面值,每种硬币饿数量的无穷,拼成指定的金额最少需要多少硬币。

问题分析
物品数量为无限个,这是一个完全背包问题。

问题分解:用面值小于等于 i 的硬币拼成金额 j,最少需要多少硬币,用 dp[i][j]。
边界条件:dp[0][j]=0;
递推公式:
(1)如果该硬币 i 面值 c 等于总金额,一个硬币就够了;
即:if(j==c) dp[i][j]=1;
(2)如果不用硬币 i 已经拼成金额 j 了,且拼成金额 j-c 有方法,则用硬币 i 代替小面值的硬币;
即:else if(dp[j]==0&&dp[j-c]!=0) dp[j]=dp[j-c]+1;
(3)剩下的可以使用硬币 i,也可以不使用硬币 i,看是哪种需要的硬币数量少,要注意使用面值为 c 硬币 i 后,剩下的金额 j-c 要可以拼成。
即:else if(dp[j-c]!=0) dp[j]=min(dp[j],dp[j-c]+1);

这里同样使用空间压缩,将二维数组变为一维数组

注意:内层循环一定要从前向后,因为拼接成大金额要在拼接成小金额的基础上,先用前 i 个硬币拼成小金额,才用递推公式推出用前 i 个硬币拼成大金额。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if(amount==0||coins.empty())
            return 0;
        vector<int>dp(amount+1,0);
        for(int c:coins)
        {
            for(int j=c;j<=amount;j++)			//从前向后
            {
                if(j==c)
                    dp[j]=1;
                else if(dp[j]==0&&dp[j-c]!=0)	//第一次拼出金额 j
                    dp[j]=dp[j-c]+1;
                else if(dp[j-c]!=0)				//用小于等于j-c的硬币必须能拼成才行
                    dp[j]=min(dp[j],dp[j-c]+1);
            }
        }
        return dp[amount]==0?-1:dp[amount];
    }
};

7.5 找零钱的组合

518. Coin Change 2(Medium)
You are given coins of different denominations and a total amount of money. Write a function to compute the number of combinations that make up that amount. You may assume that you have infinite number of each kind of coin.

Example 1:

Input: amount = 5, coins = [1, 2, 5]
Output: 4
Explanation: there are four ways to make up the amount:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

Example 2:

Input: amount = 3, coins = [2]
Output: 0
Explanation: the amount of 3 cannot be made up just with coins of 2.

Example 3:

Input: amount = 10, coins = [10]
Output: 1

Note:
You can assume that

  • 0 <= amount <= 5000
  • 1 <= coin <= 5000
  • the number of coins is less than 500
  • the answer is guaranteed to fit into signed 32-bit integer
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int n=coins.size();
        vector<int>dp(amount+1,0);
        dp[0]=1;
        for(auto coin:coins)
            for(int i=coin;i<=amount;i++)
                dp[i]+=dp[i-coin];
        return dp[amount];
    }
};
8、买卖股票

8.1 需要冷却的股票交易

309. Best Time to Buy and Sell Stock with Cooldown(Medium)
数组中每个点的值表示股票价格,可以进行任意次交易,一次交易完成后至少冷却一天还能进行下一次交易,问最多可以赚取多少利润

Example:

Input: [1,2,3,0,2]
Output: 3
Explanation: transactions = [buy, sell, cooldown, buy, sell]

解题思路
定义三种状态

  • nostock:没有股票,可以买进
  • havestock:拥有股票,可以出售
  • cool:正在冷却

更新三种状态

  • 此时是冷却状态,可以是前面就是冷却状态,继续保持,也可以是卖了股票,变成冷却状态 cool = max(cool, havestock + prices[i]);
  • 此时拥有股票,可以是前面就有股票,继续拥有,也可以是从没有股票的状态下买了股票 havestock = max(havestock, nostock - prices[i]);
  • 此时没有股票,可以是前面也没有股票,继续保持,也可以是冷却状态变成没有股票的状态(冷却结束)nostock = max(nostock, cool);
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty()) return 0;
        int n = prices.size();
        int nostock = 0;
        int havestock = -prices[0];
        int cool = 0;
        for (int i = 1; i < n; i++){
            int tmp = cool;
            cool = max(cool, havestock + prices[i]);
            havestock = max(havestock, nostock - prices[i]);
            nostock = max(nostock, tmp);
        }
        return max(nostock, cool);
    }
};

8.2 收交易费的交易

714. Best Time to Buy and Sell Stock with Transaction Fee(Medium)
可以进行任意多次交易,卖出后才能买进,卖出时需要交固定金额的交易费,求最大可以获得的利润。

Example 1:

Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
Output: 8
Explanation: The maximum profit can be achieved by:

  • Buying at prices[0] = 1
  • Selling at prices[3] = 8
  • Buying at prices[4] = 4
  • Selling at prices[5] = 9

The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

解题思路
定义两种状态,买进(buy)和卖出(sell),buy 表示此时买进获得的利润,sell 表示此时卖出得到的利润。

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int buy = INT_MIN, sell = 0;
        for (int c : prices){
            buy = max(buy, sell - c);
            sell = max(sell, buy+c-fee);
        }
        return sell;
    }
};

8.3 最多进行两次交易

123. Best Time to Buy and Sell Stock III(Hard)

最对进行两笔交易,卖出之后才能买入,最多可以获取多少利润。
Example 1:

Input: [3,3,5,0,0,3,1,4]
Output: 6
Explanation: Buy on day 4 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.
Then buy on day 7 (price = 1) and sell on day 8 (price = 4), profit = 4-1 = 3.

Example 2:

Input: [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are
engaging multiple transactions at the same time. You must sell before buying again.

Example 3:

Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.

解题思路
定义四种状态:第一次买(firbuy);第一次卖(firsell);第二次买(secbuy);第二次卖(secsell)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty())
            return 0;
        int firbuy = -prices[0];	//第一次买
        int firsell = 0;			//买了又卖
        int secbuy = -prices[0];	//买了卖了,又买
        int secsell = 0;			//买卖两次
        for (int i = 1; i < prices.size(); i++){
            if (prices[i] < (-firbuy))
                firbuy = -prices[i];
            if (firsell < prices[i] + firbuy)
                firsell = prices[i] + firbuy;
            if (secbuy < firsell - prices[i])
                secbuy = firsell - prices[i];
            if (secsell < secbuy + prices[i])
                secsell = secbuy + prices[i];
        }
        return max(firsell, secsell);
    }
};

8.4 只能进行 k 次的股票交易

188. Best Time to Buy and Sell Stock IV(Hard)

解题思路
使用“局部最优和全局最优解法”。我们维护两种量,一个是当前到达第 i 天可以最多进行 j 次交易,最大利润是多少(global[i][j]),另一个是到第 i 天,最多可进行 j 次交易,并且最后一次交易在当天最大利润是多少(local[i][j])。递推式如下:
global[i][j]=max(local[i][j],global[i-1][j])
也就是取当前局部最好的,和过往全局最好的中大的那个(因为最后一次交易如果包含当前天一定在局部最好的里面,否则一定在过往全局最优的里面)。

对于局部变量的维护,递推式是
local[i][j]=max(global[i-1][j-1]+max(diff,0),local[i-1][j]+diff)
看两个量,第一个是全局到 i-1 天进行 j-1 次交易,然后加上今天的交易,如果今天是赚钱的话(也就是前面只要 j-1 次交易,最后一次交易取当前天),第二个量则是取 local 第 i-1 天 j 次交易,然后加上今天的差值(这里因为 local[i-1][j] 包含第 i-1 天卖出的交易,所以现在变成第 i 天卖出,并不会增加交易次数,而且这里无论 diff 是不是大于 0 都一定要加上,否则就不满足local[i][j]必须在最后一天卖出的条件了)

如果交易次数大于等于数组长度的一半,直接计算所有可能的收益。

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        const int size = prices.size();
        if(size < 2) return 0;
        if(k >= size/2) { // For last test case
            int sum = 0;
            for(int i = 1; i < size; ++i) 
                sum += max(0, prices[i] - prices[i-1]);
            return sum;
        }
        vector<int> local(k+1, 0);
        vector<int> global(k+1, 0);
        for(int i = 0; i < prices.size()-1; i++) {
            int diff = prices[i+1] - prices[i];
            for(int j = k; j >= 1; j--) {
                local[j] = max(global[j-1] + max(diff, 0), local[j]+diff);
                global[j] = max(global[j], local[j]);
            }
        }
        return global[k];
    }
};