【算法面试题汇总】LeetBook列表的算法面试题汇总---动态规划题目及答案

整理不易留个小心心呗🥰
如果有更好的或者是我有错的地方还请各位大佬指出哦
有些是copy的还望不要介意

至少有k个重复字符的最长子串

题目描述:
给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。

示例:

输入:s = "ababbc", k = 2
输出:5
解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。

提示:
1 <= s.length <= 104
s 仅由小写英文字母组成
1 <= k <= 105
  • 分治

    对于字符串 ss,如果存在某个字符ch,它的出现次数大于 00 且小于 kk,则任何包含 ch 的子串都不可能满足要求。也就是说,我们将字符串按照 ch 切分成若干段,则满足要求的最长子串一定出现在某个被切分的段内,而不能跨越一个或多个段。因此,可以考虑分治的方式求解本题。

class Solution {
    public int longestSubstring(String s, int k) {
        return dfs(s,0,s.length()-1,k);
    }
    public int dfs(String s,int left,int right,int k){
        //统计每个字符出现的次数
        int[] c = new int[26];
        for(int i=left;i<=right;i++){
            c[s.charAt(i) - 'a']++;
        }

        //统计小于k的字符
        char split = 0;
        for(int i=0;i<26;i++){
            if(c[i]>0 && c[i]<k){
                split = (char)(i + 'a');
                break;
            }
        }
        //全部都大于等于k个数,则返回整个字符串
        if(split == 0){
            return right-left+1;
        }

        int l=left;
        int ans=0;
        while(l <= right){
            //跳过小于k的字符
            while(l<=right && s.charAt(l) == split){
                l++;
            }
            if(l>right){
                break;
            }
            int start=l;
            while(l<=right && s.charAt(l) != split){
                l++;
            }
            ans = Math.max(ans,dfs(s,start,l-1,k));
        }
        return ans;
    }
}
  • 滑动窗口

    不能用二分,因为不具有二段性质

    假设此时子串长度为t的这一区间满足要求,t+1长度的区间不一定满足要求:

    若新的字符在原有区间出现过,则t+1长度满足;

    若新的字符在原有区间没有出现过,则t+1长度不满足

    因此我们无法是使用「二分」,相应的也无法直接使用「滑动窗口」思路的双指针

    因为双指针其实也是利用了二段性质,当一个指针确定在某个位置,另外一个指针能够落在某个明确的分割点,使得左半部分满足,右半部分不满足。

    当确定了窗口内所包含的字符数量时,区间重新具有了二段性质

    利用字符数量有限性(可枚举)作为切入点,使得「答案子串的左边界左侧的字符以及右边界右侧的字符一定不会出现在子串中」这一性质在双指针的实现下具有单调性,也就是「让区间重新具有二段性质」。

class Solution {
    public int longestSubstring(String s, int k) {
        int n = s.length();
        int ans = 0;
        char[] c = s.toCharArray();
        int[] cnt = new int[26];
        for(int p=1;p<=26;p++){
            Arrays.fill(cnt,0);
            //total表示在这个区间内所有字符种类的数量
            //sum表示满足大于等于k的字符种类数量
            int l=0,total=0,sum=0;
            for(int r=0;r<n;r++){
                int t = c[r] - 'a';
                cnt[t]++;
                //如果添加进cnt后等于1,则表示多一种种类
                if(cnt[t] == 1) total++;
                //如果添加进cnt后等于k,则表示满足条件的字符数量加1
                if(cnt[t] == k) sum++;
                //当超过限定数量p,则左指针右移
                while(total > p){
                    int temp = c[l++] - 'a';
                    cnt[temp]--;
                    //如果减掉后等于0,则种类减1
                    if(cnt[temp] == 0)total--;
                    //如果减掉后小于k,则满足条件的字符数量-1
                    if(cnt[temp] == k-1)sum--;
                }
                //当所有字符都符合要求,更新答案
                if(total == sum){
                    ans = Math.max(ans, r-l+1);
                }
            }
        }
        return ans;
    }
}

二叉树中的最大路径和

题目描述:
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和

示例:
在这里插入图片描述

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
  • 代码实现
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int res = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        maxValue(root);
        return res;
    }
    public int maxValue(TreeNode root){
        if(root == null){
            return 0;
        }
        //递归计算左右子节点的最大贡献值
        //大于0才选取
        int left = Math.max(maxValue(root.left),0);
        int right = Math.max(maxValue(root.right),0);
        //更新最大路径和
        res = Math.max(left + right+root.val , res);
        //返回节点的最大贡献值
        //最大贡献值:等于该节点的值与其子节点的最大贡献值之和
        //叶节点的最大贡献值为该节点的值
        return Math.max(left,right)+root.val;
    }
}

