"알고리즘 시리즈"의 동적 프로그래밍

소개

  면접관들이 자주 치르는 시험의 종류는 다양하고, 면접관마다 질문하는 내용이 다르지만, 대부분의 면접관들이 좋아하는 질문이 있다면 그것은 알고리즘 문제의 진정한 왕이 아닌 동적 프로그래밍일 것 입니다 . 왜 그런 말을 해? 가변성이 크기 때문에 주제의 법칙을 찾는 것이 어렵고 대부분의 경우에도 명확하지 않으므로 이 문제를 해결하려면 동적 프로그래밍을 사용해야 합니다. 비슷한 질문을 해본 적이 없다면 다음 동적 프로그래밍을 확실히 할 것이라고 확신할 수 없습니다. 새로운 문제가 발생하더라도 우리는 여전히 무력할 수 있습니다. 게다가 인터넷의 많은 콘텐츠는 이미 복잡한 일을 더욱 복잡하게 만들었습니다. 오늘 뺄셈을 할 때 문제 해결 단계 + 상태 전이 방정식 = 동적 프로그래밍 답변이라는 한 문장만 기억하면 됩니다 .

이론적 기초

  동적 프로그래밍 (영어: 동적 프로그래밍, DP라고도 함)은 수학 , 경영학 , 컴퓨터 과학 , 경제학 , 생물정보학 에서 원래의 문제를 상대적으로 간단한 하위 문제로 분해하여 복잡한 문제를 해결하는 데 사용되는 방법입니다 .
  동적 프로그래밍은 하위 문제가 중첩되고 최적의 하위 구조 속성이 있는 문제에 적용할 수 있는 경우가 많으며 모든 하위 문제의 결과를 기록하므로 동적 프로그래밍 방법은 순진한 솔루션보다 시간이 훨씬 적게 걸리는 경우가 많습니다.
  동적 프로그래밍에는 상향식하향식의 두 가지 문제 해결 방법이 있습니다. 하향식은 메모된 재귀이고 , 상향식은 재귀입니다 . 동적 프로그래밍을 사용하여 문제를 해결하는 것은 분명한 특성을 가지고 있는데, 일단 하위 문제를 해결하면 후속 계산 과정에서 수정되지 않습니다. 이러한 특징을 후유증이 없다고 합니다. 문제를 해결하는 과정은 방향성 비순환 그래프를 형성합니다 . 동적 프로그래밍은 각 하위 문제를 한 번만 해결하며 자연스러운 가지치기 기능을 갖고 있어 계산량을 줄여준다.

문제 해결 단계

  동적 프로그래밍에는 문제에 대한 정해진 답이 없으며 각 문제는 서로 다른 상태 전이 방정식 이지만 문제 해결 단계를 요약하면 동적 프로그래밍의 어려운 문제를 광범위하게 해결할 수 있습니다. 문제 해결 단계와 상태 전이 방정식은 모든 동적 프로그래밍 문제의 템플릿입니다. 일반적인 문제 해결 단계에는 다음 세 단계가 필요합니다.

  1. dp 테이블(dp 테이블)과 아래 첨자의 의미를 결정합니다
    . 각 변경 사항에 대한 저장 매체로 동적 규칙에 배열이 필요합니다. 때로는 실제로 배열의 역할을 하는 두 개의 변수만 사용할 수도 있습니다. 그리고 배열의 전체적인 의미각 첨자의 의미가 무엇을 나타내는지 명확하게 알아야 합니다 . 배열의 전체적인 의미는 무엇을 저장하는가? 입니다. 아래 첨자의 의미는 다음과 같습니다. 각 단계에서 저장하는 값의 의미는 무엇입니까 ? 이것을 이해해야만 문제를 마친 후에도 당황하지 않을 것이며, 다음 문제를 할 때에는 자신의 느낌에 의지해야 합니다. 그렇지 않다면 적어주세요.
  2. 재귀 공식 결정, 즉 상태 전이 방정식 결정
    상태 전이 방정식은 운동 규칙 문제 해결의 핵심입니다 .. 운동 규칙은 원래 문제를 여러 작은 단계로 분해하여 해결하는 것이라고 말하지 않았습니까? 작은 단계는 어떻게 변하는가? 아니면 각 단계를 어떻게 수행해야 합니까? 실제로 각 단계 사이의 변화 관계를 기술하는 것이 바로 이 상태 전이 방정식인데, 예를 들어 i에서 i + 1까지의 변화 함수가 우리의 상태 전이 방정식입니다 .
  3. dp 배열을 초기화하고 방향을 이동하는 방법 결정 전체 문제 해결 단계에서 dp 배열의 초기화이동 방향 에 대한 일부 세부 사항을
    무시하기 쉬운 경우가 있지만 실제로는 똑같이 중요합니다. 예를 들어 초기화할 때 0부터 시작하거나 1부터 시작하면 의미도 다르고 최종 결과도 다릅니다 . 또 다른 예는 순회 방향입니다. 항상 왼쪽에서 오른쪽으로 가는 것은 아닙니다. 때로는 2차원 배열의 경우 위에서 아래로, 오른쪽에서 왼쪽으로 갈 수도 있습니다 .

