其他分享
首页 > 其他分享> > 面试_动态规划

面试_动态规划

作者:互联网

01背包

完全背包

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)

一、简单DP

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);
    }
};

最长不重复子串 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

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;
 


标签:return,nums,int,max,面试,动态,规划,dp,left
来源: https://www.cnblogs.com/douzujun/p/16481243.html