动态规划1
实验题目:减肥的小K2
题目描述:
小K是个苦命的孩子,他的师傅为了多赚钱,以减肥为理由,让他去采药,并说不完成不能吃饭。野地里有许多不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。要求在规定的时间t里,采到的草药的总价值最大。
输入要求:
第一行有2个整数T(1≤T≤1000)和M(1≤M≤100),一个空格隔开,T代表总共能够用来采药的时间,M代表山洞里的草药的数目。
接下来的M行每行包括两个在1到100之间包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出要求:
1个整数,表示在规定的时间内可以采到的草药的最大总价值。
实验代码
#include<bits/stdc++.h>
using namespace std;
int main() {
int t, m, time[101], val[101];
int final_val[102][1001] = { 0 }; // 动态规划数组,记录子问题的最优解
cin >> t >> m;
for (int i = 0;i < m;i++) {
cin >> time[i] >> val[i];
}
for (int i = 1;i <= m;i++) {
for (int j = 1;j <= t;j++) {
final_val[i][j] = final_val[i - 1][j]; // 若不选取该药材
if (j - time[i - 1] >= 0) { //判断在j采摘时间下能否采摘该药材
int temp = final_val[i - 1][j - time[i - 1]] + val[i - 1];
if (temp > final_val[i][j]) { //判断采摘后总价值是否会变大
final_val[i][j] = temp;
}
}
}
}
cout << final_val[m][t] << endl;
return 0;
}
算法分析与知识点:
很明显,这题和01背包问题非常地相似,采集草药的时间可以看做是背包的容量,草药可以看做是放入背包中的物品,由此可以得到和背包一样的递推式:
1、如果当前拥有的时间无法采集到当前的第i个草药,则实现的价值就是当前时间在前i - 1个草药情况下得到的价值
v
a
l
u
e
[
i
]
[
j
]
=
v
a
l
u
e
[
i
−
1
]
[
j
]
value\left[ i \right] \left[ j \right] =value\left[ i-1 \right] \left[ j \right]
value[i][j]=value[i−1][j]
2、如果当前拥有的时间可以采集到第i个草药,则实现的价值就是采集了当前的草药之后,用总的时间减去当前采集草药用的时间的剩余时间来采集前i - 1个草药获得的价值
再从采集第i个草药和不采集第i个草药中选择一个最大的价值作为当前价值
v
a
l
u
e
[
i
]
[
j
]
=
max
(
v
a
l
u
e
[
i
−
1
]
[
j
]
,
v
a
l
u
e
[
i
−
1
]
[
j
−
t
i
m
e
[
i
]
]
+
m
[
i
]
)
value\left[ i \right] \left[ j \right] =\max \left( value\left[ i-1 \right] \left[ j \right] ,value\left[ i-1 \right] \left[ j-time\left[ i \right] \right] +m\left[ i \right] \right)
value[i][j]=max(value[i−1][j],value[i−1][j−time[i]]+m[i])
根据上面的递推关系式可写出相应的动态规划程序。
实验题目:最大连续子段和
题目描述:
给出长度为n的数组,求最大连续子段和, 输出该最大和。
输入要求:
第1行输入一个整数n<50;表示输入数组的大小
第2行输入n个数,中间用空格隔开
输出要求:
最大连续子段和。
实验代码
#include<bits/stdc++.h>
using namespace std;
int main() {
int n, nums[51];
int max;
cin >> n;
for (int i = 0;i < n;i++)
cin >> nums[i];
max = nums[0];//初始令最大值为第一个数
for (int i = 1;i < n;i++) {
if (nums[i - 1] > 0)//判断到前一个数为止的最大子序列和是否大于0
nums[i] += nums[i - 1];//若大于0,则继续扩大子序列
if (max < nums[i])
max = nums[i];//更新最大值
}
cout << max << endl;
return 0;
}
算法分析与知识点:
本题可以在输入数组的基础上直接进行动态规划, 表示以第i个元素结尾的最大连续子序列和。根据对应的关系可以得到递推关系式如下:
n
u
m
s
[
i
]
=
{
n
u
m
s
[
i
]
,
n
u
m
s
[
i
−
1
]
<
=
0
n
u
m
s
[
i
]
+
n
u
m
[
i
−
1
]
,
n
u
m
s
[
i
−
1
]
>
0
nums\left[ i \right] =\left\{ \begin{array}{l} nums\left[ i \right] \ ,nums\left[ i-1 \right] <=0\\ nums\left[ i \right] +num\left[ i-1 \right] \ ,nums\left[ i-1 \right] >0\\ \end{array} \right.
nums[i]={nums[i] ,nums[i−1]<=0nums[i]+num[i−1] ,nums[i−1]>0
实验题目:数字三角形
题目描述:
数字三角形,从三角形顶部往下走,只能往左下或右下走,求走到最下面时所经过的数字和最大为多少?(下图为n=6时的情况)。
输入要求:
第1行:整数n(1<=n<=1000)
第2-n+1行:每行若干整数,第i行有i-1个整数,空格分隔。
输出要求:
一行:一个整数,表示所经过数字的最大和。
实验代码
#include<bits/stdc++.h>
using namespace std;
int a[1010][1010];//数字数组
int d[1010][1010];//记录自底向上到(i,j)位置的最大值
int n;
int dfs(int i, int j)
{
if (d[i][j] > 0) //说明该点已经计算过直接返回该点的值
return d[i][j];
return d[i][j] = a[i][j] + (i == n ? 0 : max(dfs(i + 1, j), dfs(i + 1, j + 1)));
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
cin >> a[i][j];
}
memset(d, -1, sizeof(d)); //将d初始化为-1
printf("%d\n", dfs(1, 1));
return 0;
}
算法分析与知识点:
本题若直接用递归的方法进行求解,会造成大量子问题的重复求解,最终导致超时。因此可以采用记忆化的递归,该方法也是用的递归,但同时把计算结果保存在数组d中,在计算d[i][j]的之前要先判断它的值是否为-1,如果不是表示这个点已经计算过了,直接放回这个点的值就行,这种方法被称为记忆化,时间复杂度为O(n^2),与直接调用递归去算相比较有了巨大的优化。
动态规划2
实验题目:最长非连续公共子序列
题目描述:
给定两个字符串,求解这两个字符串的最长非连续(允许连续或非连续)的公共子序列的长度(Longest Common Sequence)。
比如字符串1:BDCABA;字符串2:ABCBDAB。
则这两个字符串的最长公共子序列长度为4,最长公共子序列是:BCBA
输入要求:
输入2行,每行一个字符串;字符串长度<1000。
输出要求:
输出两个字符串的最长非连续的公共子序列的长度。
实验代码
#include<bits/stdc++.h>
using namespace std;
int dp[1001][1001] = { 0 }; //记录动态规划结果
string s1, s2;
int findLCS() {
int n = s1.length(), m = s2.length();
for (int i = 1;i <= n;i++) {
for (int j = 1;j <= m;j++) {
if (s1[i - 1] == s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1; //若当前两个指针指向的字符相等
}
else {//若当前两个指针指向的字符不相等
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);//解两个子问题
}
}
}
return dp[n][m];
}
int main()
{
cin >> s1 >> s2;
int res = findLCS();
cout << res << endl;
return 0;
}
算法分析与知识点:
本题主要运用了动态规划的思想,动态规划采用二维数组来标识中间计算结果,避免重复的计算来提高效率。
最长公共子序列的长度的动态规划方程
设有字符串s1[0…n],s2[0…m],下面就是递推公式。字符串s1对应的是二维数组dp的行,字符串s2对应的是二维数组dp的列。
d
p
[
i
]
[
j
]
=
{
0
,
i
=
0
o
r
j
=
0
d
p
[
i
−
1
]
[
j
−
1
]
+
1
,
s
1
[
i
]
=
s
2
[
j
]
max
(
d
[
i
]
[
j
−
1
]
,
d
p
[
i
−
1
]
[
j
]
)
,
s
1
[
i
]
≠
s
2
[
j
]
dp\left[ i \right] \left[ j \right] =\left\{ \begin{array}{l} 0,\ i=0\ or\ j=0\\ dp\left[ i-1 \right] \left[ j-1 \right] +1\text{,}s1\left[ i \right] =s2\left[ j \right]\\ \max \left( d\left[ i \right] \left[ j-1 \right] ,dp\left[ i-1 \right] \left[ j \right] \right) ,s1\left[ i \right] \ne s2\left[ j \right]\\ \end{array} \right.
dp[i][j]=⎩⎨⎧0, i=0 or j=0dp[i−1][j−1]+1,s1[i]=s2[j]max(d[i][j−1],dp[i−1][j]),s1[i]=s2[j]
根据上面递推公式可以写出相应的程序。
实验题目:切钢条
题目描述:
一家公司购买长钢条,将其切割成短钢条出售,切割本身没有成本,长度为i的短钢条的价格为Pi。那给
定一段长度为n的钢条和一个价格表Pi,求钢条的切割方案使得收益Rn最大。
输入要求:
输入钢条的长度n。
输出要求:
输出获得的最大收益。
实验代码
#include<bits/stdc++.h>
using namespace std;
int pi[11] = { 0,1,5,8,9,10,17,17,20,24,30 }; //记录已知长度钢条价值
int dp[1000] = { 0 };//记录动态规划结果
int findMaxVal(int n)
{
if (n == 0) // 若n为0直接返回
return 0;
for (int i = 1;i <= n;i++) {
for (int j = 1;j <= i && j <= 10;j++) { // 第一刀最多切10种
dp[i] = max(pi[j] + dp[i - j], dp[i]);//遍历所有切法
}
}
return dp[n];
}
int main()
{
int n;
cin >> n;
int res = findMaxVal(n);
cout << res << endl;
return 0;
}
算法分析与知识点:
本题主要运用动态规划的思想,对于长度为n的钢条,我们可以先切一刀,切下长度为1-10的钢条,共10种切法,最大收益就是切下的钢条收益和剩下钢条的最大收益之和。
- 钢条长度为1的最大收益计算出来,保存到dp[1]
- 长度为2的钢条,遍历每一种切法,收益最大的保存到dp[2]
- 计算长度为n的钢条的最大收益,此时dp数组已经保存了1——n-1长度钢条的最大收益
遍历这10中切法,就可以找到最大的收益
由于每种钢条长度的最大收益都被保存在数组dp中,避免了很多的重复计算。
d p [ i ] = max ( d [ i ] , d p [ i − j ] + p i [ j ] ) , j = 1...10 dp\left[ i \right] \ =\ \max \left( d\left[ i \right] ,dp\left[ i-j \right] +pi\left[ j \right] \right) ,j=1...10 dp[i] = max(d[i],dp[i−j]+pi[j]),j=1...10
动态规划3
实验题目:合格的盗贼
题目描述:
一条街上有N个商铺;商铺i有价值V[i]的物品,你有足够的时间在晚上光顾所有的商店,人们称呼你为盗贼;每个商店都有一个报警器,会在晚上报警,但是只有相邻的2个商店同时报警时,警察才会出动;你需要证明你是个合格的盗贼。
输入要求:
第一行一个整数N<=100,商店数。
第二行N个整数,每个商店的价值
输出要求:
输出偷盗的最大价值。
实验代码
#include<bits/stdc++.h>
using namespace std;
int dp[101] = { 0 };
int findMaxValue(int n) {
if (n == 1)
return dp[0];
else if (n == 2)
return max(dp[0], dp[1]);
dp[1] = max(dp[0], dp[1]);
for (int i = 3;i <= n;i++) {
dp[i - 1] = max(dp[i - 2], dp[i - 3] + dp[i - 1]); // 动态规划递推关系
}
return dp[n - 1];
}
int main()
{
int n;
cin >> n;
for (int i = 0;i < n;i++) {
cin >> dp[i];
}
int res = findMaxValue(n);
cout << res << endl;
return 0;
}
算法分析与知识点:
本题主要运用了动态规划的思想,在原先输入数组的及基础上进行动态规划,节约了内存空间。
根据商店的报警机制我们可以列出一下递推关系,dp数组初始为第i家商店的价值vi
d
p
[
i
]
=
max
(
d
p
[
i
−
1
]
,
d
p
[
i
−
2
]
+
d
p
[
i
]
)
dp\left[ i \right] =\max \left( dp\left[ i-1 \right] ,dp\left[ i-2 \right] +dp\left[ i \right] \right)
dp[i]=max(dp[i−1],dp[i−2]+dp[i])
根据上面递推公式可以写出相应的程序。
实验题目:小莫踩蘑菇
题目描述:
大家都知道提莫队长喜欢种蘑菇。有一天提莫正走在回约德尔国的路上,忽然看到路上长了很神奇的蘑菇,蘑菇会不断从某处长出来,但是如果不快点(1秒内)采走的话会消失。酷爱蘑菇的提莫马上去采蘑菇。说来小提莫的人品实在是太好了,这蘑菇别处都没有,就会长在他(她?它?)身旁的10米范围内。蘑菇如果不马上采走就会坏掉,所以提莫队长马上卸下身上的背包去接。但由于小路两边都不能站人,所以他只能在小路上踩。虽然小莫队长也算约德尔国的短跑健将,但是由于手脚太短,小莫每秒钟还是只能移动1米并且只能踩到1米范围内的蘑菇。
为了使问题简化,可以将小路看作从0-10的一维坐标系,开始时提莫站在5的位置,在第一秒,他只能踩到4,5,6这三个位置中其中一个位置上的蘑菇。
问提莫最多能采到多少蘑菇?
输入要求:
输入数据有多组。多组数据的第一行以正整数n(0<n<100000),表示有n个蘑菇会出现在这条路上。在接下来的n行中,每行有两个整数x,T(0<T<100000),表示在第T秒有一个蘑菇出现在x点上。同一秒钟在同一个点上可能出现多个蘑菇。## 输出要求:
每一组输入数据对应一行输出。
输出要求
输出一个整数m,表示提莫队长最多可能踩到m个蘑菇。
实验代码
#include <bits/stdc++.h>
using namespace std;
int mushroom[11][100000] = { 0 }; //记录第i秒各个位置蘑菇的生长情况
int dp[11][100000] = { 0 }; // 记录第i秒小莫站在位置j能采到的最多蘑菇量
int main() {
int n;
int x, t;
while (scanf("%d", &n) != EOF) {
memset(mushroom, 0, sizeof(mushroom)); //初始化清零
memset(dp, 0, sizeof(dp)); //初始化清零
for (int i = 0; i < n; i++) {
scanf("%d%d", &x, &t);
mushroom[x][t]++;
}
dp[5][0] = mushroom[5][0]; // 设置初始条件
int ans = 0;
for (int time = 1; time < 100000; time++) {
for (int p = 0; p < 11; p++) { //根据不同位置来判断第i秒的第j个位置是怎么来的
if (p == 0) {
dp[p][time] =
max(dp[p + 1][time - 1], dp[p][time - 1]);
}
else if (p == 10) {
dp[p][time] =
max(dp[p - 1][time - 1], dp[p][time - 1]);
}
else {
dp[p][time] =
max(max(dp[p + 1][time - 1], dp[p][time - 1]),
dp[p - 1][time - 1]);
}
dp[p][time] += mushroom[p][time];
if (dp[p][time] > ans)
ans = dp[p][time];
}
}
printf("%d\n", ans);
}
return 0;
}
算法分析与知识点:
本题主要运用动态规划的思想,根据小莫的移动情况来看第i秒j位置的可能有三种情况:分别为不动、从左来、从右来。
d
p
[
i
]
[
j
]
=
max
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
1
]
,
d
p
[
i
−
1
]
[
j
+
1
]
)
+
m
u
s
h
r
o
o
m
[
i
]
[
j
]
dp\left[ i \right] \left[ j \right] =\max \left( dp\left[ i-1 \right] \left[ j \right] ,dp\left[ i-1 \right] \left[ j-1 \right] ,dp\left[ i-1 \right] \left[ j+1 \right] \right) +mushroom\left[ i \right] \left[ j \right]
dp[i][j]=max(dp[i−1][j],dp[i−1][j−1],dp[i−1][j+1])+mushroom[i][j]
根据上面递推公式可以写出相应的程序。
动态规划-堂练
实验题目:不下降的数字序列
题目描述:
在一个数字序列中,找到一个最长的非连续子序列,使得这个子序列是不下降(非递减)。现有序列A={1,2,3,-1,-2,7,9},则A的最长不下降子序列是{1,2,3,7,9}。
如果有多个最长序列,只需选数字顺位靠后的序列从大到小输出。
输入要求:
输入2行;
第一行一个整数n,表示有n个整数的序列要输入,n<1000;
第二行共有n个整数。
输出要求:
输出最长的不下降子序列,只需选数字顺位靠后的序列从大到小输出。
实验代码
#include<iostream>
#include<algorithm>
using namespace std;
#define N 1000
//记录当前的最长的非连续子序列的长度
int dp[N];
//存储数据的数组
int a[N];
//存储当前位置元素的下一跳位置
int index[N];
//存储最终结果
int result[N];
int main() {
int i, j;
int n;
cin >> n; //数组长度
//读取数据
for (i = 1; i <= n; i++) {
cin >> a[i];
}
//初始化dp数组,设置数据都为1(自身就是1)
//初始化index数组
for (i = 1; i <= n; i++) {
dp[i] = 1;
index[i] = 0;
}
//从倒数第2个开始(倒数第一个不用记录),记录以其为首到后面的最长非递减子序列的长度
for (i = n - 1; i >= 1; i--) {
//记录后面某个子序列的最长非递减子序列的长度
int sub_sequence_length = 0;
for (j = i + 1; j <= n; j++) {
//如果后面的元素比前面的元素大,并且其对应的dp值(非递减子序列长度)也大于等于当前的,则进行更新
if (a[j] >= a[i] && dp[j] >= sub_sequence_length) {
sub_sequence_length = dp[j]; //更新当前最短长度
index[i] = j; //记录i的下一跳是j
}
}
//如果子序列不为0(即后面存在非递减子序列),则对其更新,等于后面非递减子序列的长度加1
if (sub_sequence_length) {
dp[i] = sub_sequence_length + 1;
}
}
//找到dp数组中最大的那一个数,其对应的就是最长非递减子序列的第一个元素
int max_length = 0, p;
for (i = 1; i <= n; i++) {
if (dp[i] >= max_length) {
max_length = dp[i];
p = i; //p记录当前位置的下标
}
}
for (i = 1; i <= max_length; i++) {
result[i] = a[p];
p = index[p];
}
//进行排序
// sort(首元素地址,尾元素的下一个地址,比较函数);
//我们第一个位置没有用,所以首元素地址是result + 1
sort(result + 1, result + max_length + 1);
cout << max_length << endl;
for (i = max_length; i >= 1; i--) {
if (i == 1) {
cout << result[i];
}
else
cout << result[i] << " ";
}
cout << endl;
return 0;
}
算法分析与知识点:
思路:设置一个a[]数组保存原始的数据
设置一个dp[]数组,从最后一个数据开始,记录下以其为首的最长非递减子序列的长度
设置一个索引数组index[],记录当前元素的下一跳元素
sort(首元素地址,尾元素的下一个地址,比较函数);
result[]数组记录最终的结果
从倒数第二个开始向前进行推进来更新dp[],更新后的dp[]又会被更前面的用到,全部更新完毕之后,从中选取一个最大的值作为最长非递减子序列的长度
同时Index会保存每个元素的下一跳的位置,保证元素可以被找到,找到之后,将元素存入result[]数组中,进行排序,按从大到小输出。
实验题目:爆破组
题目描述:
暴徒设计了一种便携的链形炸弹,由多个独立能量珠组合而成;由于制造是手工的,因此每个珠子的能量不同;炸弹被启动时,珠子根据控制信号按某一顺序逐一被外层的突刺刺破,刺破的两个珠子的物质融合在一起时,能量不断的聚合增大。
能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数,被称为聚合标记。对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,这两颗珠子才能产生聚合能量。如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为m×r×n ,新产生的珠子的头标记为 m,尾标记为 n。
当所有珠子聚合成一起时,就是该炸弹的综合破坏能量。
由于该该炸弹的独特的刺突设计,炸弹不能被移动和解除,只能引爆。唯一减少伤亡的方法是通过暴露在外的信号控制装置,改变信号的控制顺序,从而使其爆炸时的能量最小。请找出引爆的最小能量。
输入要求:
第一行输入珠子数量n,2<n<20
第二行输入n个珠子的头尾标记,相邻头尾相同的标记只输入一个,因此共n+1个标记值。
输出要求:
最小的杀伤能量。
实验代码
#include <bits/stdc++.h>
using namespace std;
#define N 20
int a[N + 1];
int dp[N + 1][N + 1] = { 0 };
int main() {
int n;
cin >> n;
for (int i = 0;i <= n;i++)
cin >> a[i];
for (int r = 2;r <= n;r++) { //包含最小矩阵个数
for (int i = 0;i < n - r + 1;i++) {
int j = i + r - 1; // i,j表示当前考虑范围的起始终结位置
dp[i][j] = dp[i][i] + dp[i + 1][j] + a[i] * a[i + 1] * a[j + 1]; // 赋初值
for (int k = i + 1;k < j;k++) {
int t = dp[i][k] + dp[k + 1][j] + a[i] * a[k + 1] * a[j + 1]; // 循环找到最佳分割点k
if (t < dp[i][j])
dp[i][j] = t;
}
}
}
cout << dp[0][n - 1] << endl;
return 0;
}
算法分析与知识点:
为了方便理解可将原问题转化为矩阵连乘问题,将矩阵 表示为 。问题即为计算 的最优计算策略。设计这个计算次序在矩阵 和 之间断开先计算 、 的计算量,再将结果相乘可得到最后的结果。
实验题目:能量字符串
题目描述:
两个字符串 A 和 B ,长度都为 n 的,每次从字符串A的两个端头(或左或右)取走一个字符,而B串只能按序从左往右取一个字符。每个字符的ASCII码值为该字符的能量值(区分大小写)。假设第 i 次取走的字符串A字符为 Ax(A串剩余的左端或右端字符),B字符串取第i个字符为Bi,将Ax和Bi字符结合后,其能量值为两个字符能量值的乘积;求所有A串中的字符与B串中的字符结合后,所能获得的最大能量值是多少。
输入要求:
每次输入2行字符串,分别表示字符串A和字符串B;输入保证2个字符串的长度相同。
其中字符串A和B中的字符为大写或小写的英文字符;
A、B字符串的长度都不超过10个字符。
输出要求:
输出2个字符串所能得到的最大能量值。
实验代码
#include <bits/stdc++.h>
using namespace std;
#define N 11
int ans = 0;
int n;
string a, b;
void dfs(int l, int r, int now, int sum) {
if (now == n) {
ans = max(ans, sum);
return;
}
dfs(l + 1, r, now + 1, sum + a[l] * b[now]); //递归左子问题
dfs(l, r - 1, now + 1, sum + a[r] * b[now]); // 递归右子问题
}
int main() {
cin >> a >> b;
n = a.length();
dfs(0, n - 1, 0, 0);
cout << ans << endl;
return 0;
}