动态规划算法通常用来解决最优化问题。这些问题可能存在多个解,每个解具有一个值。我们希望找到一个具有最优(最大或最小)值的解。在动态规划算法中,主要关心的是找到一个最优解和求出最优解的值,而不是找出所有的最优解。
用动态规划法求解的问题具有特征:
能够分解为相互重叠的若干子问题;
满足最优性原理(也称最优子结构性质):该问题的最优解中也包含着其子问题的最优解。
动态规划法设计算法一般分成三个阶段:
1.分段:将原问题分解为若干个相互重叠的子问题;
2.分析:分析问题是否满足最优性原理,找出动态规划函数的递推式;
3.求解:利用递推式自底向上计算,实现动态规划过程。
动态规划法利用问题的最优性原理,以自底向上的方式从子问题的最优解逐步构造出整个问题的最优解。
例:计算斐波那契数
F(n)=0,n=0
F(n)=1,n=1
F(n)=F(n-1)+F(n-2),n2
n=5时分治法计算斐波那契数的过程:
F(5) | ||||||||||
/ | \ | |||||||||
F | (4) | F(3) | ||||||||
/ | \ | / \ | ||||||||
F(3) | F(2) | F(2) | F(1) | |||||||
/ \ | / \ | / \ | ||||||||
F(2) | F(1) | F(1) | F(0) | F(1) | F(0) | |||||
/ \ | ||||||||||
F(1) | F(0) |
#include<iostream>
using namespace std;
int f(int n )
{
if (n==1||n==0) return 1;
else return f(n-1)+f(n-2); //缺点:重复计算,浪费时间
}
main()
{
int n;
cin>>n;
cout<<f(n);
}
注意到,计算F(n)是以计算它的两个重叠子问题F(n-1)和F(n-2)的形式来表达的,所以,可以设计一张表填入n+1个(n)的值。
动态规划法求解斐波那契数F(9)的填表过程:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 |
#include<iostream>
using namespace std;
int save[100];
int f(int n ) //记忆化搜索
{
if (save[n]!=0) return save[n];//key
if (n==1||n==0) { save[n]=1; return save[n]; }
save[n]= f(n-1)+f(n-2);
return save[n];
}
main()
{
memset(save,0,sizeof(save));
int n;
cin>>n;
cout<<f(n);
}
显而易见,这个算法就是最简单的搜索算法。时间复杂度为2^n,明显是会超时的。
数字三角形的演化
给你一个数字三角形, 形式如下:
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
找出从第一层到最后一层的一条路,使得所经过的权值之和最小或者最大,每一步只能向下或向右下方走。
权值之和最大的状态转移方程:
f(i, j)=a[i, j] + max{ f(i-1, j),f(i-1, j-1) }
正面思路分析问题,得出一个非常简单的递归过程:
f1=f(i-1,j-1); f2=f(i-1,j);
if (f1>f2 ) f=f1+a[i,j]; else f=f2+a[i,j];
数据输入:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出:权值之和最大为30,最小为17。
//自底向上的思路 缺点:算法有点复杂
#include<iostream>
using namespace std;
#define max 100
int a[max][max];
int sum(int i,int j)
{ int x, y;
if (i<j) return 0;
if( i==0 ) return a[0][0];
else
{x=sum(i-1,j-1);y=sum(i-1,j);
if (x>y) return x+a[i][j];
else return y+a[i][j];
}
}
main()
{ int i,j,t,m=0,n;
cin>>n;
for (i=0;i<n;i++)
for(j=0;j<=i;j++)
cin>>a[i][j];
for (j=n-1;j>=0;j--)
{t=sum(n-1,j);if (m<t) m=t;}
cout<<m;
}
//自顶向下的思路 缺点:重复计算,浪费时间
#include<iostream>
using namespace std;
#define max 100
int a[max][max];
int sum(int i,int j,int n)
{ int x,y;
if( i==n-1 ) return a[i][j];
else
{ x=sum(i+1,j,n);y=sum(i+1,j+1,n);
if (x>y) return x+a[i][j];
else return y+a[i][j];
}
}
void main()
{ int i,j,n;
cin>>n;
for (i=0;i<n;i++)
for(j=0;j<=i;j++)
cin>>a[i][j];
cout<<sum(0,0,n);
}
记忆化搜索解决数字三角问题
分析一下数字三角问题解决的搜索过程,实际上,很多调用都是不必要的,也就是把产生过的最优状态,又产生了一次。为了避免浪费,很显然,我们存放一个opt数组:
Opt[i, j] - 每产生一个f(i, j),将f(i, j)的值放入opt中,以后再次调用到f(i, j)的时候,直接从opt[i, j]来取就可以了。
于是动态规划的状态转移方程被直观地表示出来了,这样节省了思维的难度,减少了编程的技巧,而运行时间只是相差常数的复杂度。
#include<iostream>
using namespace std;
#define max 100
int a[max][max];
int opt[max][max];
int sum(int i,int j,int n)
{ if (opt[i][j]!=0) return opt[i][j];
if( i==n-1 ) { opt[i][j]=a[i][j];return a[i][j];}
else
{ opt[i+1][j]=sum(i+1,j,n);
opt[i+1][j+1]=sum(i+1,j+1,n);
if (opt[i+1][j]>opt[i+1][j+1]) return opt[i+1][j]+a[i][j];
else return opt[i+1][j+1]+a[i][j];
}
}
main()
{ int i,j,t,n;cin>>n;
memset(opt,0,sizeof(opt));
for (i=0;i<n;i++)
for(j=0;j<=i;j++)
cin>>a[i][j];
cout<<sum(0,0,n);
}
#include<iostream> //循环算法
using namespace std;
#define max 100
int a[max][max];
int opt[max][max];
int maxsum(int n)
{ int i,j;
opt[0][0]=a[0][0];
for (i=1;i<n;i++)
{ opt[i][0]=opt[i-1][0]+a[i][0];
for(j=1;j<i-1;j++)
if (a[i][j]+opt[i-1][j]>=a[i][j]+opt[i-1][j-1])
opt[i][j]= a[i][j]+opt[i-1][j];
else opt[i][j]= a[i][j]+opt[i-1][j-1];
opt[i][i]=opt[i-1][i-1]+a[i][i];
}
}
main()
{ int i,j,t,n;cin>>n;
memset(opt,0,sizeof(opt));
for (i=0;i<n;i++)
for(j=0;j<=i;j++) cin>>a[i][j];
maxsum(n);
t=0;
for(i=0;i<n;i++)
if (t<opt[n-1][i]) t=opt[n-1][i];
cout<<t;
}
动态规划问题的特征
动态规划算法的有效性依赖于问题本身所具有的两个
重要性质:
1、最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
2、重叠子问题:在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。
动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,在以后尽可能多地利用这些子问题的解。
问题:字符串对称处理
要求对任意一个字符串,通过加入若干字符,使其对称。
如Ab3bd插入两个字符后可以变成dAb3bAd或Adb3bdA,但插入两个字符以下却无法完成对称性处理。请求出需要插入的最少字符数。
利用数字三角的思路
引入Cost[i][j]表示将串S从第j位开始长度为i的子串sub处理成对称的最小插入字符数,则按子串sub的长度从大到小依次进行递推求解:
cost[i-2][j+1],st[j]=st[i+j-1]
Cost[i][j]=min{cost[i-1][j]+1,cost[i-1][j+1]+1},st[j]<>st[i+j-1]; 2=<i<=n,1=<j<=n-i+1
特殊情况:i=0或i=1时cost取值均为0。
//
问题:求组合数
按递推式分解,可以得到的二叉树结构
记忆化搜索思路
0 | 1 | 2 | 3 | 4 | 5 | |
1 | 1 | 1 | ||||
2 | 1 | 2 | 1 | |||
3 | 1 | 3 | 3 | 1 | ||
4 | 1 | 4 | 6 | 4 | 1 | |
5 | 1 | 5 | 10 | 10 | 5 | 1 |
6 | 1 | 6 | 15 | 20 | 15 | 6 |
7 | 1 | 7 | 21 | 35 | 35 | 21 |
计算组合数的动态规划算法1:
int mat[1000][1000];
int combinat(int m,int n)
{ int i,j;
if(n==0||m==n)return 1;
for(j=0;j<=n;j++)
{mat[j][j]=1;
for(i=j+1;i<=m;i++)
{if (j==0) mat[i][j]=1;
else mat[i][j]=mat[i-1][j-1]+mat[i-1][j];
} // 计算Cmn
}
return (mat[m][n]);//返回计算结果
}
免费馅饼
SERKOI最新推出了一种叫做“免费馅饼”的游戏。游戏在一个舞台上进行。舞台的宽度为W格,天幕的高度为H格,游戏者占一格。开始时,游戏者站在舞台的正中央,手里拿着一个托盘。游戏开始后,从舞台天幕顶端的格子中不断出现馅饼并垂直下落。游戏者左右移动去接馅饼。游戏者每秒可以向左或右移动一格或两格,也可以站在愿地不动。
馅饼有很多种,游戏者事先根据自己的口味,对各种馅饼依次打了分。同时在8-308电脑的遥控下,各种馅饼下落的速度也是不一样的,下落速度以格/秒为单位。当馅饼在某一秒末恰好到达游戏者所在的格子中,游戏者就收集到了这块馅饼。
写一个程序,帮助我们的游戏者收集馅饼,使得收集的馅饼的分数之和最大。输入数据:输入文件的第一行是用空格分开的两个正整数,分别给出了舞台的宽度W(1~99之间的奇数)和高度H(1~100之间的整数)。
接下来依馅饼的初始下落时间顺序给出了一块馅饼信息。由四个正整数组成,分别表示了馅饼的初始下落时刻(0 ~ 1000秒),水平位置、下落速度(1 ~ 100)以及分值。游戏开始时刻为0。从1开始自左向右依次对水平方向的每格编号。
*输出数据:输出文件的第一行给出了一个正整数,表示你的程序所收集到的最大分数之和。
*分析及解决过程
(1)馅饼信息的存储方法:矩阵矩阵第i行第j列表示的是游戏者在第i秒到达第j列所能取得的分值。这样问题便变成了一个类似数字三角形的问题:从表格的第一行开始往下走,每次只能向左或右移动一格或两格,或不移动走到下一行,怎样走才能得到最大的分值。
(2)求解方法:动态规划F[i,j]表示游戏进行到第i秒,这时游戏者站在第j列时所能得到的最大分值。
动态转移方程为:F[i,j] = Max { F[i-1,K] } + 馅饼的分值( j-2<=K<=j+2 )
//
最长公共子序列 ZOJ 1733 Common Subsequence