Bootstrap

C++学习计划 - 动态规划

学习目标

  • 掌握背包 DP
  • 掌握最大连续子段和、最长上升子序列、最长公共子序列等问题
  • 掌握区间 DP、棋盘 DP、金字塔 DP 等其他 DP

学习时间

  • 周三晚上 18:00~21:00

DP 类型题目

数字三角形 - 金字塔 DP

题目内容

观察下面的数字金字塔。

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

        7 
      3   8 
    8   1   0 
  2   7   4   4 
4   5   2   6   5 

在上面的样例中,从 7 → 3 → 8 → 7 → 5 7\rightarrow3\rightarrow8\rightarrow7\rightarrow5 73875 的路径产生的答案最大。

题解

思路:金字塔 DP,状态转移方程为:
d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − 1 ] ) + a [ i ] [ j ] dp[i][j]=\max(dp[i-1][j],dp[i-1][j-1])+a[i][j] dp[i][j]=max(dp[i1][j],dp[i1][j1])+a[i][j]
代码:

#include <bits/stdc++.h>
using namespace std;
long long n, a[1005][1005], dp[1005][1005];
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) for (int j = 1; j <= i; j++) cin >> a[i][j];
    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];
    long long ans = 0;
    for (int i = 1; i <= n; i++) ans = max(ans, dp[n][i]);
    cout << ans << endl;
    return 0;
}

骨头收藏家 - 01背包

题目内容

许多年前,在泰迪的家乡有一个人,他被称为 “骨头收藏家” 。这个人喜欢收集各种各样的骨头,比如狗的,牛的,他也去了坟墓…… .

收集骨头的人有一个体积为 V V V 的大袋子,在他收集的过程中有很多骨头,很明显,不同的骨头有不同的价值,不同的体积,现在给出每根骨头在他的过程中的价值,你能计算出收集骨头的人能得到的最大总价值吗?

题解

思路:01背包,状态转移方程为:
d p [ j ] = d p [ j − w [ i ] ] + v [ i ] dp[j]=dp[j-w[i]]+v[i] dp[j]=dp[jw[i]]+v[i](一维 DP 数组更省空间)

代码:

