面试_动态规划

01背包

剑指 Offer II 101. 分割等和子集

给定一个非空的正整数数组 nums ,请判断能否将这些数字分成元素和相等的两部分。

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:nums 可以分割成 [1, 5, 5] 和 [11] 。
示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:nums 不可以分为和相等的两部分

提示:

1 <= nums.length <= 200
1 <= nums[i] <= 100
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        //nums = [1,5,11,5] -> [1, 11],[5,5] -> true
        int nlen = nums.size();
        if (nlen < 2) {
            return false;
        }
        int total_sum = accumulate(nums.begin(), nums.end(), 0);
        // 和为奇数,肯定不可以拆分
        if (total_sum % 2) {
            return false;
        }
        //找最大值
        int max_sum = *max_element(nums.begin(), nums.end());
        int part_sum = total_sum / 2;
        if (max_sum > part_sum) {
            return false;
        }
        //1. dp代表部分和为i
        // vector<vector<int> > dp(nlen, vector<int>(part_sum + 1, 0));
        // for (int i = 0; i < nlen; ++i) {
        //     dp[i][0] = true;
        // }
        // //第0个数可以构成num[0]的部分和
        // dp[0][nums[0]] = true;
        // for (int i = 1; i < nlen; ++i)
        // {
        //     int num = nums[i];
        //     for (int j = 1; j <= part_sum; ++j)
        //     {
        //         //当前数小于部分和, 不加 | 加当前数
        //         if (nums[i] <= j) {
        //             dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
        //         } else {
        //             dp[i][j] = dp[i - 1][j];
        //         }
        //     }
        // }
        // return dp[nlen - 1][part_sum];
        
        //2. 优化
        vector<int> dp(part_sum + 1, 0);
        dp[0] = true;
        for (int i = 0; i < nlen; ++i)
        {
            int num = nums[i];
            for (int j = part_sum; j >= num; --j)
            {
                //不加 | 加
                //如果是遍历 [num, part_sum],之前得到的 j,会被之后的 j-num破坏掉
                //而逆序遍历,则总是先遍历 尚未计算的j
                dp[j] = dp[j] | dp[j - num];
            }
        }
        return dp[part_sum];
    }
};

时间复杂度:\(O(n^2)\);空间复杂度:O(n)

剑指 Offer II 102. 加减的目标值

给定一个正整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3
示例 2:

输入:nums = [1], target = 1
输出:1

提示:

1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000

法一:DFS

class Solution {
    int ans;
public:
    void dfs(vector<int>& nums, int target, int cur, int sum, int nlen)
    {
        if (cur == nlen) {
            if (target == sum) {
                ans++;
            }
        }
        else {
            dfs(nums, target, cur + 1, sum + nums[cur], nlen);
            dfs(nums, target, cur + 1, sum - nums[cur], nlen);
        }
    }
    int findTargetSumWays(vector<int>& nums, int target) {
        int nlen = nums.size();
        if (nlen == 1 && nums[0] == target) {
            return 1;
        }

        ans = 0;
        dfs(nums, target, 0, 0, nlen);
        return ans;
    }
};

法二:动态规划

class Solution {
    // int ans;
public:
    // void dfs(vector<int>& nums, int target, int cur, int sum, int nlen)
    // {
    //     if (cur == nlen) {
    //         if (target == sum) {
    //             ans++;
    //         }
    //     }
    //     else {
    //         dfs(nums, target, cur + 1, sum + nums[cur], nlen);
    //         dfs(nums, target, cur + 1, sum - nums[cur], nlen);
    //     }
    // }
    int findTargetSumWays(vector<int>& nums, int target) {
        // int nlen = nums.size();
        // if (nlen == 1 && nums[0] == target) {
        //     return 1;
        // }
        // 回溯
        // ans = 0;
        // dfs(nums, target, 0, 0, nlen);
        // return ans;

        //2. 动态规划(01背包)
        // 记数组的元素和为 sum,添加 - 号的元素之和为neg,则其余添加+的元素之和为sum−neg,得到的表达式的结果为 
        // (sum - neg) - neg  = sum - 2 * neg = target
        // neg = (sum - target) / 2
        // 由于nums中元素都是非负整数,neg也得是非负整数,所以sum-taget是 非负偶数。不符合条件直接返回0
        int nlen = nums.size();
        if (nlen == 1 && nums[0] == target) {
            return 1;
        }
        int sum_n = accumulate(nums.begin(), nums.end(), 0);
        int diff = sum_n - target;
        if (diff < 0 || diff % 2 != 0) {
            return 0;
        }
        int neg = diff / 2;

        //满足上述条件,问题转变为在nums的前i个数中选取元素,使得这些元素之和等于j的方案数
        //定义二维数据dp,dp[i][j]表示nums的前i个数中选取元素,使得这些元素之和等于j的方案数
        //dp[nlen][neg]
        // vector<vector<int> > dp(nlen + 1, vector<int>(neg + 1, 0));
        //当i=0,没有元素可以选时,元素和只能为0,对应方案数为1
        // dp[0][0] = 1;
        //1<=i<=n时,对于数组nums中第i个元素num,遍历0<=j<=neg,计算dp[i][j]的值
        //对于num > j, 则不能选num, 此时 dp[i][j] = dp[i - 1][j];
        // num <= j, 则方案数为 dp[i][j] = dp[i - 1][j] + dp[i - 1][j - num]
        // for (int i = 1; i <= nlen; ++i)
        // {
        //     int num = nums[i - 1];
        //     for (int j = 0; j <= neg; ++j)
        //     {
        //         dp[i][j] = dp[i - 1][j];
        //         if (j >= num) {
        //             dp[i][j] = dp[i][j] + dp[i - 1][j - num];
        //         }
        //     }
        // }
        // return dp[nlen][neg];

        
        //2. 优化
        vector<int> dp(neg + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < nlen; ++i)
        {
            int num = nums[i];
            for (int j = neg; j >= num; --j)
            {
                dp[j] = dp[j] + dp[j - num];
            }
        }
        return dp[neg];
    }
};

