第七周作业
动态规划
通过求解子问题的最优解,得到全局的最优解
动态规划的三要素
状态:求解一个子问题所需要的关键信息
转移式:如何通过子问题的最优解获得更大规模的问题的最优解
起始条件
动态规划的条件
最优子结构:可以通过子问题的最优解获得更大规模的问题的最优解
无后效性:已求解的子问题不影响后面的问题的求解
子问题重叠:需要多次使用同一个子问题的答案。第一次求解得到答案,下次使用时直接调用。
第一题
题目1
有一些草药,不同草药需要不同的采摘时间,每株草药需要一定的采摘时间和价值。给一个时间,问这段时间内能采摘得到的最大价值是多少。
思路1
符合动态规划的条件。求解用t时间采摘前n个草药能得到的最大价值,最终得到T时间采摘M个草药的最大价值。
状态:时间t和采摘前n个草药
状态转移方程:f(n, t) = max(f(n - 1, t), f(n - 1, t - ct[n]) + cv[n])。max前一个量为不采摘第n个草药的结果,后一个量为采摘第n个草药的结果。此外要判断t是否大于ct[n]。
起始条件:f(1, 0) = 0
代码1
#include<iostream>
#include<string.h>
using namespace std;
int main()
{
//T、M
int t, m;
//采摘每个草药需要的时间、每个草药的价值、每个子问题的答案
int ct[100], cv[100], ans[101][1001];
//将初始答案全部设置为0(同时初始条件也设置好)
memset(ans, 0, sizeof(ans));
cin >> t >> m;
for (int i = 1; i <= m; i++)
{
cin >> ct[i] >> cv[i];
}
//动态规划求解每个子问题的答案
//外层循环为前i个草药,内存循环为花j时间
for (int i = 1; i <= m; i++)
{
for (int j = 0; j <= t; j++)
{
//如果有足够的时间采摘
if (j >= ct[i])
{
//状态转移方程
ans[i][j] = max(ans[i - 1][j], ans[i - 1][j - ct[i]] + cv[i]);
}
//没有足够时间只能不采
else
{
ans[i][j] = ans[i - 1][j];
}
}
}
//花t秒采摘m个草药的结果即位题目的答案
cout << ans[m][t] << endl;
return 0;
}
第二题
题目2
有一组数,按原顺序从这些数中取出数字,得到一个序列,并保证序列中的元素递增。问最长的序列的长度。(最长上升子序列)
思路2
符合动态规划的条件,用动态规划求解。子问题为以求解第n个数结尾的最长上升子序列。
状态:第n个数
状态转移方程:f(n) = max(f(n - j)) + 1(j取1到n-1的任意数,并且原序列s[i] > s[j])
起始条件:f(1) = 1
代码2
#include<iostream>
#include<string.h>
using namespace std;
int main()
{
//n、原序列、子问题答案、题目答案(初始值为0)
int n, s[5001], ans[5001], maxans = 0;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> s[i];
//设置所有子问题的答案初始值都是1(包含了起始条件)
ans[i] = 1;
}
//动态规划
//外层循环每个数,求解所有子问题的答案
for (int i = 1; i <= n; i++)
{
//状态转移方程
for (int j = 1; j < i; j++)
{
if (s[j] < s[i] && ans[j] + 1 > ans[i])
{
ans[i] = ans[j] + 1;
}
}
//更新答案
if (maxans < ans[i])
{
maxans = ans[i];
}
}
cout << maxans << endl;
return 0;
}
第三题
题目3
给出一个序列,求该序列中和最大的连续子序列
思路3
符合动态规划的条件。子问题为以第n个元素结尾的和最大的子序列。
状态:第n个元素
状态转移方程:f(n) = max(f(n - 1) + a[n], a[n])(如果这个数和前一个数结尾和最大的子序列组成新子序列的和更大,则使用这个子序列;如果不如这个数本身大,则只由这个数组成的子序列的和大)
起始条件:f(1) = a[1]
代码3
#include<iostream>
using namespace std;
int main()
{
//长度n、序列、子问题的答案
int n, s[200001], ans[200001];
//最终答案:最大子序列的和
int maxans = -2e9;
cin >> n;
//输入和动态规划
//循环i为子问题的状态
for (int i = 1; i <= n; i++)
{
cin >> s[i];
//起始条件
if (i == 1)
{
ans[i] = s[i];
}
else
{
//状态转移方程
ans[i] = max(s[i], ans[i - 1] + s[i]);
//更新答案
if (maxans < ans[i])
{
maxans = ans[i];
}
}
}
cout << maxans << endl;
return 0;
}
第四题
题目4
求字符串s和t的最长公共子序列
思路4
符合动态规划的条件。子问题为求解s前i个字符和t前j个字符中最长的公共子序列。
状态:s的前i个字符、t的前j个字符
条件转移方程:
f(i, j) = f(i - 1, j - 1) + 1(当s[i] == t[j]时,将这个字符选入子序列,最长的长度为f(i - 1, j - 1)加上1)
f(i, j) = max(f(i - 1, j) + f(i, j - 1))(当s[i] != t[j]时,不选这两个字符,最长长度为f(i - 1, j)(s向前移一位,t不变)和f(i, j - 1)(t向前移一位,s不变)较大的一个)
起始条件:f(i, 0) = 0, f(0, j) = 0
代码4
#include<iostream>
#include<algorithm>
using namespace std;
//子问题答案(第三维两个元素分别存长度和条件转移方程的类型(用来向前寻找所有的子序列中的字符))
int ans[3001][3001][2];
int main()
{
//s和t
string s, t;
cin >> s >> t;
//两个字符串的长度m和n
int m = s.size(), n = t.size();
//在s和t最开头插入一个任意字符,字符串中真正的内容从索引1开始,便于后面循环
s.insert(s.begin(), ' ');
t.insert(t.begin(), ' ');
//动态规划
//外层循环为状态i
//内存循环为状态j
for (int i = 1; i<= m; i++)
{
for (int j = 1; j <= n; j++)
{
//第一个状态方程的情况
if (s[i] == t[j])
{
ans[i][j][0] = ans[i - 1][j - 1][0] + 1;
//定义这种情况类型为3
ans[i][j][1] = 3;
}
//第二个状态方程的情况
else
{
//第二个状态方程前一个表达式大的情况
if (ans[i - 1][j][0] >= ans[i][j - 1][0])
{
ans[i][j][0] = ans[i - 1][j][0];
//定义这种情况类型为1
ans[i][j][1] = 1;
}
else
{
//第二个状态方程后一个表达式大的情况
ans[i][j][0] = ans[i][j - 1][0];
//定义这种情况类型为2
ans[i][j][1] = 2;
}
}
}
}
//最长公共子序列
string r;
//ans[m][n][0]即使最长子序列的长度,但是题目要求子序列的内容,需要向前循环得到所有子序列中的字符
//循环得到子序列,由于不再使用m和n的值,直接使用m和n代表上面的i和j
while (ans[m][n][1] != 0)
{
//情况3,说明子序列中包含s[m](或t[n]),加到子序列中
if (ans[m][n][1] == 3)
{
r += s[m];
//更新m和n
m--;
n--;
}
//情况2和情况1,子序列不包含当前位置的字符
else if (ans[m][n][1] == 2)
{
//更新n
n--;
}
else if (ans[m][n][1] == 1)
{
//更新m
m--;
}
}
//反向得到的字符需要反转
reverse(r.begin(), r.end());
cout << r << endl;
return 0;
}