动态规划
动态规划算法通常用于求解具有某种最优性质的问题,在这类问题中,可能会有许多可行解,每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次,如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案,不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中,这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
适用条件
任何思想方法都有一定的局限性,超出了特定条件,它就失去了作用,同样,动态规划也并不是万能的。适用动态规划的问题必须满足最优化原理和无后效性。
最优化原理
最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
无后效性
将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
子问题的重叠性
动态规划算法的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其他的算法。选择动态规划算法是因为动态规划算法在空间上可以承受,而搜索算法在时间上却无法承受,所以我们舍空间而取时间。
局限性
动态规划对于解决多阶段决策问题的效果是明显的,但是动态规划也有一定的局限性。首先,它没有统一的处理方法,必须根据问题的各种性质并结合一定的技巧来处理;另外当变量的维数增大时,总的计算量及存贮量急剧增大。因而,受计算机的存贮量及计算速度的限制,当今的计算机仍不能用动态规划方法来解决较大规模的问题,这就是“维数障碍”。
经典题型
初级上台阶
一个楼梯有n阶,一次可以上1阶或2阶,问有多少种上法。
dp[1]=1,dp[2]=2;
for(i=3;i<=n;i++)
dp[i]=dp[i-2]+dp[i-1];
高级上台阶
一个楼梯有n阶,一次可以上1阶、2阶……n阶,问有多少种上法。
原理代码:
dp[1]=1;
for(i=2;i<=n;i++)
{
for(j=1;j<i;j++)
dp[i]+=dp[j];
dp[i]++;
}
优化代码:
dp[1]=1;
for(i=2;i<=n;i++)
dp[i]=2*dp[i-1];
矩形覆盖
有一个2n的大矩形以及n个21的小矩形,问铺满这个大矩形有多少种方法(小矩形可以横着铺也可以竖着铺)。
dp[1]=1,dp[2]=2;
for(i=3;i<=n;i++)
dp[i]=dp[i-2]+dp[i-1];
钱币兑换
现有n种面值不同的硬币,问m元兑成硬币有多少种兑法,结果对mod求余。
//coin数组需从小到大排序,dp[0]=1
for(i=1;i<=n;i++)
for(j=coin[i];j<=m;j++)
dp[j]=(dp[j]+dp[j-coin[i]])%mod;
三角形最大、最小路径和(数塔问题)
给定一个数字三角形,找出自上而下的路径和(经过的数字之和)。每一步只能向下一行的相邻点移动。
最大路径和
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+dp[i][j];
for(i=1;i<=n;i++)
if(dp[n][i]>ans)
ans=dp[n][i];
最小路径和
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
dp[i][j]=min(dp[i-1][j-1],dp[i-1][j])+dp[i][j];
for(i=1;i<=n;i++)
if(dp[n][i]<ans)
ans=dp[n][i];
矩形最大、最小路径和
给定一个数字矩形,找出从左上角到右下角的路径和(经过的数字之和)。每一步只能向右或向下移动。
最大路径和
for(i=1;i<=m;i++)
for(j=1;j<=n;j++)
dp[i][j]=max(dp[i-1][j],dp[i][j-1])+dp[i][j];
最小路径和
for(i=1;i<=m;i++)
for(j=1;j<=n;j++)
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+dp[i][j];
矩形路径方案和
给定一个矩形,找出从左上角到右下角走法方案的总和。每一步只能向右或向下移动。
for(i=1;i<=m;i++)
for(j=1;j<=n;j++)
dp[i][j]=dp[i-1][j]+dp[i][j-1];
最大连续子序列和
给定一个整型数组,求和最大的连续子序列的和。
for(i=1;i<=n;i++)
{
if(dp[i-1]>=0)
{
dp[i]+=dp[i-1];
if(dp[i]>ans)
ans=dp[i];
}
}
最长上升子序列(LIS)
给定一个字符型或整型数组,求长度最长的上升子序列的长度。
原理代码(n2):
for(i=2;i<=n;i++)
{
for(j=1;j<i;j++)
if(number[j]<number[i])
dp[i]=max(dp[i],dp[j]+1);
if(dp[i]>ans)
ans=dp[i];
}
优化代码(nlogn):
ans=1;
dp[1]=number[1];
for(i=2;i<=n;i++)
{
if(number[i]>dp[ans])
dp[++ans]=number[i];
else
{
offset=lower_bound(dp+1,dp+ans,number[i])-dp;//二分查找
dp[offset]=number[i];
}
}
最长公共子序列(LCS)
给定一个字符型数组,求长度最长的公共子序列的长度。
for(i=1;i<=lena;i++)
{
for(j=1;j<=lenb;j++)
{
if(a[i-1]==b[j-1])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
最长公共上升子序列(LCIS)
给定一个字符型数组,求长度最长的公共上升子序列的长度。
for(i=1;i<=lena;i++)
{
Max=1;
for(j=1;j<=lenb;j++)
{
dp[i][j]=dp[i-1][j];
if(a[i-1]==b[j-1])
dp[i][j]=max(dp[i][j],Max);
if(a[i-1]>b[j-1])
Max=max(Max,dp[i][j]+1);
}
}
for(i=1;i<=lenb;i++)
ans=max(ans,dp[lena][i]);
最长公共子串
给定一个字符型数组,求长度最长的公共子串的长度。
for(i=1;i<=lena;i++)
{
for(j=1;j<=lenb;j++)
{
if(a[i-1]==b[j-1])
{
dp[i][j]=dp[i-1][j-1]+1;
if(dp[i][j]>ans)
ans=dp[i][j];
}
else
dp[i][j]=0;
}
}
最长回文子串
给定一个字符型数组,求长度最长的回文子串的长度。
ans=1;
for(i=0;i<len;i++)
{
dp[i][i]=1;
if(i<len-1&&s[i]==s[i+1])
{
dp[i][i+1]=1;
ans=2;
}
}
for(l=3;l<=len;l++)
{
for(i=0;i<len-l+1;i++)
{
j=i+l-1;
if(dp[i+1][j-1]==1&&s[i]==s[j])
{
dp[i][j]=1;
ans=l;
}
}
}
石子合并(适用于石子堆数较少的题目)
现有n堆石子,将它们合为一堆,每次只能合并相邻的两堆,花费为两堆石子的数量和,求最大、最小花费。
sum[0]=0;
for(i=1;i<=n;i++)
{
scanf("%d",&stone[i]);
sum[i]=sum[i-1]+stone[i];
}
for(l=2;l<=n;l++)
{
for(i=1;i<=n-l+1;i++)
{
j=i+l-1;
dpmax[i][j]=0x80000000;
dpmin[i][j]=0x7fffffff;
for(k=i;k<j;k++)
{
dpmax[i][j]=max(dpmax[i][j],dpmax[i][k]+dpmax[k+1][j]+sum[j]-sum[i-1]);
dpmin[i][j]=min(dpmin[i][j],dpmin[i][k]+dpmin[k+1][j]+sum[j]-sum[i-1]);
}
}
}
矩阵连乘
现有n个矩阵,将它们乘在一起,每次只能乘相邻的两个矩阵,计算次数为第一个矩阵的行数乘以第一个矩阵的列数乘以第二个矩阵的列数,求最少计算次数。
for(l=2;l<=n;l++)
{
for(i=1;i<=n-l+1;i++)
{
j=i+l-1;
m[i][j]=0x7fffffff;
for(k=i;k<j;k++)
m[i][j]=min(m[i][j],m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j]);
}
}
例题
#include <stdio.h>
int dp[105][105];
int max(int x,int y)
{
return x>y?x:y;
}
int main()
{
int i,j,n,ans=0;
scanf("%d",&n);
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
scanf("%d",&dp[i][j]);
for(i=2;i<=n;i++)
for(j=1;j<=i;j++)
dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+dp[i][j];
for(i=1;i<=n;i++)
if(dp[n][i]>ans)
ans=dp[n][i];
printf("%d\n",ans);
return 0;
}
#include <stdio.h>
const int inf=0x80000000;
int dp[100005]={-1};
int main()
{
int i,j,t,n,ans,st,ed;
scanf("%d",&t);
for(i=1;i<=t;i++)
{
ans=inf;
scanf("%d",&n);
for(j=1;j<=n;j++)
scanf("%d",&dp[j]);
for(j=1;j<=n;j++)
if(dp[j-1]>=0)
dp[j]=dp[j-1]+dp[j];
for(j=1;j<=n;j++)
{
if(dp[j]>ans)
{
ans=dp[j];
ed=j;
}
}
for(j=ed-1;j>=0;j--)
{
if(dp[j]<0)
{
st=j+1;
break;
}
}
printf("Case %d:\n",i);
printf("%d %d %d\n",ans,st,ed);
if(i!=t)
printf("\n");
}
return 0;
}
Super Jumping! Jumping! Jumping!
#include <stdio.h>
const int inf=0x80000000;
int number[1005],dp[1005];
int max(int x,int y)
{
return x>y?x:y;
}
int main()
{
int i,j,n,ans;
while(scanf("%d",&n)!=EOF)
{
if(n==0)
break;
ans=inf;
for(i=1;i<=n;i++)
{
scanf("%d",&number[i]);
dp[i]=number[i];
}
for(i=2;i<=n;i++)
{
for(j=1;j<i;j++)
if(number[j]<number[i])
dp[i]=max(dp[i],dp[j]+number[i]);
if(dp[i]>ans)
ans=dp[i];
}
printf("%d\n",ans);
}
return 0;
}
#include <stdio.h>
#include <algorithm>
using namespace std;
int number[40005],dp[40005];
int main()
{
int i,t,n,offset,ans;
scanf("%d",&t);
while(t--)
{
ans=1;
scanf("%d",&n);
for(i=1;i<=n;i++)
scanf("%d",&number[i]);
dp[1]=number[1];
for(i=2;i<=n;i++)
{
if(number[i]>dp[ans])
dp[++ans]=number[i];
else
{
offset=lower_bound(dp+1,dp+ans,number[i])-dp;
dp[offset]=number[i];
}
}
printf("%d\n",ans);
}
return 0;
}
这题其实是让你求最长上升子序列的长度,但是数据太大,有40000这么多的话用常规的n2做法肯定会超时,所以我们采用二进制优化的做法,把时间复杂度降到nlogn,这样就不会超时了。
#include <stdio.h>
#include <string.h>
int p[105],m[105][105],s[105][105];
void output(int x,int y)
{
if(x==y)
printf("A%d",x);
else
{
printf("(");
output(x,s[x][y]);
printf(" x ");
output(s[x][y]+1,y);
printf(")");
}
}
int main()
{
int i,j,k,l,n,a,b,ans,count=1;
while(scanf("%d",&n)!=EOF)
{
if(n==0)
break;
memset(p,0,sizeof(p));
memset(m,0,sizeof(m));
memset(s,0,sizeof(s));
scanf("%d",&p[0]);
for(i=1;i<n;i++)
{
scanf("%d%d",&a,&b);
p[i]=a;
}
scanf("%d",&p[n]);
for(l=2;l<=n;l++)
{
for(i=1;i<=n-l+1;i++)
{
j=i+l-1;
m[i][j]=m[i][i]+m[i+1][j]+p[i-1]*p[i]*p[j];
s[i][j]=i;
for(k=i+1;k<j;k++)
{
ans=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(ans<m[i][j])
{
m[i][j]=ans;
s[i][j]=k;
}
}
}
}
printf("Case %d: ",count++);
output(1,n);
printf("\n");
}
return 0;
}
这题是基本的矩阵连乘问题,但让你求的不是最少计算次数,而是输出最少计算次数的方案,题目的难点除了dp外还有记录决策点这一点。代码中s数组是用来记录决策点的,至于怎么递归输出结果的我也是看了大牛的码才知道的,其中具体是什么原理就不知道了,你们可以自行百度一下。
#include <stdio.h>
#include <string.h>
char a[1005],b[1005];
int dp[1005][1005];
int max(int x,int y)
{
return x>y?x:y;
}
int main()
{
int i,j,lena,lenb;
while(scanf("%s%s",&a,&b)!=EOF)
{
memset(dp,0,sizeof(dp));
lena=strlen(a);
lenb=strlen(b);
for(i=1;i<=lena;i++)
{
for(j=1;j<=lenb;j++)
{
if(a[i-1]==b[j-1])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
printf("%d\n",dp[lena][lenb]);
}
return 0;
}
#include <stdio.h>
int r,c,dir[4][2]={-1,0,0,-1,0,1,1,0},map[105][105],dp[105][105];
int max(int x,int y)
{
return x>y?x:y;
}
int check(int x1,int y1,int x2,int y2)
{
if(map[x1][y1]<=map[x2][y2])
return 0;
if(x2<1||x2>r||y2<1||y2>c)
return 0;
return 1;
}
int search(int x,int y)
{
int i,xx,yy;
if(dp[x][y])
return dp[x][y];
dp[x][y]=1;
for(i=0;i<4;i++)
{
xx=x+dir[i][0];
yy=y+dir[i][1];
if(check(x,y,xx,yy))
dp[x][y]=max(dp[x][y],search(xx,yy)+1);
}
return dp[x][y];
}
int main()
{
int i,j,ans=-1;
scanf("%d%d",&r,&c);
for(i=1;i<=r;i++)
for(j=1;j<=c;j++)
scanf("%d",&map[i][j]);
for(i=1;i<=r;i++)
for(j=1;j<=c;j++)
ans=max(ans,search(i,j));
printf("%d\n",ans);
return 0;
}
这题是dp与搜索的结合,主函数中遍历搜索数组中的每一个元素,搜索的函数是一个记忆化的深度搜索,它先搜索完一种情况再搜索其它情况,是深度搜索,而且如果这个位置被搜索过,就直接返回之前搜索的结果,大大节省了时间。
#include <stdio.h>
int dp[55][1005];
int min(int x,int y)
{
return x<y?x:y;
}
int max(int x,int y)
{
return x>y?x:y;
}
int main()
{
int i,j,k,t,Case,b,m,ans;
scanf("%d",&t);
while(t--)
{
scanf("%d%d%d",&Case,&b,&m);
for(i=0;i<=m;i++)
{
dp[0][i]=0;
dp[1][i]=i;
}
for(i=0;i<=b;i++)
{
dp[i][0]=0;
dp[i][1]=1;
}
for(i=2;i<=b;i++)
{
for(j=2;j<=m;j++)
{
ans=0x7fffffff;
for(k=1;k<=j;k++)
ans=min(ans,max(dp[i-1][k-1],dp[i][j-k])+1);
dp[i][j]=ans;
}
}
printf("%d %d\n",Case,dp[b][m]);
}
return 0;
}
这题是著名的摔鸡蛋问题,只不过这题把鸡蛋换成了玻璃球(题目显得更符合实际了)。注意这题玻璃球每次摔下碎与不碎都是随机的,因此有两种情况,摔下后摔碎了与摔下后没摔碎,根据这两种情况再去考虑状态转移方程即可。同时还得注意dp数组的初始化,初始化不对是交不上的。