完全背包

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:

输入:coins = [2], amount = 3
输出:-1
示例 3:

输入:coins = [1], amount = 0
输出:0

提示:

1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104

法一:动态规划

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if amount == 0:
            return 0
        clen = len(coins)
        Max = amount + 1
        dp = [Max for _ in range(amount + 1)]
        dp[0] = 0
        for i in range(1, amount + 1):
            for j in range(0, clen):
                if coins[j] <= i:
                    dp[i] = min(dp[i], dp[i - coins[j]] + 1)
        
        return dp[amount] if dp[amount] <= amount else -1

法二:记忆化搜索

class Solution {
   vector<int> count;
   int dp(vector<int>& coins, int rest_amount)
   {
        if (rest_amount < 0) return -1;
        if (rest_amount == 0) return 0;
        //如果下次还要计算这个问题的值直接从数组中取出返回即可,这样能保证每个子问题最多只被计算一次
        if (count[rest_amount - 1] != 0) return count[rest_amount - 1];
        int Min = INT_MAX;
        for (int coin : coins)
        {
            //重复选
            int res = dp(coins, rest_amount - coin);
            if (res >= 0 && res < Min) {
                Min = res + 1;
            }
        }
        count[rest_amount - 1] = (Min == INT_MAX ? -1 : Min);
        return count[rest_amount - 1];
   }
public:
    int coinChange(vector<int>& coins, int amount) {
        if (amount < 1) return 0;
        //count为0~amount所需要的最小硬币数
        count.resize(amount);
        return dp(coins, amount);
    }
};

python写法:使用 @functools.lru_cache(max_len)

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        # 2. 记忆化搜索
        @functools.lru_cache(amount)
        def dp(rest_amount):
            if rest_amount < 0: return -1
            if rest_amount == 0: return 0
            mini = int(1e9)
            for coin in coins:
                res = dp(rest_amount - coin)
                if res >= 0 and res < mini:
                    mini = res + 1
            return mini if mini < int(1e9) else -1
        
        if amount < 1: return 0
        return dp(amount)

279. 完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:

输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:

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

提示:

1 <= n <= 104
class Solution {
public:
    int numSquares(int n) {
        if (n == 1) {
            return 1;
        }
        //1, 4, 9
        vector<int> dp(n + 1, n);
        dp[0] = 0;
        for (int i = 1; i <= n; ++i)
        {
            for (int j = 1; j*j <= i; ++j)
            {
                if (j*j <= i) {
                    dp[i] = min(dp[i], dp[i - j*j] + 1);
                }
            }
        }
        //必然存在解,因为有1
        return dp[n];
    }
};

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

提示:

1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 仅有小写英文字母组成
wordDict 中的所有字符串 互不相同
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict)
    {
        int slen = s.length(), wlen = wordDict.size();

        unordered_set<string> dict;
        for (string e : wordDict) {
            dict.insert(e);
        }
        vector<bool> dp(slen + 1, 0);
        dp[0] = true;
        //分割 [0, j - 1], [j, i]
        for (int i = 1; i <= slen; ++i)
        {
            for (int j = 0; j < i; ++j)
            {
                if (dp[j] && dict.count(s.substr(j, i - j))) {
                    dp[i] = true;
                    break;
                }
            }
        }        
        return dp[slen];
    }
};

140. 单词拆分 II

给定一个字符串 s 和一个字符串字典 wordDict ,在字符串 s 中增加空格来构建一个句子,使得句子中所有的单词都在词典中。以任意顺序 返回所有这些可能的句子。

注意:词典中的同一个单词可能在分段中被重复使用多次。

示例 1:

输入:s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"]
输出:["cats and dog","cat sand dog"]
示例 2:

输入:s = "pineapplepenapple", wordDict = ["apple","pen","applepen","pine","pineapple"]
输出:["pine apple pen apple","pineapple pen apple","pine applepen apple"]
解释: 注意你可以重复使用字典中的单词。
示例 3:

输入:s = "catsandog", wordDict = ["cats","dog","sand","and","cat"]
输出:[]

提示:

1 <= s.length <= 20
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 10
s 和 wordDict[i] 仅有小写英文字母组成
wordDict 中所有字符串都 不同
class Solution {
    unordered_set<string> wordSet;
    unordered_map<int, vector<string> > ans;
public:
    vector<string> wordBreak(string s, vector<string>& wordDict) {
        wordSet = unordered_set(wordDict.begin(), wordDict.end());
        backtrack(s, 0);
        return ans[0];
    }
    void backtrack(const string& s, int index)
    {
        if (!ans.count(index)) {
            if (index == s.size()) {
                ans[index] = {""};
                return;
            }
        
            ans[index] = {};
            for (int i = index + 1; i <= s.size(); ++i)
            {
                string word = s.substr(index, i - index);
                if (wordSet.count(word))
                {
                    backtrack(s, i);
                    for (const string& succ : ans[i]) {
                        ans[index].emplace_back(succ.empty() ? word : word + " " + succ);
                    }
                }
            }
        }
    }
};

法二:

class Solution {
    unordered_set<string> wordSet;
    vector<string> ans;
public:
    vector<string> wordBreak(string s, vector<string>& wordDict) {
        wordSet = unordered_set(wordDict.begin(), wordDict.end());
        int slen = s.length();
        vector<string> track;
        dfs(s, track, 0, slen);
        return ans;
    }