다른

  동적 프로그래밍에는 여전히 더 어려운 유형의 질문, 즉 배낭 문제가 있습니다 . 우리는 이 큰 소의 기사인 Nine Lectures on Backpacks를 연구할 수 있습니다 . 그 중 01 배낭 문제완전한 배낭 문제 에 중점을 둘 수 있는데 , 관련 내용은 나중에 기회가 되면 공유하도록 하겠습니다.

문제 해결 경험

  • 동적 프로그래밍은 매우 가변적이므로 질문의 의미를 찾으려면 더 많은 연습이 필요합니다.
  • 동적 질문의 배열 의미를 결정하는 것은 문제 해결의 핵심 중 하나입니다.
  • 상태 전이 방정식은 동적 규칙 문제의 핵심이며, 작성하면 한 방에 죽일 수 있습니다.
  • dp 배열이 초기화되는 방법은 탐색 방향만큼 중요하며 무시할 수 없습니다.
  • 동적 프로그래밍에는 고정된 문제 해결 템플릿이 없지만 통일된 문제 해결 단계가 있습니다. 문제 해결 단계와 상태 전이 방정식이 모든 동적 프로그래밍 문제의 템플릿입니다.
  • 동적 프로그래밍과 탐욕 알고리즘의 차이점은 탐욕에는 상태 파생이 없지만 로컬에서 가장 좋은 것을 직접 선택하고 동적 계획에서는 전역 정보를 고려해야 한다는 것입니다.
  • 배낭 문제는 동적 규칙의 고급 유형의 문제로 사용될 수 있습니다. 공부할 때는 01 배낭 문제와 전체 배낭 문제에 중점을 두세요.

알고리즘 주제

5. 가장 긴 회문 부분 문자열

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public static String longestPalindrome(String s) {
    // 边界条件判断
    if (s.length() < 2)
        return s;
    // start表示最长回文串开始的位置,
    // maxLen表示最长回文串的长度
    int start = 0, maxLen = 1;
    int length = s.length();
    boolean[][] dp = new boolean[length][length];
    for (int right = 1; right < length; right++) {
        for (int left = 0; left < right; left++) {
            // 如果两种字符不相同,肯定不能构成回文子串
            if (s.charAt(left) != s.charAt(right))
                continue;

            // 下面是s.charAt(left)和s.charAt(right)两个
            // 字符相同情况下的判断
            // 如果只有一个字符,肯定是回文子串
            if (right == left) {
                dp[left][right] = true;
            } else if (right - left <= 2) {
                // 类似于"aa"和"aba",也是回文子串
                dp[left][right] = true;
            } else {
                // 类似于"a******a",要判断他是否是回文子串,只需要
                // 判断"******"是否是回文子串即可
                dp[left][right] = dp[left + 1][right - 1];
            }
            // 如果字符串从left到right是回文子串,只需要保存最长的即可
            if (dp[left][right] && right - left + 1 > maxLen) {
                maxLen = right - left + 1;
                start = left;
            }
        }
    }
    // 截取最长的回文子串
    return s.substring(start, start + maxLen);
    }
}

