기사 디렉토리
1. 동적 프로그래밍 문제를 해결하는 단계
DP(동적 프로그래밍) 문제를 해결하기 위한 일반적인 아이디어는 다음 단계로 나눌 수 있습니다.
- 상태 정의 : 먼저 문제의 상태를 명확하게 정의해야 합니다. 상태는 문제를 설명하는 주요 정보이며 일반적으로 최적화해야 하는 변수를 포함합니다. 상태의 정의는 문제의 특성을 반영해야 하며 재귀적으로 또는 반복적으로 계산할 수 있습니다.
- 상태 전이 방정식 정의 : 상태가 정의되면 다음 단계는 상태 전이 방정식인 상태 간의 관계를 설정하는 것입니다. 상태 전이 방정식은 알려진 상태에서 새로운 상태를 계산하는 방법을 설명합니다. 이것이 동적 프로그래밍 문제의 핵심이다.
- 초기화 : 초기 상태 값, 일반적으로 문제의 경계 조건 또는 기본 사례를 결정합니다. 이러한 초기 상태는 동적 프로그래밍 알고리즘의 시작점입니다.
- 재귀 또는 반복 계산 : 상태 전이 방정식을 사용하여 초기 상태부터 시작하여 문제에 대한 최적의 솔루션을 점진적으로 계산합니다. 이는 상향식 반복 접근 방식이나 하향식 재귀 접근 방식을 통해 달성할 수 있습니다.
- 경로 기록(선택 사항): 최적 솔루션의 특정 경로를 복원해야 하는 경우 상태 전송 프로세스 중에 각 상태의 소스를 기록하여 이후 경로 복원을 용이하게 할 수 있습니다.
- 반환 결과 : 문제의 요구 사항에 따라 최종 상태에서 필요한 최적의 솔루션 또는 최적의 값을 얻습니다.
- 공간 복잡도 최적화(선택 사항): 경우에 따라 동적 프로그래밍 알고리즘의 공간 복잡도를 최적화할 수 있습니다. 예를 들어 모든 상태를 기록하는 대신 필요한 중간 상태만 유지합니다.
2. 고전적인 사례에 집중
5. 가장 긴 회문 부분 문자열
문자열 s가 주어지면 s에서 가장 긴 회문 부분 문자열을 찾습니다.
문자열의 역순이 원래 문자열과 동일하면 해당 문자열을 회문 문자열이라고 합니다.
예시 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
예 2:
输入:s = "cbbd"
输出:"bb"
- 아이디어
- 상태 정의: 문자열이 회문 하위 문자열인지 여부를
dp[i][j]
나타내는 상태를 정의합니다.s[i...j]
- 상태 전이 방정식 정의
- 문자와 동일하지 않으면 분명히
s[i]
회문 문자열 이 아닙니다.s[j]
s[i...j]
- 캐릭터
s[i]
와 동일 하다면s[j]
-
- 와
i
가j
인접한 첨자, 즉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;
}
- 초기화: 각 단일 문자는 회문 문자열이므로 (0<=i<n)
dp[i][i]
으로 초기화되고 기타 초기화는 다음과 같습니다.true
false
- 재귀 또는 반복 계산:
dp[i][j]
의존하기 때문에dp[i+1][j-1]
상태가 더 짧은 문자열에서 더 긴 문자열로 전송됩니다. - 반환 결과: 가장 긴 회문 문자열을 찾아야 하기 때문에 가장 긴 회문 부분
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] 为 '(' 或 ')'
- 아이디어
dp[i]
상태 정의: 담당자 사용문자 로i
끝나는 가장 긴 유효한 괄호 길이(i는 0부터 n-1까지 시작합니다.) 예를(()
들어dp[0]=0,dp[1]=0,dp[2]=2
;- 상태 전이 방정식을 정의합니다.
s[i]
왼쪽 대괄호인 경우(
, 당연히dp[i]=0
왼쪽 대괄호는 유효한 대괄호 문자열(예: 문자열((
) 의 끝 문자가 될 수 없기 때문입니다.s[i]
오른쪽 괄호인 경우)
이전 문자가 왼쪽 괄호인지 오른쪽 괄호인지에 따라 사례별로 논의해야 합니다.-
s[i-1]
왼쪽 대괄호인(
경우 (dp[i]=dp[i-2]+2
i>=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;
}
}
- 초기화:
dp[i]=0
i
재귀 또는 반복 계산: 같음 에서1
순회를 시작합니다 . 최소 두 문자가 유효한 대괄호를 형성할 수 있으므로 다음dp[0]
과 같아야 합니다.0
- 결과 반환: 각 계산
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
- 아이디어
- 상태 정의: 다음으로
dp[i]
표시됨i번째 숫자로 끝나는 배열의 최대 합 - 상태 전이 방정식을 정의합니다.
dp[i] = max(dp[i - 1] + nums[i], nums[i]); (i>=1)
i
th 숫자와 th 숫자로 끝나는 배열i-1
의 최대 합이 th 숫자 자체보다 작은 경우에는i
추가하지 않는 것이 좋습니다. - 초기화:
dp[0]=nums[0]
- 재귀 또는 반복 계산:
i=1
처음부터 끝까지 탐색n-1
- 반환 결과:
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 배열 버전:
-
- 단순화된 버전: 데이터
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; } };
- 단순화된 버전: 데이터