五.动态规划 (自用)

    • 背包问题

(1)01背包问题

f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i];
f[j] = max(f[j], f[j - v[i]] + w[i]);

二维写法

for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }

一维优化

for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= v[i]; j -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

可参考这个同学的讲解 https://www.acwing.com/solution/content/116859/

(2)完全背包问题

朴素做法 (会超时)

for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j ++ )
            for (int k = 0; k * v[i] <= j; k ++ )
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);

二维优化

for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }

一维优化

for (int i = 1; i <= n; i ++ )
        for (int j = v[i]; j <= m; j ++ )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

(3)多重背包问题

暴力朴素做法

for (int i = 1; i <= n; i ++ )
    for (int j = 0; j <= m; j ++ )
        for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
            f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);

(错解)用完全背包的思路优化

(正解)2^0 + 2^1 + ... + 2^(n - 1) = 2^n

凑出来 0 ~ s 的拼法

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12010, M = 2010;

int n, m;
int v[N], w[N];
int f[M];

int main()
{
    cin >> n >> m;

    int cnt = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;
        while (k <= s)
        {
            cnt ++ ;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if (s > 0)
        {
            cnt ++ ;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

    n = cnt;

    for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= v[i]; j -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;

    return 0;
}

(4)分组背包问题

for (int i = 1; i <= n; i ++ )
{
    cin >> s[i];
    for (int j = 0; j < s[i]; j ++ )
        cin >> v[i][j] >> w[i][j];
}

for (int i = 1; i <= n; i ++ )
    for (int j = 0; j <= m; j ++ )
        for (int k = 0; k < s[i]; k ++ )
            if (v[i][k] <= j)
                f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    • 线性DP

(1)数字三角形

    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            scanf("%d", &a[i][j]);

    for (int i = 0; i <= n; i ++ )
        for (int j = 0; j <= i + 1; j ++ )
            f[i][j] = -INF;

    f[1][1] = a[1][1];
    for (int i = 2; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);

    int res = -INF;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);

(2)最长上升子序列问题

朴素解法 O(n^2)

    for (int i = 1; i <= n; i ++ )
    {
        f[i] = 1; // 只有a[i]一个数
        for (int j = 1; j < i; j ++ )
            if (a[j] < a[i])
                f[i] = max(f[i], f[j] + 1);
    }

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);

如何保存最长子序列

for (int i = 1; i <= n; i ++ )
    {
        f[i] = 1; // 只有a[i]一个数
        g[i] = 0; // 
        for (int j = 1; j < i; j ++ )
            if (a[j] < a[i])
                if (f[i] < f[j] + 1)
                {
                    f[i] = f[j] + 1;
                    g[i] = j;
                }
    }

    int k = 1;
    for (int i = 1; i <= n; i ++ )
        if (f[k] < f[i])
            k = i;
    
    for (int i = 0, len = f[k]; i < len; i ++ )
    {
        cout << a[k] << ' ';
        k = g[k];
    }

优化解法(II)

长度相同的只存结尾数字小的

(1)q[] (2)长度是下标 (3)结尾数字大小是 q[] 的值

二分出来小于某个数的最大的数

    int len = 0;
    for (int i = 0; i < n; i ++ )
    {
        int l = 0, r = len;
        while (l < r)
        {
            int mid = l + r + 1 >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }// r 是 q[r] 比 a[i] 小的下标 
        len = max(len, r + 1);
        q[r + 1] = a[i];
    }
    cout << len << endl;

(3)最长上升公共子序列

f[i - 1][j] 包含 00 01

f[i][j - 1] 包含 00 10

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

(4)最短编辑距离

    // 初始化,如果 a[] 的长度为 0,或者 b[] 的长度为 0 
    for (int i = 0; i <= m; i ++ ) f[0][i] = i;
    for (int i = 0; i <= n; i ++ ) f[i][0] = i;

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
        }

编辑距离

#include <iostream>
#include <algorithm>
#include <string.h>

using namespace std;

const int N = 15, M = 1010;

int n, m;
int f[N][N];
char str[M][N];

