实验二 动态规划
一、实验目的
1、理解动态规划算法的概念;
2、掌握动态规划算法的基本要素;
3、掌握设计动态规划算法的步骤;
4、通过应用范例学习动态规划算法的设计技巧与策略。
二、实验内容和要求
实验要求:通过上机实验进行算法实现,保存和打印出程序的运行结果,并结合程序进行分析,上交实验报告和程序文件。
实验内容:
1、最长公共子序列问题:给定两个序列X={x1,x2,…,xm}和Y={y1,y2,…,yn},找出X和Y的最长公共子序列。
2、矩阵连乘问题,给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2 ,…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。
3、剪绳子问题:给你一根长度为n的绳子,请把绳子剪成m段(m,n都是整数,n>1且m>1),每段绳子的长度记为k[0],k[1],…,k[m-1],请问k[0]×k[1]×…×k[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
三、算法思想分析
1. 动态规划
(1)算法总体思想
动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
与分治法不同的是,适合用动态规划法求解的问题,经分解得到的子问题往往不是独立的,存在大量的公共子问题。因此,用动态规划算法求解问题时,我们可依据其递归式以自底向上的方式进行计算。在计算过程中保存已解决的子问题答案。每个子问题只计算一次,在后面计算需要时直接调用,从而避免大量重复计算。
(2)动态规划基本步骤
① 找出最优解的性质,并刻画其结构特征。
② 递归地定义最优值。
③ 以自底向上的方式计算出最优值。
④ 根据计算最优值时得到的信息,构造最优解。
前三个步骤是动态规划算法的基本步骤。在只需求出最优值的情况,步骤四可以省去。若需要求最优解,则必须执行步骤四,根据所记录的信息,快速构造出最优解。
2. 实验内容一分析
(1)问题形式化定义
(2)问题结构分析
(3)递归关系建立
(4)自底向上计算
(5)最优方案追踪
3. 实验内容二分析
(1)问题定义
输入:矩阵个数n、矩阵链每个矩阵的行数和最后一个矩阵的列数p[n+1]
输出:找到一种加括号的方式,以确定矩阵链乘法的计算顺序,使得最小化矩阵链标量乘法的次数
(2)问题结构分析
(3)递推关系建立
(4)自底向上计算
(5)最优方案追踪
4. 实验内容三分析
(1)问题定义
输入:绳长n、欲切成的段数m
输出:找到一种切割方法,使得最大化所有段相乘结果
(2)问题结构分析
① 给出问题表示
a[i][j]:计算长度为i,段数为j的绳子的最大乘积值
② 明确原始问题
a[n][m]:计算长度为n,段数为m的绳子的最大乘积值
(3)递推关系建立
(4)自底向上计算
(5)最优方案追踪
四、程序代码
1. 实验内容一
package com.company;
import java.util.Scanner;
public class theMaxChildString {
public static int[][] rec = new int[9999][9999]; //决策数组
public static int theMaxChildString (String text1,String text2) {
// 如果为null或为空字符串,返回
if (text1 == null || text2 == null || text1.length() == 0 || text2.length() == 0) {
return 0;
}
// 转换为字符数组
char[] charString1 = text1.toCharArray();
char[] charString2 = text2.toCharArray();
// 得到数组长度
int len1 = charString1.length;
int len2 = charString2.length;
//最长公共子序列长度计算数组
int[][] dp = new int[len1+1][len2+1];
for (int i = 1;i <= len1;++i) {
for (int j = 1;j <= len2;++j) {
// 当前两字符相等,dp[i][j]=dp[i−1][j−1]+1
if (charString1[i-1] == charString2[j-1]) {
rec[i][j] = 1;
dp[i][j] = dp[i-1][j-1] + 1;
}else {
//两个字符不相同,dp[i][j]=max{dp[i−1][j],dp[i][j−1]}
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
if (dp[i-1][j] > dp[i][j-1]) {
rec[i][j] = 2;
}else {
rec[i][j] = 3;
}
}
}
}
return dp[len1][len2];
}
//打印最长公共子序列
public static void print(int i,int j,String text) {
if (i == 0 || j == 0) {
return;
}
if (rec[i][j] == 1) {
print(i-1, j-1,text);
System.out.print(text.charAt(i-1));
}else if (rec[i][j] == 2) {
print(i-1, j, text);
}else if (rec[i][j] == 3) {
print(i,j-1,text);
}
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("请输入第一个字符串:");
String text1 = in.nextLine();
System.out.print("请输入第二个字符串:");
String text2 =in.nextLine();
System.out.println("最长公共子序列长度为:" + theMaxChildString(text1,text2));
System.out.print("公共子序列为:");
print(text1.length(),text2.length(),text1);
System.out.println();
in.close();
}
}
2. 实验内容二
#include<iostream>
#include<cstring>
using namespace std;
const int size=1000;
int p[size]; //记录矩阵行列数
int m[size][size]; //计算矩阵链乘法次数
int rec[size][size]; //决策数组
int n; //矩阵个数
void mulChain()
{
//初始化数组
memset(m,0,sizeof(m));
memset(rec, 0, sizeof(rec));
for(int r=2; r<=n; r++) //矩阵连乘的规模为r
{
for(int i=1; i<=n-r+1; i++)
{
int j = i+r-1;
m[i][j] = m[i+1][j]+p[i-1]*p[i]*p[j]; //对m[][]开始赋值
rec[i][j] = i;
for(int k=i+1; k<j; k++) //寻找最优值
{
int t = m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(t < m[i][j])
{
m[i][j]=t;
rec[i][j]=k;
}
}
}
}
}
//打印最优方案
void print(int i,int j)
{
if(i == j)
{
cout<<"A["<<i<<"]";
return;
}
cout<<"(";
print(i, rec[i][j]); //递归i到s[1][j]
print(rec[i][j] + 1, j); //递归1到s[1][j]
cout<<")";
}
int main()
{
cout<<"请输入矩阵的个数: ";
cin>>n;
cout<<"请依次输入每个矩阵的行数和最后一个矩阵的列数:"<<endl;
for(int i=0;i<=n;i++) cin>>p[i];
mulChain();
cout<<"最优矩阵链乘法结果为:"<<endl;
print(1,n);
cout<<endl;
cout<<"最小计算量为:"<<m[1][n]<<endl;
return 0;
}
3. 实验内容三
#include<iostream>
#include<cstring>
using namespace std;
#define size 100 //表格的大小
int a[size][size] ; //a[i][j]存储长度为i,段数为j的绳子的最大乘积值
int rec[size][size]; //rec[i][j]存储长度为i,段数为j的绳子最大乘积值的切点
void Cut(int n){
a[1][1]=1;
for(int i=2; i <= n; i++){
a[i][1]=i; //只能有1段
for(int j=2; j <= i; j++){
a[i][j]= a[i-1][j-1] * 1;
rec[i][j] = 1; //切点为1时
for(int k =2; k < i; k++){
//寻找最佳切点
if(a[i - k][j - 1] * k > a[i][j]){
a[i][j]= a[i - k][j - 1] * k;
rec[i][j]= k;
}
}
}
}
}
//打印剪切结果
void Print(int n, int m){
int x=n,c=m;
while(c>0){
for(int i=x;i>=0;i--){
if(rec[x][c]==i && i!=0){
cout<<i<<" ";
x-=i;
c--;
break;
}else if(rec[x][c] ==i){
cout<<i<<endl;
x=0;
x--;
break;
}
}
if(c==1){
cout<<x<<endl;
break;
}
}
}
int main(){
int n,m;
cout<<"请输入绳子长度:"<<endl;
cin >> n;
cout<<"请输入绳子想剪成的段数:"<<endl;
cin >> m;
memset(a,0,sizeof(a));
Cut(n);
cout<<"最大乘积是:"<< a[n][m] <<endl;
cout<<"每段剪成的长度分别是:";
Print(n,m);
}
五、结果运行与分析
1. 实验内容一
用户在执行程序的过程中,控制台会交互用户,提醒用户输入两个字符串序列,然后根据系统内部的动态规划求解LCS问题,并给出最终的最长公共子序列长度以及公共子序列子串。
在内部实现中,我主要编写了theMaxChildString (String text1,String text2) 函数动态规划求解最长公共子序列长度,以及print(int i,int j,String text) 函数回溯求解输出公共子序列子串。
算法的时间复杂度为O(n*m)。其中,n和m为两个字符串的长度。
结果如下图所示,结果正确。
2. 实验内容二
用户在执行程序的过程中,控制台提醒用户输入矩阵的个数、每个矩阵的行数和最后一个矩阵的列数。然后根据系统内部的动态规划求解问题,并给出最优矩阵链乘法的最小计算量和加括号的方式。
内部实现中,我主要编写了mulChain()函数动态规划求解最终的最小计算量,以及print(int i,int j)函数回溯求解最佳矩阵链乘法加括号的方式。
算法的时间复杂度为O(n^3)。其中,n为矩阵个数。
结果如下图所示,结果正确。
3. 实验内容三
用户在执行程序的过程中,控制台提醒用户输入身子长度以及想要剪成的段数。然后根据编程的动态规划进行求解,给出最终绳子各段长度相乘的最大值和剪切方法。
内部实现中,我主要编写了Cut(int n)函数动态规划求解最大乘积,以及print(int i,int j)函数回溯求解最佳矩阵链乘法加括号的方式。
算法的时间复杂度为O(n^3)。其中,n为绳子长度。
结果如下图所示,结果正确。
六、心得与体会
本次实验,我运用动态规划思想,以Java语言和C++语言编程,解决了最长公共子序列问题、矩阵连乘问题和剪绳子问题。
其中,最长公共子序列和矩阵连乘问题在上课时尹老师已经为我们详细地讲解过,所以依照上课的ppt整理动态规划的思路,很快就完成了编码。而剪绳子问题在课上没有涉及,所以我依照动态规划的基本步骤自行对问题进行了分析,推出其递推式,后自底向上寻找最优解,完成程序编写。
首先,我想先谈谈自己和动态规划算法的一些接触。在算法课上第一次接触动态规划后,我觉得它和分治法很类似,都是将待求解问题分解成若干个子问题,先求解子问题,然后从子问题的解中得到原问题的解。但不同的是,动态规划在求解问题时,已经保存了已解决的子问题答案,在后面计算需要用到时直接调用,大大减少了重复计算量。
其中动态规划分为四步骤:①找出最优解的性质,并刻画其结构特征;②递归地定义最优值;③以自底向上的方式计算出最优值;④根据计算最优值时得到的信息,构造最优解。我认为其中最难最核心的就是递归定义最优值,其他无非就是计算问题,递推关系建立才是我们的核心。在求最长公共子序列问题中,这里的递推式就是关于C[i,j]的数组求解,如果X与Y的字符相等则C[i,j]=C[i-1,j-1]+1,否则取上一个记录的C[i-1,j]与C[i,j-1]中的最大值保存。(后两个问题递推式在上文中已给出,不再重复表述)。
通过本次实验,我更加深入地理解了动态规划算法的概念,掌握了动态规划算法的基本思路及方法,并且通过实现案例提高了针对类似问题的设计技巧和编程能力。