算法设计与分析——动态规划
目录
一、背包问题
1.01背包(已考过)
2.完全背包
3.多重背包
二、最长公共子序列
三、最大子连续子序列(连续)
四、最长上升子序列(不连续)
五、常考经典问题
1.流水作业调度(待写)
2.数字三角形
3.矩阵连乘(已考过)
4.石子合并
5.最短编辑距离
一、背包问题
1.01背包(已考过)
有 N件物品和一个容量是 V的背包。每件物品只能使用一次。
第 i件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N行,每行两个整数 vi,wi,用空格隔开,分别表示第 i件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
输入样例
4 5 1 2 2 4 3 4 4 5
输出样例:
8
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1005;
int v[N]; // 体积
int w[N]; // 价值
int f[N][N];// f[i][j]表示在j体积下前i个物品的最大价值
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
if(j < v[i]) // 当前背包容量装不进第i个物品,则价值等于前i-1个物品
f[i][j] = f[i - 1][j];
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
2.完全背包
有 N种物品和一个容量是 V的背包,每种物品都有无限件可用。
第 i种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。输入格式
第一行两个整数,N,V用空格隔开,分别表示物品种数和背包容积。
接下来有 N行,每行两个整数 vi,wi用空格隔开,分别表示第 i种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
输入样例
4 5 1 2 2 4 3 4 4 5
输出样例:
10
1.三重循环的朴素做法
#include<iostream>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][N];
int main(){
cin >> n >> m;
for(int i = 1;i <= n;i++){
cin >> v[i] >> w[i];
}
for(int i = 1;i <= n;i++)
{
for(int j = 0;j <= m;j++)
{
for(int k = 0;k <= j/v[i];k++){
f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
2.两重循环代码优化
#include<iostream>
using namespace std;
const int N = 1010;
int n, m;
int f[N][N], v[N], w[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ )
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i ++ )
{
for(int j = 0; j <= m; j ++ )
{
if(v[i] <= j)
f[i][j] =max(f[i - 1][j], f[i][j - v[i]] + w[i]);
else
f[i][j] = f[i - 1][j];
}
}
cout << f[n][m] << endl;
}
3.多重背包
有 N种物品和一个容量是V 的背包。
第 ii 种物品最多有 si件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。输入格式
第一行两个整数,N,VN,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 NN 行,每行三个整数 vi,wi,sivi,wi,si,用空格隔开,分别表示第 ii 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
输入样例
4 5 1 2 3 2 4 1 3 4 3 4 5 2
输出样例:
10
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int v[N], w[N], s[N];
int f[N][N];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++)
cin >> v[i] >> w[i] >> s[i];
for(int i = 1; i <= n; i ++)//枚举背包
{
for(int j = 0; j <= m; j ++)//枚举体积
{
for(int k = 0; k <= s[i]; k ++)//枚举背包个数
{
if(j >= k * v[i]){
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
}
cout << f[n][m] << endl;
return 0;
}
二、最长公共子序列
给出两个长度为n的整数序列,求它们的最长公共子序列(LCS)的长度,保证第一个序列中所有元素都不重复。
注意:
- 第一个序列中的所有元素均不重复。
- 第二个序列中可能有重复元素。
- 一个序列中的某些元素可能不在另一个序列中出现。
输入格式
第一行包含一个整数 n。
接下来两行,每行包含 nn 个整数,表示一个整数序列。
输出格式
输出一个整数,表示最长公共子序列的长度。
输入样例1:
5 1 2 3 4 5 1 2 3 4 5
输出样例1:
5
输入样例2:
5 1 2 3 5 4 1 2 3 4 5
输出样例2:
4
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010;
int n;
int A[N], B[N] ,f[N][N];
int main()
{
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]) {
f[i][j] = f[i - 1][j - 1] + 1;
}
else {
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
}
cout << f[n][n] << endl;
return 0;
}
三、最大子连续子序列(连续)
1.子序列、子串、子段
1、子序列:任意地选取,即不用连续。
2、子串:任意个连续的字符组成的子序列称为该串的子串。
3、子段:等于子串+子序列,具体根据题目要求进行判断
2.最大连续子序列和
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1000010, INF = 0x3f3f3f3f;
int a[N], n;
int f[N]; //以i结尾的最大子序列
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) cin>>a[i];
int res = -INF, b = 1, e = 1, btmp = 1;
for (int i = 1; i <= n; i ++)
{
f[i] = max(f[i - 1] + a[i], a[i]);//状态转移方程
if (f[i - 1] < 0) btmp = i;//记录收尾坐标
if (res < f[i]) res = f[i], e = i, b = btmp;
}
if (res < 0) res = 0, b = 1, e = n;
cout << res <<' '<< a[b] <<' '<< a[e] << endl;
return 0;
}
/*for(int i = 1; i <= n; i ++ )//连续子数组或连续子段和最大
{
cin >> a[i];
dp[i] = max(dp[i - 1] - a[i], -a[i]);
res = max(res, dp[i]);
}*/
3.限定长度的最大子序列
输入一个长度为 n 的整数序列,从中找出一段长度不超过 m 的连续子序列,使得子序列中所有数的和最大。
注意: 子序列的长度至少是 1。
输入格式
第一行输入两个整数 n,mn,m。
第二行输入 nn 个数,代表长度为 nn 的整数序列。
同一行数之间用空格隔开。
输出格式
输出一个整数,代表该序列的最大子序和。
输入样例:
6 4 1 -3 5 1 -2 3
输出样例:
7
#include <algorithm>
#include <climits>
#include <deque>
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 3e5 + 10;
LL n, m, s[N], res = -1e8,ma=-1e10;
deque<int> q;
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> s[i];
ma=max(ma,s[i]);
s[i] += s[i - 1];
}
q.push_back(0);//相当于在队列里插入一个0,也就是s[0],如果不插入0的话,计算最大值时可能会忽略从开头开始的情况。
for (int i = 1; i <= n; i++)
{
if (q.size() && i - q.front() > m) q.pop_front();//长度是否超限
res = max(res, s[i] - s[q.front()]);
while (q.size() && s[q.back()] >= s[i]) q.pop_back();//构成单调递增
q.push_back(i);//插入的是下标
}
if(res<0) cout<<ma<<endl;//如果都是则取最大的一个负数
else cout << res << endl;
return 0;
}
四、最长上升子序列(不连续)
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
输入样例:
7 3 1 2 1 8 5 6
输出样例:
4
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, a[N], f[N];
int main ()
{
cin >> n;
for (int i = 1;i <= n;i++) cin >> a[i];
int ans = 0;
for (int i = 1;i <= n;i++)
{
f[i] = 1;
for (int j = 1;j < i;j++)
{
if (a[j] < a[i]) f[i] = max (f[i],f[j] + 1);
}
ans = max (ans,f[i]);
}
cout << ans << endl;
return 0;
}
五、常考经典问题
1.流水作业调度(待写)
#include<bits/stdc++.h>
using namespace std;
const int N = 5;
class Jobtype
{
public:
int key,index;//key为机器所用时间,index为作业序号
bool job;//job为ture表示M1,false表示M2
inline bool operator <(const Jobtype &a) const //重载 <=
{
return(key<a.key);
}
};
int FlowShop(int n,int a[],int b[],int c[]);
int main()
{
int a[] = {2,4,3,6,1};
int b[] = {5,2,3,1,7};
int c[N];
int minTime = FlowShop(N,a,b,c);
cout<<"作业在机器1上的运行时间为:"<<endl;
for(int i=0; i<N; i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
cout<<"作业在机器2上的运行时间为:"<<endl;
for(int i=0; i<N; i++)
{
cout<<b[i]<<" ";
}
cout<<endl;
cout<<"完成作业的最短时间为:"<<minTime<<endl;
cout<<"编号从0开始,作业调度的顺序为:"<<endl;
for(int i=0; i<N; i++)
{
cout<<c[i]<<" ";
}
cout<<endl;
return 0;
}
int FlowShop(int n,int a[],int b[],int c[])
{
Jobtype *d = new Jobtype[n];
for(int i=0; i<n; i++)
{
d[i].key = a[i]>b[i]?b[i]:a[i];//按Johnson法则分别取对应的b[i]或a[i]值作为关键字
//找作业在两台机器上处理时间小的那个作业
d[i].job = a[i]<=b[i];//给符合条件a[i]<b[i]的放入到N1子集标记为true
d[i].index = i;
}
sort(d,d+n);//对数组d按关键字升序进行排序 快排
int j = 0,k = n-1;
for(int i=0; i<n; i++)
{
if(d[i].job)//N1集合,ai<=bi
{
c[j++] = d[i].index;//将排过序的数组d,取其中作业序号属于N1的从前面进入
}
else//N2集合,ai>bi
{
c[k--] = d[i].index;//属于N2的从后面进入,从而实现N1的非减序排序,N2的非增序排序
}
}
j = a[c[0]];//第一个作业在M1上的处理时间
k = j+b[c[0]];//第一个作业处理完所需时间
for(int i=1; i<n; i++)
{
j += a[c[i]];//M1在执行c[i]作业的同时,M2在执行c[i-1]号作业,最短执行时间取决于M1与M2谁后执行完
k = j<k?k+b[c[i]]:j+b[c[i]];//计算最优加工时间
}
delete d;
return k;
}
2.数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
输入格式
第一行包含整数 n,表示数字三角形的层数。
接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
输入样例:
5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
输出样例:
30
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 510;
int f[N][N], n;
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
cin >> f[i][j];
for (int i = n - 1; i >= 1; i--)//倒序从下至上
{
for (int j = 1; j <= i; j++)
{
f[i][j] = max((f[i + 1][j + 1]+ f[i][j]), (f[i + 1][j]+ f[i][j])) ;
}
}
cout << f[1][1] << endl;
}
3.矩阵连乘(已考过)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, f[N][N], p[N];
int main()
{
cin>>n;
for(int i = 1 ; i <= n ; i ++)
{
int a,b;
scanf("%d%d",&a,&b);
p[i-1] = a ; p[i] = b;
}
for(int len = 2 ; len <= n; len ++)
{
for(int i = 1; i <= n - len + 1 ; i ++)
{
int j = i + len -1;
f[i][j] = 1e9;
for(int k = i; k <= j - 1; k ++)
{
f[i][j] = min(f[i][j],f[i][k]+f[k+1][j]+p[i-1]*p[k]*p[j]);
}
}
}
cout<<f[1][n];
return 0;
}
4.石子合并
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为
1 3 5 2
, 我们可以先合并 1、2堆,代价为 4,得到4 5 2
, 又合并 1、2堆,代价为 9,得到9 2
,再合并得到 11,总代价为 4+9+11=24;如果第二步是先合并 2、3 堆,则代价为 7,得到
4 7
,最后一次合并代价为 11,总代价为 4+7+11=22。问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数 N 表示石子的堆数 N。
第二行 N个数,表示每堆石子的质量。
输出格式
输出一个整数,表示最小代价。
输入样例:
4 1 3 5 2
输出样例:
22
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n, s[N], f[N][N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]);
for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];//前缀和
for (int len = 2; len <= n; len ++ )
for (int i = 1; i + len - 1 <= n; i ++ )
{
int l = i, r = i + len - 1;
f[l][r] = 1e8;
for (int k = l; k < r; k ++ )
{
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
}
printf("%d\n", f[1][n]);
return 0;
}
5.最短编辑距离
给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:
- 删除–将字符串 A 中的某个字符删除。
- 插入–在字符串 A 的某个位置插入某个字符。
- 替换–将字符串 A 中的某个字符替换为另一个字符。
现在请你求出,将 A 变为 B 至少需要进行多少次操作。
输入格式
第一行包含整数 nn,表示字符串 AA 的长度。
第二行包含一个长度为 nn 的字符串 AA。
第三行包含整数 mm,表示字符串 BB 的长度。
第四行包含一个长度为 mm 的字符串 BB。
字符串中均只包含大小写字母。
输出格式
输出一个整数,表示最少操作次数。
输入样例:
10 AGTCTGACGC 11 AGTAAGTAGGC
输出样例:
4
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
scanf("%d%s", &n, a + 1);
scanf("%d%s", &m, b + 1);
for (int i = 0; i <= m; i ++ ) f[0][i] = i;
for (int i = 0; i <= n; i ++ ) f[i][0] = i;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
printf("%d\n", f[n][m]);
return 0;
}