10. 정규식 매칭

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();

        boolean[][] f = new boolean[m + 1][n + 1];
        f[0][0] = true;
        for (int i = 0; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (p.charAt(j - 1) == '*') {
                    f[i][j] = f[i][j - 2];
                    if (matches(s, p, i, j - 1)) {
                        f[i][j] = f[i][j] || f[i - 1][j];
                    }
                } else {
                    if (matches(s, p, i, j)) {
                        f[i][j] = f[i - 1][j - 1];
                    }
                }
            }
        }
        return f[m][n];
    }

    public boolean matches(String s, String p, int i, int j) {
        if (i == 0) {
            return false;
        }
        if (p.charAt(j - 1) == '.') {
            return true;
        }
        return s.charAt(i - 1) == p.charAt(j - 1);
    }
}

42. 빗물 잡기

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if (n == 0) {
            return 0;
        }

        int[] leftMax = new int[n];
        leftMax[0] = height[0];
        for (int i = 1; i < n; ++i) {
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }

        int[] rightMax = new int[n];
        rightMax[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans += Math.min(leftMax[i], rightMax[i]) - height[i];
        }
        return ans;
    }
}

44. 와일드카드 매칭

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true;
        for (int i = 1; i <= n; ++i) {
            if (p.charAt(i - 1) == '*') {
                dp[0][i] = true;
            } else {
                break;
            }
        }
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (p.charAt(j - 1) == '*') {
                    dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
                } else if (p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
            }
        }
        return dp[m][n];
    }
}

53. 최대 하위 배열 합계

여기에 이미지 설명을 삽입하세요
주제 분석: 현재 숫자를 오른쪽 경계로 하여 가장 큰 숫자를 찾은 다음, 그 중에서 가장 큰 숫자를 찾습니다.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int maxSubArray(int[] nums) {
        int res = nums[0];
        int max = nums[0];
        // 找出以当前数为右边界的最大数max,再从中找出res
        for (int i = 1; i < nums.length; i++) {
            if (max >= 0) {
                max += nums[i];
            } else {
                max = nums[i];
            }
            res = Math.max(res, max);
        }
        return res;
    }
}

62. 다른 길

여기에 이미지 설명을 삽입하세요
주제 분석: 매번 계산하는 대신 공간을 시간으로 교환하는 캐싱이 필요한 동적 프로그래밍의 단계에 따라 문제를 해결합니다.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int uniquePaths(int m, int n) {
        // 申请内存用于缓存子路径结果,不用每次都计算,提高算法效率
        int[][] nums = new int[m][n];
        return helper(nums, m - 1, n - 1);
    }

    public int helper(int[][] nums, int row, int column) {
        int res = 0;
        // 递归出口
        if (row == 0 && column == 0) {
            res = 1;
        }
        if (row > 0 && column == 0) {
            // 判断是否在缓存中
            if (nums[row - 1][column] != 0) {
                res = nums[row - 1][column];
            } else {
                res = helper(nums, row - 1, column);
                // 将结果缓存
                nums[row][column] = res;
            }
        }
        if (row == 0 && column > 0) {
            // 判断是否在缓存中
            if (nums[row][column - 1] != 0) {
                res = nums[row][column - 1];
            } else {
                res = helper(nums, row, column - 1);
                // 将结果缓存
                nums[row][column] = res;
            }
        }
        if (row > 0 && column > 0) {
            // 判断是否在缓存中
            if (nums[row - 1][column] != 0 && nums[row][column - 1] != 0) {
                res = nums[row - 1][column] + nums[row][column - 1];
            } else {
                res = helper(nums, row - 1, column) + helper(nums, row, column - 1);
                // 将结果缓存
                nums[row][column] = res;
            }
        }
        return res;
    }
}