最长连续序列

题目描述:
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
  • 代码实现

    时间O(n)空间O(n)

class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for(int num:nums){
            set.add(num);
        }
        int ans=0;
        for(int num:set){
            if(!set.contains(num-1)){
                int curNum = num;
                int curStreak=1;
                while(set.contains(curNum+1)){
                    curNum+=1;
                    curStreak+=1;
                }
                ans = Math.max(ans,curStreak);
            }
        }
        return ans;
    }
}
  • 超出O(n)的做法

    这种做法Arrays.sort的时间已经是O(nlogn)

class Solution {
    public int longestConsecutive(int[] nums) {
        if(nums.length == 0){
            return 0;
        }
        if(nums.length == 1){
            return 1;
        }
        Arrays.sort(nums);
        int max = 1;
        int count = 1;
        for(int i = 1;i<nums.length;i++){
            if((nums[i-1]) == nums[i]){
                continue;
            }
            if((nums[i-1] + 1) == nums[i]){
                count++;
            }else{
                //断掉连续,count重新置为1
                max = Math.max(count,max);
                count= 1;  
            }
        }
        max = Math.max(count,max);
        return max;
    }
}

打家劫舍

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

示例:

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

    时间O(n)空间O(1)

class Solution {
    public int rob(int[] nums) {
        if(nums == null || nums.length==0){
            return 0;
        }
        if(nums.length==1){
            return nums[0];
        }
        int f=nums[0],s=Math.max(nums[0],nums[1]);
        for(int i=2;i<nums.length;i++){
            int temp = s;
            s = Math.max(f+nums[i],s);
            f = temp;
        }
        return s;
    }
}
  • 法二:
class Solution {
    public int rob(int[] nums) {
        if(nums == null || nums.length==0){
            return 0;
        }
        if(nums.length==1){
            return nums[0];
        }
        int n = nums.length;
        for(int i=2;i<n;i++){
            if(i>=3){
               nums[i] = Math.max(nums[i-2],nums[i-3]) + nums[i];
            }else{
                nums[i] = Math.max(nums[i-2]+nums[i],nums[i-1]);
            }
        }
        return Math.max(nums[n-1],nums[n-2]);
    }
}

完全平方数

题目描述:
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例:

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

输入:n = 13
输出:2
解释:13 = 4 + 9
  • 代码实现
class Solution {
    public int numSquares(int n) {
        //记录每个整数最少需要多少个数的平方来表示
        //默认初始化每个整数为0
        int[] dp = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            //最坏情况是每个数+1,eq.dp[4]=4
            dp[i] = i;
            for (int j = 1; j * j <= i; j++) {
                //区间落在[1,√n]
                //假设当前枚举到j时,还需要取若干数的平方构成i-j²
                dp[i] = Math.min(dp[i],dp[i - j*j]+1);
            }
        }
        return dp[n];
    }
}
  • 四平方和定理

    四平方和定理证明了任意一个正整数都可以被表示为至多四个正整数的平方和。这给出了本题的答案的上界。

    当且仅当 n≠4^k * (8m+7)时,n 可以被表示为至多三个正整数的平方和。因此,当 n = 4^k ** (8m+7)时,n 只能被表示为四个正整数的平方和。此时我们可以直接返回 4。

    当 n≠4^k * (8m+7)* 时,我们需要判断到底多少个完全平方数能够表示 n,我们知道答案只会是 1,2,3中的一个:

    答案为 1时,则必有 n为完全平方数,这很好判断;

    答案为 2 时,则有 n=a2+b2,我们只需要枚举所有的 a(1≤a ≤√n)判断 n-a^2是否为完全平方数即可;

    答案为 3时,我们很难在一个优秀的时间复杂度内解决它,但我们只需要检查答案为 1或 2的两种情况,即可利用排除法确定答案。

class Solution {
    public int numSquares(int n) {
        if (isPerfectSquare(n)) {
            return 1;
        }
        if (checkAnswer4(n)) {
            return 4;
        }
        for (int i = 1; i * i <= n; i++) {
            int j = n - i * i;
            if (isPerfectSquare(j)) {
                return 2;
            }
        }
        return 3;
    }

    // 判断是否为完全平方数
    public boolean isPerfectSquare(int x) {
        int y = (int) Math.sqrt(x);
        return y * y == x;
    }

    // 判断是否能表示为 4^k*(8m+7)
    public boolean checkAnswer4(int x) {
        while (x % 4 == 0) {
            x /= 4;
        }
        return x % 8 == 7;
    }
}


最长上升子序列*

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

示例:

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

