动态规划经典模型

什么是动态规划

动态规划 \(Dynamic Programming (DP)\) 是研究多步决策过程最优化问题的一种数学方法。在动态规划中,为了寻找一个问题的最优解(即最优决策过程),将整个问题划分成若干个相应的阶段,并在每个阶段都根据先前所作出的决策作出当前阶段最优决策,进而得出整个问题的最优解。

能采用动态规划求解的问题的一般要具有三个基本条件:

  • 重叠子问题
    (1)子问题是原大问题的小版本,计算步骤完全一样;
    (2)计算大问题的时候,需要多次重复计算小问题。
    例如 0/1背包:从小背包扩展到大背包,计算步骤完全一样
  • 最优子结构
    (1)大问题的最优解包含小问题的最优解;
    (2)可以通过小问题的最优解推导出大问题的最优解。
    例如 0/1背包:小背包的解用于大背包
  • 无后效性:只关心前面的结果,不关心前面的过程
    例如 0/1背包:只用到小背包的结果,与小背包如果计算无关

动态规划问题的基本分析步骤

  • 状态:依据经验,进行模型转化,定义状态
  • 转移:保证数据不会遗漏,方程可不可以转移
    • 初始化,边界处理
    • 分类依据:选|不选、增加|减少|不变、00|01|10|11、...
  • 目标:最后的答案
  • 优化:
    • 空间优化,状态压缩
    • 时间优化,二进制优化、数据结构优化、状态优化

背包问题

01背包

已知现在有 \(n\) 种物品,第 \(i\) 种物品的体积为 \(v_i\),价值为 \(w_i\),数量为 \(1\)
有一个容量为 \(m\) 的背包,问能装入背包的最大价值是多少,最大体积是多少。

  • 状态:\(f_{i,j}\) 表示前 \(i\) 种物品装入剩余容量为 \(j\) 的背包时能获得的最大价值。
  • 转移:\(f_{i,j} = max\{ f_{i-1,j},f_{i-1,j-v_i}+w_i \}.\)
    • 初始化:\(f_{i,j}=0\)
    • 分类依据:第 \(i\) 种物品选或不选
  • 目标:\(f_{n,m}\)
int slove1(){
    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]);
        }
    }
    return f[n][m];
}
  • 优化:时间上已经无法优化了,空间上可以使用滚动数组优化或者压缩到一维。
  • \(f_{j}=max\{ f_j, f_{j-v_i}+w_i\}\)
// int f[2][M]; 滚动数组优化
int slove2() {
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            f[i & 1][j] = f[(i - 1) & 1][j];
            if (j >= v[i])
                f[i & 1][j] = max(f[i & 1][j], f[(i - 1) & 1][j - v[i]] + w[i]);
        }
    }
    return f[n & 1][m];
}
// int f[M]; 状态压缩
int slove3() {
    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]);
    return f[m];
}

完全背包

已知现在有 \(n\) 种物品,第 \(i\) 种物品的体积为 \(v_i\),价值为 \(w_i\),数量无穷。
有一个容量为 \(m\) 的背包,问能装入背包的最大价值是多少,最大体积是多少。

  • 状态:\(f_{i,j}\) 表示前 \(i\) 种物品装入剩余容量为 \(j\) 的背包时能获得的最大价值。
  • 转移:\(f_{i,j} = max\{ f_{i-1,j-k*v_i}+k*w_i \}.\)
    • 分类依据:第 \(i\) 种物品选 \(k\) 个。
  • 目标:\(f_{n,m}\)
int slove1() {
    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 - k * v[i]] + k * w[i]);
    return f[n][m];
}
  • 优化:可以在选择第 i种物品的基础上继续选择,那么也就是需要 \(f_{i,j-v_i}\),所以可以按照 j 递增循环。
  • 如果上述描述难以理解,可以看如下公式简化
/* 公式简化
f[i, j]   = max{ f[i-1, j-k*v] +k*w };
                        k=0          k=1             k=2                   k=k
f[i, j]   = max{ f[i-1, j],   f[i-1, j-v] +w, f[i-1, j-2v]+2w, ..., f[i-1, j-kv]+kw };
f[i, j-v] = max{ f[i-1, j-v], f[i-1, j-2v]+w, f[i-1, j-3v]+2w, ..., f[i-1, j-kv]+(k-1)w };
f[i,j]    = max{ f[i-1, j], f[i, j-v]+w };
*/
int slove2() {
    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]);
        }
    return f[n][m];
}
  • 优化:状态压缩
  • \(f_j = max\{ f_j, f_{j-v_i}+w_i \}\)
