[알고리즘 질문] 동적 프로그래밍 중간 단계에서 가장 긴 회문 부분 문자열, 괄호 생성, 점핑 게임

머리말

동적 프로그래밍(Dynamic Programming, 줄여서 DP)은 다단계 의사 결정 프로세스의 최적화 문제를 해결하는 방법입니다. 복잡한 문제를 중복되는 하위 문제로 분해하고 각 하위 문제에 대한 최적 솔루션을 유지하여 문제에 대한 최적 솔루션을 도출하는 전략입니다.

동적 프로그래밍의 주요 아이디어는 해결된 하위 문제의 최적 솔루션을 사용하여 더 큰 문제의 최적 솔루션을 도출하여 반복 계산을 피하는 것입니다. 따라서 동적 프로그래밍은 일반적으로 상향식 접근 방식을 사용하여 소규모 문제를 먼저 해결한 다음 전체 문제의 최적 솔루션이 해결될 때까지 점진적으로 대규모 문제를 도출합니다.

동적 프로그래밍에는 일반적으로 다음과 같은 기본 단계가 포함됩니다.

  1. 상태 정의: 문제를 여러 하위 문제로 나누고 하위 문제의 솔루션을 나타내는 상태를 정의합니다.
  2. 상태 전이 방정식 정의: 하위 문제 간의 관계에 따라 상태 전이 방정식, 즉 알려진 상태에서 미지 상태의 계산 프로세스를 추론하는 방법을 설계합니다.
  3. 초기 상태 결정: 가장 작은 하위 문제의 솔루션을 정의합니다.
  4. 상향식 솔루션: 상태 전이 방정식에 따라 모든 상태의 최적 솔루션을 계산합니다.
  5. 최적의 솔루션에서 문제의 솔루션을 구성합니다.

동적 프로그래밍은 최단 경로 문제, 배낭 문제, 가장 긴 공통 하위 시퀀스 문제, 편집 거리 문제 등과 같은 많은 실용적인 문제를 해결할 수 있습니다. 동시에 동적 프로그래밍은 분할 정복 알고리즘, 그리디 알고리즘 등과 같은 다른 많은 알고리즘의 핵심 아이디어이기도 합니다.

동적 프로그래밍은 다단계 의사 결정 과정의 최적화 문제를 해결하는 방법으로 복잡한 문제를 중첩되는 하위 문제로 분해하고 각 하위 문제의 최적 솔루션을 유지하여 문제의 최적 솔루션을 도출합니다. 동적 프로그래밍에는 상태 정의, 상태 전이 방정식 설계, 초기 상태 결정, 상향식 솔루션 및 문제 솔루션 구성과 같은 단계가 포함됩니다. 동적 프로그래밍은 많은 실제 문제를 해결할 수 있으며 다른 알고리즘의 핵심 아이디어 중 하나이기도 합니다.

1. 가장 긴 회문 하위 문자열

문자열 s가 주어지면 s에서 가장 긴 회문 부분 문자열을 찾습니다.

문자열의 역순이 원래 문자열과 동일한 경우 문자열을 회문이라고 합니다.

예 1:

입력: s = "babad"
출력: "bab"
설명: "aba"도 질문의 의미에 맞는 답입니다.

예 2:

입력: s = "cbbd"
출력: "bb"

출처: LeetCode.

1.1 아이디어

하위 문자열의 경우 회문이고 길이가 2보다 크면 처음과 마지막 두 글자를 제거한 후에도 여전히 회문입니다. 예를 들어 문자열 "ababa"의 경우 "bab"이 회문이라는 것을 이미 알고 있으면 "ababa"는 첫 글자와 마지막 글자가 모두 "a"이므로 회문이어야 합니다.

이 아이디어에 따르면 동적 프로그래밍 방법을 사용하여 이 문제를 해결할 수 있습니다. P(i,j)를 사용하여 문자열 s의 i에서 j까지의 문자로 구성된 문자열(이하 s[i:j]라고 함)이 회문인지 여부를 나타냅니다.
여기에 이미지 설명 삽입

여기서 "기타 상황"에는 두 가지 가능성이 있습니다.

  • s[i,j] 자체는 회문이 아닙니다.
  • i>j, 현재 s[i,j] 자체는 불법입니다.

그러면 동적 프로그래밍의 상태 전이 방정식을 작성할 수 있습니다.
여기에 이미지 설명 삽입
즉, s[i+1:j−1]만이 회문이고 s의 i와 j 문자가 같을 때 s[i:j] It 팰린드롬이 됩니다.

위의 모든 논의는 하위 문자열의 길이가 2보다 크다는 전제를 기반으로 합니다. 또한 동적 프로그래밍의 경계 조건, 즉 하위 문자열의 길이가 1 또는 2라는 경계 조건도 고려해야 합니다. 길이가 1인 하위 문자열의 경우 분명히 회문이고 길이가 2인 하위 문자열의 경우 두 문자가 같으면 회문입니다. 따라서 동적 프로그래밍을 위한 경계 조건을 작성할 수 있습니다.

여기에 이미지 설명 삽입