输入:nums = [0,1,0,3,2,3]
输出:4
  • 代码实现
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        if(n==0){
            return 0;
        }
        int[] dp = new int[n];
        dp[0] = 1;
        int count=1;
        for(int i=1;i<n;i++){
            dp[i] = 1;
            for(int j=0;j<i;j++){
                if(nums[i]>nums[j]){
                    dp[i] =Math.max(dp[i],dp[j]+1);
                }
            }
            count = Math.max(count,dp[i]);
        }
        return count;
    }
}
  • 贪心+二分查找*

    数组d[i]表示长度为i的最长上升子序列的末尾元素的最小值。

    如上升子序列为1,3,5或5,6,7。但d[3]=3

    以nums=[1,3,5,6,7,2,5,7,9,10]为例,到1,3,5,6,7递增,长度len=5,这是答案一,

    当遇到2(nums[5])时,可能出现另一个答案,从1, 2,… (nums[0], nums[5]…),记为答案二

    此时便要验证答案二,依赖于2以后的答案

    此时原来的[1,3,5,6,7]中的3便不在答案二的序列中了,把3替换为2,依次进行,如果新的答案比第一次答案长,整个序列被替换,如果没有第一个长,替换的次数不够,原来的答案一最大的数字还在末尾,原始的d的长度不会被替换,始终用d的长度为结果

class Solution {
    public int lengthOfLIS(int[] nums) {
        int len = 1, n = nums.length;
        if (n == 0) {
            return 0;
        }
        int[] d = new int[n + 1];
        d[len] = nums[0];
        for (int i = 1; i < n; ++i) {
            if (nums[i] > d[len]) {
                d[++len] = nums[i];
            } else {
                int l = 1, r = len, pos = 0; // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
                //找到第 1 个大于等于 nums[i] 的元素
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    if (d[mid] < nums[i]) {
                        pos = mid;
                        l = mid + 1;
                    } else {
                        r = mid - 1;
                    }
                }
                //找到后更新
                d[pos + 1] = nums[i];
            }
        }
        return len;
    }
}

零钱兑换

题目描述:
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。

示例:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1
  • 动态规划

    以示例为例
    F(i):金额为i时的最小硬币数量
    F(0):1 //金额为0不能由硬币组成
    F(1): 1 //F(1)=min(F(1-1),F(1-2),(1-5))+1 = 1
    F(2): 1 //F(2)=min(F(2-1),F(2-2),(2-5))+1 = 1

    F(11):3 //F(11)=min(F(11-1),F(11-2),(11-5))+1 = 3

class Solution {
    public int coinChange(int[] coins, int amount) {
        //dp表示金额为i的最少硬币数
        //最多的硬币情况是全是1,共有amount+1个状态(0)
        int dp[] = new int[amount + 1];
        //找最少的硬币个数,先赋值最大值
        Arrays.fill(dp,amount+1);
        //最小的硬币数为0
        dp[0] = 0;
        for(int i=1;i<=amount;i++){
            for(int coin:coins){
                //可以用该面值的硬币
                if(coin<=i){
                    //该金额的最小硬币数和该金额-硬币面值的最小硬币数+1对比,哪个较小
                    dp[i] = Math.min(dp[i],dp[i-coin]+1);
                }
            }
        }
        //若amount金额的最小硬币数还是一开始赋予的最大值,则表示没有任何一种硬币组合能组成总金额
        return dp[amount]>amount ? -1 : dp[amount];
    }
}
  • 记忆化搜索
    在这里插入图片描述
class Solution {
    //金额为i时可以换取的最少硬币数
    int[] count;
    public int coinChange(int[] coins, int amount) {
        if(amount < 1){
            return 0;
        }
        count = new int[amount];
        return find(coins,amount);
    }
    private int find(int[] coins,int amount){
        if(amount<0){
            return -1;
        }
        if(amount==0){
            return 0;
        }
        //该金额的最少硬币数已经计算过并记录了,直接返回即可
        if(count[amount-1] != 0){
            return count[amount-1];
        }
        int min = Integer.MAX_VALUE;
        for(int coin : coins){
            int res = find(coins,amount-coin);
            if(res >= 0 && res < min){
                //res兑换的硬币数+1
                min = res + 1;
            }
        }
        //若还是等于最大值,则表示没有可以兑换的硬币
        count[amount-1] = (min == Integer.MAX_VALUE) ? -1 :min;
        return count[amount-1];
    }
}

矩阵中的最长递增路径

题目描述:
给定一个 m x n 整数矩阵 matrix ,找出其中 最长递增路径 的长度。
对于每个单元格,你可以往上,下,左,右四个方向移动。 你 不能 在 对角线 方向上移动或移动到 边界外(即不允许环绕)。

