[Likou] 동적 프로그래밍 질문의 "최고" 시리즈

1. 동적 프로그래밍 문제를 해결하는 단계

DP(동적 프로그래밍) 문제를 해결하기 위한 일반적인 아이디어는 다음 단계로 나눌 수 있습니다.

  1. 상태 정의 : 먼저 문제의 상태를 명확하게 정의해야 합니다. 상태는 문제를 설명하는 주요 정보이며 일반적으로 최적화해야 하는 변수를 포함합니다. 상태의 정의는 문제의 특성을 반영해야 하며 재귀적으로 또는 반복적으로 계산할 수 있습니다.
  2. 상태 전이 방정식 정의 : 상태가 정의되면 다음 단계는 상태 전이 방정식인 상태 간의 관계를 설정하는 것입니다. 상태 전이 방정식은 알려진 상태에서 새로운 상태를 계산하는 방법을 설명합니다. 이것이 동적 프로그래밍 문제의 핵심이다.
  3. 초기화 : 초기 상태 값, 일반적으로 문제의 경계 조건 또는 기본 사례를 결정합니다. 이러한 초기 상태는 동적 프로그래밍 알고리즘의 시작점입니다.
  4. 재귀 또는 반복 계산 : 상태 전이 방정식을 사용하여 초기 상태부터 시작하여 문제에 대한 최적의 솔루션을 점진적으로 계산합니다. 이는 상향식 반복 접근 방식이나 하향식 재귀 접근 방식을 통해 달성할 수 있습니다.
  5. 경로 기록(선택 사항): 최적 솔루션의 특정 경로를 복원해야 하는 경우 상태 전송 프로세스 중에 각 상태의 소스를 기록하여 이후 경로 복원을 용이하게 할 수 있습니다.
  6. 반환 결과 : 문제의 요구 사항에 따라 최종 상태에서 필요한 최적의 솔루션 또는 최적의 값을 얻습니다.
  7. 공간 복잡도 최적화(선택 사항): 경우에 따라 동적 프로그래밍 알고리즘의 공간 복잡도를 최적화할 수 있습니다. 예를 들어 모든 상태를 기록하는 대신 필요한 중간 상태만 유지합니다.

2. 고전적인 사례에 집중

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

문자열 s가 주어지면 s에서 가장 긴 회문 부분 문자열을 찾습니다.
문자열의 역순이 원래 문자열과 동일하면 해당 문자열을 회문 문자열이라고 합니다.

예시 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

예 2:

输入:s = "cbbd"
输出:"bb"
  • 아이디어
  1. 상태 정의: 문자열이 회문 하위 문자열인지 여부를 dp[i][j]나타내는 상태를 정의합니다.s[i...j]
  2. 상태 전이 방정식 정의
  • 문자와 동일하지 않으면 분명히 s[i]회문 문자열 이 아닙니다.s[j]s[i...j]
  • 캐릭터 s[i]와 동일 하다면s[j]
    • ij인접한 첨자, 즉 j-i==1이면 s[i...j]분명히 회문 문자열입니다.
    • i및 가 j인접한 첨자가 아닌 경우 회문인지 여부는 회문인지 여부 s[i...j]에 따라 달라집니다 .s[i+1...j-1]

그런 다음 상태 전이 방정식은 다음과 같습니다.

	if (s[i] == s[j]) {
    
    
        dp[i][j] = j - i == 1 ? true : dp[i + 1][j - 1];
     } else {
    
    
        dp[i][j] = false;
     }
  1. 초기화: 각 단일 문자는 회문 문자열이므로 (0<=i<n) dp[i][i]으로 초기화되고 기타 초기화는 다음과 같습니다.true false
  2. 재귀 또는 반복 계산: dp[i][j]의존하기 때문에 dp[i+1][j-1]상태가 더 짧은 문자열에서 더 긴 문자열로 전송됩니다.
  3. 반환 결과: 가장 긴 회문 문자열을 찾아야 하기 때문에 가장 긴 회문 부분 start문자열의 시작 첨자를 나타내는 변수와 maxLen가장 긴 회문 부분 문자열의 길이를 나타내는 변수를 정의한 다음 매번 회문 부분 문자열을 찾을 때마다 , 문자열의 길이가 현재 가장 긴 회문 부분 문자열의 길이보다 큰지 확인하고, 더 길면 가장 긴 회문 부분 문자열로 표시된 두 변수의 정보를 업데이트합니다. 최종 반환은 s.substr(start, maxLen)가장 긴 회문 부분 문자열입니다.
  • C++ 코드