이 아이디어에 따르면 동적 계획법을 완성할 수 있으며 최종 답은 모든 P(i,j)=true 중 j-i+1(즉, 부분 문자열의 길이)의 최대값입니다. 참고: 상태 전이 방정식에서 길이가 짧은 문자열에서 길이가 긴 문자열로 변환하므로 동적 프로그래밍의 루프 순서에 주의해야 합니다.

1.2 코드 구현

#include <iostream>
#include <string>
#include <vector>

using namespace std;

class Solution {
    
    
public:
    string longestPalindrome(string s) {
    
    
        int n = s.size();
        if (n < 2) {
    
    
            return s;
        }

        int maxLen = 1;
        int begin = 0;
        // dp[i][j] 表示 s[i..j] 是否是回文串
        vector<vector<int>> dp(n, vector<int>(n));
        // 初始化:所有长度为 1 的子串都是回文串
        for (int i = 0; i < n; i++) {
    
    
            dp[i][i] = true;
        }
        // 递推开始
        // 先枚举子串长度
        for (int L = 2; L <= n; L++) {
    
    
            // 枚举左边界,左边界的上限设置可以宽松一些
            for (int i = 0; i < n; i++) {
    
    
                // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
                int j = L + i - 1;
                // 如果右边界越界,就可以退出当前循环
                if (j >= n) {
    
    
                    break;
                }

                if (s[i] != s[j]) {
    
    
                    dp[i][j] = false;
                } else {
    
    
                    if (j - i < 3) {
    
    
                        dp[i][j] = true;
                    } else {
    
    
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }

                // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
                if (dp[i][j] && j - i + 1 > maxLen) {
    
    
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substr(begin, maxLen);
    }
};

시간 복잡도: O (n 2 ) O(n^2)( 2 ).
공간 복잡도:O (n 2 ) O(n^2)( 2 )

2. 브래킷 생성

숫자 n은 괄호 생성의 로그를 나타내며 가능한 모든 괄호 조합을 생성할 수 있는 함수를 설계해 주십시오.

예 1:

입력: n = 3
출력: ["((()))", "(()())", "(())()", "()(())", "()()( )”]

예 2:

입력: n = 1
출력: ["()"]

출처: LeetCode.

2.1 아이디어

i<n에 대한 괄호의 가능한 모든 생성 순열을 알고 있을 때 i=n인 경우 전체 괄호 순열에서 가장 왼쪽 괄호를 고려합니다.
왼쪽 괄호여야 합니다. 그런 다음 해당 오른쪽 괄호와 함께 괄호 "( )"의 완전한 집합을 형성할 수 있습니다. 우리는 이 그룹이 n-1에 비해 추가된 괄호라고 생각합니다.

그렇다면 나머지 n-1개의 괄호 세트는 어디에 있을까요? 나머지 괄호는 새 괄호 세트의 내부 또는 외부(오른쪽)에 있습니다.

이제 i<n의 경우를 알았으므로 모든 경우를 반복할 수 있습니다.

"(" + [i=p일 때 모든 괄호의 배열과 조합] + ")" + [i=q일 때의 모든 괄호의 배열과 조합]

여기서 p + q = n-1이고 pq는 모두 음이 아닌 정수입니다.

실제로 위의 p를 0에서 n-1로 취하고 q를 n-1에서 0으로 취하면 모든 경우가 통과됩니다.

참고: 위의 순회는 반복되지 않습니다. 즉, (p1,q1)≠(p2,q2)일 때 위의 방법으로 취한 괄호의 조합은 달라야 합니다.

2.2 코드 구현

class Solution {
    
    
public:
    vector<string> generateParenthesis(int n) {
    
    
        //这里dp[i]表示的是i对括号形成的一堆合法的括号序列
        vector<vector<string>> dp(n + 1);
        // unordered_map<string, bool> f;
        //初始化
        dp[1].push_back("()");
        dp[0].push_back("");

        //从2开始计算
        for(int i = 2; i <= n; i ++)
        {
    
    
            // j从0开始枚举
            for(int j = 0; j < i; j ++)
            {
    
    
                for(auto s1: dp[j])
                {
    
    
                    string s4 = "(" + s1;
                    for(auto s2 : dp[i - 1 - j])
                    {
    
    
                        string s3 = s4 + ")" + s2;
                        dp[i].push_back(s3);
                    }
                }
            }
        }
        return dp[n];
    }
};


3. 점핑 게임 II

길이가 n인 인덱스가 0인 정수 배열 num이 주어집니다. 초기 위치는 nums[0]입니다.

각 요소 nums[i]는 인덱스 i에서 앞으로 이동할 최대 길이를 나타냅니다. 즉, nums[i]에 있는 경우 임의의 nums[i + j]로 이동할 수 있습니다.

0 <= j <= nums[i]
i + j < n은
nums[n - 1]에 도달하기 위한 최소 점프 수를 반환합니다. 생성된 테스트 케이스는 nums[n - 1]에 도달할 수 있습니다.

예 1:

입력: nums = [2,3,1,1,4]
출력: 2
설명: 마지막 위치에 도달하기 위한 최소 홉 수는 2입니다.
인덱스 0에서 인덱스 1로 점프하고 1단계 점프한 다음 3단계 점프하여 배열의 마지막 위치에 도달합니다.

예 2:

입력: 숫자 = [2,3,0,1,4]
출력: 2

3.2 아이디어

  1. 점핑 포인트로 사용되는 그리드의 점프 거리가 3이면 다음 3개의 그리드를 모두 점프 포인트로 사용할 수 있음을 의미합니다. 점프 지점으로 사용할 수 있는 모든 그리드에 대해 한 번 점프를 시도하고 점프할 수 있는 가장 먼 거리를 계속 업데이트할 수 있습니다.

  2. 이 점프 지점에서 점프하는 것을 첫 번째 점프라고 하면 다음 3개의 그리드에서 점프하는 것을 두 번째 점프라고 할 수 있습니다.

  3. 따라서 점프가 끝나면 다음 그리드에서 시작하여 지금까지 점프할 수 있는 가장 먼 거리가 다음 점프의 시작점입니다. for 루프를 사용하여 각 점프를 시뮬레이션하고, 한 번 점프한 후 다음 점프 지점의 범위를 업데이트하고, 새 범위 내에서 점프하고, 점프할 수 있는 가장 먼 거리를 업데이트합니다.

  4. 점프 횟수를 기록하고 끝까지 점프하면 결과가 나옵니다.
    여기에 이미지 설명 삽입

3.2 코드 구현

int jump(vector<int> &nums)
{
    
    
    int ans = 0;
    int start = 0;
    int end = 1;
    while (end < nums.size())
    {
    
    
        int maxPos = 0;
        for (int i = start; i < end; i++)
        {
    
    
            // 能跳到最远的距离
            maxPos = max(maxPos, i + nums[i]);
        }
        start = end;      // 下一次起跳点范围开始的格子
        end = maxPos + 1; // 下一次起跳点范围结束的格子
        ans++;            // 跳跃次数
    }
    return ans;
}

최적화:

  • 위의 코드 관찰에서 while에 포함된 for 루프에서 i가 처음부터 끝까지 실행됨을 알 수 있습니다.
  • 점프가 완료되면 다음에 점프할 수 있는 가장 먼 거리만 업데이트하면 됩니다.
  • 그리고 이 순간을 점프 수를 업데이트할 기회로 사용하십시오.
  • for 루프에서 처리할 수 있습니다.
int jump(vector<int>& nums)
{
    
    
    int ans = 0;
    int end = 0;
    int maxPos = 0;
    for (int i = 0; i < nums.size() - 1; i++)
    {
    
    
        maxPos = max(nums[i] + i, maxPos);
        if (i == end)
        {
    
    
            end = maxPos;
            ans++;
        }
    }
    return ans;
}

요약하다

동적 프로그래밍(Dynamic Programming)은 다단계 의사 결정 최적화 문제를 해결하는 방법으로 복잡한 문제를 중첩되는 하위 문제로 분해하고 각 하위 문제의 최적 솔루션을 유지하여 문제의 최적 솔루션을 도출합니다. 동적 프로그래밍은 최단 경로 문제, 배낭 문제, 가장 긴 공통 하위 시퀀스 문제, 편집 거리 문제 등과 같은 많은 실용적인 문제를 해결할 수 있습니다.

동적 프로그래밍의 기본 아이디어는 해결된 하위 문제의 최적 솔루션을 사용하여 더 큰 문제의 최적 솔루션을 도출하여 반복 계산을 피하는 것입니다. 일반적으로 상향식 접근 방식을 사용하여 소규모 문제를 먼저 해결한 다음 전체 문제의 최적 솔루션이 해결될 때까지 점진적으로 대규모 문제를 도출합니다.

동적 프로그래밍에는 일반적으로 다음과 같은 기본 단계가 포함됩니다.

  1. 상태 정의: 문제를 여러 하위 문제로 나누고 하위 문제의 솔루션을 나타내는 상태를 정의합니다.
  2. 상태 전이 방정식 정의: 하위 문제 간의 관계에 따라 상태 전이 방정식, 즉 알려진 상태에서 미지 상태의 계산 프로세스를 추론하는 방법을 설계합니다.
  3. 초기 상태 결정: 가장 작은 하위 문제의 솔루션을 정의합니다.
  4. 상향식 솔루션: 상태 전이 방정식에 따라 모든 상태의 최적 솔루션을 계산합니다.
  5. 최적의 솔루션에서 문제의 솔루션을 구성합니다.

동적 프로그래밍의 시간 복잡도는 일반적으로 O (n 2 ) O(n^2)( 2 )또는O (n 3) O (n ^ 3)( 3 )에서 공간 복잡도는 O(n)이며 여기서 n은 문제의 규모를 나타냅니다. 실제 응용 프로그램에서 공간 복잡성을 줄이기 위해 일반적으로 롤링 배열과 같은 기술을 사용하여 동적 프로그래밍 알고리즘을 최적화할 수 있습니다.

여기에 이미지 설명 삽입

Guess you like

Origin blog.csdn.net/Long_xu/article/details/131462122