int edit_distance(char a[], char b[])
{
    int la = strlen(a + 1), lb = strlen(b + 1);

    for (int i = 0; i <= lb; i ++ ) f[0][i] = i;
    for (int i = 0; i <= la; i ++ ) f[i][0] = i;

    for (int i = 1; i <= la; i ++ )
        for (int j = 1; j <= lb; j ++ )
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }

    return f[la][lb];
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i ++ ) scanf("%s", str[i] + 1);

    while (m -- )
    {
        char s[N];
        int limit;
        scanf("%s%d", s + 1, &limit);

        int res = 0;
        for (int i = 0; i < n; i ++ )
            if (edit_distance(str[i], s) <= limit)
                res ++ ;

        printf("%d\n", res);
    }

    return 0;
}

3.区间DP

(1)石子合并

每次合并两堆

前缀和求 i ~ j 的全部重量

    for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1]; // 前缀和 

    for (int len = 2; len <= n; len ++ )
        for (int i = 1; i + len - 1 <= n; i ++ )
        {
            int l = i, r = i + len - 1;
            f[l][r] = 1e8;
            for (int k = l; k < r; k ++ )
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }

    printf("%d\n", f[1][n]);
每次合并 n 堆 (较复杂,等以后找题写)

4.计数类DP

(1)整数划分

用完全背包问题的思路求解

f[i][j] = f[i - 1][j] + f[i][j - i];
f[j]    =               f[j - i];
    f[0] = 1;
    for (int i = 1; i <= n; i ++ )
        for (int j = i; j <= n; j ++ )
            f[j] = (f[j] + f[j - i]) % mod;

其他解法

f [ i - 1 ][ j - 1 ] 表示把 1 去掉

f [ i - j ][ j ] 表示把每个数减去 1

    f[1][1] = 1; // f[0][0] = 1;
    for (int i = 2; i <= n; i ++ ) // for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = (res + f[n][i]) % mod;

5.数位统计DP

(1)计数问题

求 a ~ b 中 0 ~ 9 出现的次数

求前缀

边界问题:

(1)1 出现在第一位

(2)当取 0 时,前三位 从 001 ~ abc - 1

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 10;

/*

001~abc-1, 999

abc
    1. num[i] < x, 0
    2. num[i] == x, 0~efg
    3. num[i] > x, 0~999

*/

int get(vector<int> num, int l, int r) // 求 xxx 
{
    int res = 0;
    for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
    return res;
}

int power10(int x) // 求 yyy 的位数 
{
    int res = 1;
    while (x -- ) res *= 10;
    return res;
}

int count(int n, int x)
{
    if (!n) return 0;

    vector<int> num; // 将 n 的每一位存在 num 中 
    while (n)
    {
        num.push_back(n % 10); // 低位 --> 高位 
        n /= 10;
    }
    n = num.size(); // n = n 的位数 

    int res = 0; // 出现的次数 
                      // 如果 x == 0 ,那么 0 在最高位不存在,所以从 最高位 - 1 开始计算 
    for (int i = n - 1 - !x; i >= 0; i -- ) // x 在第 i 位出现的次数 
    {
        if (i < n - 1)
        {
            res += get(num, n - 1, i + 1) * power10(i); 求 xxx 中出现的次数 
            if (!x) res -= power10(i); // 如果 x 等于 0 
        }

        if (num[i] == x) res += get(num, i - 1, 0) + 1;
        else if (num[i] > x) res += power10(i);
        //else res += 0;
    }

    return res;
}

int main()
{
    int a, b;
    while (cin >> a >> b , a)
    {
        if (a > b) swap(a, b); // 小数在前,大数在后 

        for (int i = 0; i <= 9; i ++ ) // 统计 0 ~ 9 出现的次数 
            cout << count(b, i) - count(a - 1, i) << ' '; // 用前缀和的思路求解 
        cout << endl;
    }

    return 0;
}

6.状态压缩DP(用一个整数表示一个状态,二进制数表示)

(1)蒙德里安的梦想

切割数 = 横向摆放小方格的方法数

朴素写法(1000ms)

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12, M = 1 << N; // M = 2^N 

int n, m;
long long f[N][M]; // 存储状态 
bool st[M]; // 判断每列是否合法 

