一、实验目的
1.加深学生对分治法算法设计方法的基本思想、基本步骤、基本方法的理解与掌握;
2.提高学生利用课堂所学知识解决实际问题的能力;
3.提高学生综合应用所学知识解决实际问题的能力。
二、实验任务
用动态规划算法实现:
1、矩阵链相乘问题
2、投资问题
3、求解完全背包问题
问题描述:有n 种重量和价值分别为wi 、vi (1≤i ≤n )的物品,从这些物品中挑选总重量不超过W 的物品,求出挑选物品价值总和最大的挑选方案,这里每种物品可以挑选任意多件。4、数字三角形
问题描述:在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。
4、数字三角形
问题描述:在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。
用贪心算法实现:
5、最小生成树问题(Prim算法和Kruskal算法)
设G=(V,E)是一个无向连通网,生成树上各边的权值之和称为该生成树的代价,在G的所有生成树中,代价最小的生成树称为最小生成树(Minimal Spanning Trees)。
6、背包问题
【问题描述】设有编号为1、2、...、n的n个物品,它们的重量分别为w1、w2、...、wn,价值分别为v1、v2、...、vn,其中wi、vi(1≤i≤n)均为正数。
有一个背包可以携带的最大重量不超过W。求解目标:在不超过背包负重的前提下,使背包装入的总价值最大(即效益最大化),与0/1背包问题的区别是,这里的每个物品可以取一部分装入背包。
三、实验设备及编程开发工具
实验设备:惠普Win10电脑
开发工具:Java和python环境下,eclipse和pycharm编程工具
四、实验过程设计(算法思路及描述,代码设计)
一.矩阵相乘问题
基本原理和思路:
1、动态规划的第一步:寻找最优子结构。为方便起见,使用Ai...j表示AiAi+1...Aj的乘积结果矩阵。对于k(i<=k<j), 计算Ai...j所需要的计算量为:Ai...k 和 Ak+1...j 以及二者相乘的代价和。
2、设m[i][j]为Ai...j的最优计算顺序所要花费的代价。则其求解公式为:
if i == j, m[i][j] = 0; //因为只有一个矩阵时计算代码为0,即不需要计算。
m[i][j]=min{m[i][k] + m[k+1][j] + Pi-1PkPj} i<=k<j
3、为了能够输出求解顺序,需要保存区间中的一些分割点。假如Ai...j中的最优分割点为k,则我们使用s[i][j]=k。即在Ai...j中,分别计算Ai...k 和 Ak+1...j 所用的计算开销最小。
4、采用自底向上的表格法。依次求解矩阵长度为2,3,...,n的最优计算顺序。
代码实现如下:
cpp
#include <stdio.h>
int m[1002][1002],s[1002][1002];
void matrix_chain(int a[], int n)
{
int l, i, j, k, tmp;
for(l=2; l<=n; l++)
{
for(i=1; i<=n-l+1; i++) //长度为l的区间,其最小下标为1~n-l+1
{
j=i+l-1;
m[i][j] = 0x7fffffff;
for(k=i; k<j; k++) //i~k, k+1~j, 所以k<j
{
tmp = m[i][k]+m[k+1][j]+a[i-1]*a[k]*a[j];
if(tmp < m[i][j])
{
m[i][j] = tmp;
s[i][j] = k;
}
}
}
}
}
void print(int i, int j)
{
if(i == j)
printf("A%d",i);
else{
printf("(");
print(i, s[i][j]);
print(s[i][j]+1, j);
printf(")");
}
}
int main()
{
int n, a[1002];
int i,j,l;
while(scanf("%d",&n)==1) //输入有n个矩阵
{
for(i=0; i<n+1; i++)
scanf("%d",&a[i]);
//memset(m, 0x7fffffff,sizeof(m));
for(i=0; i<n+1; i++)
m[i][i] = 0;
matrix_chain(a, n);
printf("%d\n",m[1][n]);
print(1, n);
printf("\n");
}
return 0;
}
分析:时间复杂度为O(N3), 我们只需要存储一个矩阵就可以了,所以空间复杂度是 O(N2)。
二.投资问题
基本原理和思路:假设分配给第 i 个项目的钱数是 xi,问题描述为:
目标函数:max{f1(x1)+f2(x2)+...+fn(xn)}
约束条件:x1+x2+...+xn=m,xi∈N;设Fk(x)表示x元投给前k个项目的最大效益,k=1,2,...,n,x=1,2,...,m递推方程:Fk(x)=max{fk(xk)+Fk-1(x-xk)}(0≤xk≤x),k=2,3,...,n边界条件:F1(x)=f1(x),Fk(0)=0,k=1,2,...,n*说明:第k步,前后共分配x万元,分配给第k个项目xk;x-xk万元,分配给前k-1个项目;
代码实现如下:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
int m, n;//m元钱,n项投资
int i, j;
int tmp_m, tmp_F = 0;
cout << "请输入投资金额和项目数" << endl;
cin >> m >> n;
vector<vector<int>> f(n, vector<int>(m + 1));//f[i][x], 0<i<=n,0<=x<=m;
vector<vector<int>> F(n, vector<int>(m + 1));//F[i][x],将x元钱投入到前i个项目上最大的收益
//在第(i+1)个项目上投入0元,收益为0,注意i从0开始
for (i = 0; i < n; i++) {
f[i][0] = 0;
}
cout << "请输入各项目对应投资金额的收益(从1开始)" << endl;
for (i = 0; i < n; i++) {
for (j = 1; j < m + 1; j++) {
cin >> f[i][j];
}
}
//初始化,给F[0][0-m]赋值
for (j = 0; j < m + 1; j++) {
F[0][j] = f[0][j];//第一个项目上投入0-m元钱的最大收益等于f[0][0-m]
}
for (i = 1; i < n; i++) {//项目编号,从1开始
for (j = 0; j < m + 1; j++) {//钱数,从0开始
for (tmp_m = 0; tmp_m <= j; tmp_m++) {
//递推公式
tmp_F = F[i - 1][j - tmp_m] + f[i][tmp_m];
//取最大值
if (tmp_F > F[i][j]) {
F[i][j] = tmp_F;
}
}
}
}
cout << "最大总收益: " << F[n - 1][m] << endl;
}
分析:复杂度 W(n,m)=O(nm2)。
三. 求解完全背包问题
基本原理和思路:1.设置动态规划二维数组dp,dp[i][j]表示从前i个物品中选出重量不超过j(或者剩余容量为j)的物品的最大总价值。
①显然有边界条件:dp[i][0]=0(背包不能装入任何物品时,总价值为0),dp[0][j]=0(没有任何物品可装入时,总价值为0),可以采用memset函数一次性初始化为0.
②另外设置二维数组fk,其中fk[i][j]存放dp[i][j]得到最大值时物品i挑选的件数。
代码实现如下:
cpp
//求解完全背包问题的算法
#include <stdio.h>
#include <string.h>
#define MAXN 20 //最多物品数
#define MAXW 100 //最大限制重量
#define max(x,y) ((x)>(y)?(x):(y))
//问题表示
int n,W;
int w[MAXN],v[MAXN];
//求解结果表示
int dp[MAXN+1][MAXW+1],fk[MAXN+1][MAXW+1];
int solve() //求解多重背包问题
{
int i,j,k;
for (i=1;i<=n;i++)
{
for (j=0;j<=W;j++)
for (k=0;k*w[i]<=j;k++)
{
if (dp[i][j]<dp[i-1][j-k*w[i]]+k*v[i])
{
dp[i][j]=dp[i-1][j-k*w[i]]+k*v[i];
fk[i][j]=k; //物品i取k件
}
}
}
return dp[n][W];
}
void Traceback() //回推求最优解
{
int i=n,j=W;
while (i>=1)
{
printf("物品%d共%d件 ",i,fk[i][j]);
j-=fk[i][j]*w[i]; //剩余重量
i--;
}
printf("\n");
}
void prin(){ //查看dp数组与fk数组
int i,j,k;
printf("fk[i][j]:\n");
for (i=1;i<=n;i++)
{
for (j=0;j<=W;j++){
printf("%d ",fk[i][j]);
}
printf("\n");
}
printf("dp[i][j]:\n");
for (i=1;i<=n;i++)
{
for (j=0;j<=W;j++){
printf("%d ",dp[i][j]);
}
printf("\n");
}
}
int main()
{
w[1]=3; w[2]=2; w[3]=6;w[4]=2;
v[1]=7; v[2]=2; v[3]=5;v[4]=3;
n=4; W=9;
memset(dp,0,sizeof(dp));
memset(fk,0,sizeof(fk));
printf("最优解:\n");
printf(" 总价值=%d\n",solve());
printf(" 方案: ");Traceback();
printf("\n");
prin();
return 0;
}
分析:空间复杂度O(nV) 时间复杂度O(nV)。
四.数字三角形
基本原理和思路:MaxSum(i,j):从第i行j列到底边的最大数字之和
从最后一行开始递推,MaxSum(n,j)=D(n,j)//n行j列,MaxSum(n-1,j) = D(n-1,j) + max( MaxSum(n,j) , MaxSum(n,j+1) )
然后为了减少空间,不需要用二维数组来存储MaxSum(n,j)的值,只需要求MaxSum(n,j)的时候存储下一行MaxSum(n+1,j)的值就可以,然后计算完第n行的MaxSum之后再覆盖原来的第n+1行的MaxSum的值。
代码实现如下:
cpp
#include <iostream>
#include <algorithm>
using namespace std;
#define Max 101
int D[Max][Max];
int n;
int maxSum[Max][Max];
int MaxSum(int i,int j)
{
if(maxSum[i][j]!=-1)
return maxSum[i][j];
if(i==n)
maxSum[i][j]=D[i][j];
else
{
int x=MaxSum(i+1,j);
int y=MaxSum(i+1,j+1);
maxSum[i][j]=max(x,y)+D[i][j];
}
return maxSum[i][j];
}
int main()
{
int i,j;
cin>>n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
{
cin>>D[i][j];
maxSum[i][j]=-1;
}
cout<<MaxSum(1,1)<<endl;
return 0;
}
分析:时间复杂度是n2。
五.最小生成树问题
prim算法
基本原理和思路:设G=(V, E)是具有n个顶点的连通网,
T=(U, TE)是G的最小生成树,
T的初始状态为U={u0}(u0∈V),TE={ },
重复执行下述操作:
在所有u∈U,v∈V-U的边中找一条代价最小的边(u, v)并入集合TE,同时v并入U,直至U=V。
数组lowcost[n]:用来保存集合V-U中各顶点与集合U中顶点最短边的权值,lowcost[v]=0表示顶点v已加入最小生成树中;
数组adjvex[n]:用来保存该边所依附的(集合V-U中各顶点与集合U中顶点的最短边)集合U中的顶点。
代码实现如下:
cpp
void prime(MGraph G){
for(int i=1;i<G.vertexNu;i++){
lowcost[i]=G.arc[0][i]; adjvex[i]=0;
}
lowcost[0]=0;
for(i=1;i<G.vertexNum;i+++){
k=MinEdge(lowcost,G.vertexNum)
cout<<K<<adjvex[k]<<lowcost[k];
lowcost[k]=0;
for(j=1;j<G.vertexNum;j++)
if((G.arc[k][j]<lowcost[j]){
lowcost[j]=G.arc[k][j];
arcvex[j]=k;
}
}
六.背包问题
基本原理和思路:利用动态规划思想 ,子问题为:f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。其状态转移方程是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]} //这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。将前i件物品放入容量为v的背包中"这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只和前i-1件物品相关的问题。
1.如果不放第i件物品,那么问题就转化为"前i-1件物品放入容量为v的背包中",价值为f[i-1; v];
2.如果放第i件物品,那么问题就转化为"前i-1件物品放入剩下的容量为
v-Ci的背包中",此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值Wi
。
代码实现如下:
cpp
int n = 5;
double W = 100;
struct NodeType
{
double w;
double v;
double p;
bool operator<(const NodeType &s)const
{
return p > s.p;
}
};
NodeType A[] = { {0},{10,20},{20,30},{30,66},{40,40},{50,60} };
double V;
double x[MAXN];
void Knap()
{
V = 0;
double weight = W;
memset(x, 0, sizeof(x));
int i = 1;
while (A[i].w < weight)//物品可以全部装入
{
x[i] = 1;
weight -= A[i].w;
V += A[i].v;
i++;
}
if (weight > 0)//余下物品重量大于0
{
x[i] = weight / A[i].w;
V += x[i] * A[i].v;
}
}
分析:时间复杂度为O(nlog2n)
实验小结(包括问题和解决方法、心得体会等)
经过这次试验收获颇多,代码实现过程中也遇到一些问题,有的问题确实也有一定的难度,所以也是通过了网络搜索才得出的解决方案,思维上得到了很好的训练,同时也明白了一个道理:纸上得来终觉浅,绝知此事要躬行。尤其是算法和编程这门课程更是要勤于动手方能获得收获。希望下次实验或者之后的编程学习能吸取这些教训。