递归是从n逐步化简直到递归出口的过程(递归出口往往十分简单),而动态规划则是从原来设计的递归出口,反向分析到n的过程,动态规划往往比递归运行效率更高。动态规划可以算作递归的剪枝优化版,由于使用到了额外的空间保存已经计算过的信息,可以节省大量重复计算的时间。
动态规划就是制表的过程
算法设计与分析系列主要是完成书上的例题或习题,题面可能不完善或简略。
装配线调度问题
装配线调度问题
求进厂到出厂中时间最短的线路,由上图。
①状态转移方程:
fp[i] = min{fp[i - 1], fp'[i - 1] + tp'[i - 1]}
②设计表格f(+号表示同类的延展,下同)
存放:最短时间 | 结点编号+ |
第一条装配线 |
|
第二条装配线 |
核心代码:
f[0][1] += a[1];
f[1][1] += b[1];
for(int i = 2; i <= n; i++)
{
f[0][i] = min(f[0][i - 1], f[1][i - 1] + t[1][i - 1]) + a[i];
f[1][i] = min(f[0][i - 1] + t[0][i - 1], f[1][i - 1]) + b[i];
}
完全可运行的代码:
#include
#include
using namespace std;
const int N = 1001;
int f[2][N];
int main()
{
int n;
cin >> n;
int a[n + 10], b[n + 10], t[2][n + 10];
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++)
cin >> t[0][i];
for(int i = 1; i <= n; i++)
cin >> t[1][i];
cin >> f[0][1] >> f[1][1];
f[0][1] += a[1];
f[1][1] += b[1];
for(int i = 2; i <= n; i++)
{
f[0][i] = min(f[0][i - 1], f[1][i - 1] + t[1][i - 1]) + a[i];
f[1][i] = min(f[0][i - 1] + t[0][i - 1], f[1][i - 1]) + b[i];
}
/*输出表格*/
for(int i = 1; i <= n; i++)
{
cout << f[0][i] << '\t';
}
cout << endl;
for(int i = 1; i <= n; i++)
{
cout << f[1][i] << '\t';
}
int mmin = min(f[0][n] + t[0][n], f[1][n] + t[1][n]);
cout << mmin;
}
给出测试数据:
//测试数据结构: 节点个数 第一条流水线处理时间 第二条流水线处理时间 第一条流水线转至第二条流水线消耗时间 第二条流水线转至第一条流水线消耗时间 入场时间(入1流水,入2流水) //输入结束 |
6
7 9 3 4 8 4
8 5 6 4 5 7
2 3 1 3 4 3
2 1 2 2 1 2
2 4
得到输出
9 18 20 24 32 35
12 16 22 25 30 37 38
答案是正确的,路径记录则再开一个数组,可以轻松实现。
矩阵链乘法问题
给出一串矩阵相乘(一定满足可乘性质)求最小乘法次数。
由矩阵乘法的定义,一个p行q列乘q行r列的矩阵的乘法次数为pqr。
当划分为1个矩阵时不需要乘,即为0;
①状态转移方程
f[i, j] = min( f[i, k] + f[k + 1, j] + 对应pqr)
边界条件:如果只有一个矩阵,那乘法不需要进行,为0
填表方向:沿对角线
②设计表格f
存放:从i乘到j最小乘法次数 |
右边界:j+ |
左边界:i+ |
③结果:
f[i, j]
核心代码:
for(int i = 1; i <= n; i++)
f[i][i] = 0; // 初始化边界条件
for(int m = 2; m <= n; m++)//观察到i从1到n-1循环,j先从2开始到n,再从3开始到n,用m表示起点
for(int j = m, i = 1; j <= n; j++, i++, i == n ? i = 1 : 1)
for(int k = i; k < j; k++)//关键:沿着对角线方向依次打表
{
int tmp = f[i][k] + f[k + 1][j] + height[i] * weight[k] * weight[j];
f[i][j] = (tmp < f[i][j])||!f[i][j] ? tmp : f[i][j];
}
完全代码:
#include
#include
using namespace std;
const int N = 1010;
int f[N][N];
int main()
{
int n;
cin >> n;
int height[n + 10], weight[n + 10];
for(int i = 1; i <= n; i++)
cin >> height[i] >> weight[i];
for(int i = 1; i <= n; i++)
f[i][i] = 0; // 初始化边界条件
for(int m = 2; m <= n; m++)//观察到i从1到n-1循环,j先从2开始到n,再从3开始到n,用m表示起点
for(int j = m, i = 1; j <= n; j++, i++, i == n ? i = 1 : 1)
for(int k = i; k < j; k++)//关键:沿着对角线方向依次打表
{
int tmp = f[i][k] + f[k + 1][j] + height[i] * weight[k] * weight[j];
f[i][j] = (tmp < f[i][j])||!f[i][j] ? tmp : f[i][j];
}
/*打印表*/
for(int i = 1; i <= n; i++, cout << endl)
for(int k = 1; k <= n; k++)
cout << f[i][k] << '\t';
cout << f[1][n];
}
使用测试用例:
6
30 35
35 15
15 5
5 10
10 20
20 25
//书上的测试用例,第一行表示矩形个数,后面依次表示行、列数
得到结果:
0 15750 7875 9375 11875 15125
0 0 2625 4375 7125 10500
0 0 0 750 2500 5375
0 0 0 0 1000 3500
0 0 0 0 0 5000
0 0 0 0 0 0
15125
得到的结果是正确的。
最长公共子序列问题
最长公共子序列问题曾经在acwing学习过
点击阅读
最长公共子序列问题,简称LCS(Longest Common Subsequence)是指寻找两字符串中最长的公共子序列算法,在生活中具有许多应用。使用一般枚举的办法时间复杂度为O(n*2^m)为指数级,这里研究动规解法。
有字符串{x1,...,xi},{y1, ..., yj}要寻找两字符串中最长公共子序列。
①状态转移方程
用f[i][j]表示x字符串1~i,y字符串1~j中最长公共子序列长度,可以得到如下状态转移方程
如果i位置和j位置字符相同,则
f[i][j] = f[i - 1][j - 1] + 1;
如果i位置和j位置字符不同,证明该位置对公共子序列无贡献
f[i][j]的值应取f[i][j - 1]和f[i - 1][j]中的大值
f[i][j] = max{f[i][j - 1], f[i - 1][j]}
边界条件:如果字符串长度为0,那必然没有,于是为0
填表方向:横向
②设计表格
存放:X:1~i,Y:1~j最长公共子序列长度 |
s2:j+ |
s1:i+ |
③结果
结果取f[i][j]
核心代码:
for(int i = 1; i < s1.length(); i++)
for(int j = 1; j < s2.length(); j++)
if(s1[i] == s2[j]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
全代码:
#include
#include
using namespace std;
const int N = 1000;
int f[N][N];
int main()
{
string s1, s2;
cin >> s1 >> s2;
for(int i = 0; i < s1.length(); i++)
f[i][0] = 0;
for(int j = 0; j < s2.length(); j++)
f[0][j] = 0;
//边界条件
s1 = ' ' + s1;
s2 = ' ' + s2;
//处理,使得s1从1开始,便于后续存放
for(int i = 1; i < s1.length(); i++)
for(int j = 1; j < s2.length(); j++)
if(s1[i] == s2[j]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
/*打印表格*/
for(int i = 0; i < s1.length(); i++, cout << endl)
for(int j = 0; j < s2.length(); j++)
cout << f[i][j] << '\t';
cout << f[s1.length() - 1][s2.length() - 1];
}
测试数据:
使用书上的测试数据
ABCBDAB
BDCABA
输出:
0 0 0 0 0 0 0
0 0 0 0 1 1 1
0 1 1 1 1 2 2
0 1 1 2 2 2 2
0 1 1 2 2 3 3
0 1 2 2 2 3 3
0 1 2 2 3 3 4
0 1 2 2 3 4 4
4
得到的表格与书上一致。
01背包问题
背包问题也曾发过推送
点击查看背包问题
这里介绍01背包,物品不能分割,只有两种状态,拿或不拿,求给定容积的背包能拿的最大价值。
对于背包空间为w,物品1~i,求最大价值
①状态转移方程
根据物品i要么放要么不放的性质
对物品i讨论
f[i][w] = max{ f[i - 1][w], f[i - 1][w - wi] + vi}
状态转移方程即对不放和放两种状态取最大值
边界条件:当i = 0无物可放 或 w = 0 空间耗尽时为0
填表方向:横向
②设计表格
存放:当前i,w所能存放的最大价值 |
容积w+ |
物品数i+ |
③结果
取结果f[i][w]
核心代码:
for(int i = 0; i <= n; i ++)
f[i][0] = 0;
for(int i = 0; i <= wmax; i++)
f[0][i] = 0;//边界条件
for(int i = 1; i <= n; i++)
for(int j = 1; j <= wmax; j++)
{
f[i][j] = f[i - 1][j];
if(w[i] <= j)f[i][j] = max(f[i][j], f[i - 1][j - w[i]] + v[i]);
}//横向制表
全代码:
#include
using namespace std;
const int N = 1000;
int f[N][N];
int main()
{
int w[N], v[N];
int n, wmax;
cin >> n >> wmax;
for(int i = 1; i <= n; i ++)
cin >> w[i] >> v[i];
for(int i = 0; i <= n; i ++)
f[i][0] = 0;
for(int i = 0; i <= wmax; i++)
f[0][i] = 0;//边界条件
for(int i = 1; i <= n; i++)
for(int j = 1; j <= wmax; j++)
{
f[i][j] = f[i - 1][j];
if(w[i] <= j)f[i][j] = max(f[i][j], f[i - 1][j - w[i]] + v[i]);
}
/*打印表格*/
for(int i = 0; i <= n; i++, cout << endl)
for(int j = 0; j <= wmax; j++)
cout << f[i][j] << '\t';
cout << f[n][wmax];
}
测试数据:
使用书上的测试数据
4 5
2 12
1 10
3 20
2 15
输出:
0 0 0 0 0 0
0 0 12 12 12 12
0 10 12 22 22 22
0 10 12 22 30 32
0 10 15 25 30 37
37
结果和书上一致。