int slove3() {
    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]);
    return f[m];
}

多重背包

已知现在有 \(n\) 种物品,第 \(i\) 种物品的体积为 \(v_i\),价值为 \(w_i\),数量为 \(s_i\)
有一个容量为 \(m\) 的背包,问能装入背包的最大价值是多少,最大体积是多少。

  • 状态:\(f_{i,j}\) 表示前 \(i\) 种物品装入剩余容量为 \(j\) 的背包时能获得的最大价值。
  • 转移:\(f_{i,j} = max\{ f_{i-1,j-k*v_i}+k*w_i \}.\)
    • 分类依据:第 \(i\) 种物品选 \(k\) 个。
  • 目标:\(f_{n,m}\)
int slove1() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        for (int j = 0; j <= m; j++)
            for (int k = 0; k * a <= j && k <= c; k++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * a] + k * b);
    }
    return f[n][m];
}
  • 优化:二进制拆分,将问题转化为01背包
int f[N], cnt;
int slove2() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int a, b, c, base = 1;
        cin >> a >> b >> c;
        while (c >= base) {  // 二进制拆分
            v[++cnt] = a * base, w[cnt] = b * base;
            c -= base, base *= 2;
        }
        if (c) v[++cnt] = a * c, w[cnt] = b * c;
    }
    // 转为 01背包
    for (int i = 1; i <= cnt; i++)
        for (int j = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    return f[m];
}
  • 单调队列优化

多重背包基础转移方程:\(f[i][j] = max{f[i-1][j-k*v] + k*w}.\)
可以发现: 每一个状态只能由 \(j-k*v\) 转移过来,也就是说能影响它的点,是模 \(v\) 同余的点。

所以可以按照模 \(v\) 的余数进行分组。