63. 다른 길 II

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍 단계에 따라 문제를 해결할 수 있습니다. 이러한 종류의 문제는 일반적으로 역추적을 사용하여 모든 경로를 찾고 동적 규칙을 사용하여 모든 숫자를 찾을 수 있습니다 . 2차원 배열을 수행할 수 있습니다. .
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int row = obstacleGrid.length;
        int column = obstacleGrid[0].length;
        // 记录所有数量的数组
        int[][] nums = new int[row][column];
        nums[0][0] = 1;
        // 对二维数组做操作
        for (int r = 0; r < row; r++) {
            for (int c = 0; c < column; c++) {
                if (obstacleGrid[r][c] == 1) {
                    nums[r][c] = 0;
                } else if (r > 0 && c > 0) {
                    nums[r][c] = nums[r - 1][c] + nums[r][c - 1];
                } else if (r > 0) {
                    nums[r][c] = nums[r - 1][c];
                } else if (c > 0) {
                    nums[r][c] = nums[r][c - 1];
                }
            }
        }
        return nums[row - 1][column - 1];
    }
}

64. 최소 경로 합계

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계에 따라 문제를 해결합니다. 상태 전이 방정식은 다음과 같습니다. f(x,y) = min(f(x-1,y) + a[i], f(x,y-1 ) + a [i]).
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划 
 */
class Solution {
    public int minPathSum(int[][] grid) {
        int row = grid.length;
        int column = grid[0].length;
        // 申请一个结果缓存空间,缓存结果,避免重复计算
        int[][] map = new int[row][column];
        int res = helper(grid, map, row - 1, column - 1);
        return res;
    }

    public int helper(int[][] grid, int[][] map, int x, int y) {
        int sum = 0;
        if (x == 0 && y == 0) {
            return grid[0][0];
        }
        if (x > 0 && y > 0) {
            // 如果缓存里有,直接获取
            if (map[x][y] != 0) {
                sum = map[x][y];
            } else {
                // 多种情况下,选择小的路径
                sum = Math.min(helper(grid, map, x - 1, y) + grid[x][y], helper(grid, map, x, y - 1) + grid[x][y]);
                map[x][y] = sum;
            }
        }
        if (x > 0 && y == 0) {
            // 如果缓存里有,直接获取
            if (map[x][y] != 0) {
                sum = map[x][y];
            } else {
                sum = helper(grid, map, x - 1, y) + grid[x][y];
                map[x][y] = sum;
            }
        }
        if (x == 0 && y > 0) {
            // 如果缓存里有,直接获取
            if (map[x][y] != 0) {
                sum = map[x][y];
            } else {
                sum = helper(grid, map, x, y - 1) + grid[x][y];
                map[x][y] = sum;
            }
        }
        return sum;
    }
}

70. 계단 오르기

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍 단계에 따라 문제를 해결할 수 있습니다. 상태 전이 방정식은 f(n) = f(n-1) + f(n-2)입니다.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int climbStairs(int n) {
        // 申请内存,用以缓存分支结果,不用每次都计算该值,提升运行效率
        int[] nums = new int[46];
        return helper(nums, n);
    }

    public int helper(int[] nums, int n) {
        if (n == 1) {
            return 1;
        }
        if (n == 2) {
            return 2;
        }

        int n1 = 0;
        int n2 = 0;

        // 曾计算过该值,则直接使用
        if (nums[n - 1] != 0) {
            n1 = nums[n - 1];
        } else {
            n1 = helper(nums,n - 1);
            nums[n - 1] = n1;
        }

        // 曾计算过该值,则直接使用即可
        if (nums[n - 2] != 0) {
            n2 = nums[n - 2];
        } else {
            n2 = helper(nums, n - 2);
            nums[n - 2] = n2;
        }
        return n1 + n2;
    }
}

72. 거리 편집

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划 
 */
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m + 1][n + 1];
        // 初始化
        for (int i = 1; i <= m; i++) {
            dp[i][0] =  i;
        }
        for (int j = 1; j <= n; j++) {
            dp[0][j] = j;
        }
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 因为dp数组有效位从1开始
                // 所以当前遍历到的字符串的位置为i-1 | j-1
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
                }
            }
        }
        return dp[m][n];
    }
}