    void dfs(const string& s, vector<string>& track, int startIdx, int slen)
    {
        if (startIdx == slen) {
            string t = accumulate(track.begin(), track.end(), string(), 
                [](string& ss, string& s) {
                    return ss.empty() ? s : ss + " " + s;
                });
            ans.emplace_back(t);
            return;
        }

        for (int i = startIdx; i < slen; ++i)
        {
            string subw = s.substr(startIdx, i - startIdx + 1);
            if (wordSet.count(subw)) {
                track.emplace_back(subw);
                dfs(s, track, i + 1, slen);
                track.pop_back();
            }
        }
    }

};

一、简单DP

221. 最大正方形

在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。

示例 1:

输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4

示例 2:
输入:matrix = [["0","1"],["1","0"]]
输出:1

示例 3:
输入:matrix = [["0"]]
输出:0
提示:

m == matrix.length
n == matrix[i].length
1 <= m, n <= 300
matrix[i][j] 为 '0' 或 '1'
class Solution:
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        rows, columns = len(matrix), len(matrix[0])
        if rows == 1 and columns == 1:
            return int(matrix[0][0])

        dp = [[0] * (columns) for _ in range(rows)]

        ans = 0
        for i in range(0, rows):
            for j in range(0, columns):
                if matrix[i][j] == "1":
                    if i == 0 or j == 0:
                        dp[i][j] = 1
                    else:
                        dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1
                    
                    ans = max(ans, dp[i][j])
        
        return ans ** 2
                
        # maxSide = 0
        # for i in range(rows):
        #     for j in range(columns):
        #         if matrix[i][j] == '1':
        #             # 遇到一个 1 作为正方形的左上角
        #             maxSide = max(maxSide, 1)
        #             # 计算可能的最大正方形边长
        #             currentMaxSide = min(rows - i, columns - j)
        #             for k in range(1, currentMaxSide):
        #                 # 判断新增的一行一列是否均为 1
        #                 flag = True
        #                 if matrix[i + k][j + k] == '0':
        #                     break
        #                 for m in range(k):
        #                     if matrix[i + k][j + m] == '0' or matrix[i + m][j + k] == '0':
        #                         flag = False
        #                         break
        #                 if flag:
        #                     maxSide = max(maxSide, k + 1)
        #                 else:
        #                     break
        
        # maxSquare = maxSide * maxSide
        # return maxSquare

时间复杂度:O(mn)

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

输入:m = 3, n = 7
输出:28
示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向下
    示例 3:

输入:m = 7, n = 3
输出:28
示例 4:

输入:m = 3, n = 3
输出:6

提示:

1 <= m, n <= 100
题目数据保证答案小于等于 2 * 109
class Solution {
    int ans;
public:
    // 1. dfs:超时
    // void dfs(int i, int j, int m, int n)
    // {
    //     if (i < 0 || i == m || j < 0 || j == n) {
    //         return;
    //     }
    //     if (i == m - 1 && j == n - 1) {
    //         ans += 1;
    //         return;
    //     }

    //     int dirs[][2] = {{0, 1}, {1, 0}};
    //     for (int k = 0; k < 2; k++)
    //     {
    //         int x = i + dirs[k][0], y = j + dirs[k][1];
    //         if (x < 0 || x == m || y < 0 || y == n) {
    //             continue;
    //         }
    //         dfs(x, y, m, n);
    //     }
    // }
    // int uniquePaths(int m, int n) {
    //     dfs(0, 0, m, n);
    //     return ans;
    // }
    //2. 动态规划: O(n^2)
    // int uniquePaths(int m, int n)
    // {
    //     vector<vector<int> > dp(m + 1, vector<int>(n + 1));
    //     for (int i = 0; i < m; ++i) {
    //         dp[i][0] = 1;
    //     }
    //     for (int j = 0; j < n; ++j) {
    //         dp[0][j] = 1;
    //     }
    //     for (int i = 1; i < m; ++i)
    //     {
    //         for (int j = 1; j < n; ++j)
    //         {
    //             dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
    //         }
    //     }
    //     return dp[m - 1][n - 1];
    // }

    // O(m)
    int uniquePaths(int m, int n)
    {
        // C(m + n - 2, m - 1): m + n - 2次移动,有m-1次向下,n-1次向右。因此路径总数
        long long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y)
        {
            ans = ans * x / y;
        }
        return ans;
    }
};

动态规划:时间复杂度O(n2),空间复杂度O(n2)
组合数:时间复杂度O(m), 空间O(1)

85. 最大矩形

给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

示例 1:

输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:6
解释:最大矩形如上图所示。

示例 2:
输入:matrix = []
输出:0

示例 3:
输入:matrix = [["0"]]
输出:0

示例 4:
输入:matrix = [["1"]]
输出:1

示例 5:
输入:matrix = [["0","0"]]
输出:0

提示:

rows == matrix.length
cols == matrix[0].length
1 <= row, cols <= 200
matrix[i][j] 为 '0' 或 '1'
class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        int row = matrix.size();
        if (row == 0) {
            return 0;
        }
        int col = matrix[0].size();
        
        // left[i][j] 为矩阵第 i 行第 j 列元素的左边连续 1 的数量。
        vector<vector<int> > left(row, vector<int>(col));
        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                if (matrix[i][j] == '1') {
                    left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
                }
            }
        }
        int ret = 0;
        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                if (matrix[i][j] == '0') {
                    continue;
                }
                int width = left[i][j];
                int area = width;
                for (int k = i - 1; k >= 0; k--) {
                    width = min(width, left[k][j]);
                    area = max(area, (i - k + 1) * width);
                }
                ret = max(ret, area);
            }
        }
        return ret;
    }
};