int main()
{
    while (cin >> n >> m, n || m)
    {
        for (int i = 0; i < 1 << n; i ++ ) // 预处理 st[] ,判断每列相邻 0 的个数是不是偶数 
        {             // i < 2^n
            int cnt = 0;
            st[i] = true;
            for (int j = 0; j < n; j ++ )
                if (i >> j & 1) // 等价于 i 除以 2^j ,然后与 1 做与运算 
                { // 如果最后一位是 1 
                    if (cnt & 1) st[i] = false;// 判断 cnt 是不是偶数 
                    cnt = 0;
                }
                else cnt ++ ;
            if (cnt & 1) st[i] = false;
        }

        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )
            for (int j = 0; j < 1 << n; j ++ )
                for (int k = 0; k < 1 << n; k ++ )
                    if ((j & k) == 0 && st[j | k]) // j 和 k 在相邻列是合法的 
                        f[i][j] += f[i - 1][k];

        cout << f[m][0] << endl;
    }
    return 0;
}

视频中提前将 j 和 k 相邻列合法的情况提取出来

(2)最短Hamilton路径

#include <cstring> // memset(f, 0x3f, sizeof f)
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 20, M = 1 << N;

int n;
int w[N][N];
int f[M][N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            cin >> w[i][j]; // 权重 

    memset(f, 0x3f, sizeof f); // 初始化,将距离初始化为正无穷 
    f[1][0] = 0;

    for (int i = 0; i < 1 << n; i ++ ) // i 和 j 枚举每种状态 
        for (int j = 0; j < n; j ++ )
            if (i >> j & 1) // 如果从 0 走到 j ,那么 i 里一定要包含 j 
                for (int k = 0; k < n; k ++ ) // 枚举所有转移的状态,从哪个点转移过来 
                    if (i >> k & 1) // if ((i - (1 << j)) >> k & 1) 
                  // 第 k 位一定要是 1 // 如果想从 k 转移过来,那么 i 除去 j 这个点之后,一定要包含 k 
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);

    cout << f[(1 << n) - 1][n - 1] << endl;

    return 0;
}

7.树形DP

(1)没有上司的舞会(深搜)

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 6010;

int n;
int h[N], e[N], ne[N], idx;
int happy[N];
int f[N][2];
bool has_fa[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u)
{
    f[u][1] = happy[u];
                  // i != -1
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dfs(j);

        f[u][1] += f[j][0];
        f[u][0] += max(f[j][0], f[j][1]);
    }
}

int main()
{
    scanf("%d", &n);

    for (int i = 1; i <= n; i ++ ) scanf("%d", &happy[i]);

    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b;
        scanf("%d%d", &a, &b); // b 是上司 
        add(b, a); // b --> a 
        has_fa[a] = true; // a 有上司 
    }

    int root = 1;
    while (has_fa[root]) root ++ ; // 找根节点 

    dfs(root);

    printf("%d\n", max(f[root][0], f[root][1]));

    return 0;
}

8.记忆化搜索(递归)

(1)滑雪

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 310;

int n, m;
int g[N][N]; // 高度 
int f[N][N];

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; // 偏移量,四个方向 

int dp(int x, int y)
{
    int &v = f[x][y]; // 简化代码,用 v 来表示这个状态,C++ 特性,等价于 f[x][y] 
    if (v != -1) return v; // v 算过了,就返回 

    v = 1; // 初始化,最小值是 1 
    for (int i = 0; i < 4; i ++ ) // 枚举四个方向 
    {
        int a = x + dx[i], b = y + dy[i];
         // 在棋盘范围内                            // 高度要下降 
        if (a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b])
            v = max(v, dp(a, b) + 1);
    }

    return v;
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            scanf("%d", &g[i][j]);

    memset(f, -1, sizeof f);

    int res = 0;
    for (int i = 1; i <= n; i ++ ) // 枚举从每个点出发 
        for (int j = 1; j <= m; j ++ )
            res = max(res, dp(i, j));

    printf("%d\n", res);

    return 0;
}

记忆化搜索的优点

代码复杂度 低

思路简单

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值