87. 문자열 스크램블링

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    // 记忆化搜索存储状态的数组
    // -1 表示 false,1 表示 true,0 表示未计算
    int[][][] memo;
    String s1, s2;

    public boolean isScramble(String s1, String s2) {
        int length = s1.length();
        this.memo = new int[length][length][length + 1];
        this.s1 = s1;
        this.s2 = s2;
        return dfs(0, 0, length);
    }

    // 第一个字符串从 i1 开始,第二个字符串从 i2 开始,子串的长度为 length,是否和谐
    public boolean dfs(int i1, int i2, int length) {
        if (memo[i1][i2][length] != 0) {
            return memo[i1][i2][length] == 1;
        }

        // 判断两个子串是否相等
        if (s1.substring(i1, i1 + length).equals(s2.substring(i2, i2 + length))) {
            memo[i1][i2][length] = 1;
            return true;
        }

        // 判断是否存在字符 c 在两个子串中出现的次数不同
        if (!checkIfSimilar(i1, i2, length)) {
            memo[i1][i2][length] = -1;
            return false;
        }
        
        // 枚举分割位置
        for (int i = 1; i < length; ++i) {
            // 不交换的情况
            if (dfs(i1, i2, i) && dfs(i1 + i, i2 + i, length - i)) {
                memo[i1][i2][length] = 1;
                return true;
            }
            // 交换的情况
            if (dfs(i1, i2 + length - i, i) && dfs(i1 + i, i2, length - i)) {
                memo[i1][i2][length] = 1;
                return true;
            }
        }

        memo[i1][i2][length] = -1;
        return false;
    }

    public boolean checkIfSimilar(int i1, int i2, int length) {
        Map<Character, Integer> freq = new HashMap<Character, Integer>();
        for (int i = i1; i < i1 + length; ++i) {
            char c = s1.charAt(i);
            freq.put(c, freq.getOrDefault(c, 0) + 1);
        }
        for (int i = i2; i < i2 + length; ++i) {
            char c = s2.charAt(i);
            freq.put(c, freq.getOrDefault(c, 0) - 1);
        }
        for (Map.Entry<Character, Integer> entry : freq.entrySet()) {
            int value = entry.getValue();
            if (value != 0) {
                return false;
            }
        }
        return true;
    }
}

91. 디코딩 방법

여기에 이미지 설명을 삽입하세요
주제 분석: 계단 걷기의 향상된 버전에서는 동적 프로그래밍의 단계에 따라 문제를 해결할 수 있습니다. 현재 숫자는 0입니다: dp[i] = dp[i-2]. 현재 숫자가 0이 아닙니다: dp[i] = dp[i-1];
코드는 다음과 같습니다.

/**
 * 动态规划
 */
class Solution {
    public int numDecodings(String s) {
        final int length = s.length();
        if(length == 0) return 0;
        if(s.charAt(0) == '0') return 0;

        int[] dp = new int[length+1];
        dp[0] = 1;

        for(int i=0;i<length;i++){
            dp[i+1] = s.charAt(i)=='0'?0:dp[i];
            if(i > 0 && (s.charAt(i-1) == '1' || (s.charAt(i-1) == '2' && s.charAt(i) <= '6'))){
                dp[i+1] += dp[i-1];
            }
        }
        
        return dp[length];
    }
}

96. 다양한 이진 검색 트리

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍 단계에 따라 문제를 풀 수 있습니다. 상태 전이 방정식은 다음과 같습니다. G(n) = G(0) G(n-1)+G(1) (n-2)+... +G(n-1)*G(0).
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int numTrees(int n) {
        if(n <= 2) return n;
        int[] dp = new int[n + 1];
        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;

        // 外层的循环为了填充这个dp数组
        for(int i = 3; i <=n ; i++ ){
            // 内层循环用来遍历各个元素用作根的情况
            for(int j = 1; j <= i; j++){
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
}

97. 인터리브 문자열

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int s1len = s1.length();
        int s2len = s2.length();
        int s3len = s3.length();

        if (s1len + s2len != s3len) return false;

        boolean[][] dp = new boolean[s1len + 1][s2len + 1];

        dp[0][0] = true;
        for (int i = 1; i <= s1len && (dp[i-1][0] && s1.charAt(i-1) == s3.charAt(i-1) ); i++) dp[i][0] = true;
        for (int i = 1; i <= s2len && (dp[0][i-1] && s2.charAt(i-1) == s3.charAt(i-1)); i++) dp[0][i] = true;

        for (int i = 1; i <= s1.length(); i++) { //s1
            for (int j = 1; j <= s2.length(); j++) { //s2

                dp[i][j] = (dp[i-1][j] && s1.charAt(i-1) == s3.charAt(i + j - 1))
                        || (dp[i][j-1] && s2.charAt(j-1) == s3.charAt(i + j -1));
            }

        }

        return dp[s1len][s2len];
    }
}