#include <bits/stdc++.h>
using namespace std;
const int N = 20010;
int n, m, q[N], f[N], g[N];

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof(f));
        for (int j = 0; j < v; j++) {
            int h = 0, t = -1;
            for (int k = j; k <= m; k += v) {
                if (h <= t && q[h] < k - s * v) h++;
                // while(h<=t && g[k]-(k-j)/v*w >= g[q[t]]-(q[t]-j)/v*w) t--;
                while (h <= t && g[k] >= g[q[t]] + (k - q[t]) / v * w) t--;
                q[++t] = k;
                f[k] = g[q[h]] + (k - q[h]) / v * w;
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

混合背包

\(n\) 种物品和一个容量是 \(m\) 的背包,第 \(i\) 种物品的体积是 \(v_i\),价值是 \(w_i\),数量是 \(s_i\)
当物品数量为 \(-1\)时,可以使用无限次,问能装入背包的最大价值是多少。

通过分析发现这是 01背包、完全背包、多重背包的混合。
可以每个单独解决,也可以直接将其转为多重背包。

分组背包

已知现在有 \(n\) 种物品,第 \(i\) 种物品的体积为 \(v_i\),价值为 \(w_i\),数量为 \(1\)
同时每个物品属于一个组,同组内最多只能选择一个物品。
有一个容量为 \(m\) 的背包,问能装入背包的最大价值是多少,最大体积是多少。

  • 状态:\(f_j\) 表示背包容量为 \(j\) 的最大价值
  • 转移:\(f_j=max\{ f_j, f_{j-v_{i,k}}+w_{i,k} \}\)
  • 例题:洛谷P1757 通天之分组背包
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e3 + 10, INF = 0x3f3f3f3f;
int n, m, v[N][N], w[N][N], cnt[N], f[N];
// v[i][j] - 第i组第j个物品的使用代价
int main() {
    cin >> m >> n;
    int a, b, c, t = 0;
    for (int i = 1; i <= n; i++) {
        cin >> a >> b >> c;
        int& s = ++cnt[c];
        v[c][s] = a, w[c][s] = b, t = max(t, c);
    }
    for (int i = 1; i <= t; i++)                // 第i组
        for (int j = m; j >= 0; j--)            // 背包容量
            for (int k = 1; k <= cnt[i]; k++)   // 第k个
                if (j >= v[i][k])
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    cout << f[m];
    return 0;
}

二维费用背包

\(n\) 种物品和一个容量是 \(m\) 的背包,背包能承受的最大重量是 \(q\)
\(i\) 种物品的体积是 \(v_i\),重量是 \(u_i\),价值是 \(w_i\)
每种物品可以使用一次,问能装入背包的最大价值是多少。

  • 状态:\(f_{i,j,k}\) 从前 \(i\)个物品选,费用 1不超过 \(j\),费用 2不超过 \(k\) 的最大价值。
  • 状态转移:\(f_{i,j,k}=max\{f_{i-1,j,k}, f_{i−1,j-v1_i, k-v2_i}+w_i \}\)
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 410;
int n, m, f[N][N], V1, V2, v1[N], v2[N], w[N];

int main() {
    cin >> V1 >> V2 >> n;
    for (int i = 1; i <= n; i++)
        cin >> v1[i] >> v2[i] >> w[i];
    for (int i = 1; i <= n; i++)
        for (int j = V1; j >= v1[i]; j--)
            for (int k = V2; k >= v2[i]; k--)
                f[j][k] = max(f[j][k], f[j - v1[i]][k - v2[i]] + w[i]);
    cout << f[V1][V2];
    return 0;
}

子序列问题

最长上升子序列(Longest Increasing Subsequence - LIS)

给定一个长度为 \(N\) 的数列,求数值严格单调递增的子序列的长度最长是多少。

  • 状态:\(f_{i}\) 表示以第 \(i\) 个数结尾的最长上升子序列长度。
  • 转移:\(f_i=max\{f_j+1\} , a_i > a_j\)
  • 目标:\(res=max\{f_i\}\)
int lis1() {
    int res = 0;
    for (int i = 1; i <= n; i++) {
        f[i] = 1;
        for (int j = 1; j < i; j++)
            if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
        res = max(res, f[i]);
    }
    return res;
}
  • 优化:最长上升子序列具有单调上升的特性,可以利用二分优化,这时候更像是贪心。
  • \(f[i]\) 表示长度为 \(i\) 的 LIS 末尾元素的最小值,\(len\) 表示当前已知 LIS 的长度。发现 \(f[i]\) 单调不降,考虑进来一个元素 \(a[i]\)
  1. \(a[i]>f[len]\),将该元素插入到 \(f\) 序列的末尾,同时 \(len\)++。
  2. \(a[i]≤f[len]\),找到 \(f\) 序列中第一个大于等于它的元素,替换它(二分查找)。
    初始化:\(f[1]=a[1], len=1\)
    目标答案:\(len\),复杂度:\(O(nlogn)\)
int f[N], len = 0;
int lis2() {
    for (int i = 1; i <= n; i++) {
        if (i == 1 || f[len] < a[i]) f[++len] = a[i];
        else *lower_bound(f + 1, f + 1 + len, a[i]) = a[i];
    }
    return len;
}

最长不下降子序列,需要修改两个细节。

int f[N], len = 0;
int lis3() {
    for (int i = 1; i <= n; i++) {
        if (i == 1 || f[len] <= a[i]) f[++len] = a[i];
        else *upper_bound(f + 1, f + 1 + len, a[i]) = a[i];
    }
    return len;
}

最长公共子序列(longest common subsequence - LCS)

给定两个长度分别为 \(N,M\) 的数列 \(A,B\),求两者的最长公共子序列的长度。

  • 状态:\(f_{i,j}\) 表示 \(A[1...i],B[1...j]\) 最长公共子序列长度。
  • 转移:

\[f_{i,j}=\begin{cases} f_{i-1,j-1}+1 , &A_i = B_j\\ max\{f_{i-1,j}, f_{i,j-1}\} , &A_i \neq B_j\\ \end{cases} \]

  • 目标:\(res=f_{n,m}\)
int lcs() {
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
            else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
    return f[n][m];
}
  • LCS AtCoder - dp_f:求 \(LCS\) 的串是什么?
#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 10;
int n, m, f[N][N];
string s, t;

void lcs() {  // s[0..n)  t[0...m)
    n = s.size(), m = t.size(), memset(f, 0, sizeof(f));
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (s[i - 1] == t[j - 1]) f[i][j] = f[i - 1][j - 1] + 1;
            else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
        }
    }
    int x = n, y = m;
    string res;
    while (x > 0 && y > 0) {
        if (f[x][y] == f[x - 1][y]) x--;
        else if (f[x][y] == f[x][y - 1]) y--;
        else res += s[x - 1], x--, y--;
    }
    reverse(res.begin(), res.end());
    cout << res << endl;
}
void lcs2() {  // s[1..n]  t[1...m]
    s = " " + s, t = " " + t;
    n = s.size() - 1, m = t.size() - 1, memset(f, 0, sizeof(f));
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (s[i] == t[j]) f[i][j] = f[i - 1][j - 1] + 1;
            else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
        }
    }
    int x = n, y = m;
    string res;
    while (x > 0 && y > 0) {
        if (f[x][y] == f[x - 1][y]) x--;
        else if (f[x][y] == f[x][y - 1]) y--;
        else res += s[x], x--, y--;
    }
    reverse(res.begin(), res.end());
    cout << res << endl;
}
int main() {
    while (cin >> s >> t) {
        // lcs();
        lcs2();
    }
    return 0;
}

