chapter12-动态规划

动态规划:就是用于求解优化问题的方法。优化问题比如说求最大值or最小值。动态规划的做法就是采用小心地策略进行暴力搜索,在多项式时间内求得最优解。

这里的小心策略就是复用子问题的解,将已解决子问题的答案保存下来,在需要子问题答案的时候便可以直接获得,而不需要重复计算,提高效率。拆分出来的子问题可以用递归的方式求解,而要复用子问题的解就要记忆化。

1.递推求解

接下来通过Fibonacci数列的求解来体会动态规划是如何降低时间复杂度的,从朴素的递归,到递归+记忆化(自顶向下求解),再到递推求解(自底向上求解)。

递归+记忆化
//Fibonacci数列求解的3种方式 2024-03-16
#include <iostream>
#include <cstdio>

using namespace std;
const int MAXN = 100;

//朴素递归求解,时间复杂度O(2^n),因为递归树的每个结点都要计算
int Fibonacci(int n) {
    int answer;
    if(n == 0 || n == 1) {
        answer = n;
    } else {
        answer = Fibonacci(n - 1) + Fibonacci(n - 2);
    }
    return answer;
}

//朴素递归+记忆化,时间复杂度O(n)
int memo[MAXN];
int Fibonacci2(int n) {
    if(memo[n] != -1) {
        return memo[n];
    }
    int answer;
    if(n == 0 || n == 1) {
        answer = n;
    } else {
        answer = Fibonacci2(n - 1) + Fibonacci2(n - 2);
    }
    memo[n] = answer;
    return answer;
}

//递推求解,时间复杂度O(n),和记忆化的本质和原理上是一模一样的
int fibo[MAXN];
int Fibonacci3(int n) {
    for(int i = 0; i <= n; ++i) {
        if(i == 0 || i == 1) {
            fibo[i] = i;
        } else {
            fibo[i] = fibo[i - 1] + fibo[i - 2];
        }
    }
    return fibo[n];
}


int main() {
    int n;
    fill(memo, memo + MAXN, -1);
    while(cin >> n) {
        // cout << Fibonacci(n) << endl;
        cout << Fibonacci2(n) << endl;
    }
    return 0;
}

动态规划的策略非常简单,就是将原始的问题拆分成多个子问题,并将子问题的结果记录下来,以方便我们之后复用这些子问题的解。

2.最大连续子序列和

动态规划最经典的问题之一。这个问题讨论的是,在一个给定的序列\(\{A1,A2,...,An\}\)中,找出一个连续的子序列\(\{Ai,...,Aj\}\),使得这个连续的子序列的和最大,输出这个最大的序列和。

最大连续子序列的和.jpg
这个问题难处理就在于左右两个端点都不定,所以我们首先假定右边端点A[j]是定的,在此基础上考虑把A[j]作为末尾的最大连续子序列和为可能为多少。

最大序列和
//最大连续子序列和 2024-03-17
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;
const int MAXN = 1e6 + 10;
const int INF = INT_MAX;

long long arr[MAXN];

//朴素递归+记忆化,O(n)
long long dp[MAXN];
long long MaxComponent(int n) {
    if(dp[n] != -1) {
        return dp[n];
    }
    long long answer;
    if(n == 0) {
        answer = arr[n];
    } else {
        answer = max(MaxComponent(n - 1) + arr[n], arr[n]);
    }
    dp[n] = answer;
    return answer;
}

//递推求解
long long dp2[MAXN];
void MaxComSum(int n) {
    for(int i = 0; i < n; ++i) {
        int answer;
        if(0 == i) {
            answer = arr[i];
        } else {
            answer = max(dp2[i - 1] + arr[i], arr[i]);
        }
        dp2[i] = answer;
    }
    return;
}

int main() {
    int n;
    while(cin >> n) {
        fill(dp, dp + MAXN, -1);
        for(int i = 0; i < n; ++i) {
            cin >> arr[i];
        }
        
        long long maximum = -INF;
        for(int i = 0; i < n; ++i) {
            maximum = max(maximum, MaxComponent(i));
        }
        cout << maximum << endl;
    }
    return 0;
}

3.最长递增子序列

动态规划最经典的问题之一。该问题描述的是在一个已知序列\(\{A_1,A_2,...,A_n\}\)中,取出若干元素(不必连续)组成一个新的序列\(\{A_x,...,A_y\}\),新序列中的各个数之间依旧保持原序列的先后顺序。若这个子序列中的任意下标\(x < y\)\(A_x < A_y\),则称该子序列为原序列的一个递增子序列。最长递增子序列问题就是求给定序列的所有递增子序列中最长的那个子序列长度

最长递增子序列.jpg
同样是固定右边的一个端点,往前推这个子序列可以有几个元素。

最长上升子序列
//最长递增子序列 2024-03-17
#include <iostream>
#include <cstdio>

using namespace std;

const int MAXN = 1000 + 10;
const int INF = INT_MAX;
int arr[MAXN];

//朴素递归
int Fun1(int n) {
    int answer;
    if(n == 0) { //A[0]前面没有其他值,作为递归出口
        answer = 1;
    } else {
        answer = 1;
        for(int i = 0; i < n; ++i) {
            if(arr[i] < arr[n]) {
                answer = max(answer, Fun1(i) + 1);
            }
        }
    }
    return answer;
}

//朴素递归+记忆化,O(n^2)
int memo[MAXN];
int Fun2(int n) {
    if(memo[n] != -1) {
        return memo[n];
    }
    int answer;
    if(n == 0) {
        answer = 1;
    } else {
        answer = 1;
        for(int i = 0; i < n; ++i) {
            if(arr[i] < arr[n]) {
                answer = max(answer, Fun2(i) + 1);
            }
        }
    }
    memo[n] = answer;
    return answer;
}

//递推求解
int dp[MAXN];
void Fun3(int n) {
    for(int j = 0; j < n; ++j) {
        int answer;
        if(j == 0) {
            answer = 1;
        } else {
            answer = 1;
            for(int i = 0; i < j; ++i) {
                if(arr[i] < arr[j]) {
                    answer = max(answer, dp[i] + 1);
                }
            }
        }
        dp[j] = answer;
    }
    return;
}

int main() {
    int n;
    while(cin >> n) {
        for(int i = 0; i < n; ++i) {
            cin >> arr[i];
        }

        //recursive
        // int maximum = 0;
        // for(int i = 0; i < n; ++i) {
        //     maximum = max(maximum, Fun1(i));
        // }
        // printf("%d\n", maximum);

        //recursive + memorize
        // fill(memo, memo + MAXN, -1);
        // int maximum = 0;
        // for(int i = 0; i < n; ++i) {
        //     maximum = max(maximum, Fun2(i));
        // }

        //递推
        fill(dp, dp + MAXN, -1);
        Fun3(n);
        int maximum = 0;
        for(int i = 0; i < n; ++i) {
            maximum = max(maximum, dp[i]);
        }
        printf("%d\n", maximum);
    }
    return 0;
}

最长公共子序列先留个坑,以后用到再说咯,也和上面的做法差不多,要固定一侧端点,分析有几种情况 ,递归出口是什么。

posted @ 2024-03-17 13:27  paopaotangzu  阅读(44)  评论(0)    收藏  举报