115. 다양한 하위 시퀀스

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划 
 */
class Solution {
    public int numDistinct(String s, String t) {
        // 以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        // 初始化
        for (int i = 0; i < s.length() + 1; i++) {
            dp[i][0] = 1;
        }

        for (int i = 1; i < s.length() + 1; i++) {
            for (int j = 1; j < t.length() + 1; j++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                }else{
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        
        return dp[s.length()][t.length()];
    }
}

118. 양희삼각형

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> res = new ArrayList<>();
        for (int i = 1; i <= numRows; i++) {
            // 每一行的结果
            List<Integer> list = new ArrayList<>();
            for (int j = 1; j <= i; j++) {
                // 第一列和最后一列为1
                if (j == 1 || j == i) {
                    list.add(1);
                } else {
                    list.add(res.get(i - 2).get(j - 2) + res.get(i - 2).get(j - 1));
                }
            }
            res.add(list);
        }
        return res;
    }
}

119. 양희삼각형 2

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public List<Integer> getRow(int rowIndex) {
        List<Integer> res = new ArrayList<>(rowIndex + 1);
        long cur = 1;
        for (int i = 0; i <= rowIndex; i++) {
            res.add((int) cur);
            cur = cur * (rowIndex - i) / (i + 1);
        }
        return res;
    }
}

120. 삼각형 최소 경로 합

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍 단계에 따라 문제를 해결합니다. 상태 전이 방정식: dp[j] = Math.min(dp[j],dp[j+1]) + curTr.get(j);.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        if (triangle == null || triangle.size() == 0){
            return 0;
        }
        // 滚动记录每一层的最小值
        int[] dp = new int[triangle.size()+1];

        for (int i = triangle.size() - 1; i >= 0; i--) {
            List<Integer> curTr = triangle.get(i);
            for (int j = 0; j < curTr.size(); j++) {
                // 这里的dp[j] 使用的时候默认是上一层的,赋值之后变成当前层
                dp[j] = Math.min(dp[j],dp[j+1]) + curTr.get(j);
            }
        }
        return dp[0];
    }
}

121. 주식을 사고팔기에 가장 좋은 시기

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍 단계에 따라 문제를 해결할 수 있습니다. 상태 전이 방정식은 다음과 같습니다. 이전 i일의 최대 수입 = max{이전 i-1일의 최대 수입, i-일의 가격 일-이전 i-1일의 최소 가격}.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length <= 1) {
            return 0;
        }
        int min = prices[0], max = 0;
        for (int i = 1; i < prices.length; i++) {
            // 状态转移方程式
            max = Math.max(max, prices[i] - min);
            // 更新最小值
            min = Math.min(min, prices[i]);
        }
        return max;
    }
}

122. 주식을 사고 파는 가장 좋은 시기 II

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; ++i) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
}

123. 주식을 사고 파는 가장 좋은 시기 III

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {

    public int maxProfit(int[] prices) {
        int len = prices.length;
        // 边界判断, 题目中 length >= 1, 所以可省去
        if (prices.length == 0) return 0;
        // dp[i][j] 中i表示第i天,j为[0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金
        int[][] dp = new int[len][5];
        dp[0][1] = -prices[0];
        // 初始化第二次买入的状态,是为了确保最后结果是最多两次买卖的最大利润
        dp[0][3] = -prices[0];

        for (int i = 1; i < len; i++) {
            dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
            dp[i][2] = Math.max(dp[i - 1][2], dp[i][1] + prices[i]);
            dp[i][3] = Math.max(dp[i - 1][3], dp[i][2] - prices[i]);
            dp[i][4] = Math.max(dp[i - 1][4], dp[i][3] + prices[i]);
        }

        return dp[len - 1][4];
    }
}