class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        int row = matrix.size();
        if (row == 0) {
            return 0;
        }
        int col = matrix[0].size();
        
        // left[i][j] 为矩阵第 i 行第 j 列元素的左边连续 1 的数量。
        vector<vector<int> > left(row, vector<int>(col, 0));
        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                if (matrix[i][j] == '1') {
                    left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
                }
            }
        }
        int ret = 0;
        //单调栈
        //对于每一列,使用基于柱状图的方法
        for (int j = 0; j < col; ++j)
        {
            vector<int> up(row, 0), down(row, 0);
            stack<int> stk;
            for (int i = 0; i < row; ++i)
            {
                while (!stk.empty() && left[stk.top()][j] >= left[i][j]) {
                    stk.pop();
                }
                up[i] = stk.empty() ? -1 : stk.top();  //
                stk.push(i);
            }
            stk = stack<int>();
            for (int i = row - 1; i >= 0; --i) 
            {
                while (!stk.empty() && left[stk.top()][j] >= left[i][j]) {
                    stk.pop();
                }
                down[i] = stk.empty() ? row : stk.top();  //
                stk.push(i);
            }
            for (int i = 0; i < row; ++i)
            {
                int height = down[i] - up[i] - 1;
                int area = height * left[i][j];
                ret = max(ret, area);
            }
        }
        return ret;
        //使用柱状图的优化暴力方法
        // for (int i = 0; i < row; ++i)
        // {
        //     for (int j = 0; j < col; ++j)
        //     {
        //         if (matrix[i][j] == '0') {
        //             continue;
        //         }
        //         int width = left[i][j];
        //         int area = width;
        //         for (int k = i - 1; k >= 0; k--) {
        //             width = min(width, left[k][j]);
        //             area = max(area, (i - k + 1) * width);
        //         }
        //         ret = max(ret, area);
        //     }
        // }
        // return ret;
    }
};

礼物最大价值(矩阵贪心类题目)剑指 Offer 47

class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        
        for(int i = 0; i < m; i++)
        {
            for (int j = 0; j < n; j++)
            {
                if (i == 0 && j ==0) {
                    continue;
                }
                if (i == 0) {
                    grid[i][j] += grid[i][j - 1];
                }
                else if (j == 0) {
                    grid[i][j] += grid[i - 1][j];
                }
                else {
                    grid[i][j] += max(grid[i][j - 1], grid[i - 1][j]);
                }
            }
        }
        return grid[m - 1][n - 1];
    }
};

时间复杂度 O(MN):M, N分别为矩阵行高、列宽;动态规划需遍历整个grid矩阵,使用 O(MN)时间。
空间复杂度 O(1):原地修改使用常数大小的额外空间。

爬楼梯 剑指 Offer 10- II.

class Solution {
public:
    int numWays(int n) {
        if (n == 0 || n == 1) 
        {
            return 1;    
        }
        if (n == 2)
        {
            return 2;
        }
        int a = 1, b = 2;
        int res = 0, MOD = 1000000007;
        for (int i = 3; i <= n; i++)
        {
            res = (a + b) % MOD;
            a = b;
            b = res;
        }
        return res ;
    }
};

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

152. 乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。子数组 是数组的连续子列。

示例 1:

输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

示例 2:

输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

提示:

1 <= nums.length <= 2 * 104
-10 <= nums[i] <= 10
nums 的任何前缀或后缀的乘积都 保证 是一个 32-位 整数
class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int nlen = nums.size();
        if (nlen == 1) {
            return nums[0];
        }
        int min_pro = nums[0], max_pro = nums[0];
        int ans = nums[0];
        for (int i = 1; i < nlen; ++i)
        {
            int t_max = max_pro, t_min = min_pro;
            max_pro = max(t_max * nums[i], max(t_min * nums[i], nums[i]));
            min_pro = min(t_min * nums[i], min(t_max * nums[i], nums[i]));
            ans = max(ans, max_pro);
        }
        return ans;
    }   
};

1186. 删除一次得到子数组最大和

给你一个整数数组,返回它的某个 非空 子数组(连续元素)在执行一次可选的删除操作后,所能得到的最大元素总和。换句话说,你可以从原数组中选出一个子数组,并可以决定要不要从中删除一个元素(只能删一次哦),(删除后)子数组中至少应当有一个元素,然后该子数组(剩下)的元素总和是所有子数组之中最大的。

注意,删除一个元素后,子数组 不能为空。

示例 1:

输入:arr = [1,-2,0,3]
输出:4
解释:我们可以选出 [1, -2, 0, 3],然后删掉 -2,这样得到 [1, 0, 3],和最大。

示例 2:
输入:arr = [1,-2,-2,3]
输出:3
解释:我们直接选出 [3],这就是最大和。

示例 3:
输入:arr = [-1,-1,-1,-1]
输出:-1
解释:最后得到的子数组不能为空,所以我们不能选择 [-1] 并从中删去 -1 来得到 0。
我们应该直接选择 [-1],或者选择 [-1, -1] 再从中删去一个 -1。

提示:

1 <= arr.length <= 105
-104 <= arr[i] <= 104
class Solution {
public:
    int maximumSum(vector<int>& arr) {
        int alen = arr.size();
        if (alen == 1) {
            return arr[0];
        }
        //a: 未执行任何删除操作得到的当前子数组最大和
        //b: 已经执行删除后得到的当前子数组最大和
        int a = arr[0], b = 0, ans = arr[0];
        for (int i = 1; i < alen; ++i)
        {

            b = max(b + arr[i], a);           // 考虑执行删除,max(之前已执行删除现在只能加上当前值,之前最大子数组未执行删除-故当前位置可删除)
            a = max(a + arr[i], arr[i]);      // 不考虑执行删除,直接更新a=max(a + nums[i], nums[i])
            ans = max(ans, max(a, b));
        }
        return ans;
    }
};

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

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

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

1 <= nums.length <= 100
0 <= nums[i] <= 400
class Solution:
    def rob(self, nums: List[int]) -> int:
        nlen = len(nums)
        if nlen == 1:
            return nums[0]
        if nlen == 2:
            return max(nums[0], nums[1])
        
        # dp = [0 for _ in range(nlen)]
        # dp[0], dp[1] = nums[0], max(nums[0], nums[1])
      
        # for i in range(2, nlen):
        #     dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]) 
        
        # return dp[nlen - 1]
        
        # 动态数组
        first, second = nums[0], max(nums[0], nums[1])
        for i in range(2, nlen):
            temp = second
            second = max(second, first + nums[i])
            first = temp
        
        return second