class Solution {
    
    
public:
    string longestPalindrome(string s) {
    
    
        if (s.size() <= 1) {
    
    
            return s;
        }
        int n = s.size();
        // 定义状态:dp[i][j]表示s[i...j]是否为回文串
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        // 初始化
        for (int i = 0; i < n; ++i) {
    
    
            dp[i][i] = true;
        }
        int start = 0;  // 记录最长回文子串的起始下标
        int maxLen = 1; // 记录最长回文子串的长度(最少一个字符本身就是回文串)
        for (int len = 2; len <= n; ++len) {
    
      // 注意需要从长度较短的字符串向长度较长的字符串进行转移
            for (int i = 0; i < n; ++i) {
    
    
                int j = len + i - 1;
                if (j >= n) {
    
    
                    break;
                }
                // 状态转移方程
                if (s[i] == s[j]) {
    
    
                    dp[i][j] = j - i == 1 ? true : dp[i + 1][j - 1];
                } else {
    
    
                    dp[i][j] = false;
                }
                
                // 更新最长回文子串信息
                if (dp[i][j] && j - i + 1 > maxLen) {
    
    
                    start = i;
                    maxLen = j - i + 1;
                }
            }
        }
        return s.substr(start, maxLen);
    }
};

32. 가장 긴 유효 브래킷

'(' 및 ')'만 포함하는 문자열이 주어지면 가장 긴 유효(잘 구성된 연속) 대괄호 하위 문자열의 길이를 찾습니다.

예시 1:

输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"

예 2:

输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

예시 3:

输入:s = ""
输出:0

힌트:

0 <= s.length <= 3 * 104
s[i] 为 '(' 或 ')'
  • 아이디어
  1. dp[i]상태 정의: 담당자 사용문자 로 i끝나는 가장 긴 유효한 괄호 길이(i는 0부터 n-1까지 시작합니다.) 예를 (()들어 dp[0]=0,dp[1]=0,dp[2]=2;
  2. 상태 전이 방정식을 정의합니다.
  • s[i]왼쪽 대괄호인 경우 (, 당연히 dp[i]=0왼쪽 대괄호는 유효한 대괄호 문자열(예: 문자열 (() 의 끝 문자가 될 수 없기 때문입니다.
  • s[i]오른쪽 괄호인 경우 )이전 문자가 왼쪽 괄호인지 오른쪽 괄호인지에 따라 사례별로 논의해야 합니다.
    • s[i-1]왼쪽 대괄호인 (경우 ( dp[i]=dp[i-2]+2i>=2), [예: 문자열 (())(), dp[5]=dp[3]+2=4+2=6]
    • s[i-1]오른쪽 괄호인 경우 )먼저 i-1번째 문자로 끝나는 가장 긴 유효한 괄호 앞의 문자, 즉 s[i-dp[i-1]-1],
      • 이 문자가 왼쪽 대괄호인 경우 (, dp[i]=dp[i-1]+2+dp[i-dp[i-1]-2][예: string ()(()), dp[5]=dp[4]+2+dp[1]=2+2+2=6]
      • 이 문자가 오른쪽 대괄호인 경우 )( dp[i]=0유효한 대괄호 문자열을 구성할 수 없음) [예: )())유효하지 않은 문자열입니다]

상태 전이 방정식은 다음과 같이 축약됩니다.

if (s[i] == ')') {
    
    
    if (s[i - 1] == '(') {
    
    
        dp[i] = i >= 2 ? dp[i - 2] + 2 : 2;
    } else if (s[i - 1] == ')' && i - dp[i - 1] - 1 >= 0 && s[i - dp[i - 1] - 1] == '(') {
    
    
        dp[i] =  i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] + dp[i - 1] + 2 : dp[i - 1] + 2;
    }
}
  1. 초기화:dp[i]=0
  2. i재귀 또는 반복 계산: 같음 에서 1순회를 시작합니다 . 최소 두 문자가 유효한 대괄호를 형성할 수 있으므로 다음 dp[0]과 같아야 합니다.0
  3. 결과 반환: 각 계산 dp[i]프로세스가 동시에 업데이트됩니다.maxLen
  • C++ 코드