#include <bits/stdc++.h>
using namespace std;
int n, V, dp[1005], w[1005], v[1005];
int main() {
    int t;
    cin >> t;
    while (t--) {
        cin >> n >> V;
        for (int i = 1; i <= n; i++) cin >> v[i];
        for (int i = 1; i <= n; i++) cin >> w[i];
        memset(dp, 0, sizeof(dp));
        for (int i = 1; i <= n; i++) {
            for (int j = V; j >= w[i]; j--) dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
        cout << dp[V] << endl;
    }
    return 0;
}

疯狂的采药 - 多重背包

题目内容

LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。

医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是 LiYuxiang,你能完成这个任务吗?

此题和原题的不同点:

  1. 每种草药可以无限制地疯狂采摘。

  2. 药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!

题解

思路:多重背包,和01背包做法 一模一样 相似,状态转移方程为:
d p [ j ] = d p [ j − a [ i ] ] + b [ i ] dp[j]=dp[j-a[i]]+b[i] dp[j]=dp[ja[i]]+b[i]
代码:

#include <bits/stdc++.h>
using namespace std;
long long T, m, a[10005], b[10005], dp[10000005];
int main() {
    cin >> T >> m;
    for (long long i = 1; i <= m; i++) cin >> a[i] >> b[i];
    for (long long i = 1; i <= m; i++) {
        for (long long j = a[i]; j <= T; j++) {
            dp[j] = max(dp[j], dp[j - a[i]] + b[i]);
        }
    }
    cout << dp[T] << endl;
    return 0;
}

最大连续子段和

题目内容

给定有 n n n 个整数(可能为负整数)组成的序列 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,,an,求该序列连续的子段和的最大值。

如果该序列的所有元素都是负整数时定义其最大子段和为 0 0 0。例如,当 ( a 1 , a 2 , a 3 , a 4 , a 5 ) = ( − 5 , 11 , − 4 , 13 , − 4 , − 2 ) (a_1,a_2,a_3,a_4,a_5)=(-5,11,-4,13,-4,-2) (a1,a2,a3,a4,a5)=(5,11,4,13,4,2) 时,最大子段和为 11 + ( − 4 ) + 13 = 20 11+(-4)+13=20 11+(4)+13=20

题解

思路:最大连续子段和,状态转移方程为:
d p [ i ] = { d p [ i − 1 ] ≥ 0 d p [ i − 1 ] + a [ i ] d p [ i − 1 ] < 0 a [ i ] dp[i] = \begin{cases} dp[i-1]\ge0 & dp[i-1]+a[i] \\ dp[i-1]<0 & a[i] \end{cases} dp[i]={dp[i1]0dp[i1]<0dp[i1]+a[i]a[i]
代码:

#include <bits/stdc++.h>
using namespace std;
const int inf = 1000000000;
int a[100005], dp[100005];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i], dp[i] = a[i];
    for (int i = 1; i <= n; i++) {
        if (dp[i - 1] >= 0) dp[i] += dp[i - 1];
    }
    int ans = -inf;
    for (int i = 1; i <= n; i++) ans = max(ans, dp[i]);
    cout << ans << endl;
    return 0;
}

最长上升子序列

题目内容

给定 N N N 个数,求这 N N N 个数的最长上升子序列的长度。

什么是最长上升子序列? 就是给你一个序列,请你在其中求出一段不断严格上升的部分,它不一定要连续。

比如: 2 , 3 , 4 , 7 2,3,4,7 2,3,4,7 2 , 3 , 4 , 6 2,3,4,6 2,3,4,6 就是序列 2 , 5 , 3 , 4 , 1 , 7 , 6 2,5,3,4,1,7,6 2,5,3,4,1,7,6 的两种选取方案。最长的长度是4。

题解

思路:最长上升子序列,状态转移方程为:
( 1 ≤ j < i   &   a [ j ] < a [ i ] ) d p [ i ] = max ⁡ ( d p [ i ] , d p [ j ] + 1 ) (1\le j<i\ \&\ a[j]<a[i])\quad dp[i]=\max(dp[i],dp[j]+1) (1j<i & a[j]<a[i])dp[i]=max(dp[i],dp[j]+1)
注意: d p [ i ] dp[i] dp[i] 需要初始化为 1 1 1

代码:

#include <bits/stdc++.h>
using namespace std;
int a[5005], dp[5005];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) {
        dp[i] = 1;
        for (int j = 1; j < i; j++) {
            if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1);
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) ans = max(ans, dp[i]);
    cout << ans << endl;
    return 0;
}

最长公共子序列

题目内容

给定两个长度为 n n n 的数列,求他们的最长公共子序列的长度。

题解

思路:最长上升子序列,状态转移方程为:
d p [ i ] [ j ] = { a [ i ] = b [ j ] d p [ i − 1 ] [ j − 1 ] + 1 a [ i ] ≠ b [ j ] max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j] = \begin{cases} a[i]=b[j] & dp[i-1][j-1]+1 \\ a[i]\ne b[j] & \max(dp[i-1][j],dp[i][j-1]) \end{cases} dp[i][j]={a[i]=b[j]a[i]=b[j]dp[i1][j1]+1max(dp[i1][j],dp[i][j1])

#include <bits/stdc++.h>
using namespace std;
int a[3005], b[3005], dp[3005][3005];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) cin >> b[i];
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (a[i] == b[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
            else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
        }
    }
    cout << dp[n][n] << endl;
    return 0;
}

最长锯齿子序列

题目内容

给定 n n n 个整数组成的序列 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,,an,请从中挑出尽量长的子序列,形成一个锯齿序列。所谓锯齿序列,就是它的差分序列(由相邻数字的差组成的序列)是正负交替的。为了避免差为 0 0 0 时不方便区分正负,保证给定的每个数字都不相同。

