跳转至

动态规划 经典问题再入门

前置知识

动态规划 经典问题入门

目标

经典问题,递归实现,循环迭代实现,打印方案 积累代码经验

最长上升子序列问题

递归实现, 以子序列长度作为参数设计函数

// TLE

#include <bits/stdc++.h>

using namespace std;

const int N = 1e3 + 10;

int a[N], n;
int k[N], ret;                          // 用一个k[N]来存下标,不是直接存具体数值 B[k[N]]
                    // 而是存有关联的信息,下标
                    // 这是常见的“简化”

void dfs(int len){                         // 传递的参数len,是最长子序列的长度
    if (len > ret) ret = len;              

    for (int i = k[len] + 1; i <= n; i++)  // 枚举下一个位置是谁
        if (a[i] > a[k[len]]){             // 满足更长一点的条件,就加上
            k[len + 1] = i;
            dfs(len + 1);
        }
}

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    k[0] = 0, a[0] = -2e9;
    dfs(0);

    cout << ret << '\n';

    return 0;
}

// 这个搜索,和我们平时的dfs的编写,有些不同
// 平时的dfs,框架是问题的边界,求解子问题
// 这个函数,并没有显性的写出来边界,但是在for循环的时候,如果到了最后一个数,就不会进入for循环
// 也就实现了终止钻下去的过程
#include <bits/stdc++.h>

using namespace std;

const int N = 1e3 + 10;

int a[N], n;
int ret;                      // 我们发现这个K[]数组是多余,因为每次只用最后一个数
                      // 那么,这最后一个数,就作为函数参数吧

void dfs(int len, int tail){
    if (len > ret) ret = len;

    for (int i = tail + 1; i <= n; i++)
        if (a[i] > a[tail]){
            dfs(len + 1, i);
        }
}

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    a[0] = -2e9;
    dfs(0, 0);

    cout << ret << '\n';

    return 0;
}

递归实现, 子集枚举, 选或者不选

int numList[] = {5, 2, 7, 3, 4, 6}; 
// solution is finally set of 0s and 1s..pick or leave.
//               0  1  0  1  1  1

int m = 6;

// called with LIS(0, m)
int LIS(int i, int prev)
{
    if(i == m)
        return 0;

    int choice1 = LIS(i+1, prev);                // LEAVE
    int choice2 = 0;

    if(prev == m || numList[prev] <= numList[i])
        choice2 = LIS(i+1, i) + 1;

    return max(choice1, choice2);
}


LIS(0, m);  // 需要分析一下,为甚调用的时候prev,传的是m
            // 传-1行不行

循环实现, 刷表法, 填表法

// 刷表法
#include <bits/stdc++.h>

using namespace std;

const int N = 1e3 + 10;

int a[N], n;
int dp[N], ret;

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    // 刷表法
    a[0] = -2e9, dp[0] = 0;
    for (int i = 0; i < n; i++)                 // 枚举以i为结尾
        for (int j = i + 1; j <= n; j++)    // 枚举下一个数
            if (a[j] > a[i]) dp[j] = max(dp[j], dp[i] + 1); 

    for (int i = 1; i <= n; i++) ret = max(ret, dp[i]);
    cout << ret << '\n';

    return 0;
}
// 填表法
#include <bits/stdc++.h>

using namespace std;

const int N = 1e3 + 10;

int a[N], n;
int dp[N], ret;

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    // 填表法
    a[0] = -2e9, dp[0] = 0;
    for (int i = 1; i <= n; i++)                 // 枚举以i为结尾
        for (int j = 0; j < i; j++)          // 枚举从哪个数来
            if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1); 

    for (int i = 1; i <= n; i++) ret = max(ret, dp[i]);
    cout << ret << '\n';

    return 0;
}

// 刷表法,更符合人的正常思考
// 常用的填表法,当前状态,从哪些状态转移过来
// 逆向思维,而已

打印方案

#include <bits/stdc++.h>

using namespace std;

const int N = 1e3 + 10;

int a[N], n;
int dp[N], ret, tail;
int pre[N];                 // 记录方案的核心要点是:记录决策

void print(int u){
    if (u == 0) return ;

    print(pre[u]);
    printf("%d ", a[u]);
}

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    // 填表法
    a[0] = -2e9, dp[0] = 0;
    for (int i = 1; i <= n; i++)             // 枚举以i为结尾
        for (int j = 0; j < i; j++)      // 枚举从哪个数来
            if (a[j] < a[i]){
                if (dp[i] < dp[j] + 1){
                    dp[i] = dp[j] + 1;
                    pre[i] = j;
                }
            }

    for (int i = 1; i <= n; i++){
        if (ret < dp[i]) ret = dp[i], tail = i;
    }
    cout << ret << '\n';
    print(tail);                    // 找到LIS的最后一个字符,然后开始递归打印方案
    puts("");

    return 0;
}

最长公共子序列问题

递归实现

string str1 = "abcdefgzh";
string str2 = "ackghefhlmn";

// abcdefgzh
// 101011001

// ackghefhlmn
// 11000111000

// acefh

// called with LCS(0, 0)
int LCS(int i, int j)
{
    if(i == sz(str1) || j == sz(str2))
        return 0;

    if(str1[i] == str2[j])
        return 1 + LCS(i+1, j+1);

    int choice1 = LCS(i+1, j);
    int choice2 = LCS(i, j+1);

    return max(choice1, choice2);
}

循环实现, O(n^4)

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

char A[N], B[N];
int n, m, dp[N][N]; // dp[i][j]表示以A[i] B[j]作为结尾的两个子串,最长公共子序列的长度