class Solution {
    
    
public:
    int longestValidParentheses(string s) {
    
    
        int n = s.size();
        vector<int> dp(n, 0);
        int maxLen = 0;
        for (int i = 1; i < n; ++i) {
    
    
            if (s[i] == '(') {
    
    
                continue;
            }
            if (s[i - 1] == '(') {
    
    
                dp[i] = i >= 2 ? dp[i - 2] + 2 : 2;
            } else if (s[i - 1] == ')' && i - dp[i - 1] - 1 >= 0 && s[i - dp[i - 1] - 1] == '(') {
    
    
                dp[i] =  i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] + dp[i - 1] + 2 : dp[i - 1] + 2;
            }
            maxLen = max(maxLen, dp[i]);
        }
        return maxLen;
    }
};

53. 최대 하위 배열 합계

정수 배열 nums가 주어지면 최대 합계(하위 배열에는 최소한 하나의 요소가 포함됨)가 있는 연속 하위 배열을 찾아 최대 합계를 반환하세요.

하위 배열은 배열의 연속된 부분입니다.

예시 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

예 2:

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

예시 3:

输入:nums = [5,4,-1,7,8]
输出:23

힌트:

1 <= nums.length <= 105
-104 <= nums[i] <= 104
  • 아이디어
  1. 상태 정의: 다음으로 dp[i]표시됨i번째 숫자로 끝나는 배열의 최대 합
  2. 상태 전이 방정식을 정의합니다.
    dp[i] = max(dp[i - 1] + nums[i], nums[i]); (i>=1)
    
    ith 숫자와 th 숫자로 끝나는 배열 i-1의 최대 합이 th 숫자 자체보다 작은 경우에는 i추가하지 않는 것이 좋습니다.
  3. 초기화:dp[0]=nums[0]
  4. 재귀 또는 반복 계산: i=1처음부터 끝까지 탐색n-1
  5. 반환 결과: maxSum초기 값을 로 설정하면 nums[0]각 후속 순회 라운드에서 maxSum크기가 업데이트됩니다 .
  • C++ 코드
    • dp 배열 버전:
      class Solution {
              
              
      public:
          int maxSubArray(vector<int>& nums) {
              
              
              int n = nums.size();
              // dp[i]表示以第i个数结尾的数组的最大连续子数组的和
              vector<int> dp(n, 0);
              // 初始化
              dp[0] = nums[0];
              int maxSum = nums[0];
              for (int i = 1; i < n; ++i) {
              
              
                  dp[i] = max(dp[i - 1] + nums[i], nums[i]);
                  maxSum = max(maxSum, dp[i]);
              }
              return maxSum;
      }
      
    • 단순화된 버전: 데이터 dp[i]만 사용하므로 변수를 직접 사용하여 대체할 수 있습니다.dp[i-1]sum
      class Solution {
              
              
      public:
          int maxSubArray(vector<int>& nums) {
              
              
              int n = nums.size();
              int sum = nums[0];
              int maxSum = sum;
              for (int i = 1; i < n; ++i) {
              
              
                  sum = max(sum + nums[i], nums[i]);
                  maxSum = max(maxSum, sum);
              }
              return maxSum;
          }
      };
      

추천

출처blog.csdn.net/weixin_36313227/article/details/133396298