124. 이진 트리의 최대 경로 합

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    int maxSum = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        maxGain(root);
        return maxSum;
    }

    public int maxGain(TreeNode node) {
        if (node == null) {
            return 0;
        }
        
        // 递归计算左右子节点的最大贡献值
        // 只有在最大贡献值大于 0 时,才会选取对应子节点
        int leftGain = Math.max(maxGain(node.left), 0);
        int rightGain = Math.max(maxGain(node.right), 0);

        // 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
        int priceNewpath = node.val + leftGain + rightGain;

        // 更新答案
        maxSum = Math.max(maxSum, priceNewpath);

        // 返回节点的最大贡献值
        return node.val + Math.max(leftGain, rightGain);
    }
}

132. 분할 회문 II

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int minCut(String s) {
        int n = s.length();
        boolean[][] g = new boolean[n][n];
        for (int i = 0; i < n; ++i) {
            Arrays.fill(g[i], true);
        }

        for (int i = n - 1; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                g[i][j] = s.charAt(i) == s.charAt(j) && g[i + 1][j - 1];
            }
        }

        int[] f = new int[n];
        Arrays.fill(f, Integer.MAX_VALUE);
        for (int i = 0; i < n; ++i) {
            if (g[0][i]) {
                f[i] = 0;
            } else {
                for (int j = 0; j < i; ++j) {
                    if (g[j + 1][i]) {
                        f[i] = Math.min(f[i], f[j] + 1);
                    }
                }
            }
        }

        return f[n - 1];
    }
}

139. 단어 분할

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍 단계에 따라 문제를 해결합니다. 상태 전이 방정식은 if (wordDict.contains(s.substring(j,i)) && valid[j]) valid[i] = true;입니다.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // 表示能否拆分为一个或多个在字典中出现的单词
        boolean[] valid = new boolean[s.length() + 1];
        valid[0] = true;
        for (int i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i; j++) {
                if (wordDict.contains(s.substring(j,i)) && valid[j]) {
                    valid[i] = true;
                }
            }
        }
        return valid[s.length()];
    }
}

174. 던전 게임

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int calculateMinimumHP(int[][] dungeon) {
        int n = dungeon.length, m = dungeon[0].length;
        int[][] dp = new int[n + 1][m + 1];
        for (int i = 0; i <= n; ++i) {
            Arrays.fill(dp[i], Integer.MAX_VALUE);
        }
        dp[n][m - 1] = dp[n - 1][m] = 1;
        for (int i = n - 1; i >= 0; --i) {
            for (int j = m - 1; j >= 0; --j) {
                int minn = Math.min(dp[i + 1][j], dp[i][j + 1]);
                dp[i][j] = Math.max(minn - dungeon[i][j], 1);
            }
        }
        return dp[0][0];
    }
}

188. 주식을 사고 파는 가장 좋은 시기 IV

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int maxProfit(int k, int[] prices) {
        if (prices.length == 0) {
            return 0;
        }

        int n = prices.length;
        k = Math.min(k, n / 2);
        int[][] buy = new int[n][k + 1];
        int[][] sell = new int[n][k + 1];

        buy[0][0] = -prices[0];
        sell[0][0] = 0;
        for (int i = 1; i <= k; ++i) {
            buy[0][i] = sell[0][i] = Integer.MIN_VALUE / 2;
        }

        for (int i = 1; i < n; ++i) {
            buy[i][0] = Math.max(buy[i - 1][0], sell[i - 1][0] - prices[i]);
            for (int j = 1; j <= k; ++j) {
                buy[i][j] = Math.max(buy[i - 1][j], sell[i - 1][j] - prices[i]);
                sell[i][j] = Math.max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]);   
            }
        }

        return Arrays.stream(sell[n - 1]).max().getAsInt();
    }
}