int main(){
    cin >> n >> m;
    scanf("%s%s", A + 1, B + 1);

    // 这种枚举方式,是知道具体公共的位置,O(n^4) ,超时
    // 同时,我们也并不需要知道具体是哪个位置公共的
    for (int i = 0; i < n; i++)          // 枚举A取到了i位置, B取到了j位置
        for (int j = 0; j < m; j++){     
            if (A[i] != B[j]) continue;  // 当A[i] == B[j],就符合公共条件,可以看下一个位置

            for (int ni = i + 1; ni <= n; ni++)  // 两重循环枚举下一个可能位置是什么
                for (int nj = j + 1; nj <= m; nj++)
                    if (A[ni] == B[nj]) dp[ni][nj] = max(dp[ni][nj], dp[i][j] + 1);
        }

    int ret = 0;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            ret = max(ret, dp[i][j]);

    cout << ret << '\n';

    return 0;
}

循环实现, O(n^2)

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

char A[N], B[N];
int n, m, dp[N][N]; // dp[i][j]表示以A[i] B[j]作为结尾的两个子串,最长公共子序列的长度

int main(){
    cin >> n >> m;
    scanf("%s%s", A + 1, B + 1);

    for (int i = 1; i <= n; i++) dp[i][0] = 0;  // B串0个字符,公共长度就是0
    for (int j = 1; j <= m; j++) dp[0][j] = 0;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++){
            // dp[i-1][j] 以A[i-1] B[j]为结尾的两个子串,最长公共子序列长度
            // dp[i][j-1] 以A[i] B[j-1]为结尾的两个子串,最长公共子序列长度
                        // 并不需要子序列的结尾就是i,或者就是j
            dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); 
            if (A[i] == B[j]) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
        }

    cout << dp[n][m] << '\n';

    return 0;
}

打印方案

// 打印方案

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

char A[N], B[N];
int n, m, dp[N][N]; // dp[i][j]表示以A[i] B[j]作为结尾的两个子串,最长公共子序列的长度
int pre[N][N];      // 核心:记录如何转移的, 记决策

void print(int i, int j){
    if (i == 0 || j == 0) return ;

    if (pre[i][j] == 3){
        print(i - 1, j - 1);
        printf("%c", A[i]);
    }
    else if (pre[i][j] == 1) print(i - 1, j);
    else if (pre[i][j] == 2) print(i, j - 1);
}

int main(){
    cin >> n >> m;
    scanf("%s%s", A + 1, B + 1);

    for (int i = 1; i <= n; i++) dp[i][0] = 0;  // B串0个字符,公共长度就是0
    for (int j = 1; j <= m; j++) dp[0][j] = 0;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++){
            if (dp[i - 1][j] > dp[i][j - 1]){
                dp[i][j] = dp[i - 1][j];
                pre[i][j] = 1;
            }
            else{
                dp[i][j] = dp[i][j - 1];
                pre[i][j] = 2;
            }

            if (A[i] == B[j]){
                if (dp[i - 1][j - 1] + 1 > dp[i][j]){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    pre[i][j] = 3;
                }
            } 
        }

    cout << dp[n][m] << '\n';
    print(n, m);
    puts("");

    return 0;
}

数字三角形问题

递归实现, TLE

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

int a[N][N], dp[N][N], f[N][N];
int n;

int solve(int x, int y)
{
    // 问题的边界
    if (x == n) return a[x][y];

    // 求解子问题
    int u = solve(x + 1, y);
    int v = solve(x + 1, y + 1);

    if (u > v) return a[x][y] + u;
    else return a[x][y] + v;
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= i; j++)
            cin >> a[i][j];
    }

    cout << solve(1, 1) << '\n';

    return 0;
}

递归实现, 记忆化

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

int mem[N][N];
int a[N][N], dp[N][N], f[N][N];
int n;

int solve(int x, int y)
{
    //问题的边界
    if (x == n) return a[x][y];

    int &ret = mem[x][y];
    if (ret != -1) return ret;

    //求解子问题
    int u = solve(x + 1, y);
    int v = solve(x + 1, y + 1);

    if (u > v) return ret = a[x][y] + u;
    else return ret = a[x][y] + v;
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= i; j++)
            cin >> a[i][j];
    }

    //递归+记忆化
    memset(mem, -1, sizeof mem);
    cout << solve(1, 1) << '\n';

    return 0;
}

循环实现, 从上往下

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

int mem[N][N];
int a[N][N], dp[N][N], f[N][N];
int n;

void recur01()
{
    dp[1][1] = a[1][1];
    for (int i = 2; i <= n; i++)
        for (int j = 1; j <= i; j++)
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + a[i][j];

    int maxn = -2e9;
    for (int j = 1; j <= n; j++)
        maxn = max(maxn, dp[n][j]);

    cout << maxn << '\n';
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= i; j++)
            cin >> a[i][j];
    }

    recur01();

    return 0;
}

循环实现, 从下往上

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

int mem[N][N];
int a[N][N], dp[N][N], f[N][N];
int n;

void recur02()
{
    for (int j = 1; j <= n; j++) f[n][j] = a[n][j];

    for (int i = n - 1; i >= 1; i--)
        for (int j = i; j >= 1; j--){
            f[i][j] = a[i][j] + max(f[i+1][j], f[i+1][j+1]);
        }

    cout << f[1][1] << '\n';
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= i; j++)
            cin >> a[i][j];
    }

    recur02();

    return 0;
}

总结

经典问题,常看常新,多加回顾总结