DP总结
最近也写了一些dp,感觉对于状态的转移有了点小小的体会
像之前写的都是一些经典的dp背包啊,LIS之类的,最近写了一些感觉更在于寻找状态的dp,当然也不是很难,太难的我也不会。
目标答案一定是由前一个状态来表示从而地推,dp的难处就在于如何用简单的数组来表示复杂的状态转移的过程,这也是我们要做和练习的事情。
由于一些复杂状态的题目(状态压缩之类的),有时候我们不得不对数组多开一维(或者是利用二进制位运算)来实现这个状态,把这个复杂的状态
表示出来,利用状态转移方程地推下去即可。
谈谈我学的dp,主要都是线性dp,难点的我也没学过
一类就是一维数组即可实现的线性dp,一般相对简单点,因为状态容易表示出来,一般是对于一串数字进行的操作,也不乏抽象为别的东西。
一类是区间dp,为了求得一个大区间的最优解我们通常要知道所有小区间的最优解才能进行下去,一般用二维数组dp[i][j]表示区间【i,j】的最优解,
对于此类dp有平行不等式的优化操作我暂时不会,经典问题有合并石子,最大累乘等。
一类是状态压缩dp,真正的装压dp目前还未掌握(日后学会在更),先是针对于一些简单的01状态而言,一般可以用dp[n][0]-dp[n][1]来简单的表示这两种状态对应的最优解,
经典问题有开关灯等
意思就是一种方程是当前的状态是由前面所计算出的状态决定的,一种是由当前的状态直接推出后面的某些状态。
例如斐波那契数 dp[i]=dp[i-1]+dp[i-2] 显然他是一个人人为我,用我为人人得话显然不是正确选择。
例:数字三角形(POJ 1163)
Time Limit: 1000MS | Memory Limit: 10000K | |
Total Submissions: 45053 | Accepted: 27208 |
Description
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
(Figure 1)
Input
Output
Sample Input
5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
Sample Output
30
Source
基本思路
用二维数组存放数字三角形。
D( r, j) : 第r行第 j 个数字(r,j从1开始算)
MaxSum(r, j) : 从D(r,j)到底边的各条路径中,
最佳路径的数字之和。
问题:求 MaxSum(1,1)
典型的递归问题。
D(r, j)出发,下一步只能走D(r+1,j)或者D(r+1, j+1)。故对于N行的三角形:
if ( r == N)
MaxSum(r,j) = D(r,j)
else
MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j)
然后你会发现:
为什么超时
重复计算
如果采用递规的方法,深度遍历每条路径,存在大量重复计算。则时间复杂度为 2n,对于 n = 100 行,肯定超时。
改进
如果每算出一个MaxSum(r,j)就保存起来,下次用到其值的时候直接取用,则可免去重复计算。那么可以用O(n2)时间完成计算。因为三角形的数字总数是 n(n+1)/2
改进代码
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX]; int n;
int maxSum[MAX][MAX];
int MaxSum(int i, int j)
{
if (maxSum[i][j] != -1)
return maxSum[i][j];
if (i == n) maxSum[i][j] = D[i][j];
else {
int x = MaxSum(i + 1, j);
int y = MaxSum(i + 1, j + 1);
maxSum[i][j] = max(x, y) + D[i][j];
}
return maxSum[i][j];
}
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
{
cin >> D[i][j];
maxSum[i][j] = -1;
}
cout << MaxSum(1, 1) << endl;
}
转化为递推
“人人为我”型递推
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX]; int n;
int maxSum[MAX][MAX];
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
cin >> D[i][j];
for (int i = 1; i <= n; ++i)
maxSum[n][i] = D[n][i];
for (int i = n - 1; i >= 1; --i)
for (int j = 1; j <= i; ++j)
maxSum[i][j] = max(maxSum[i + 1][j], maxSum[i + 1][j + 1]) + D[i][j];
cout << maxSum[1][1] << endl;
}
这里说的人人为我型递归就是在求DP[i][j]的过程中, 使用DP[i-1][1]→DP[i - 1][i - 1]来推到出DP[i][j];
“我为人人”型递归
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX]; int n;
int maxSum[MAX][MAX];
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
cin >> maxSum[i][j];
for (int i = 1; i < n; i++)
{
for (int j = 1; j <= i; j++)
{
maxSum[i + 1][j] = max(maxSum[i + 1][j], maxSum[i + 1][j] + maxSum[i][j]);
maxSum[i + 1][j + 1] = max(maxSum[i + 1][j + 1], maxSum[i + 1][j + 1] + maxSum[i][j]);
}
}
int maxx = -9999999999;
for (int i = 1; i <= n; i++)
maxx = max(maxx, maxSum[n][i]);
cout << maxx << endl;
}
这里说的人人为我型递归就是在求DP[i][j]的过程中, 求出DP[i + 1][j]和DP[i + 1][j +1]的其中一个解。
空间优化
没必要用二维maxSum数组存储每一个MaxSum(r,j),只要从底层一行行向上递推,那么只要一维数组maxSum[100]即可,即只要存储一行的MaxSum值就可以。
进一步考虑,连maxSum数组都可以不要,直接用D的第n行替代maxSum即可。
节省空间,时间复杂度不变
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX];
int n; int * maxSum;
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
cin >> D[i][j];
maxSum = D[n]; //maxSum指向第n行
for (int i = n - 1; i >= 1; --i)
for (int j = 1; j <= i; ++j)
maxSum[j] = max(maxSum[j], maxSum[j + 1]) + D[i][j];
cout << maxSum[1] << endl;
}
总结三种动态规划的形式
人人为我
状态i的值Fi由若干个值已知的状态值Fk,Fm,..Fy推出,如求和,取最大值
我为人人
状态i的值Fi在被更新(不一定是最终求出)的时候,依据Fi去更新(不一定是最终求出)和状态i相关的其他一些状态的值Fk,Fm,..Fy
注意:在选取最优备选状态的值Fm,Fn,…Fy时,有可能有好的算法或数据结构可以用来显著降低时间复杂度。
记忆型递归
优点:只经过有用的状态,没有浪费。递推型会查看一些没用的状态,有浪费
缺点:可能会因递归层数太深导致爆栈,函数调用带来额外时间开销。无法使用滚动数组节省空间。总体来说,比递推型慢。
关于人人为我和我为人人合适的使用,
当推导当前状态时,是由前面几个固定数量的状态推导而来的,一般使用人人为我,因为这个人好找。
而当推导这个状态时,决定这个状态的状态个数可能无法确定,需要根据具体情况挨个枚举,这样如果非要写成人人为我的话不仅代码丑效率也低,
这时考虑我为人人,如果根据这个状态能推导出所有的由他引申出的状态,显然我们应该采用我为人人的方法。