198. 강도

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍 단계인 상태 전이 방정식: dp[i]=max(dp[i−2]+nums[i],dp[i−1])에 따라 문제를 해결합니다.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int rob(int[] nums) {
        int length = nums.length;
        if (nums == null || length == 0) {
            return 0;
        }
        if (length == 1) {
            return nums[0];
        }
        int[] dp = new int[length];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < length; i++) {
            dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[length - 1];
    }
}

213. 타가 劫舍 II

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍 단계에 따라 문제를 해결할 수 있으며, 원래 기반에서 첫 번째 또는 마지막 배열을 제거하고 다시 입력하는 것을 고려하십시오.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0)
            return 0;
        int len = nums.length;
        if (len == 1)
            return nums[0];
        return Math.max(robAction(nums, 0, len - 1), robAction(nums, 1, len));
    }

    int robAction(int[] nums, int start, int end) {
        int x = 0, y = 0, z = 0;
        for (int i = start; i < end; i++) {
            y = z;
            z = Math.max(y, x + nums[i]);
            x = y;
        }
        return z;
    }
}

221. 가장 큰 광장

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划 
 */
class Solution {
    public int maximalSquare(char[][] matrix) {
        int m = matrix.length;
        if(m < 1) return 0;
        int n = matrix[0].length;
        int max = 0;
        // 表示以第i行第j列为右下角所能构成的最大正方形边长
        int[][] dp = new int[m+1][n+1];
        
        for(int i = 1; i <= m; ++i) {
            for(int j = 1; j <= n; ++j) {
                if(matrix[i-1][j-1] == '1') {
                    dp[i][j] = 1 + Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1]));
                    max = Math.max(max, dp[i][j]); 
                }
            }
        }
        
        return max * max;
    }
}

264. 종수 II

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계에 따라 문제를 해결합니다. 추악한 수는 이전 추악한 수에 2, 3, 5를 곱하여 구해야 합니다. 왼쪽에서 오른쪽으로 세 개의 포인터를 사용하고 매번 시간을 가져옵니다. 가장 작은 추악한 숫자는 다음 숫자입니다.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int nthUglyNumber(int n) {
        int[] dp = new int[n + 1];
        dp[1] = 1;
        int p2 = 1, p3 = 1, p5 = 1;
        for (int i = 2; i <= n; i++) {
            int num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5;
            dp[i] = Math.min(Math.min(num2, num3), num5);
            if (dp[i] == num2) {
                p2++;
            }
            if (dp[i] == num3) {
                p3++;
            }
            if (dp[i] == num5) {
                p5++;
            }
        }
        return dp[n];
    }
}

279. 완벽한 사각형

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍의 단계를 따라 문제를 해결하세요.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int numSquares(int n) {
        int max = Integer.MAX_VALUE;
        // 和为i的完全平方数的最少数量为dp[i]
        int[] dp = new int[n + 1];
        // 初始化
        for (int j = 0; j <= n; j++) {
            dp[j] = max;
        }
        // 当和为0时,组合的个数为0
        dp[0] = 0;
        // 遍历物品
        for (int i = 1; i * i <= n; i++) {
            // 遍历背包
            for (int j = i * i; j <= n; j++) {
                if (dp[j - i * i] != max) {
                    dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
                }
            }
        }
        return dp[n];
    }
}

300. 가장 긴 증가 부분 수열

여기에 이미지 설명을 삽입하세요
주제 분석: 동적 프로그래밍 단계에 따라 문제를 해결합니다. 상태 전이 방정식은 dp[i] = max(dp[i], dp[j] + 1)입니다.
코드는 아래와 같이 표시됩니다.

/**
 * 动态规划
 */
class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        int res = 0;
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            // 取最长的子序列
            if(dp[i] > res) res = dp[i];
        }
        return res;
    }
}

홈페이지로 돌아가기

Leetcode 500개 이상의 질문에 대한 소감

다음

"알고리즘 시리즈" 디자인

추천

출처blog.csdn.net/qq_22136439/article/details/126798330