动态规划:DP(Dynamic Programming)。
最经典的DP问题:
01背包问题:
题意:有n个重量和价值分别为wi, vi的物品。从这些物品中挑选出总质量不超过W的物品,求所有挑选方案中价值总和的最大值。
输入:
n = 4
(w, v) = {(2, 3), (1, 2), (3, 4), (2, 2)}
W = 5
输出:
7(选择第0、1、3号物品)
这个经典问题,我们先使用最朴素的搜索试一下:
int n, W;
int w[MAX_N], v[MAX_N];
int rec(int i, int j)
{
int res;
if(i == n)
{
res = 0;
}
else if(j < w[i])
{
res = rec(i + 1, j);
}
else
{
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
}
return res;
}
void solve()
{
printf("%d\n", rec(0, w));
}
这种方法的搜索深度是n,最坏时间复杂度就会O(2^n), 不算乐观,只好进行优化代码。
这里发现,代码搜索时候出现重复数据的计算,我们第一次优化的方向就很明确的,使用空间换取时间,开记录数组,使用记忆化搜索优化解题算法。
int dp[MAX_N + 1][MAX_W + 1];
int rec(int i, int j)
{
if(dp[i][j] >= 0)
{
return dp[i][j];
}
int res;
if(i == n)
{
res = 0;
}
else if(j < w[i])
{
res = rec(i + 1, j);
}
else
{
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
}
return dp[i][j] = res;
}
void solve()
{
memset(dp, -1, sizeof(dp));
printf("%d\n", rec(0, W));
}
由于参数的组合不过nW种, 所以时间复杂度仅为O(nW)。记忆化搜索让此解题代码的时间复杂度大大降低。
这里提一下memset, memset速度很快,可以在字节层次上对内存进行填充,-1的每一位二进制位都是1, 所以memset可以让高维数组初始化0或-1,其他数就不行了。
咱们看记忆化搜索的递推式:
dp[n][j] = 0
dp[i][j] = dp[i + 1][j] (j < w[i])
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i])
简洁点,通过二次循环:
int dp[MAX_N + 1][MAX_W + 1];
void solve()
{
for(int i=n-1;i>=0;i--)
{
for(int j=0;j<=W;j++)
{
if(j < w[i])
{
dp[i][j] = dp[i + 1][j];
}
else
{
dp[i][j]=max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i]);
}
}
}
printf("%d\n", dp[0][W]);
}
从记忆化搜索出发推导出递推式,也可以直接得到递推式,这种一步步按顺序求出问题的解的方法称为动态规划法,叫做DP,形式简介,时间复杂度是O(nW)。
这个DP中关于i的循环是逆向进行的,我们也可以改变一下递推关系,关于i的循环就能正向进行。
dp[0][j] = 0
dp[i + 1][j] = dp[i][j]
dp[i + 1][j] = max(dp[i][j - w[i]] + v[i])
void solve()
{
for(int i=0;i<n;i++)
{
for(int j=0;j<=W;j++)
{
if(j < w[i])
{
dp[i + 1][j] = dp[i][j];
}
else
{
dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
}
}
}
printf("%d\n", dp[n][W]);
}
好了,讨论完这些,我们来看一下另一个使用DP的经典问题:最长公共子序列问题(LCS)。
使用递推公式可以在O(n2)时间内解决这个问题。
int n;
int a[MAX_N];
int dp[MAX_N];
void solve(){
int res = 0;
for(int i=0;i<n;i++){
dp[i] = 1;
for(int j=0;j<i;j++) if(a[j] < a[i]){
dp[i] = max(dp[i], dp[j]+1);
}
res = max(res, dp[i]);
}
printf("%d\n",res);
}
我们可以对这个方法进行优化,dp[i] 为长度是i+1的上升子序列中末尾元素的最小值,不存在就是INF。使用二分搜索,即:
int dp[MAX_N];
void solve(){
fill(dp, dp + n, INF);
for(int i=0;i<n;i++){
*lower_bound(dp, dp+n,a[i]) = a[i];
}
printf("%d\n", lower_bound(dp,dp+n,INF)-dp);
}