例如给定的序列是 1 , 3 , 5 , 2 , 4 , 6 1,3,5,2,4,6 1,3,5,2,4,6,那么它的子序列 1 , 5 , 2 , 6 1,5,2,6 1,5,2,6 是一个锯齿序列,因为它的差分序列是 4 , − 3 , 4 4,-3,4 4,3,4;而 1 , 3 , 5 1,3,5 1,3,5 不是,因为这三个数字是递增的。

题解

思路:最长锯齿子序列,状态转移方程为:
( 1 ≤ j < i ) { a [ i ] > b [ j ] d p [ 1 ] [ i ] = max ⁡ ( d p [ 1 ] [ i ] , d p [ 0 ] [ j ] + 1 ) a [ i ] < b [ j ] d p [ 0 ] [ i ] = max ⁡ ( d p [ 0 ] [ i ] , d p [ 1 ] [ j ] + 1 ) (1\le j<i)\quad \begin{cases} a[i]>b[j] & dp[1][i]= \max(dp[1][i],dp[0][j]+1)\\ a[i]<b[j] & dp[0][i]= \max(dp[0][i],dp[1][j]+1) \end{cases} (1j<i){a[i]>b[j]a[i]<b[j]dp[1][i]=max(dp[1][i],dp[0][j]+1)dp[0][i]=max(dp[0][i],dp[1][j]+1)
代码:

#include <bits/stdc++.h>
using namespace std;
int n, a[10005], dp[2][10005];
int main() {
    cin >> n;
    if (n <= 2) {
        cout << n << endl;
        return 0;
    }
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) {
        dp[0][i] = dp[1][i] = 1;
        for (int j = 1; j < i; j++) {
            if (a[j] < a[i]) dp[1][i] = max(dp[1][i], dp[0][j] + 1);
            else dp[0][i] = max(dp[0][i], dp[1][j] + 1);
        }
    }
    cout << max(dp[0][n], dp[1][n]) << endl;
    return 0;
}

合唱队 - 区间 DP

题目内容

为了在即将到来的晚会上有更好的演出效果,作为 AAA 合唱队负责人的小 A 需要将合唱队的人根据他们的身高排出一个队形。假定合唱队一共 n n n 个人,第i个人的身高为 h i ​ h_i​ hi ( 1000 ≤ h i ​ ≤ 2000 ) (1000\le h_i​\le2000) (1000hi2000),并已知任何两个人的身高都不同。假定最终排出的队形是 A A A 个人站成一排,为了简化问题,小 A 想出了如下排队的方式:他让所有的人先按任意顺序站成一个初始队形,然后从左到右按以下原则依次将每个人插入最终棑排出的队形中:

第一个人直接插入空的当前队形中。

对从第二个人开始的每个人,如果他比前面那个人高( h h h 较大),那么将他插入当前队形的最右边。如果他比前面那个人矮( h h h 较小),那么将他插入当前队形的最左边。

当n个人全部插入当前队形后便获得最终排出的队形。

例如,有 6 6 6 个人站成一个初始队形,身高依次为 1850 , 1900 , 1700 , 1650 , 1800 , 1750 1850,1900,1700,1650,1800,1750 1850,1900,1700,1650,1800,1750,那么小 A 会按以下步骤获得最终排出的队形:

1850 1850 1850

1850 , 1900 1850,1900 1850,1900,因为 1900 > 1850 1900>1850 1900>1850

1700 , 1850 , 1900 1700,1850,1900 1700,1850,1900,因为 1700 < 1900 1700<1900 1700<1900

1650 , 1700 , 1850 , 1900 1650,1700,1850,1900 1650,1700,1850,1900,因为 1650 < 1700 1650<1700 1650<1700

1650 , 1700 , 1850 , 1900 , 1800 1650,1700,1850,1900,1800 1650,1700,1850,1900,1800,因为 1800 > 1650 1800>1650 1800>1650

1750 , 1650 , 1700 , 1850 , 1900 , 1800 1750,1650,1700,1850,1900,1800 1750,1650,1700,1850,1900,1800,因为 1750 < 1800 1750<1800 1750<1800

因此,最终排出的队形是 1750 , 1650 , 1700 , 1850 , 1900 , 1800 1750,1650,1700,1850,1900,1800 1750,1650,1700,1850,1900,1800

小 A 心中有一个理想队形,他想知道多少种初始队形可以获得理想的队形。

请求出答案对 19650827 19650827 19650827 取模的值。

题解

思路:区间 DP,这类题的模板:

for (int len = 1; len <= n; len++) {
	for (int l = 1, r = len; r <= n; l++, r++) {
		dp[l][r] = 0x3f3f3f3f;
		for (int k = l; k <= r; k++) dp[l][r] = max(dp[l][r], dp[l][k] + dp[r][k];
	}
}

代码:

#include <bits/stdc++.h>
using namespace std;
long long n, a[1005], sum[1005], dp[1005][1005][2];
long long mod = 19650827;
int main() {
    cin >> n;
    for (long long i = 1; i <= n; i++) cin >> a[i], dp[i][i][0] = 1;
    for (long long len = 1; len < n; len++) {
        for (long long l = 1, r = l + len; r <= n; l++, r++) {
            if (a[l] < a[l + 1]) dp[l][r][0] += dp[l + 1][r][0];
            if (a[l] < a[r]) dp[l][r][0] += dp[l + 1][r][1];
            if (a[r] > a[r - 1]) dp[l][r][1] += dp[l][r - 1][1];
            if (a[r] > a[l]) dp[l][r][1] += dp[l][r - 1][0];
            dp[l][r][1] %= mod;
            dp[l][r][0] %= mod;
        }
    }
    cout << (dp[1][n][0] + dp[1][n][1]) % mod << endl;
    return 0;
}

过河卒 - 棋盘 DP

题目内容

棋盘上 A A A 点有一个过河卒,需要走到目标 B B B 点。卒行走的规则:可以向下、或者向右。同时在棋盘上 C C C 点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为 “马拦过河卒”。
棋盘用坐标表示, A A A ( 0 , 0 ) (0,0) (0,0) B B B ( n , m ) (n,m) (n,m) ,同样马的位置坐标是需要给出的。如果马能够控制 A A A 点,那么卒将寸步难行。

现在要求你计算出卒从 A A A 点能够到达 B B B 点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。

题解

思路:棋盘 DP,状态转移方程为:
d p [ i ] [ j ] = d p [ i ] [ j ] + { i = 0 d p [ i − 1 ] [ j ] j = 0 d p [ i ] [ j − 1 ] i ≠ 0   &   j ≠ 0 d p [ i ] [ j − 1 ] + d p [ i − 1 ] [ j ] dp[i][j] = dp[i][j]+ \begin{cases} i=0 & dp[i-1][j]\\ j=0 & dp[i][j-1]\\ i\ne0\ \&\ j\ne0 & dp[i][j-1]+dp[i-1][j] \end{cases} dp[i][j]=dp[i][j]+ i=0j=0i=0 & j=0dp[i1][j]dp[i][j1]dp[i][j1]+dp[i1][j]
注意: 特判马是否控制点 A A A 或点 B B B,若成立则直接输出 0 0 0

代码:

#include <bits/stdc++.h>
using namespace std;
bool v[25][25];
long long f[25][25];
int dir[8][2] = {{2, 1}, {2, -1}, {1, 2}, {1, -2}, {-2, 1}, {-2, -1}, {-1, 2}, {-1, -2}};
int main() {
    int n, m, x, y;
    cin >> n >> m >> x >> y;
    v[x][y] = true;
    for (int i = 0; i < 8; i++) {
        int sx = x + dir[i][0];
        int sy = y + dir[i][1];
        if (sx >= 0 && sx <= n && sy >= 0 && sy <= m) v[sx][sy] = true;
    }
    if (v[n][m] || v[0][0]) {
        cout << 0 << endl;
        return 0;
    }
    f[0][0] = 1;
    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            if (j) f[i][j] += f[i][j - 1]);
            if (i) f[i][j] += f[i - 1][j]);
            if (v[i][j]) f[i][j] = 0;
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

总结

DP 的题目实际并不难,考验的是找到正确的 DP 方式。

;