目录
动态规划适用的基本条件:
满足无后效性
- "过去的步骤只能通过当前状态影响未来的发展,当前的状态是历史的总结"
这条特征说明动态规划只适用于解决当前决策与过去状态无关的问题。
- 如果当前问题的具体决策,会对解决其它未来的问题产生影响,如果产生影响,就无法保证决
策的最优性。
(如果可以通过排序等方法来消除后效性,也可以使用动态规划。例如下面例题(咸鱼翻身)。)
最大字段和
给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。
输入数据
7
2 -4 3 -1 2 -4 3
输出数据
4
分析:一道很好的dp入门题
我们要设置好转化过程,及把大问题拆分成小问题,并之间要有合理联系。这里设置dp[i]表示以i结尾的最大字段和,转化过程为dp[i-1]<=0,dp[i]为a[i],否则dp[i]=dp[i-1]+a[i].
#include <bits/stdc++.h>
using namespace std;
#define int long long
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
vector<int> a(n), dp(n);
for (int i = 0; i<n; i++){
cin >> a[i];
}
int ma = a[0];
dp[0] = a[0];
for (int i = 1; i<n; i++){
dp[i] = max(dp[i-1]+a[i], a[i]);
ma = max(ma, dp[i]);
}
cout << ma << endl;
return 0;
}
DNA序列编辑距离
题目描述:
小R正在研究DNA序列,他需要一个函数来计算将一个受损DNA序列(dna1)转换成一个未受损序列(dna2)所需的最少编辑步骤。编辑步骤包括:增加一个碱基、删除一个碱基或替换一个碱基。
输入:
dna1 = "AGT",dna2 = "AGCT"
输出:1
输入:
dna1 = "AACCGGTT",dna2 = "AACCTTGG"
输出:4
输入:
dna1 = "GGGG",dna2 = "TTTT"
输出:4
删除操作:
考虑从dna1
中删除dna1[i-1]
这个字符。这样,dna1
的前i-1
个字符与dna2
的前j
个字符之间的编辑距离就需要计算。由于我们已经删除了dna1
的一个字符,所以操作数需要加1,即dp[i-1][j] + 1
。这里的dp[i][j-1]
表示在删除操作之前的状态,即假设dna1
的第i
个字符已经不存在,我们需要将dna1
的前i-1
个字符(实际上只考虑了前i-1
个,因为第i
个已被删除)与dna2
的前j
个字符进行匹配的最少编辑距离。
替换操作:
如果我们决定替换dna1[i-1]
为dna2[j-1]
,那么我们需要查看在替换之前两个字符串的状态。即,我们需要知道将dna1
的前i-1
个字符转换成dna2
的前j-1
个字符所需的最少编辑距离。这是因为替换操作只影响当前字符,而不影响之前的字符。
插入操作比较简单(略)。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
int solution(std::string dna1, std::string dna2) {
int n = dna1.size();
int m = dna2.size();
// 创建一个二维数组 dp
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(m + 1, 0));
// 初始化 dp 数组, 进行边界处理
for (int i = 0; i<=n; i++){
dp[i][0] = i;
}
for (int i = 0; i<=m; i++){
dp[0][i] = i;
}
for (int i = 1; i<=n; i++){
for (int j = 1; j<=m;j++){
if (dna1[i-1] == dna2[j-1]){
dp[i][j] = dp[i-1][j-1];
}
else{
dp[i][j] = std::min({dp[i-1][j-1]+1, dp[i-1][j]+1, dp[i][j-1]+1});
//替换 删除 插入
}
}
}
return dp[n][m];
}
int main() {
// 你可以添加更多测试用例
std::cout << (solution("AGCTTAGC", "AGCTAGCT") == 2) << std::endl;
std::cout << (solution("AGCCGAGC", "GCTAGCT") == 4) << std::endl;
return 0;
}
总结:一道比较经典的关于字符串的DP题,拿来入门很好。
这里的dp[i][j]表示前 i 个dna1转换成前 j 个dna2所需的最少编辑距离。
先进行初始化,首列和首行
自己试着写出对应的状态和转移方向,例下 (虽然例子不符合题意,但思路一样)
黄色表示字符不相等时,删除、插入、替换操作,
蓝色表示字符相等时的操作。
最长不下降子序列(LIS)
输入:
13 7 9 16 38 24 37 18 44 19 21 22
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
void solve()
{
int n, m, k;
cin >> n;
vector<int> a(n);
for (int i = 0; i<n; i++) cin >> a[i];
vector<int> dp(n, 1);
for (int i = 0; i<n; i++){
for (int j = 0; j<i; j++){
if (a[j] <= a[i] && dp[j]+1 > dp[i]){
dp[i] = dp[j]+1;
}
}
}
for (int i = 0; i<n; i++){
cout << dp[i] << " \n"[i==n-1];
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
最长公共子序列
这里给出一个例子,可以自己填写红色部分,根据代码结合理解,[i][j]表示s1前i个字符和s2前j个字符比较的到的最大公共子序列长度,和转移方向。
#include <bits/stdc++.h>
#include <cstring>
#include <algorithm>
using namespace std;
int n, s1[10005], s2[10005], dp[10005][10005];
void solve()
{
cin >> n;
memset(dp, 0, sizeof(dp));
for (int i=1; i<=n; i++) {
cin >> s1[i];
}
for (int i=1; i<=n; i++) {
cin >> s2[i];
}
for (int i=1; i<= n; i++)
{
for (int j=1; j<=n; j++)
{
if (s1[i] == s2[j])
{
dp[i][j] = 1+dp[i-1][j-1];
}
else
{
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
cout << dp[n][n] << endl;
}
void show(int i, int j) //递归
{
if (i == 0 && j == 0) return ;
if (s1[i-1] == s2[j-1])
{
show(i-1, j-1);
cout << s1[i-1];
}
else if (dp[i][j-1] > f[i-1][j])
{
show(i, j-1);
}
else{
show(i-1, j);
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t=1;
// cin >> t;
while(t--) {
solve();
}
show(n,n);//路径查询
return 0;
}
背包
装箱问题
有一个箱子容量为V(正整数,0 ≤ V ≤ 20000),同时有n个物品(0<n ≤ 30),每个物品有一个体积(正整数)。
要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
法1:
#include <bits/stdc++.h>
using namespace std;
#define int long long
void solve()
{
int n, v;
cin >> v >> n;
vector<int> a(n+1);
for (int i = 1; i<=n; i++) cin >> a[i];
int ans = 0;
vector<vector<int>> f(2, vector<int>(v+1, 0));
f[0][0] = 1;
for (int i = 1; i<=n; i++){
for (int j = 0; j<=v; j++){
if (j >= a[i]){
f[i%2][j] = f[(i-1)%2][j] || f[(i-1)%2][j-a[i]];
}
else f[i%2][j] = f[(i-1)%2][j];
}
}
for (int i = v; i>=0; i--){
if (f[n%2][i] == 1){
ans = i;
break;
}
}
cout << v-ans << endl;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
法2:内存优化
#include <bits/stdc++.h>
using namespace std;
#define int long long
void solve()
{
int n, v;
cin >> v >> n;
vector<int> a(n+1);
for (int i = 1; i<=n; i++) cin >> a[i];
int ans = 0;
vector<int> f(v+1);
f[0] = 1;
for (int i = 1; i<=n; i++){
for (int j = v;j>=a[i]; j--){
f[j] = f[j]||f[j-a[i]];
}
}
for (int i = v; i>=0; i--){
if (f[i] == 1){
ans = i;
break;
}
}
cout << v-ans << endl;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
01背包
完全背包
有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。放入第i种物品的费用是 Cì ,价值是 Wi。
求解:将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包容量,且价值总和最大。
for (int i = 1; i<=n; i++){
for (int j = c[i]; j<=m; j++){
if (f[j-c[i]]+w[i] > f[j])
f[j] = f[j-c[i]]+w[i];
}
}
多重背包
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用每件费用是c[i],价值是w[i]。求解将哪些物品装入背包,可使这些物品的费用总和不超过背包容量,且价值总和最大。
这里需要用二进制来优化成打包分组
#include <iostream>
using namespace std;
#define int long long
const int siz = 1e5 + 10;
int w[siz], v[siz], k[siz];
int f[siz];
signed main()
{
int n, m, cnt = 1, a, b, c;
cin >> n >> m;
for (int i = 1; i <= n; ++i)
{
cin >> a >> b >> c;
for (int j = 1; j <= c; j <<= 1)
{
v[++cnt] = j * a;
w[cnt] = j * b;
c -= j;
}
if (c)
{
v[++cnt] = a * c;
w[cnt] = b * c;
}//二进制优化
}
for (int i = 1; i <= cnt; ++i)
{
for (int j = m; j >= w[i]; --j)
{
f[j] = max(f[j], f[j - w[i]] + v[i]);
}
}
cout << f[m] << endl;
return 0;
}
二维费用背包
二维费用的背包问题是指:对于每件物品,具有两种不同的费用,选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[]和b]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。
设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是
f[i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}
解题思路:
- 当你发现题目是由熟悉的动态规划题目添加了限制条件变形得来时
- 可以尝试在原题的状态中加一维以满足新的限制条件。
#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
int t1[202], m1[202], f[202][202];
int main()
{
int n, m, t;
cin >> n >> m >> t;
for (int i = 1; i <= n; i++)
{
cin >> m1[i] >> t1[i];
}
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= m1[i]; j--)
{
for (int k = t; k >= t1[i]; k--)
{
f[j][k] = max(f[j][k], f[j - m1[i]][k - t1[i]] + 1);
}
}
}
cout << f[m][t];
return 0;
}
分组背包
有N件物品和一个容量为V的背包。第i件物品的费用是c[],价值是w[]。这些物品被划分为若干组,
每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
本题和前面的题最大的不同是,每组物品有若干种策略:选择本组的某一件,或者一件都不选。
f[k][v]表示前k组物品花费费用v能取得的最大权值
f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]物品i属于组k}
·使用一维数组的伪代码如下:
·for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]= max{f[v],f[v-c[i]]+w[i]}
“for v=V..0”这一层循环必须在“for 所有的i属于组k”之外这样才能保证每一组内的物品最多只有一个会被添加到背包中。
#include <bits/stdc++.h>
using namespace std;
int v, n, t, m, q;
int x;
int g[205][205];
int i, j, k;
int w[10001], z[10001];
int b[10001];
int dp[10001];
int main()
{
cin >> v >> n;
for (i = 1; i <= n; i++)
{
cin >> w[i] >> z[i] >> x;
t = max(t, x);
b[x]++;
g[x][b[x]] = i;
}
for (int i = 1; i <= t; i++)
{
for (j = v; j >= 0; j--)
{
for (k = 1; k <= b[i]; k++)
{
if (j >= w[g[i][k]])
{
dp[j] = max(dp[j], dp[j - w[g[i][k]]] + z[g[i][k]]);
}
}
}
}
cout << dp[v] << endl;
return 0;
}
区间dp
括号匹配
给出一个括号序列,求出其中匹配的括号数。
([]]) 4
- f[i][j] 表示ai....aj的串中,有多少个已经匹配的括号
- 如果ai与ak是匹配的
- f[i][j]= max(f[i][j],f[i + 1][k-1]+ f[k + 1][j]+ 2)
- (相当于是将i到j分成[xxxxx]xxxxx两部分)
- 否则f[i][j] = f[i + 1][j]
- (将第一个元素去掉--因为它肯定不能算)
- 边界 f[ij[i] = 0
推荐使用记忆化搜索
如果用递推的话,应该是区间大小由小到大递增作为最外层循环
·for (int |= 2; |<= n; |++) //枚举区间长度
for (int i=0;i+|-1<n; i++) { //枚举区间起点
int j=i+1-1; //计算区间终点
... ...
... ...
}
最长回文子序列长度
法1:将原字符串倒序,计算二者最长公共子序列
法2:
- f[i][j] 表示ai......āj的串中,最长回文子序列长度
- 如果ai与aj是一样的
- f[i][j]= f[i+1][j-1]+2
- 否则:f[i][j]= max(f[i+1][j],f[i][j-1])
最长回文子串长度 (扩展)
- bool数组f[i][j] 表示ai......āj的串中,最长回文子串长度
- 如果ai与aj是一样的 && f[i+1][j-1]=1
- f[i][j]= 1
- 否则:f[i][j]= 0
石子合并
先考虑没有环的情况:
- f[i][j]表示合并 i 到 j 的所有石子的得分
- f[ij]= max(f[i][j], f[i][k]+ f[k + 1][j] + sum[i][j])
- sum[i][j] 表示i到j的石子的价值和!(也可以前缀和实现 sum[i]表示前i个石子的价值,那么我们需要的就是sum[j]-sum[i -1])
- 但是现在有环!--可以通过取模的方法把它变成循环的!
- 也可以将序列加倍:变成'12341234',就可以完全用链的方法解决了!
- 边界:f[ i ][ i ]=0
凸多边形的三角拆分
给定一个具有 N(N <=50)个顶点(从 1 到 N 编号)的凸多边形,每个顶点的权均已知。问如何把 这个凸多边形划分成 N-2 个互不相交的三角形,使得这些三角形顶点的权的乘积之和最小。
- 设f[ i ][ j ](i <j)表示从顶点 i 到顶点 j 的凸多边形三角剖分后所得到的最大乘积,我们可以得到下面的动态转移方程f[ i ][ j ]= Min(f[ i ][ k ]+f[ k ][ j ]+s[ i ]*s[ j ]*s[ k ]) (i<k<j)
- 显然,目标状态为:f[0][n-1]
田忌赛马
题目背景:秦王一定按照从高到低出马。
田 | 350 | 秦 | 300 |
320 | 250 | ||
310 | 200 | ||
190 | 180 |
由上表知:田忌每一次要么出最强的,要么出最弱的。分析:到秦出300时,田可出350-310,出310时,秦后面的也能对住,所以不用在意先后,当秦大于所有田时,拿最少的来对。
单一的贪心做法,强的对弱的,弱的对强的。当最强的不战平时,这样的贪心是可以的,而...
在第二种情况,应拿最强的对最强,剩余能赢一场。所以最强的战平,结果取决于剩下的来决定。所以单一的贪心不能解决问题。
f[k][i][j] 表示后k轮比赛,田忌使用了第i匹到第j匹马能获得的最多的钱。
f[k][i][j] = max (齐k和田i结果+f[k+1][i+1][j] , 齐k和田j结果+f[k+1][i][j-1])
边界条件 : f[n][i][i] 齐n和田i比赛结果
进行内存优化可以去掉k
F[i][j]表示田忌区间[i,j]的马比下去的最优得分
f[i][j]:=max(f[i+1][j]+cost(i,k),f[i][j-1]+cost(j,k));
k表示齐王当前出的马,cost(i,k)是田忌第i匹马与齐王第k匹马相比的结果。
(本题还有若干其他的状态转移方程,大家可以自己思考)
滑雪
Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael想知道载一个区域中最长底滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子
输入:
5 5 1 2 3 4 5 16 17 18 19 6 15 24 25 20 7 14 23 22 21 8 13 12 11 10 9
输出:
25
思路:
第一步:确定状态
每个点都能做为起点,f[i][j]表示从(i,j)滑下的最长路径长度。
第二步:确定状态转移方程 ----由上下左右四个方向转移过来。
所以单是两层循环无法实现。应该使用(记忆+深搜).
代码:
#include <iostream>
#include <cmath>
#include <string>
#include <cstring>
#include <cstdlib>
#include <ctime>
#include <algorithm>
#include <cstdio>
#include <map>
#include <vector>
#include <set>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
#define INF 0x7fffffff
#define MAX(a,b) a>b?a:b
#define MIN(a,b) a>b?b:a
#define N 101
int m,n;
int dp[N][N];
int a[N][N];
void init(){
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
{
scanf("%d",&a[i][j]);
dp[i][j]=0;
}
}
int dfs(int x,int y){
if(dp[x][y]) return dp[x][y];
int bottom=0,left=0,top=0,right=0;
if(x&&a[x-1][y]<a[x][y]) top=dfs(x-1,y);
if(y&&a[x][y-1]<a[x][y]) right=dfs(x,y-1);
if(x<n-1&&a[x+1][y]<a[x][y]) bottom=dfs(x+1,y);
if(y<m-1&&a[x][y+1]<a[x][y]) left=dfs(x,y+1);
int maxx=0;
maxx=MAX(maxx,left);
maxx=MAX(maxx,right);
maxx=MAX(maxx,bottom);
maxx=MAX(maxx,top);
return maxx+1;
}
void solve(){
int ans=1;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
{
dp[i][j]=dfs(i,j);
ans=MAX(ans,dp[i][j]);
}
printf("%d\n",ans);
}
int main()
{
scanf("%d%d",&n,&m);
init();
solve();
return 0;
}
练习题
咸鱼翻身
一天,5897号咸鱼在沙滩上晒太阳,为了成为一个合格的咸鱼,他要保证身体每一面都得到充足的阳光,他有一种能力,捕捉脑海中一闪而过的灵感带来的N个问题,解决一个问题后能够为自己积攒能量,能量越多,翻身的可能性越大,解决第i个问题能为他提供ai的能量,但第i个问题他最迟在di时刻完成,解决第i个问题需要ti秒,现在他可以随意安排完成问题的顺序,为了能让5897号咸鱼能够顺利翻身,请你帮忙计算出他能获得的最大能量。
输入样例:
5
50 2 1
100 3 3
1 5 1
5000 2 3
30 5 4
输出样例:
101
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define vi vector<int>
struct node{
int a, d, t;
};
void solve()
{
int n, m, k;
int ma = 0;
cin >> n;
vector<node> v(n);
for (int i = 0; i < n; i++){
cin >> v[i].a >> v[i].d >> v[i].t;
ma = max(ma, v[i].d);
}
sort(v.begin(), v.end(), [](node a, node b){
return a.d < b.d;
});
int ans = 0;
vector<int> dp(ma+1, 0);
for (int i = 0; i<n; i++){
for (int j = v[i].d; j>=v[i].t; j--){
dp[j] = max(dp[j], dp[j-v[i].t]+v[i].a);
}
}
int maxn = 0;
for (int i = 1; i<=ma; i++){
maxn = max(maxn, dp[i]);
}
cout << maxn << endl;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
// cin >> t;
while (t--)
{
solve();
}
return 0;
}
卡牌翻面求和问题
小M有 nn 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 aiai,背面是 bibi。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对 109+7109+7 取模。
例如:如果有3张卡牌,正反面数字分别为 (1,2)
,(2,3)
和 (3,2)
,你需要找到所有满足这3张卡牌正面或背面朝上的数字之和可以被3整除的组合数。
AC代码:
#include <iostream>
#include <vector>
#include <string>
const int mod = 1e9+7;
using namespace std;
int solution(int n, std::vector<int> a, std::vector<int> b) {
// PLEASE DO NOT MODIFY THE FUNCTION SIGNATURE
// write code here
vector<vector<int>> dp(n+1, vector<int>(3, 0));
dp[0][0] = 1;
for (int i=0; i<n; i++){
for (int j = 0; j<3; j++){
// 选择正面
dp[i+1][(j+a[i])%3] = (dp[i+1][(j+a[i])%3]+dp[i][j]);
// 选择反面
dp[i+1][(j+b[i])%3] = (dp[i+1][(j+b[i])%3]+dp[i][j]);
}
}
return dp[n][0];
}
int main() {
std::cout << (solution(3, {1, 2, 3}, {2, 3, 2}) == 3) << std::endl;
std::cout << (solution(4, {3, 1, 2, 4}, {1, 2, 3, 1}) == 6) << std::endl;
std::cout << (solution(5, {1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}) == 32) << std::endl;
return 0;
}
这道题很有动态规划的风格,思路清楚后,代码就很简单。
dp[i][j]表示到第i张牌余j的可能数。
如此循环下到返回dp[n][0]及是答案
最大的两个不重叠字串和
给你一个长度为n的数列,找到其中的两个不重叠的子串,使得其和最大。
在最大的字串和中,有f[i]数组,表示取第i个数结尾时的最大字串和。
设g[i]表示前i个数的最大字串和,g[i] = max(f[i], g[i-1])
[a,b],[c,d]
我们就可以枚举c,分成两部分取最大值g[i-1]表示[a,b];
而[c,d]段需要重新求 ff[i] 表示后i的数组得i个数必选的字串和。
多个不相交字段和问题
给定一个长度为n的数组,求在其中找m个不相交子串和最大值的问题。
f[i][j]表示前i个数(第i个数必须取)组成j个不相交子段能取得的最大和
f[i][j] = max(f[i-1][j]+a[i], f[k][j-1]+a[i])表示作为结尾时,作为一个新的开头时(k从0~i-1枚举)O(n*n*m)
设maxn[i][j]表示f数组第j列前i行的最大值
f[i][j][0/1]表示前i个数选了j个字串的i个数不选/选的最大值
f[i][j][0] = max(f[i-1][j][0], f[i-1][j][1])
f[i][j][1] = max(f[i-1][j][1]+a[i],f[i-1][j-1][0]+a[i],f[i-1][j-1][1]+a[i])