算法设计与分析-----动态规划(c语言)
一、动态规划
1、定义
动态规划是一种解决多阶段决策问题的优化方法,把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。
2、动态规划问题的解法
(1)逆序解法
(2)顺序解法
3、动态规划求解的基本步骤
采用动态规划求解的问题的一般要具有3个性质:
- **最优性原理:**如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优性原理。
- 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
- 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)。
实际应用中简化的步骤:
① 分析最优解的性质,并刻画其结构特征。
② 递归的定义最优解。
③ 以自底向上或自顶向下的记忆化方式计算出最优值。
④ 根据计算最优值时得到的信息,构造问题的最优解。
4、动态规划与其他方法的比较
①动态规划的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。
在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
②动态规划方法又和贪心法有些相似,在动态规划中,可将一个问题的解决方案视为一系列决策的结果。
不同的是,在贪心法中,每采用一次贪心准则便做出一个不可回溯的决策,还要考察每个最优决策序列中是否包含一个最优子序列。
5、求解整数拆分问题
【问题描述】求将正整数n无序拆分成最大数为k(称为n的k拆分)的拆分方案个数,要求所有的拆分方案不重复。
【问题求解】设n=5,k=5,对应的拆分方案有:
① 5=5
② 5=4+1
③ 5=3+2
④ 5=3+1+1
⑤ 5=2+2+1
⑥ 5=1+1+1+1
⑦ 5=1+1+1+1+1
为了防止重复计数,让拆分数保持从大到小排序。正整数5的拆分数为7。
采用动态规划求解整数拆分问题。设f(n,k)为n的k拆分的拆分方案个数:
(1)当n=1,k=1时,显然f(n,k)=1。
(2)当n<k时,有f(n,k)=f(n,n)。
(3)当n=k时,其拆分方案有将正整数n无序拆分成最大数为n-1的拆分方案,以及将n拆分成1个n(n=n)的拆分方案,后者仅仅一种,所以有
f(n,n)=f(n,n-1)+1。
(4)当n>k时,根据拆分方案中是否包含k,可以分为两种情况:
① 拆分中包含k的情况:即一部分为单个k,另外一部分为{x1,x2,…,xi},后者的和为n-k,后者中可能再次出现k,因此是(n-k)的k拆分,所以这种拆分方案个数为f(n-k,k)。
② 拆分中不包含k的情况:则拆分中所有拆分数都比k小,即n的(k-1)拆分,拆分方案个数为f(n,k-1)。
代码如下:
#include <stdio.h>
#define MAXN 500
int dp[MAXN][MAXN]; //动态规划数组
void Split(int n,int k) //求解算法
{
for (int i=1;i<=n;i++)
for(int j=1;j<=k;j++)
{
if (i==1 || j==1)
dp[i][j]=1;
else if (i<j)
dp[i][j]=dp[i][i];
else if (i==j)
dp[i][j]=dp[i][j-1]+1;
else
dp[i][j]=dp[i][j-1]+dp[i-j][j];
}
}
int main()
{
int n=5,k=5;
Split(n,k);
printf("(%d,%d)=%d\n",n,k,dp[n][k]); //输出:7
}
6、求解最大连续子序列和问题
【问题描述】给定一个有n(n≥1)个整数的序列,要求求出其中最大连续子序列的和。
例如
序列(-2,11,-4,13,-5,-2)的最大子序列和为20
序列(-6,2,4,-7,5,3,2,-1,6,-9,10,-2)的最大子序列和为16
规定一个序列最大连续子序列和至少是0,如果小于0,其结果为0。
【问题求解】对于含有n个整数的序列a,设
bj=MAX{ai+ai+1+…+aj} (1≤j≤n)
表示a[1…j]的前j个元素中的最大连续子序列和,则bj-1表示a[1…j-1]的前j-1个元素中的最大连续子序列和。
当bj-1>0时,bj=bj-1+aj,当bj-1≤0时,放弃前面选取的元素,从aj开始重新选起,bj=aj。用一维动态规划数组dp存放b,对应的状态转移方程如下:
dp[0]=0 边界条件
dp[j]=MAX{dp[j-1] +aj,aj} 1≤j≤n
则序列a的最大连续子序列和等于dp[j](1≤j≤n)中的最大者。
代码如下:
#include <stdio.h>
#define max(x,y) ((x)>(y)?(x):(y))
#define MAXN 20
//问题表示
int n=6;
int a[]={0,-2,11,-4,13,-5,-2}; //a数组不用下标为0的元素
//求解结果表示
int dp[MAXN];
void maxSubSum() //求dp数组
{
dp[0]=0;
for (int j=1;j<=n;j++)
dp[j]=max(dp[j-1]+a[j],a[j]);
}
void dispmaxSum() //输出结果
{
int k;
int maxj=1;
for (int j=2;j<=n;j++) //求dp中最大元素dp[maxj]
if (dp[j]>dp[maxj]) maxj=j;
for (k=maxj;k>=1;k--) //找前一个值小于等于0者
if (dp[k]<=0) break;
printf(" 最大连续子序列和: %d\n",dp[maxj]);
printf(" 所选子序列: ");
for (int i=k+1;i<=maxj;i++)
printf("%d ",a[i]);
printf("\n");
}
int main()
{
maxSubSum();
printf("求解结果\n");
dispmaxSum();
}
二、动态规划算法实验
1、实验一 fibonacci数列
输入n的值,求得并输出第n个fibonacci数列中的数值
分析
f(1)=1
f(2)=1
f(3)=f(1)+f(2)
f(4)=f(2)+f(3)
如果n<=2
f(n)=1
否则
f(n)=f(n-2)+f(n-1)
低吗如下:
#include<stdio.h>
int f(int n)
{
int y;
if(n<=2)
y=1;
else
y=f(n-2)+f(n-1);
return y;
}
int main()
{
int n,y;
scanf("%d",&n);
y=f(n);
printf("%d\n",y);
return 0;
}
缺点:重复计算,会耗费运行时间。
把每次计算的结果用一个数组来存放!【动态规划解决问题的思路之一】
我们同样会用到动态划分存储空间的操作【动态规划解决问题的思路之一】
改进1:
#include<stdio.h>
//*(memo+i) 等价 memo[i]
int qiu(int n,int *memo)
{
if(memo[n]!=-1)
return memo[n];
if(n<=2)
memo[n]=1;
else
memo[n]=qiu(n-2,memo)+qiu(n-1,memo);
printf("n=%d\n",n);
return memo[n];
}
int f(int n)
{
int i;
int memo[n+1];
for(i=0;i<=n;i++)
memo[i]=-1;
return qiu(n,memo);
}
int main()
{
int n,y;
scanf("%d",&n);
y=f(n);
printf("%d\n",y);
return 0;
}
动态规划:为了介绍重复计算!【初衷之一】
动态规划:在解决问题的中尽可能的减少内存的耗费!!!【初衷之二】
改进2:
#include<stdio.h>
#include<stdlib.h>
//*(memo+i) 等价 memo[i]
int f(int n)
{
int i,a,b;
if(n<=0)
return n;
a=1;
b=1;
for(i=3;i<=n;i=i+2)
{
a=a+b;
b=a+b;
}
if(n%2==1)
return a;
else
return b;
}
int main()
{
int n,y;
scanf("%d",&n);
y=f(6);
printf("%d\n",y);
return 0;
}
2、实验二 拆分方案的个数的求解。
输入一个正整数给n,对n进行拆分(且拆分中的最大数值是k),编程求得当对n进行按最大值为k的拆分方案的个数的求解。
分析:
假设n=5,k=5。答案是7种组合
① 5=5
② 5=4+1
③ 5=3+2
④ 5=3+1+1
⑤ 5=2+2+1
⑥ 5=2+1+1+1
⑦ 5=1+1+1+1+1
为了防止重复计算,让拆分数据的分析一直保持一个状态从大到小的排序!
采用动态规划的思想来求解拆分问题,寻找问题求解的规律【方法】
我们准备一个函数f(n,k),对n进行最大值为k的拆分方案个数的求解
详细分析
第1种情况 当n=1的时候 或者 k=1的时候 ,f(n,k)=1
第2种情况 当n<k的时候 例如f(5,10) 等价 f(5,5)
f(n,k)=f(n,n)
第3种情况 当n=k的时候 分成2个部分
【一个包含本身,一个不包含本身】
f(n,k)=f(n,n-1)+1
【这里最后+1 其实就是下面黄色一种组合】
① 5=5
② 5=4+1
③ 5=3+2
④ 5=3+1+1
⑤ 5=2+2+1
⑥ 5=2+1+1+1
⑦ 5=1+1+1+1+1
第4种情况 当n>k的时候
(1)拆分中包含k的情况
f(n-k,k)
(2)拆分中不包含k的情况
f(n,k-1)
最后是f(n,k)=f(n-k,k)+f(n,k-1)
低吗如下:
#include<stdio.h>
int f(int n,int k)
{
if(n==1 || k==1)
return 1;
else if(n<k)
return f(n,n);
else if(n==k)
return f(n,n-1)+1;
else if(n>k)
return f(n-k,k)+f(n,k-1);
}
int main()
{
int n,k,y;
scanf("%d",&n);
scanf("%d",&k);
y=f(n,k);
printf("%d\n",y);
return 0;
}
改进:
#include<stdio.h>
int a[100][100];
int f(int n,int k)
{
int i,j;
for(i=1;i<=n;i++)
for(j=1;j<=k;j++)
{
if(i==1 || j==1)
a[i][j]=1;
else if(i<j)
a[i][j]=a[i][i];
else if(i==j)
a[i][j]=a[i][i-1]+1;
else if(i>j)
a[i][j]=a[i-j][j]+a[i][j-1];
}
return a[n][k];
}
int main()
{
int n,k,y;
scanf("%d",&n);
scanf("%d",&k);
y=f(n,k);
printf("%d\n",y);
return 0;
}