动态规划和分治法的区别,
首先,分治法:将问题划分成一些独立的子问题,递归的求解各子问题,然后合并子问题的解而得到原问题的解。
动态规划:适用于子问题不是独立的情况,也就是各子问题包含公共的子子问题,鉴于会重复的求解各子问题,DP对每个问题只求解一遍,将其保存在一张表中,从而避免重复计算。
DP算法的设计可以分为四个步骤:
①.描述最优解的结构。
②.递归定义最优解的值。
③.按自底而上的方式计算最优解的值。
④.由计算出的结果创造一个最优解。
例1 装配线问题
#include "stdafx.h"
#include<iostream>
using namespace std;
#include<assert.h>
#include<vector>
#include<queue>
#include<map>
#include<cmath>
#include<string>
#include<stack>
//动态规划,工厂转配线
//e[i]是进入装配线i需要的时间
//x[i]是离开装配线i需要的时间
//a[1][j]表示在装配站S[1][j]所需时间
//t[1][j]表示底盘从S[1][j]移动到S[2][j+1]所需时间,同理t[2][j]
//f[i][j]分别表示在第i装配线上第j个装配站的最优解
//ln[i][j]记录第i条装配线上,最优解时第j个装配站的前一个装配线是什么
// 最优解是,f代表最小花费时间,ln表示最后出来时是从装配线1还是装配线2
const int n = 7;//6站,0不使用
int fastestWay(int l[][n], int f[][n], int a[][n],int t[][n-1],int *e,int *x, int n )
{
int i = 0, j = 0, ff = 0;
//初始化
f[1][1] = e[1] + a[1][1];
f[2][1] = e[2] + a[2][1];
//站点j依次计算,f[1][j] = min{f[1][j-1]+a[1][j], f[2][j-1]+t[2][j-1]+a[1][j]},
//同理,f[2][j] = min{f[2][j-1]+a[2][j], f[1][j-1]+t[1][j-1]+a[2][j]}
for(j = 2; j < n; ++ j)
{
if(f[1][j-1]+a[1][j] >= f[2][j-1]+t[2][j-1]+a[1][j])
{
f[1][j] = f[2][j-1]+t[2][j-1]+a[1][j];
l[1][j] = 2;
}
else
{
f[1][j] = f[1][j-1]+a[1][j];
l[1][j] = 1;
}
if(f[2][j-1]+a[2][j] >= f[1][j-1]+t[1][j-1]+a[2][j])
{
f[2][j] = f[1][j-1]+t[1][j-1]+a[2][j];
l[2][j] = 1;
}
else
{
f[2][j] = f[2][j-1]+a[2][j];
l[2][j] = 2;
}
}
if(f[1][n-1]+x[1] < f[2][n-1]+x[2])
{
ff = f[1][n-1]+x[1];
l[1][1] = 1;//利用l[1][1]的空间存放最后经过的装配线
}
else
{
ff = f[2][n-1]+x[2];
l[1][1] =2;
}
return ff;
}
int main()
{
int i, j, k;//i装配线个数,j装配线上的站数,k返回值变量,n是站点数+1(1-7)
int a[3][7],t[3][6];
int e[3]={0,2,4};
int x[3]={0,3,2};
int l[3][7], f[3][7];
int b[2][6]={{7,9,3,4,8,4}, {8,5,6,4,5,7}};
int c[2][5]={{2,3,1,3,4},{2,1,2,2,1}};
int cc[n+1];//用于将l[i][j]中的结果正向输出
for(i=0; i < 2 ; ++ i)
for(j=0; j < 6; ++ j)
a[i+1][j+1] = b[i][j];
for(i=0; i < 2; ++ i)
for(j = 0; j < 5; ++ j)
t[i+1][j+1] = c[i][j];
k = fastestWay(l,f,a,t,e,x,n);
cout<<"最少的时间是:"<<k<<endl;
cc[n] = l[1][1];
for(i=6; i >=2; -- i)
cc[i] = l[cc[i+1]][i];//l[i][j]记录第i条装配线上,最优解时第j个装配站的前一个装配线是什么
//cc[i]装配站i的前一站所在的装配线,即i-1所在装配线
for(i=2; i <=n; ++ i)
cout<<"汽车装配经过的装配线和装配站情况如下:"<<cc[i]<<","<<i-1<<endl;
}
例2 矩阵乘链
//动态规划,矩阵乘链
//设m[i][j]为计算矩阵Ai...j所需的标量乘法运算次数的最小值;
//对整个问题,计算A1...n的最小代价就是m[1][n]。
// m[i][j]=0。i=j时
// m[i][j]=min{ m[i][k]+m[k+1][j]+pi-1pkpj} 在i!=j时。因为Ai是pi-1*pi.
//定义s[i][j]为这样的一个k值:在该处分裂乘积AiAi+1...Aj后可得一个最优加全部括号。
//亦即s[i][j]等于使得m[i][j]取最优解的k值。
const int n = 7;
int matrixChainOrder(int *p, int s[][n] )
{
int max = 20000;
int i = 0, j = 0, l = 0, k = 0, q = 0, c = n-1;//7个p,6个矩阵
int m[n][n];
//初始化
for(i = 0; i < n ;++ i)
m[i][i] = 0;
for(l=2; l <= c; ++ l) //确定步长,即i与j之间的距离,l=2时表示j-i等于1
{
for(i=1; i <= c-l+1; ++ i)//确定起点
{
j=i+l-1;//确定终止点
m[i][j] = max;
for(k=i; k < j; ++ k)//确定k值,起点与终点间最好的k值
{
q = m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(q < m[i][j])
{
m[i][j] = q;
s[i][j] = k;
}
}
}
}
return m[1][n-1];
}
void printOptimalParens(int s[][n],int i,int j)
{
if(i==j)
printf("A%d",i);
else
{
printf("(");
printOptimalParens(s,i,s[i][j]);
printOptimalParens(s,s[i][j]+1,j);
printf(")");
}
}
int main()
{
int k;//k返回值,矩阵相乘所需的标量乘法的最小值
int s[n][n];
int p[n]={30,35,15,5,10,20,25};
k=matrixChainOrder(p,s);
cout<<"矩阵相乘所需的标量乘法的最小值为:"<<k<<endl;
cout<<"最终的最优全括号形式为:"<<endl;
printOptimalParens(s,1,6);
}
例 3 最长公共子序列
#include "stdafx.h"
#include<iostream>
using namespace std;
#include<assert.h>
#include<vector>
#include<queue>
#include<map>
#include<cmath>
#include<string>
#include<stack>
//动态规划,最长公共子序列LCS
//给定两个序列x和y,称z是x和y的公共子序列,如果z既是x的子序列,又是y的子序列;
//最长的公共子序列称作最长公共子序列LCS(longest common subsequence)
//(1)LCS的最优子结构
//
// 设zk是xm和yn的一个LCS,则,如果x和y的最后一个元素相同,则z中去掉最后一个元素之后zk-1仍为xm-1和yn-1的LCS
//
// 如果xm!=yn,若zk!=xm,则z是xm-1和y的一个LCS,若zk!=yn,则z是xm和yn-1的LCS。
//
//(2)一个递归解
//
// 设c[i][j]为序列xi和yj的一个LCS的长度,则有:
//
// c[i][j]=0 i=0或j=0
//
// c[i][j]=c[i-1][j-1]+1 xi=yj且i,j>0
// c[i][j]=max(c[i][j-1] , c[i-1][j]) xi!=yj且i,j>0
//(3)计算LCS的长度
void LCS_Length(char* x, char* y, int** c, int** b,int m, int n)
{
int i = 0, j = 0;
cout<<m<<","<<n<<endl;
//初始化
for(i = 1; i <= m; ++ i)
{
c[i][0] = 0;//0列
//cout<<c[i][0]<<endl;
}
for(j = 0; j <=n; ++ j)
{
c[0][j] = 0;//0行
//cout<<c[0][j]<<endl;
}
for(i = 1; i <=m; ++ i)
for(j = 1; j <= n; ++j)
{
if(x[i-1] == y[j-1])//因为x,y都是从0下标开始计算的
{
c[i][j] = c[i-1][j-1]+1;
b[i][j] = 0;
/* cout<<c[i][j]<<endl;*/
}
else if(c[i-1][j] >= c[i][j-1])//b[i][j]记录最大值来自哪里,左边2,上面1,对角0
{
c[i][j] = c[i-1][j];
b[i][j] = 1;
//cout<<c[i][j]<<endl;
}
else
{
c[i][j] = c[i][j-1];
b[i][j] = 2;
//cout<<c[i][j]<<endl;
}
}
}
void Print_LCS(int** b, char* x, int i, int j)
{
if(i == 0 || j == 0)
return;
if(b[i][j] == 0)
{
Print_LCS(b,x,i-1,j-1);
cout<<x[i-1];
}
else if(b[i][j] == 1)
Print_LCS(b,x,i-1,j);
else
Print_LCS(b,x,i,j-1);
}
int main()
{
cout<<"LCS问题,请输入X,Y字符串的长度:"<<endl;
int m = 0, n = 0, i = 0;
int** c = new int*[m+1];//c[0...m,0...n]
int** b = new int*[m+1];//b[1...m,1...n]索引的是从1-m,1-n但是保证可以索引1-m,1-n必须有0-m,0-n的空间
cin>>m>>n;
cout<<m<<","<<n<<endl;
char* x = new char[m];
char* y = new char[n];
for(i = 0; i < m; ++ i)
cin>>x[i];
//for(i = 0; i < m; ++ i)
// cout<<x[i];
//cout<<endl;
for(i = 0; i < n; ++ i)
cin>>y[i];
//for(i = 0; i < n; ++ i)
// cout<<y[i];
//cout<<endl;
for(i = 0; i <= m; ++ i)
{
c[i] = new int[n+1];
b[i] = new int[n+1];
}
LCS_Length(x,y,c,b,m,n);
Print_LCS(b,x,m,n);
}
坑爹的地方1,LCS中第二个循环,(j=0;j<=n;++i)写错了不报错,一直循环赋值,找了半天才找到错误
坑爹的地方2,b[1...m,1...n]记录长度来源的地方,为了使用下标1...m,1...n,需要有0...m,0...n的空间不然访问不存在的空间。
课后习题15.4-2,不使用b的情况,根据c的值判断应该怎么输出,但是还是需要O(M*N)的空间
void Print_LCS(int** c, char* x, int i, int j)
{
if(i == 0 || j == 0)
return;
if(c[i][j] == (c[i-1][j-1]+1))
{
Print_LCS(c,x,i-1,j-1);
cout<<x[i-1];
}
else if(c[i][j] == c[i-1][j])
Print_LCS(c,x,i-1,j);
else
Print_LCS(c,x,i,j-1);
}
15.4-4 如何将空间使用减少到2*min(m,n),首先使用string作为输入,简单方便,其次,细节问题就是因为m,n大小事先不知道,需要判断,使得x中总是保存长的string,这样在循环中保证数组是min(m,n)+O(1)空间,因为例子是7,6数组看不出差别。
两行,一行是pre,一行是cur只保存这两个就够了
void LCS_Length(string x, string y)
{
int i = 0, j = 0, k=0;
int m = x.length();
int n = y.length();
int t = m > n ? n:m;
if(m<=n)
{
string temp = x;
x = y;
y = temp;
int count = m;
m = n;
n = count;
}
int *pre = new int[t+1];
int *cur = new int[t+1];
//初始化
for(i = 0; i <= t; ++ i)
{
pre[i] = 0;
}
<span style="white-space:pre"> </span>cur[0] = 0;
for(i = 1; i <=m; ++ i)
{
for(j = 1; j <= n; ++j)
{
if(x[i-1] == y[j-1])//因为x,y都是从0下标开始计算的
{
cur[j] = pre[j-1]+1;
}
else if(cur[j-1] <= pre[j] )
{
cur[j] = pre[j];
}
else
{
cur[j] = cur[j-1];
}
}
for(k=1; k <= t; ++ k)
{
pre[k] = cur[k];
cur[k] =0;
}
}
cout<<pre[t]<<endl;
}
int main()
{
cout<<"LCS问题,请输入X,Y字符串:"<<endl;
string x, y;
cin>>x;
cin>>y;
LCS_Length(x,y);
}
改进2,使得空间使用为min(m,n)+O(1),相当的巧妙,看了好久还画图才明白,![]()
void LCS_Length(string x, string y)
{
int i = 0, j = 0, k=0;
int m = x.length();
int n = y.length();
int t = m > n ? n:m;
if(m<=n)
{
string temp = x;
x = y;
y = temp;
int count = m;
m = n;
n = count;
}
int *a = new int[t];
//初始化
for(i = 0; i <t; ++ i)
{
a[i] = 0;
}
int cij = 0,ci1 = 0;
for(i = 1; i <=m; ++ i)
{
if(x[i-1] == y[0])
ci1 = 1;
else if(a[1]>=0)
ci1 = a[1];
a[0] = ci1;
cout<<ci1;
for(j = 2; j <= n; ++j)
{
if(x[i-1] == y[j-1])//因为x,y都是从0下标开始计算的
{
cij = a[j-1]+1;
cout<<cij;
}
else if(a[0] <= a[j] )
{
cij = a[j];
cout<<cij;
}
else
{
cij = a[0];
cout<<cij;
}
a[j-1] = a[0];
a[0] = cij;
}
cout<<endl;
}
cout<<a[t-1]<<endl;
}