示例:
在这里插入图片描述

输入:matrix = [[3,4,5],[3,2,6],[2,2,1]]
输出:4 
解释:最长递增路径是 [3, 4, 5, 6]。注意不允许在对角线方向上移动。
  • 深度优先搜索
class Solution {
    int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
    int rows,columns;
    public int longestIncreasingPath(int[][] matrix) {
        if(matrix == null || matrix.length==0 || matrix[0].length==0){
            return 0;
        }
        rows = matrix.length;
        columns = matrix[0].length;
        //记录每个单元格的最长递增路径长度
        int[][] m = new int[rows][columns];
        int ans = 0;
        for(int i=0;i<rows;i++){
            for(int j=0;j<columns;j++){
                ans = Math.max(ans,dfs(matrix,i,j,m));
            }
        }
        return ans;
    }

    private int dfs(int[][] matrix,int row,int column,int[][]m){
        //若已经记录过则直接返回
        if(m[row][column] != 0){
            return m[row][column];
        }
        ++m[row][column];
        for(int[] dir:dirs){
            int newRow = row+dir[0],newColumn = column+dir[1];
            if(newRow>=0 && newRow<rows && 
            newColumn >=0 && newColumn < columns && 
            matrix[newRow][newColumn] > matrix[row][column]){
                m[row][column] = Math.max(m[row][column],dfs(matrix,newRow,newColumn,m)+1);
            }
        }
        return m[row][column];

    }
}
  • 排序后查找

    dfs方法若按示例为例子,3的计算会连带着4的结果,但此时4作为起点的结果还未计算,此时就出现重复计算

    但如先计算大的值,再计算小的值就可以利用大的值的结果

class Solution {
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    public int longestIncreasingPath(int[][] matrix) {
        int rows = matrix.length;
        int columns = matrix[0].length;

        //先计算大的数,再计算小的数,所以先排序
        List<int[]> list = new ArrayList<>();
        for(int i=0;i<rows;i++){
            for(int j=0;j<columns;j++){
                //存储节点值以及坐标
                list.add(new int[]{matrix[i][j],i,j});
            }
        }
        list.sort((a,b)->b[0]-a[0]);

        int ans = 0;
        //记录该值出发的最长递增路径
        int[][]dp = new int[rows][columns];
        for(int i=0;i<rows;i++){
            Arrays.fill(dp[i],1);
        }
        for(int[] n:list){
            int val = n[0];
            int row = n[1];
            int column = n[2];
            for(int[] dir : dirs){
                int newRow = row + dir[0];
                int newColumn = column + dir[1];
                if(newRow>=0 && newRow<rows && 
                    newColumn >=0 && newColumn < columns && 
                    matrix[newRow][newColumn] > matrix[row][column]){
                        dp[row][column] = Math.max(dp[row][column],dp[newRow][newColumn]+1);

                }
            }
            ans = Math.max(ans,dp[row][column]);
        }
        return ans;
    }
}
  • 拓扑排序*

    保证先计算大的值,可以使用拓扑排序,把整个矩阵转换成有向无环图

    把符合题目要求的点连起来就是有一张有向无环图

在这里插入图片描述

class Solution {
    // 上下左右的方向
    int[][] dirs = new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public int longestIncreasingPath(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        // 记录每个节点的出度
        int[][] outDegree = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                for (int[] dir : dirs) {
                    int nextI = i + dir[0];
                    int nextJ = j + dir[1];
                    // 只要旁边节点的值比它大,它的出度就加1
                    if (nextI >= 0 && nextJ >= 0 && nextI < m && nextJ < n && matrix[nextI][nextJ] > matrix[i][j]) {
                        outDegree[i][j]++;
                    }
                }
            }
        }

        //出度为0的入队
        Queue<int[]> queue = new LinkedList<>();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (outDegree[i][j] == 0) {
                    queue.offer(new int[] {i, j});
                }
            }
        }

        int ans = 0;
        while (!queue.isEmpty()) {
            ans++;
            // 一次遍历一批,每遍历一批,相当于最长路径又加了一
            int size = queue.size();
            for (int c = 0; c < size; c++) {
                int[] pos = queue.poll();
                int i = pos[0];
                int j = pos[1];
                for (int[] dir : dirs) {
                    int preI = i + dir[0];
                    int preJ = j + dir[1];
                    if (preI >= 0 && preI < m && preJ >= 0 && preJ < n && matrix[preI][preJ] < matrix[i][j]) {
                        // 指向当前元素的节点的出度减1,减到0了入队
                        if (--outDegree[preI][preJ] == 0) {
                            queue.offer(new int[] {preI, preJ});
                        }
                    }
                }
            }
        }
        return ans;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值