C++——线性动态规划
线性动态规划
引入:
1.爬楼梯
爬楼梯类型的问题可谓是线性DP的入门题目以及经典中的经典。我们先来看一下题目。
爬楼梯
题目描述
有一天,三萩实在太无聊了,竟然无聊到去数台阶了。有一个楼梯一共有m级,刚开始三萩在第一级,他就想,若每次只能跨上一级或者二级,要走上m级,共有多少种走法?
输入格式
输入数据一行包括一个整数m(1≤m≤40),表示楼梯数。
输出格式
对于测试样例,输出一行一个整数,表示不同走法的数量。
题目分析
首先我们想一下,如果我们要走到第N阶楼梯,前一步应该在那一阶楼梯上?
因为每一次能走两个楼梯或者一个楼梯,所以上一步肯定在N-1或者N-2楼梯上。
DP数组的设计
我们令DP[N]表示走到第N阶楼梯的总方案数
状态转移与边界条件
我们走到第N阶楼梯的总方案数应该是所有走到N-2阶楼梯的方案数(走两个楼梯)+所有走到N-1阶楼梯的方案数(走一个楼梯)
DP[N] = DP[N - 1] + DP[N - 2];
参考代码
//递推
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m, dp[45];
cin >> n;
dp[0] = 0;//初始化
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= 40; i++)
dp[i] = dp[i - 1] + dp[i - 2];//状态转移方程
cout << dp[n];
}
2.数字三角形
数字三角形和爬楼梯是类似的,只不过“爬”的范围变成二维平面了,本质上是没有区别的。
数字三角形1
题目描述
给定一个由 n 行数字组成的数字三角形,一个示例如下图所示。试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大。
对于给定的由 n 行数字组成的数字三角形,编程计算从三角形的顶至底的路径经过的数字和的最大值。
输入格式
第 1 行是数字三角形的行数 n,1≤n≤100。接下来 n 行是数字三角形各行中的数字。所有数字在 0~99 之间。
输出格式
将计算结果输出, 第 1 行中的数是计算出的最大值。
题目分析
首先我们将数据重新排列一下
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
我们可以发现当我们走到第\(i,j\)个数据的时候,我们上一步一定是从\(i - 1, j - 1\)或者\(i - 1, j\)走过来的
DP数组的设计
我们令DP[i][j]表示走到\(i,j\)的最大值
状态转移与边界条件
这个题目和走楼梯的不同的是,并不是让我们计算方案数,而是计算最大值,最后一行的DP数组为\(dp[N][i]\),我们还要在最后一行中找出其中最大的那个才行
DP[i][j] = max(DP[i - 1][j - 1], DP[i - 1][j]) + a[i][j];
如图:
参考代码
#include <bits/stdc++.h>
using namespace std;
long long dp[101][101];
long long maxx = 0;
int a[101][101];
long long max(long long a, long long b)
{
if (a > b)
return a;
else return b;
return 0;
}
int main() {
int n;
cin >> n;
for (int i = 0; i <= n; i++)
for (int j = 0; j <= i; j++)
dp[i][j] = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
{
cin >> a[i][j];
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
dp[i][j] = max(dp[i - 1][j - 1], dp[i - 1][j]) + a[i][j];//状态转移
for (int i = 1; i <= n; i++)
maxx = max(maxx, dp[n][i]);//还要在最后一行找最大值
cout << maxx;
return 0;
}
3.倒序DP
有些题目正序不容易看出来状态是怎么转移的,如果倒过来看的话反而会会更加容易求解
充电桩的收益
题目描述
小可可在小区里安装了一个电动汽车充电桩,将自家充电桩的空闲时间开放给其他电动车用户付费使用。这种共享充电模式能充分提高闲置充电桩的利用率既可以让小可可获得收益,也缓解了其他车主的充电焦虑。现在共有 \(n\) 个使用充电桩的申请,编号从 \(0\) 到 \(n-1\)。小可可将按编号顺所依次处理所有申请,每个申请 \(Q_i(0\le i\le n-1)\) 信息包含两个正整数 \(a_i\) 和 \(b_i\)。
对于申请 \(Q_i\) 小可可有两种处理策略:
(1) 接受申请 \(Q_i\),将获得 \(a_i\) 元收益,但必须放弃接下来的 \(b_i\) 个申请。
(2) 拒绝申请 \(Q_i\),没有收益,继续处理下一个申请。
请帮助小可可计算出共享充电桩能获得的最大收益?
输入格式
共 \(n+1\) 行,第一行一个整数 \(n\),表示使用充电桩的申请数量。
接下 \(n\) 行,第 \(i\) 行包含两个正整数 \(a_i\) 和 \(b_i\)。表示接受申请 \(Q_i\),将获得 \(a_i\) 元收益,但必须放弃接下来的 \(b_i\) 个申请。
输出格式
一行一个正整数,表示小可可共享充电桩获得的最大收益。
样例
输入样例:
4
3 2
5 4
4 4
3 5
输出样例
6
解释#1
小可可共收到 \(4\) 个使用充电桩的申请,最佳策略为接受申请 \(0\) 和申请 \(3\)
(1)接受申请 \(0\),获得 \(3\) 元收益,但接下来 \(2\) 个申请都必须拒绝。
(2)接受申请 \(3\),获得 \(3\) 元收益。
总收益为:\(3\) 元 \(+3\) 元 \(=6\) 元。
题目分析
我们分析这道题目的时候会发现一个问题
这个题目好像不满足dp的无后效性,选一个的话就要放弃后面几个,明显是当前的决策会影响到后续的决策。
其实对于这样一个线性的DP问题,我们既可以从左往右看,也可以从右往左看,这道题如果从右往左看的话无疑会简单很多:
当前第n个充电桩我们有两个决策:选这个充电或者不充电,如果不充电的话,我们的收益应该不变。如果充电的话,我们当前的收益受到右边某个位置的收益的影响,而我们是从右往左dp的,不会对我们未来的决策产生影响
dp数组的设计
我们令DP[N]表示从右到左第N个充电桩为止的最大收益
状态转移与边界条件
我们走到第N个充电桩的收益取决于不选(和到N+1的方案收益一样),或者选(右边某个坐标的收益加上自己的收益),而我们最终的收益为\(dp[1]\)(因为是从右到左dp的,倒序最后一个就是正序的第一个)
dp[i] = max(dp[i], a[i] + (c > n ? 0 : dp[c]));
//c为右边某个充电桩,当c大于n时为0(因为该位置越界了,收益肯定为0)
参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
long long a[N], b[N];
long long dp[N];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
}
dp[n + 1] = 0;//初始收益为0
for (int i = n; i >= 1; i--) {
dp[i] = dp[i + 1];//拒绝
int c = i + b[i] + 1;
dp[i] = max(dp[i], a[i] + (c > n ? 0 : dp[c]));//接受
}
cout << dp[1];
}
4.LCS最长公共子序列
LCS是线性DP中的经典题目之一,我们先看题面
数字三角形1
题目描述
一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切地说,若给定序列X=<x1, x2,…, xm>,则另一序列Z=<z1, z2,…, zk>是X的子序列是指存在一个严格递增的下标序列 <i1, i2,…, ik>,使得对于所有j=1,2,…,k有
例如,序列Z=<B,C,D,B>是序列X=<A,B,C,B,D,A,B>的子序列,相应的递增下标序列为<2,3,5,7>。给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。例如,若X=<A, B, C, B, D, A, B>和Y=<B, D, C, A, B, A>,则序列<B, C, A>是X和Y的一个公共子序列,序列<B, C, B, A>也是X和Y的一个公共子序列。而且,后者是X和Y的一个最长公共子序列,因为X和Y没有长度大于4的公共子序列。
给定两个序列X=<x1, x2, …, xm>和Y=<y1, y2, … , yn>,要求找出X和Y的一个最长公共子序列。
输入格式
输入文件共有两行,每行为一个由大写字母构成的长度不超过200的字符串,表示序列X和Y。
输出格式
输出文件第一行为一个非负整数,表示所求得的最长公共子序列的长度,若不存在公共子序列,则输出文件仅有一行输出一个整数0,
样例
输入数据
ABCBDAB
BDCABA
输出数据
4
题目分析
如果用暴力法求解该题目,时间复杂度将会达到一个天文数字(指数级),所以这道题需要用DP求解,关键是数组的设计和状态转移。
DP数组的设计
我们令\(dp[i][j]\)表示A序列前i个元素和B序列前j个元素的最长公共子序列
例子:
状态转移与边界条件
当我们判断dp[i][j]
时,正好是以A序列第i个元素结尾和B序列第j个元素结尾。自然而然会有两种情况
- a[i] == b[j] 那么公共序列长度会加1
dp[i][j] = dp[i - 1][j - 1] + 1;
- a[i] != b[j] 那么公共序列长度并没有变化,但是\(dp[i][j]\)此时不应该等于\(dp[i-1][j-1]\),而是应该会变成两个子问题:A序列的前i个和B序列的前j - 1个以及A序列的前i - 1个和B序列的前j个。
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
如图:
最终我们求解的问题也应该是\(dp[i][j]\)
参考代码
#include <bits/stdc++.h>
using namespace std;
int dp[250][250];
int main() {
string str1, str2;
cin >> str1;
cin >> str2;
dp[0][0] = 0;
for (int i = 1; i <= str1.size(); i++)
for (int j = 1; j <= str1.size(); j++)
if (str1[i - 1] == str2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
cout << dp[str1.size()][str2.size()];
}
5.LIS最长递增子序列
LIS
也是一种经典的题型,重点在于dp
数组的设计
最长不下降子序列
题目描述
设有由 \(n\) 个不相同的整数组成的数列,记为:\(a_{1},a_{2},...,a_{n} \),例如 \(3,18,7,14,10,12,23,41,16,24\)。若存在 \(i1<i2<i3< \cdots < ie\) 且有 \(a_{i1}<a_{i2}<\cdots <a_{ie}\) 则称为长度为 \(e\) 的不下降序列。如上例中 \(3,18,23,24\) 就是一个长度为 \(4\) 的不下降序列,同时也有 \(3,7,10,12,16,24\) 长度为 \(6\) 的不下降序列。程序要求,当原数列给出之后,求出最长的不下降序列。
输入格式
第一行为 \(n \),表示 \(n\) 个数
第二行 \(n\) 个整数,数值之间用一个空格分隔
输出格式
最长不下降子序列的长度 。
样例
输入数据
3
1 2 3
输出数据
3
题目分析
这道题麻烦在我们不知道这个最长的数列是以那个元素结尾的(最大的元素不一定是结尾的那个元素)
DP数组的设计
我们令\(dp[i]\)表示以序列第i个元素结尾的序列的长度
例子:
状态转移与边界条件
当我们判断到\(dp[i]\)的时候,若要令以i结尾的序列最大,肯定要找一个j,且这个a[j]一定是小于a[i]的(保持递增),所以
dp[i] = max(dp[j]) + 1;//0 < j < i && a[j] < a[i]
我们并不知道这个序列是以那个元素结尾的,所以我们最终所求的结果为
for (int i = 1; i <= n; i++)
maxx = max(maxx, dp[i]);
参考代码
#include <bits/stdc++.h>
using namespace std;
int max(int a, int b)
{
if (a > b)
return a;
else
return b;
return 0;
}
int main() {
//dp数组设计为以当前数字结尾的最长不下降子序列的长度
int n, maxx = INT_MIN;
int a[1001], dp[1001];
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
dp[i] = 1;
for (int i = 2; i <= n; i++)
for (int j = 1; j < i; j++)
if (a[i] >= a[j])
{
dp[i] = max(dp[i], dp[j] + 1);
maxx = max(dp[i], maxx);
}
cout << maxx;
}
如图:
6.编辑距离
同样经典的DP类型题目,乍一看可能没思路,同样需要仔细分析
编辑距离
题目描述
设 A 和 B 是 2 个字符串。要用最少的字符操作将字符串 \(A\) 转换为字符串 \(B\)。这里所说的字符操作包括
(1)删除一个字符;
(2)插入一个字符;
(3)将一个字符改为另一个字符。
将字符串 \(A\) 变换为字符串 \(B\) 所用的最少字符操作数称为字符串 \(A\) 到 \(B\) 的编辑距离,记为 \(d(A,B)\)。试设计一个有效算法,对任给的 \(2\) 个字符串 \(A\) 和 \(B\),计算出它们的编辑距离 \(d(A,B)\)。
输入格式
第一行是字符串 \(A\),文件的第二行是字符串 \(B\)。
输出格式
输出编辑距离 \(d(A,B)\)。
样例
输入#1
fxpimu
xwrs
输出#1
5
题目分析
这道题给了我们三个操作,然后问我们要用最少的操作来改变其中一个串使得这个串和另一个串一模一样,看似复杂,实则分析每个状态后问题将会变得更加清晰
DP数组的设计
我们令\(dp[i][j]\)表示把A序列前i个字符转换成B序列前j个字符所需要的操作步骤
状态转移与边界条件
然后我们不断地判断当前的\(a[i]\)和\(b[j]\)
- 如果该两个字符相等 说明不用任何操作
dp[i][j] = dp[i - 1][j - 1]
如果这两个字符不相等的话
如果当前字符对应不上,会有三种操作,其中对于插入和删除操作我们要比较dp[i - 1][j]和dp[i][j - 1]这两种之前的状态谁的代价更低
插入dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1
删除dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1
替换dp[i][j] = dp[i - 1][j - 1] + 1
初始化
对于任意dp[0
][i]或者dp[i
][0]来说,无论你要进行什么操作使两个序列相同,最少次数都是i,因为其中一个序列长度为0
参考代码
#include <bits/stdc++.h>
using namespace std;
int min(int a, int b)
{
if (a > b)
return b;
else
return a;
return 0;
}
int dp[2005][2005];
int main() {
//字符编辑距离
//如果当前字符对应不上
//插入dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1
//删除dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1
//替换abc//abd dp[i][j] = dp[i - 1][j - 1] + 1
string str1, str2;
cin >> str1 >> str2;
for (int i = 0; (i <= str1.size() || i <= str2.size()); i++)
{
dp[i][0] = i;
dp[0][i] = i;
}
for (int i = 1; i <= str1.size(); i++)
for (int j = 1; j <= str2.size(); j++)
{
if (str1[i - 1] != str2[j - 1])
dp[i][j] = min((min(dp[i - 1][j], dp[i][j - 1]) + 1), (dp[i - 1][j - 1] + 1));
else
dp[i][j] = dp[i - 1][j - 1];
}
cout << dp[str1.size()][str2.size()];
}