本篇博客仅为对动态规划基础问题的状态转移方程进行求解,然后给出对应的注释代码,有关题目的具体内容可在算法导论或网络上进行查看
目录
1.钢管切割(最小值)
2.两条流水线调度
3.多条流水线调度
4.最长上升子序列
5.矩阵链乘
6.OBST
内容
1.钢管切割
实现解释:
先设数组price[i]存储着i长度钢管切割后的最小值,p[i]存储着i长度钢管不切割的值,price数组既是本问题的dp数组。
经过分析可知状态转移方程为:
price[0] = 0;
price[i] = min(p[1]+price[i-1],p[2]+price[i-2],...p[i-1]+price[1],p[i]);
因为price[i]已经是当前情况下的最小值了,所以只需要遵循转移方程进行代码的完善即可。
若要计算最大值只需在计算前设置最大值为负数(保证最小),然后在判断递归判断小于即可。
坑点:
初始化和状态转移方程的书写
完整代码:
//钢管切割最小值 #include<iostream> using namespace std; int main() { ios::sync_with_stdio(false); int length,MIN;//分别为长度和最小值 while(cin >> length) { int p[length+1]; for(int i = 1;i<=length;i++) cin >> p[i];//依据题意,i长度的价值 int price[length+1];//保存每段价值 price[0] = 0;//会使用到price[0],防止出错初始化 for(int i = 1;i<=length;i++) { MIN = 2147483647;//获得最小值时需要设为最大值 for(int j = 1;j<=i;j++) { if(MIN > p[j]+price[i-j]) //不断将i长度切割为前j部分和后i-j部分 //以找到i长度切割后的最小值 { MIN = p[j] + price[i-j];//替换最小值 price[i] = MIN; } //状态转移方程介绍见实现解释 } } cout << price[length] << ' '; //输出最长部分(i)切割后的最小值 } return 0; }
2.两条流水线调度
实现解释:
实现方法即得出状态转移方程后完善即可,设a[][i]存储着第一二条线上各家的时间花费,t[][i]存储着i处进行线路切换的花费,f[][i]存储着各线在i处的最小花费。
则对每一个f[][i]应有如下的转移方程:
f[0][1] = a[0][1];
f[1][1] = a[1][1];
f[0][i] = min(f[0][i-1]+a[0][i],f[1][i-1]+t[1][i-1]+a[0][i]);
f[1][i] = min(f[1][i-1]+a[1][i],f[0][i-1]+t[0][i-1]+a[1][i]);
前两个为初始定义,后两个为具体的转换。
min函数中前一个值表示直接在当前这条线的前一个结点前进的花费,后一个表示从另一条线的前一个结点先转移过来后再前进的花费。此时只有两条线因此只有这两种情况,所以比较后便可得出到达该线该位置的最小花费。
多条线的情况可参考:题解:说好的ALS呢?(后续添加链接)
最后比较到达n处(最后一个家)哪条线的时间花费最小即可。
坑点:
注意多次输入后数组的初始化,以及状态转移方程的正确转化即可
完整代码:
//流水线调度基本问题 #include<iostream> using namespace std; int main() { ios::sync_with_stdio(false); int n; int fend; int i; while(cin >> n) { int a[2][n+1],t[2][n]; //两条线上每一家花费的时间 for(i = 1;i<=n;i++) cin >> a[0][i]; for(i = 1;i<=n;i++) cin >> a[1][i]; //两条线不同位置转移的时间花费 for(i = 1;i<n;i++) cin >> t[0][i]; for(i = 1;i<n;i++) cin >> t[1][i]; //存储两条线不同位置的最小时间 int f[2][n+1]; //初始化防止错误 f[0][1] = a[0][1]; f[1][1] = a[1][1]; for(i = 2;i<=n;i++) { //具体状态转移方程介绍见实现解释 if(f[0][i-1]+a[0][i]<f[1][i-1]+t[1][i-1]+a[0][i]) f[0][i] = f[0][i-1]+a[0][i]; else f[0][i] = f[1][i-1]+t[1][i-1]+a[0][i]; if(f[1][i-1]+a[1][i]<f[0][i-1]+t[0][i-1]+a[1][i]) f[1][i] = f[1][i-1]+a[1][i]; else f[1][i] = f[0][i-1]+t[0][i-1]+a[1][i]; } //计算两条线分别的最后时间花费得最小值 if(f[0][n]<f[1][n]) fend = f[0][n]; else fend = f[1][n]; cout << fend << ' '; } return 0; }
3.多条流水线调度
实现解释:
相比两条线的其实只是需要多判断几次
设a[i][j]存储着第i条线上第j个位置的花费,t[i][j]存储着第i条线到第j条线的调度费用,f[i][j]存储着i条线在j处的最小花费。
则对每一个f[][i]应有如下的转移方程:
第一个站台:f[i][1] = a[i][1];
其余站台:f[i][j] = min(f[i][j-1]+t[i][i]+a[i][j],f[1][j-1]+t[1][i]+a[i][j],...,f[n][j-1]+t[n][i]+a[i][j]);
注意站台间转移时需要考虑自己调度到自己的情况(视题意而定)
min函数中前一个值表示直接在当前这条线的前一个结点前进的花费,后面则表示从其他线的前一个结点先转移过来后再前进的花费,一次比较后便可得出到达当前线该位置的最小花费。
最后比较到达n处(最后一个站台)哪条线的时间花费最小即可。
坑点:
注意按照题意判断是否要考虑自身的调度t[i][i]即可
完整代码:
#include<iostream> #include<cstring> #include<algorithm> using namespace std; int main() { ios::sync_with_stdio(false); int num,n; int fend; int i,j; int tempf; while(cin >> num >> n)//分别为线路个数和站台个数 { int a[num][n+1],t[num][num]; for(i = 0;i<num;i++) for(j = 1;j<=n;j++) cin >> a[i][j];//i条线j站台的花费 for(i = 0;i<num;i++) for(j = 0;j<num;j++) cin >> t[i][j];//i条线到j条线的调度费用 int f[num][n+1];//一维为当前线路,二维为站台位置 for(i = 0;i<num;i++) { f[i][1] = a[i][1];//每条线第一个站台的最小花费就是第一个 } for(i = 2;i<=n;i++) { for(j = 0;j<num;j++) { tempf = 2147483647;//为了找到最小值先设定最大的 for(int k = 0;k<num;k++) {//循环判断所有条道路的情况 if(tempf > f[k][i-1]+t[k][j]+a[j][i]) { tempf = f[k][i-1]+t[k][j]+a[j][i]; } } f[j][i] = tempf;//存储j条线在i站台的最小值 } } tempf = f[0][n]; for(i = 1;i<num;i++) { if(tempf > f[i][n]) { tempf = f[i][n]; } } fend = tempf;//得到最大值 cout << fend << ' '; } return 0; }
4.最长上升子序列
实现解释:
这里介绍的其实是优化后的方案,即只存储长度,而不是以dp[i][j]的形式存储ij之间的最长长度,不过也是很好理解的,所以就直接分享这一篇吧。
a[i]存储总序列的内容,dp[i]表示以i为结尾的最长子序列长度
那么首先由dp[i]开始一定是1(自己是一个序列)
后面的状态转移方程即:
dp[i] = max(dp[j])+1(j<i&&a[j]<a[i])
解释:由于是上升序列,而且dp[i]是以i结尾的最长长度,因此长度增加时有两个条件:新的数字在旧数字的后面,新的数字大于旧的数字(新数字:dp[i],旧数字:dp[j]),也是唯一的转移方程。
坑点:
注意最后获取最长序列时,不能直接dp[n]输出,因为可能是在序列中间有最长的上升子序列,也需要循环判断。
完整代码:
#include<iostream> using namespace std; int main() { int n; cin >> n; int a[n],dp[n]; for(int i = 0;i<n;i++) { cin >> a[i]; dp[i] = 1;//自身一定是一个长度的序列 } for(int i = 1;i<n;i++) { for(int j = 0;j<i;j++) { if(a[i] > a[j])//因为是上升,所以需要只有比前面的值大才可能形成最长上升子序列 { if(dp[i] < dp[j]+1) dp[i] = dp[j] + 1;//记录i处最长的上升序列长度 //即前面的序列长度最大长度+1即是i处的最大长度 } } } int max = dp[0];//不一定最后一个是最长的,因此需要获取最大值 for(int i = 1;i<n;i++) { if(max < dp[i]) max = dp[i]; } cout << max << ' '; return 0; }
5.矩阵链乘
实现解释:
数据介绍:m[0]存储第一个矩阵的行数,m[i]存储第i个矩阵的列数和第i+1个矩阵的行数
num[i][j]记录第i个矩阵和第j个矩阵之间的最小计算次数
cut仅是记录切割位置所用cut[i][j]表示第ij个矩阵之间的最优切割位置
则按照矩阵乘法的知识,两个矩阵相乘的计算次数便可直接由以下的状态转移方程得到:
i=j时num[i][j] = 0
i<j时num[i][j] = min(num[i][k]+num[k+1][j]+m[i-1]*m[k]*m[j])(k为[i,j)间的值,即表示以第i个矩阵到j-1个矩阵之间第k个矩阵作为分割处)
解释:i=j自然不用计算,第二个既是当k作为分割点时总次数等于i到k个矩阵的相乘最小次数+第k+1个矩阵到第j个矩阵的相乘最小次数+合并时的产生的新次数(最左行数*分割点列数*最右行数)
然后便是正常的翻译了。
其中对于分割方案的输出,只需按照矩阵范围依据cut数组获取切割点输出即可,具体可参考代码。
坑点:
循环范围的设定,注意按是否能到达设定范围
完整代码:
#include<iostream> using namespace std; int length; int cut[2000][2000]; int count; void printCut(int i,int j) { if(i == j) cout << 'A' << i; else { cout << '('; printCut(i,cut[i][j]); printCut(cut[i][j]+1,j); count++; cout << ')'; } } int main() { ios::sync_with_stdio(false); int n; cin >> n; length = n; int m[n+1]; for(int i = 0;i<=n;i++) cin >> m[i]; int num[n+1][n+1];//对应矩阵个数即可 for(int i = 1;i<=n;i++) num[i][i] = 0; int temp; count = 0; for(int l = 2;l<=n;l++) //l为1的情况不必讨论,因为就是矩阵自己,所以从2到n依次进行 { for(int i = 1;i<=n-l+1;i++) //从第一个矩阵一直到最后一个可到达的矩阵 //这样i表示的既是连续l个矩阵的第一个矩阵 //根据输入,第i个矩阵的行数:i-1的值,列数:i的值 { int j = i+l-1;//当前l个矩阵的最后一个矩阵下标 num[i][j] = 2147483647;//用于比较 for(int k = i;k<=j-1;k++) //k是节点,代表第k和k+1个矩阵之间的那个下标 //所以需要到达j-1,也就是第j个矩阵前的那个位置才算结束 //由位置可知k的值为第k个矩阵的列数即划分开时需要乘的值 { temp = num[i][k]+num[k+1][j]+m[i-1]*m[k]*m[j]; //当前划分状态的计算次数,两个num分别为两侧矩阵链的次数,最后一项为合并后的新增次数 if(num[i][j]>temp) { num[i][j] = temp; cut[i][j] = k; } } } } printCut(1,n); cout << ' '; cout << count << ' ';//切割次数 cout << num[1][n] << ' ';//计算次数 return 0; }
6.OBST
实现解释:
实际等价于矩阵链乘,状态转移方程都是将左侧和右侧的最优相加后再加上合并新增的次数即可。
新增的次数对矩阵链乘来说就是两个最优部分相乘的次数
对OBST来说则是左右子树本来的搜索代价+左右子树深度加一产生的新代价+此时根节点的搜索代价(其中深度加一的新代价和根节点的搜索代价便可合并为此时所有节点的检索频率之和)
解释:深度加一时左右子树节点的搜索代价都增加了一个结点,因此他们的代价分别增加了一次搜索概率(自己被搜索到的概率),而根节点的搜索代价即本身的搜索概率,相加既是范围内所有结点的搜索概率和。
于是数据记录如下:
e[i][j]存储i到j个结点之间的最小搜索代价
w[i][j]存储i到j个结点的总搜索概率之和(用于状态转移快捷增加搜索概率和)
root[i][j]存储i到j个结点中有最小搜索代价时的根节点的脚标
状态转移方程即:
j = i-1时e[i][j] = 0(不存在根节点,因此也没有搜索代价)
j >= i时e[i][j] = min(e[i][r-1]+e[r+1][j]+w[i][j])(r即某次选择作为根节点的脚标)
对建树方案的实现和矩阵链乘的实现同理,只是由于伪关键字的加入需要进行一些特殊处理,即如果某个方向没有子树则需要手动添加伪关键字d形成的结点,其余子树的输出只需做好根节点和左右的判断即可,具体可参考代码,方案不唯一。
坑点:
注意dp数组的初始化,初始化既是对没检索到情况的初始化(因此添加的值只是伪关键字的搜索概率)
主要难点即理解搜索子树扩增时平均搜索的变化
完整代码:
#include<iostream> using namespace std; const int MAX = 2147483647; double **root; int di; int n; void printOBST(int l,int r) { if(l == 1&&r == n) { di = 0; cout << "k" << root[1][n] << "为根" << ' '; } int t,lt,rt; t = root[l][r]; if(l == t) cout << "d"<<di++ << "是k"<<t << "的左孩子" << ' '; else if(l < t) { lt = root[l][t-1]; cout << "k"<<lt << "是k"<<t << "的左孩子" << ' '; printOBST(l,t-1); } else return ; if(r == t) cout << "d"<<di++ << "是k"<<t << "的右孩子" << ' '; else if(r > t) { rt = root[t+1][r]; cout << "k"<<rt << "是k"<<t << "的右孩子" << ' '; printOBST(t+1,r); } else return ; } int main() { int maxi; double temp; while(cin >> n) { double p[n+1],q[n+1]; for(int i = 1;i<=n;i++) cin >> p[i]; for(int i = 0;i<=n;i++) cin >> q[i]; double e[n+2][n+1];//需要存储q的内容,因此n+2 double w[n+2][n+1];//同上 root = new double *[n+1];//只是在p中选root因此n+1即可 for(int i = 0;i<=n;i++) root[i] = new double[n+1]; for(int i = 1;i<=n+1;i++) { e[i][i-1] = q[i-1]; w[i][i-1] = q[i-1]; } for(int l = 1;l<=n;l++) { for(int i = 1;i<=n-l+1;i++) { maxi = i+l-1; //i+l-1就是长度l时能到达的最大的数据下标 e[i][maxi] = MAX; w[i][maxi] = w[i][maxi-1]+p[maxi]+q[maxi]; //i--j的总概率和即等于i--j-1的加上新增的maxi处的k和d的概率 for(int j = i;j<=maxi;j++) { temp = e[i][j-1]+e[j+1][maxi]+w[i][maxi]; if(temp < e[i][maxi]) { e[i][maxi] = temp; root[i][maxi] = j; } } } } cout << "最小搜索期望为:" << e[1][n] << ' '; cout << "树的形状如下:" << ' '; printOBST(1,n); } return 0; }