DP(动态规划):
- 概念: 用来解决一类最优化问题的算法思想,即 将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解得到原问题的最优解,DP会将每个求解过程的自问题的解记录下来,下次碰到同样的子问题时,就可以直接使用之前记录的结果,而不是重复计算
总之:一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划去解决 - 动态规划的递归写法
/*
斐波那契数列为例
int f(int n){
if(n==0||n==1) return 1;
else return f(n-1)+f(n-2);
}
这个递归会涉及到很多重复的计算,比如n==5时,f(5)=f(4)+f(3),接下来计算f(4)时会有
f(4)=f(3)+f(2),所以f(3)会被重复计算,实际复杂度为O(2^n)
为了避免重复计算,开一个一维数组dp保存已经计算过的结果,dp[n]记录f(n)的结果 dp[n]=-1表示没有被计算过
int f(int n){
if(n==0||n==1) return 1;
if(dp[n]!=-1) return dp[n];//说明已经计算过,直接返回结果,无需重复计算
else{
dp[n]=f(n-1)+f(n-2);
return dp[n];
}
}
复杂度降到了O(n)
*/
*******:
如果一个问题可以被分解为若干个子问题,且这些问题会重复出现,那么就称这个问题拥有
重叠子问题,动态规划通过记录重叠子问题的解,来使下次碰到相同的子问题时就直接使用
记录的结果,避免大量重复计算,所以,一个问题必须拥有子问题,才能使用动态规划
- 动态规划的递推写法
数塔问题为例:
第n层有n个数字,现在从第1层走到第n层,每次只能走向下一层连接的两个数字中的一个,问:最后将路径上所有数字相加后得到的和最大是多少?
如果穷举所有路径,复杂度为O(2^n),会导致反复访问。
可以在第一次枚举时把某个位置能到达底层的所有路径的最大和记录下来,再次访问这个 位置时就可以直接获取这个最大值
令dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径中能得到的最大和
dp[1][1]即最终答案
如果要求出dp[i][j],那么一定要先求出它的两个子问题--从位置(i+1,j)到达最
底层的最大和dp[i+1][j] 和 从位置(i+1,j+1)到达最底层的最大和dp[i+1,j+1],
即进行了一次决策:走位置(i,j)的左下还是右下,dp[i][j]就是dp[i+1][j+1]和
dp[i+1][j+1]的较大值加上f[i][j],即为:
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j];
dp[i][j]称为问题的状态,把上面的式子称作状态转移方程,把状态dp[i][j]转移为
dp[i+1][j]和dp[i+1][j+1],dp[i][j]的状态只与第i+1层的状态有关,与其他层状
态无关,什么时候到头呢,数塔的最后一层的dp值总是等于元素本省,把这种可以直
接确定结果的部分称为边界,动态规划的递推写法总是从这些边界出发,通过状态转
移方程扩散到整个dp数组
从最底层各位置的dp值开始,不断往上求出每一层各位置的dp值,最后就得到dp[1][1]
int f[1000][1000],dp[1000][1000]
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;<=i;j++) cin>>f[i][j];//读入数据
}
//边界dp值
for(int i=1;i<=n;i++) dp[n][i]=f[n][i];
//从第n-1层不断往上计算dp[i][j]
for(int i=n-1;i>=1;i--){
for(int j=1;j<=i;j++){
//状态转移方程
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j];
}
}
cout<<dp[1][1];
return 0;
}
*******:
如果一个问题的最优解可以由其他子问题的最优解有效构造出来,那么称这个问题拥有最优子结构,最优子结构保证动态规划中原问题的最优解可以由子问题的最优解推倒出来,因此,一个问题必须拥有最优子结构,才能用动态规划去解决