写在前面的话:
动态规划是一种问题求解方法,它本身并不是一个特定的算法,而是一种思想、一种手段。
状态、状态转移方程、最优子结构(全局最优解包含局部最优)
三角形上的动态规划
(d为路径距离,a为单个距离)
状态方程:
d(I,j) = a(I,j) + max(d(I+1,j),d(I+1,j+1))
- 得出递归代码(重复计算):
int solve(int i,int j){
return a[i][j] + (i==n?0:max(solve(i,j+1),solve(i+1,j+1)));
}
- 递推计算(从下往上):
int i,j;
for(j = 1;j<=n;j++){ // 最后一行赋值
d[n][j] = a[n][j];
}
for(i = n-1;i>=1;i--){
for(j = 1;j<=i-1;j++){
d[i][j] = a[i][j] + max(b[i+1][j],b[i+1][j+1]);
}
}
return d[1][1]; // 递推到最上面,一定是最优解
- 记忆化搜索(仍然使用递归,不过通过初始化来避免重复)
memset(d,-1,sizeof(d)); // 标记
int solve(int i,int j){
if(d[i][j]) >= 0 return d[i][j];
return d[i][j] = a[i][j] + (i==n?0:max(solve(i+1,j),solve(i+1,j+1));
DAG上的动态规划
有向无环图上的动态规划是学习动态规划的基础,很多问题都可以转化为DAG上的最长路、最短路或路径计数问题。
- 分析矩形可嵌套:
可嵌套关系是一个典型的二元关系,而二元关系可以用图来建模。如果X能被嵌套在Y中,则X,Y中间有一条有向边连接。
没有确定的终点和起点 - 分析硬币问题:
设初始状态为S,终点为0
本质上也是一个路径问题,通过使用硬币,实现状态的转换。
考虑从状态S开始的最长路径:
状态转换公式也是:d(i) = max(d(j)+1)(i到j能够转换,必须满足V[i] <= i)
以下是代码
memset(d,-1,sizeof(d)); // 初始化所有的状态都没有访问过
int dp(int s){
int& ans = d[s];
int(ans != -1) return ans; // 被访问过(得到的可能是路径长,也可能代表无法到达0状态)
ans = -(1<<30); // 不成立 和-1 和 其他正数区别开来
for(int i = 1;i <= n;i++){
if(s >= V[i]){ // 满足条件,可以转换
ans = max(ans,dp(s-V[i])+1);
}
}
return ans;
}
如果既要求最大值也要求最小值,则需要写两个记忆化搜索,比较麻烦。
// 最大值
memset(d,-1,sizeof(d)); // 初始化所有的状态都没有访问过
int dp(int s){
int& ans = d[s];
int(ans != -1) return ans; // 被访问过(得到的可能是路径长,也可能代表无法到达0状态)
ans = -(1<<30); // 不成立 和-1 和 其他正数区别开来
for(int i = 1;i <= n;i++){
if(s >= V[i]){ // 满足条件,可以转换
ans = max(ans,dp(s-V[i])+1);
}
}
return ans;
}
// 最小值
memset(d,-1,sizeof(d)); // 初始化所有的状态都没有访问过
int dp(int s){
int& ans = d[s];
int(ans != -1) return ans; // 被访问过(得到的可能是路径长,也可能代表无法到达0状态)
ans = (1<<30); // 不成立 和-1 和 其他正数区别开来
for(int i = 1;i <= n;i++){
if(s >= V[i]){ // 满足条件,可以转换
ans = min(ans,dp(s-V[i])+1);
}
}
return ans;
}
此时用递推更加方便:
for(int i = 1;i<=S;i++){
minv[i] = INF;
maxv[i] = -INF;
}
for(int i = 1;i<=S;i++){
for(int j = 1;j<=n;j++){
if(i>=V[j){
minv[i] = min(minv[i],minv[i-v[j]]+1);
maxv[i] = max(maxv[i],maxv[i-v[j]]+1);
}
cout << minv[S] << maxv[S] << endl;