杭电oj(1087、1203、1003)题解
DP 即动态规划(Dynamic Programming),是一种通过把原问题分解为相对简单的子问题,并保存子问题的解来避免重复计算,从而解决复杂问题的算法策略。以下从几个方面简述动态规划:
基本思想
动态规划的核心在于 “分治” 和 “最优子结构”。它将一个复杂问题分解为一系列相互关联的子问题,通过求解子问题并保存其解,避免对相同子问题的重复求解,从而减少计算量。同时,原问题的最优解可以由子问题的最优解组合而成,这就是最优子结构性质。
适用条件
- 最优子结构性质:问题的最优解包含其子问题的最优解。即可以通过子问题的最优解推导出原问题的最优解。例如,在背包问题中,对于一个容量为
W
的背包和n
个物品,要得到其最优装法,可以先考虑容量为W - w[i]
(w[i]
为第i
个物品的重量)的背包和前n - 1
个物品的最优装法。 - 子问题重叠性质:在求解过程中,会多次遇到相同的子问题。动态规划通过保存子问题的解,避免了对这些子问题的重复计算,从而提高了效率。比如在斐波那契数列的计算中,直接递归计算会有大量重复的子问题计算,而使用动态规划可以避免这种情况。
求解步骤
- 定义状态:明确问题的状态表示,即如何用一个或多个变量来描述子问题。例如,在最长上升子序列问题中,可以定义状态
dp[i]
表示以第i
个元素结尾的最长上升子序列的长度。 - 确定状态转移方程:根据问题的最优子结构性质,找出状态之间的转移关系。状态转移方程描述了如何从子问题的解推导出原问题的解。例如,在最长上升子序列问题中,状态转移方程为
dp[i] = max(dp[j] + 1) (0 <= j < i 且 a[j] < a[i])
。 - 初始化:确定初始状态的值,这些初始状态是问题的边界条件。例如,在斐波那契数列的动态规划求解中,
dp[0] = 0
,dp[1] = 1
就是初始状态。 - 计算顺序:根据状态转移方程,确定计算状态的顺序,一般是从简单的子问题开始,逐步求解更复杂的问题。
应用场景
动态规划在很多领域都有广泛的应用,常见的问题包括:
- 背包问题:如 0 - 1 背包问题、完全背包问题等,用于在一定的约束条件下选择物品,使得总价值最大。
- 最长公共子序列问题:找出两个序列中最长的公共子序列,在生物信息学、版本控制等领域有应用。
- 最短路径问题:如 Floyd - Warshall 算法,用于求解图中任意两点之间的最短路径。
- 资源分配问题:在多个项目或任务之间分配资源,以达到最优的效益。
目录
1087
题目
思路
代码详细步骤
动态规划初始化
状态转移
找出最大值
代码
1203
题目
思路
代码
1003
题目
思路
代码
1087
题目
思路
动态规划算法的核心在于将原问题分解为一系列子问题,并通过求解子问题来得到原问题的解。在这个问题中,我们定义状态 dp[i]
表示以第 i
个元素结尾的最长上升子序列的最大和。通过遍历序列中的每个元素,我们可以根据之前的状态来更新当前状态,最终找出所有状态中的最大值,即为最长上升子序列的最大和。
代码详细步骤
动态规划初始化
dp[0]=a[0];
:将dp[0]
初始化为a[0]
,因为以第一个元素结尾的最长上升子序列就是它本身,其和就是a[0]
。状态转移
- 外层循环
for(int i = 1; i < n; i++)
:遍历数组中的每个元素,从第二个元素开始。dp[i]=a[i];
:将dp[i]
初始化为a[i]
,表示以第i
个元素结尾的最长上升子序列至少包含该元素本身。- 内层循环
for(int j = 0; j < i; j++)
:遍历当前元素a[i]
之前的所有元素。if(a[i]>a[j])
:判断a[i]
是否大于a[j]
,如果满足条件,说明可以将a[i]
接到以a[j]
结尾的最长上升子序列后面。dp[i]=max(a[i]+dp[j],dp[i]);
:更新dp[i]
的值,取a[i] + dp[j]
和dp[i]
中的最大值。a[i] + dp[j]
表示将a[i]
接到以a[j]
结尾的最长上升子序列后面得到的新子序列的和。找出最大值
int maxnum=-1e9;
:将maxnum
初始化为一个较小的值-1e9
,确保后续找到的任何dp[i]
值都能更新maxnum
。for(int i = 0; i < n; i++){ maxnum=max(maxnum,dp[i]); }
:遍历dp
数组,找出其中的最大值。
代码
#include<iostream>
#include<algorithm>
using namespace std;
int dp[1010],a[1010],n;
int main(){while(cin>>n&&n){for(int i=0;i<n;i++){cin>>a[i];}dp[0]=a[0];for(int i=1;i<n;i++){dp[i]=a[i];for(int j=0;j<i;j++){if(a[i]>a[j]){dp[i]=max(a[i]+dp[j],dp[i]);}}}int maxnum=-1e9;for(int i=0;i<n;i++){maxnum=max(maxnum,dp[i]);}cout<<maxnum<<endl;}return 0;
}
1203
题目
思路
外层循环
for(int i = 0; i < m; i++)
:遍历每一个物品。内层循环
for(int j = n; j >= a[i]; j--)
:从背包的总容量n
开始,倒序遍历到当前物品的重量a[i]
。倒序遍历是为了保证每个物品只被选择一次,这是 0 - 1 背包问题的常见处理方式。状态转移方程
dp[j]=max(dp[j],dp[j-a[i]]+prob[i]-dp[j-a[i]]*prob[i]);
:
dp[j]
表示不选择当前物品i
时,背包容量为j
的最大成功概率。
dp[j - a[i]]
表示在不放入当前物品i
时,背包容量为j - a[i]
的最大成功概率。
dp[j - a[i]] + prob[i] - dp[j - a[i]] * prob[i]
表示选择当前物品i
时的成功概率。这里基于概率的计算原理,假设物品的成功是相互独立事件,使用公式P(A 或 B) = P(A) + P(B) - P(A) * P(B)
来计算选择当前物品后的成功概率。通过
max
函数取两者中的最大值更新dp[j]
。
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
double dp[N];
double prob[N];
int a[N];
int n,m;
int main(){while(cin>>n>>m){if(n==0 && m==0) break;for(int i=0;i<m;i++){cin>>a[i]>>prob[i];}memset(dp,0,sizeof(dp));for(int i=0;i<m;i++){for(int j=n;j>=a[i];j--){dp[j]=max(dp[j],dp[j-a[i]]+prob[i]-dp[j-a[i]]*prob[i]);}}printf("%.1lf%%\n",dp[n]*100);}return 0;
}
1003
题目
思路
状态转移方程
dp[j]=max(dp[j],dp[j-a[i]]+prob[i]-dp[j-a[i]]*prob[i]);
:
dp[j]
表示不选择当前物品i
时,背包容量为j
的最大成功概率。
dp[j - a[i]]
表示在不放入当前物品i
时,背包容量为j - a[i]
的最大成功概率。
dp[j - a[i]] + prob[i] - dp[j - a[i]] * prob[i]
表示选择当前物品i
时的成功概率。这里基于概率的计算原理,假设物品的成功是相互独立事件,使用公式P(A 或 B) = P(A) + P(B) - P(A) * P(B)
来计算选择当前物品后的成功概率。通过
max
函数取两者中的最大值更新dp[j]
。
代码
#include<iostream>
using namespace std;
int n,m,l,r,maxnum,sum,temp,num;
int main(){cin>>n;for(int i=0;i<n;i++){l=0,r=0,temp=1,maxnum=-20000,sum=0;cin>>m;for(int j=1;j<=m;j++){cin>>num;sum+=num;if(sum>maxnum){r=j;maxnum=sum;l=temp;}if(sum<0){temp=j+1;sum=0;}}cout<<"Case "<<(i+1)<<":"<<endl<<maxnum<<" "<<l<<" "<<r<<endl;if(i<n-1) cout<<endl;}return 0;
}