213. 打家劫舍 II

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

示例 1:

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

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

输入:nums = [1,2,3]
输出:3

提示:

1 <= nums.length <= 100
0 <= nums[i] <= 1000
class Solution {
public:
    int rob(vector<int>& nums) {
        int nlen = nums.size();
        if (nlen == 0) 
        {
            return 0;
        }
        if (nlen == 1)
        {
            return nums[0];
        }
        return max(my_rob(nums, 0, nlen-1), my_rob(nums, 1, nlen));
    }

    int my_rob(vector<int>& nums, int start, int end)
    {
        int cur = 0,  pre = 0;
        for (int i = start; i < end; i++)
        {
            int t = cur;
            cur = max(pre + nums[i], cur);
            pre = t;
        }
        return cur;
    }
};

337. 打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

示例 1:

输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:

树的节点数在 [1, 104] 范围内
0 <= Node.val <= 104
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
    unordered_map<TreeNode*, int> f, g;  // f拿,g不拿
public:
    void dfs(TreeNode* node)
    {
        if (!node) {
            return;
        }
        dfs(node->left);
        dfs(node->right);
        f[node] = node->val + g[node->left] + g[node->right];  // 拿当前节点,则不拿子节点
        g[node] = max(f[node->left], g[node->left]) + max(f[node->right], g[node->right]);
    }
    int rob(TreeNode* root) {
        dfs(root);
        return max(f[root], g[root]);
    }
};

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
    struct SubTreeStatus {
        int selected;
        int notSelected;
    };
public:
    SubTreeStatus dfs(TreeNode* node) {
        if (!node) {
            return {0, 0};
        }
        auto left = dfs(node->left);
        auto right = dfs(node->right);
        int selected = node->val + left.notSelected + right.notSelected;
        int notSelected = max(left.selected, left.notSelected) + max(right.selected, right.notSelected);
        return {selected, notSelected};
    }

    int rob(TreeNode* root) {
        auto rootStatus = dfs(root);
        return max(rootStatus.selected, rootStatus.notSelected);
    }
};

二、字符串+DP

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。

示例 3:

输入: 
s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

提示:

1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 仅有小写英文字母组成
wordDict 中的所有字符串 互不相同
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        int slen = s.length(), wlen = wordDict.size();
        unordered_set<string> dict;
        for (const auto& e : wordDict) {
            dict.insert(e);
        }
        //dp[i] 表示字符串s前i个字符组成的字符串s[0..i−1] 
        //是否能被空格拆分成若干个字典中出现的单词
        vector<bool> dp(slen + 1, 0);
        dp[0] = true;
        for (int i = 1; i <= slen; ++i)
        {
            //分割 [0, j - 1], [j, i - 1]
            for (int j = 0; j < i; ++j)
            {
                if (dp[j] && dict.count(s.substr(j, i - j))) { 
                    dp[i] = true;
                    break;
                }
            }

        }
        return dp[slen];
    }
    
};

编辑距离 leetcode 72

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.length(), n = word2.length();
        if (m * n == 0) {
            return m + n;
        }
        int dp[m + 1][n + 1];

        for (int i = 0; i <= m; i++)
        {
            dp[i][0] = i;    // 当word2为空时, word1需要删掉i个字符
        }
        for (int j = 0; j <= n; j++)
        {
            dp[0][j] = j;
        }
        for (int i = 1; i <= m; ++i)
        {
            for (int j = 1; j <= n; ++j)
            {
                int flag = (word1[i - 1] == word2[j - 1] ? 0 : 1);
                dp[i][j] = min(dp[i - 1][j] + 1, min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + flag));
            }
        }
        return dp[m][n];
    }
};