区间DP

P1775 石子合并(弱化版)

  • P1775 石子合并(弱化版)
    设有 \(N(N \le 300)\) 堆石子排成一排,其编号为 \(1,2,3,\cdots,N\)。每堆石子有一定的质量 \(m_i\ (m_i \le 1000)\)。现在要将这 \(N\) 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。

【分析】区间DP
区间dp是一类通过合并小区间来求解大区间的问题,总的问题是求解整个区间,子问题是求解子区间,状态定义的核心在于如何表示区间。
本题我们可以定义 dp[i][j] 表示区间[i,j]的最优解(即左端点为第i个石子,右端点为第j个石子的区间合并成一堆的最小花费)。
例如区间[1,5]可以有很多种合并方式,如[1,1]+[2,5]、[1,2]+[ 3,5]、[1,3]+[4,5]、[1,4]+[5,5]

选择由代价最小的合并方式来转移。
\(dp[i,k]=min(dp[i,j]+dp[j+1,k]+cost) (i≤j<k)\)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 305, INF = 0x3f3f3f3f;
int t = 1, n, m, a[N];
ll s[N], f[N][N];

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
    memset(f, 0x3f, sizeof f);
    for (int len = 1; len <= n; len++) {
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            if (len == 1) f[i][j] = 0;
            else for (int k = i; k < j; k++)
                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
        }
    }
    cout << f[1][n];
    return 0;
}

P1880 [NOI1995] 石子合并

在一个圆形操场的四周摆放 \(N\) 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 \(2\) 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 \(N\) 堆石子合并成 \(1\) 堆的最小得分和最大得分。
\(1\leq N\leq 100\)\(0\leq a_i\leq 20\)

【分析】区间DP,破换成链,将数据 a[1...n] 拷贝一份给 a[n+1 ... 2*n],那么这时候对于新的数据进行区间DP即可,具体看代码。

#include <bits/stdc++.h>
using namespace std;
const int N = 210, INF = 0x3f3f3f3f, MOD = 1E9 + 7;
int n, w[N], s[N], f[N][N], g[N][N];

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> w[i], w[i + n] = w[i];
    for (int i = 1; i <= 2 * n; i++) s[i] = s[i - 1] + w[i];
    memset(f, 0x3f, sizeof f);
    memset(g, -0x3f, sizeof g);
    for (int len = 1; len <= n; len++)
        for (int l = 1; l + len - 1 <= 2 * n + 1; l++) {
            int r = l + len - 1;
            if (len == 1) f[l][r] = g[l][r] = 0;
            else 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]);
                g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
            }
        }
    int mn = INF, mx = -INF;
    for (int i = 1; i <= n; i++) {
        mn = min(mn, f[i][i + n - 1]);
        mx = max(mx, g[i][i + n - 1]);
    }
    cout << mn << endl << mx;
    return 0;
}
posted @ 2022-11-04 14:03  HelloHeBin  阅读(315)  评论(0编辑  收藏  举报