最长公共子序列 leetcode 1143

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.length(), n = text2.length();
        if (m * n == 0) {
           return 0;
        }
        int dp[m + 1][n + 1];
        memset(dp, 0, sizeof(dp));
        // vector<vector<int> > dp(m + 1, vector<int>(n + 1, 0));
        
        for (int i = 1; i <= m; ++i)
        {
            for (int j = 1; j <= n; ++j)
            {
                if(text1[i - 1] == text2[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else
                {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
                
            }
        }
        return dp[m][n];
    }
};

516. 最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

提示:

1 <= s.length <= 1000
s 仅由小写英文字母组成
class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        slen = len(s)

        # i,j范围内最长回文子串长度
        dp = [[0]*(slen) for _ in range(slen)]

        for i in range(slen - 1, -1, -1):
            dp[i][i] = 1
            for j in range(i + 1, slen):
                if s[i] == s[j]:
                    # [i + 1, j - 1], 两边符合,则向中间字符串判断
                    dp[i][j] = dp[i + 1][j - 1] + 2
                else:
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
                
        return dp[0][slen - 1]

        # 转换成两个字符串最长公共子序列
        # dp = [[0]*(slen + 1) for _ in range(slen + 1)]
        # ss = s[::-1]
        # for i in range(1, slen+1):
        #     for j in range(1, slen+1):
        #         if s[i - 1] == ss[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[slen][slen]

时间复杂度、空间复杂度:\(O(n^2)\)

最长回文字串 leetcode 5

中心拓展算法

class Solution {
public:
    pair<int, int> expandAroundCenter(const string& s, int left, int right)
    {
        while (left >= 0 && right < s.size() && s[left] == s[right])
        {
            --left;
            ++right;
        }
        return {left + 1, right - 1};
    }
    string longestPalindrome(string s) {
        int slen = s.length();
        if (slen == 1) 
        {
            return s;
        }
        int start = 0, end = 0;
        for (int i = 0; i < s.size(); ++i)
        {
            auto [left1, right1] = expandAroundCenter(s, i, i);
            auto [left2, right2] = expandAroundCenter(s, i, i + 1);
            if (right1 - left1 > end - start)  // 相当于
            {
                start = left1;
                end = right1;
            }
            if (right2 - left2 > end - start)
            {
                start = left2;
                end = right2;
            }
        }
        return s.substr(start, end - start + 1);
    }
};

能看懂的写法

class Solution {
public:
    string longestPalindrome(string s) {
        int slen = s.length();
        if (slen == 1) 
        {
            return s;
        }
        
        int max_left = 0, max_right = -1;
        for (int i = 0; i < slen; ++i)
        {
            int left = i, right = i;
            while (left >= 0 && s[left] == s[i]) --left;
            while (right < slen && s[right] == s[i]) ++right;
            while (left >= 0 && right < slen && s[left] == s[right]) 
            {
                --left; ++right;
            }
            if (max_right - max_left < right - left)
            {
                max_left = left;
                max_right = right;
            }
        }
        return s.substr(max_left + 1, max_right - max_left - 1);
    }
};

409. 最长回文串(贪心)

给定一个包含大写字母和小写字母的字符串 s ,返回 通过这些字母构造成的 最长的回文串 。

在构造过程中,请注意 区分大小写 。比如 "Aa" 不能当做一个回文字符串。

示例 1:

输入:s = "abccccdd"
输出:7
解释:
我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
示例 2:

输入:s = "a"
输入:1
示例 3:

输入:s = "bb"
输入: 2

提示:

1 <= s.length <= 2000
s 只能由小写和/或大写英文字母组成
class Solution:
    def longestPalindrome(self, s: str) -> int:
        slen = len(s)
        if slen == 1:
            return slen
        cnt = [0] * 128
        for i in range(slen):
            cnt[ord(s[i])] += 1
        
        ans = 0
        for v in cnt:
            ans += v // 2 * 2   # 如果偶数次字符,则ans +  n对 * 2
            if v % 2 == 1 and ans % 2 == 0: # 第一次遇到奇数才+1
                ans += 1
        
        return ans

剑指 Offer II 020. 回文子字符串的个数

给定一个字符串 s ,请计算这个字符串中有多少个回文子字符串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

提示:

1 <= s.length <= 1000
s 由小写英文字母组成
class Solution:
    def countSubstrings(self, s: str) -> int:
        # slen = len(s)
        # ans = 0
        # # 1. 动规
        # dp = [[True]*slen for _ in range(slen)]
        # for i in range(slen - 1, -1, -1):
        #     for j in range(i + 1, slen):
        #         dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
        
        # for i in range(slen):
        #     for j in range(i, slen):
        #         if dp[i][j]:
        #             ans += 1

        # 2. 中心拓展法, 时间复杂度:O(n^2), 空间复杂度:O(1)
        slen = len(s)
        ans = 0
        for i in range(0, 2 * slen - 1):
            left, right = i // 2, i // 2 + i % 2
            while left >= 0 and right < slen and s[left] == s[right]:
                left -= 1 
                right += 1
                ans += 1 

        return ans

最长不重复子串 leetcode 3

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        occ = set()
        slen = len(s)

        # 右指针,初始值为-1,相当于我们在字符串的左边界的左侧,还没有开始移动
        rk, ans = -1, 0
        for i in range(slen):
            if i != 0:
                # 左指针向右移动一格,移除一个字符
                occ.remove(s[i - 1])
            while rk + 1 < slen and s[rk + 1] not in occ:
                # 不断的移动右指针
                occ.add(s[rk + 1])
                rk += 1
            # 第 i 到 rk个字符是一个极长的无重复字符串子串
            ans = max(ans, rk - i + 1)
        return ans

44. 通配符匹配

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。

示例 1:
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

示例 2:
输入:
s = "aa"
p = ""
输出: true
解释: '
' 可以匹配任意字符串。

示例 3:
输入:
s = "cb"
p = "?a"
输出: false
解释: '?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。

示例 4:
输入:
s = "adceb"
p = "ab"
输出: true
解释: 第一个 '' 可以匹配空字符串, 第二个 '' 可以匹配字符串 "dce".

示例 5:
输入:
s = "acdcb"
p = "a*c?b"
输出: false

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        m, n = len(s), len(p)

        dp = [[False] * (n + 1) for _ in range(m + 1)]
        dp[0][0] = True
        
        for j in range(1, n + 1):
            if p[j - 1] == '*':
                dp[0][j] = True
            else:
                break
        
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if p[j - 1] == '*':
                    # 使用星号和不使用星号
                    # aa a*,   a, ab*
                    dp[i][j] = dp[i][j - 1] | dp[i - 1][j]
                elif p[j - 1] == '?' or s[i - 1] == p[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]
        
        
        return dp[m][n]

10. 正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:

输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:

输入:s = "aa", p = "a"
输出:true
解释:因为 '
' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:

输入:s = "ab", p = "."
输出:true
解释:".
" 表示可匹配零个或多个('*')任意字符('.')。

提示:

1 <= s.length <= 20
1 <= p.length <= 30
s 只包含从 a-z 的小写字母。
p 只包含从 a-z 的小写字母,以及字符 . 和 *。
保证每次出现字符 * 时,前面都匹配到有效的字符
class Solution {
public:
    bool isMatch(string s, string p) {
        int slen = s.length(), plen = p.length();

        auto matches = [&](int i, int j) {
            if (i == 0) {
                return false;
            }
            if (p[j - 1] == '.') {
                return true;
            }
            return s[i - 1] == p[j - 1];
        };
        
        // dp的i范围: 1~slen, j范围: 1~plen
        vector<vector<int> > dp(slen + 1, vector<int>(plen + 1));
        dp[0][0] = true;
        for (int i = 0; i <= slen; ++i)
        {
            //把 a* 组合看成整体
            for (int j = 1; j <= plen; ++j)
            {
                //p的第j个字符是*
                if (p[j - 1] != '*') {
                    if (matches(i, j)) {
                        dp[i][j] = dp[i][j] | dp[i - 1][j - 1];
                    }
                }
                else {
                    dp[i][j] = dp[i][j] | dp[i][j - 2];  
                    //比较s第i个字符和p第j-1个字符
                    if (matches(i, j - 1)) {
                        //dp[i-1][j]:匹配s末尾的一个字符,将该字符扔掉,而该组合还可以继续进行匹配;
                        //dp[i][j-2]:不匹配字符,将该组合扔掉,不再进行匹配。
                        dp[i][j] = dp[i][j] | dp[i - 1][j]; 
                    }
                }
            }
        }
        return dp[slen][plen];
    }
};

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:

输入:n = 1
输出:["()"]

提示:

1 <= n <= 8
class Solution {
public:
    void backtrack(vector<string>& ans, string& current, int open, int close, int n)
    {
        if (current.size() == n * 2) {
            ans.emplace_back(current);
            return;
        }
        if (open < n) {
            current.push_back('(');
            backtrack(ans, current, open + 1, close, n);
            current.pop_back();
        }
        if (close < open) {
            current.push_back(')');
            backtrack(ans, current, open, close + 1, n);
            current.pop_back();
        }
    }
    vector<string> generateParenthesis(int n) {
        if (n == 1) {
            return {"()"};
        }
        vector<string> ans;
        string current;
        backtrack(ans, current, 0, 0, n);
        return ans;
    }
};

三、贪心算法

55. 跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

提示:

1 <= nums.length <= 3 * 104
0 <= nums[i] <= 105
class Solution {
public:
    bool canJump(vector<int>& nums) {
        int nlen = nums.size();
        if (nlen == 1 || nums[0] >= nlen - 1) {
            return true;
        }
        
        //每次记录当前位置能到达的最远下标,如果当前位置小于上一次记录能到达的最远下标,则fasle
        int fastLength = nums[0];
        for (int i = 1; i < nlen; i++)
        {
            if (fastLength < i) {
                return false;
            }
            fastLength = max(i + nums[i], fastLength);
            if (fastLength >= nlen - 1) {
                return true;
            }
        }
        return false;
    }
};

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

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

1 <= nums.length <= 2500
-104 <= nums[i] <= 104
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int nlen = nums.size();
        if (nlen == 0) {
            return 0;
        }
        
        //1. dp: O(n^2)
        //nums = [10,9,2,5,3,7,101,18]
        // vector<int> dp(nlen, 0);
        // int ans = 0;
        // for (int i = 0; i < nlen; ++i)
        // {
        //     dp[i] = 1;
        //     for (int j = 0; j < i; ++j)
        //     {
        //         if (nums[j] < nums[i]) {
        //             dp[i] = max(dp[i], dp[j] + 1);
        //         }
        //     }
        //     if (ans < dp[i]) {
        //         ans = dp[i];
        //     }
        // }
        // return ans;

        //2. 贪心+二分查找 O(nlogn)
        //我们维护一个数组 d[i] ,表示长度为 i 的最长上升子序列的末尾元素的最小值
        //用 len 记录目前最长上升子序列的长度,起始时 len 为 1, d[1] = nums[0]。
        int len = 1;
        vector<int> d(nlen + 1, 0);
        d[len] = nums[0];
        //d[i]关于i单调递增的
        //依次遍历nums中每个元素,更新d和len的值,如果nums[i] > d[len]则更新len=len+1
        //否则d[1..len]中找到 d[i-1] < nums[j] <d[i],更新d[i] = nums[j]
        //流程:
        //1. 设已求出最大子序列长度为len,从前向后遍历nums,遍历到nums[i]时:
        // - 如果nums[i] > d[len], 则之间加入d数组末尾,并更新len++
        // - 否则,在d数组中二分查找,找到第一个比nums[i]小的数d[k],并更新d[k+1] = nums[i]
        for (int i = 1; i < nlen; ++i)
        {
            if (nums[i] > d[len]) {
                d[++len] = nums[i];
            } else {
                // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
                int left = 1, right = len, pos = 0;
                while (left <= right) {
                    int mid = (left + right) >> 1;
                    if (d[mid] < nums[i]) {
                        pos = mid;
                        left = mid + 1;
                    } else {
                        right = mid - 1;
                    }
                }
                d[pos + 1] = nums[i];
                cout << pos + 1 << " " << nums[i] << endl;
            }
        }
        return len;
 


四、回溯+dp

131. 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:

输入:s = "a"
输出:[["a"]]
 
提示:

1 <= s.length <= 16
s 仅由小写英文字母组成

法一:回溯

class Solution {
    vector<vector<string> > ans;
public:
    bool judge(const string& s, int left, int right)
    {
        while (left <= right) {
            if (s[left] != s[right]) {
                return false;
            }
            left++; right--;
        }
        return true;
    }

    void dfs(const string& s, vector<string>& track, int startIdx, int nlen)
    {
        if (startIdx == nlen) {
            ans.emplace_back(track);
            return;
        }
        for (int i = startIdx; i < nlen; ++i)
        {
            string sub = s.substr(startIdx, i + 1 - startIdx);
            if (judge(s, startIdx, i)) {
                track.emplace_back(sub);
                dfs(s, track, i + 1, nlen);
                track.pop_back();
            }
        }
    }

    vector<vector<string>> partition(string s) {
        //法一
        int slen = s.length();
        vector<string> track;
        dfs(s, track, 0, slen);
        return ans;
       
    }

};

法二:回溯法+dp

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        nlen = len(s)
        f = [[True] * nlen for _ in range(nlen)]

        # f(i, j)表示s[i..j]是否为回文串
        # f(i, j) = True, i >= j
        # f(i, j) = f(i + 1, j - 1) ^ (s[i] == s[j]),  i < j
        for i in range(nlen - 1, -1, -1):   # 第i个字符串开始
            for j in range(i + 1, nlen):    # 长度为j
                f[i][j] = (s[i] == s[j]) and f[i + 1][j - 1]
        
        ret = list()
        track = list()

        def dfs(i):
            if i == nlen:
                ret.append(track[:])
                return
            
            for j in range(i, nlen):
                if f[i][j]:
                    track.append(s[i : j + 1])
                    dfs(j + 1)
                    track.pop()
        
        dfs(0)
        return ret    

时间复杂度:\(O(n*2^n)\);空间复杂度:\(O(n^2)\)

五、数学+动态规划

343. 整数拆分

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积 。

示例 1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:

输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

提示:

2 <= n <= 58
class Solution:
    def integerBreak(self, n: int) -> int:
        # 输入: n = 10
        # 输出: 36
        # 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
        # dp = [0 for _ in range(n + 1)]
        # dp[0] = dp[1] = 0

        # for i in range(2, n + 1):
        #     curMax = 0
        #     for j in range(1, i//2+1):
        #         curMax = max(curMax, max(j * (i - j), j * dp[i - j]))
        #     dp[i] = curMax
        
        # return dp[n]

        if (n < 4):
            return n - 1
        
        dp = [0 for _ in range(n + 1)]
        dp[2] = 1
        for i in range(3, n + 1):
            dp[i] = max(max(2 * (i - 2), 2 * dp[i - 2]), max(3 * (i - 3), 3 * dp[i - 3]))
        
        return dp[n]

338. 比特位计数

给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。

示例 1:

输入:n = 2
输出:[0,1,1]
解释:
0 --> 0
1 --> 1
2 --> 10
示例 2:

输入:n = 5
输出:[0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101

提示:

0 <= n <= 105

进阶:
很容易就能实现时间复杂度为 O(n log n) 的解决方案,你可以在线性时间复杂度 O(n) 内用一趟扫描解决此问题吗?
你能不使用任何内置函数解决此问题吗?(如,C++ 中的 __builtin_popcount )

class Solution:
    def countBits(self, n: int) -> List[int]:
         # 法一:Brian Kernighan, O(nlogn)
        # def countOnes(x):
        #     ones = 0
        #     while x > 0:
        #         x &= (x - 1)
        #         ones += 1
        #     return ones
        
        # bits = [countOnes(i) for i in range(n + 1)]
        # return bits

        # 法二:动态规划--最高有效位, O(n)
        # bits = [0]
        # highBit = 0
        # for i in range(1, n + 1):
        #     if i & (i - 1) == 0:
        #         highBit = i
        
        #     bits.append(bits[i - highBit] + 1)
        
        # return bits

        # 法三:动态规划--最低有效位, O(n)
        # 右移,得到的数是|_x/2_|,如果bit[|_x/2_|]已知,则可以得到bits[x]的值
        # 如果x是偶数,则bits[x] = bit[|_x/2_|]
        # 如果x是奇数,则bits[x] = bit[|_x/2_|] + 1
        # bits = [0]
        # for i in range(1, n + 1):
        #     bits.append(bits[i >> 1] + (i & 1))

        # return bits    

        # 动态规划:最低设置位, O(n)
        bits = [0]
        for i in range(1, n + 1):
            # i & (i - 1): 将最低位的1变成0
            bits.append(bits[i & (i - 1)] + 1)

        return bits

六、记忆化搜索+dp

312. 戳气球

有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

示例 1:
输入:nums = [3,1,5,8]
输出:167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 315 + 358 + 138 + 181 = 167
示例 2:

输入:nums = [1,5]
输出:10

提示:

n == nums.length
1 <= n <= 300
0 <= nums[i] <= 100
class Solution:
    # 输入:nums = [3,1,5,8]
    # 输出:167
    # 解释:
    # nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
    # coins =  3*1*5    +   3*5*8   +  1*3*8  + 1*8*1 = 167
    def maxCoins(self, nums: List[int]) -> int:
        nlen = len(nums)
        if nlen == 1:
            return nums[0]
        
        vals = [1] + nums + [1]

        @cache
        def solve(left, right):
            if left >= right - 1:
                return 0
            
            # 枚举开区间(i, j)内的全部位置mid, 
            # 令mid为当前区域第一个添加的气球,
            # 该操作能够得到的硬币数为 val[i] x val[mid] x val[j]
            # 递归地计算分割出的两区间, solve(i, mid) 和 solve(mid, j)
            best = 0
            for i in range(left + 1, right):
                total = vals[left] * vals[i] * vals[right]
                total += solve(left, i) + solve(i, right)
                best = max(best, total) 
            
            return best

        return solve(0, nlen + 1)
class Solution {
    vector<vector<int> > rec;
    vector<int> val;
public:
    int solve(int left, int right) {
        if (left >= right - 1) {
            return 0;
        }
        if (rec[left][right] != -1) {
            return rec[left][right];
        }
        for (int i = left + 1; i < right; ++i)
        {
            int sum = val[left] * val[i] * val[right];
            sum += solve(left, i) + solve(i, right);
            rec[left][right] = max(rec[left][right], sum);
        }
        return rec[left][right];
    }
    int maxCoins(vector<int>& nums) {
        int n = nums.size();
        val.resize(n + 2);
        for (int i = 1; i <= n; ++i)
        {
            val[i] = nums[i - 1];
        }
        val[0] = val[n + 1] = 1;
        rec.resize(n + 2, vector<int>(n + 2, -1));
        return solve(0, n + 1);
    }
};

posted @ 2022-07-15 14:22  douzujun  阅读(105)  评论(0编辑  收藏  举报