Dynamic Programming
DP的前提:
有可解顺序:大问题可以拆成小问题。如有环的图上不能跑DAG最短路算法,无环时才能使用DP求出最短路。
- 最优子结构性质:大问题的最优解必须时从小问题的最优解(而不是其他垃圾解)推出来的。
- 无后效性:不用记录先前子结构的过程,只需记录结果。
DP三连
from 阮行止
我是谁 | 把实际局面表达成数学状态 |
我从哪里来 | 设计pull型的转移 |
我到哪里去 | 设计push型的转移 |
如果满足了上述条件就可以DP,写出状态转移方程之后,全体状态必定性能成一个DAG,DP的过程实际上就是在DAG上一次求出每个节点的value:大事化小,小事化了。
两种实现方式
按顺序递推?记忆化搜索
前者多用于push型的转移,后者多用于pull型的转移。使用前者实现的过程必定能用后者实现,反之亦然。
本质上,这两种方法反映了拓扑排序的两种实现方式。
线性DP举例
线性DP指的是在序列上执行的DP。
- 例 LIS问题
给定数组a[],问最长的不下降的子序列的长度。
朴素做法:
三连:
设计状态:记dp[x]:以a[x]结尾的LIS长度。
#include <iostream>
int n, a[100005];
void inp()
{
scanf("%d", &n);
for (int i = 1; i <=n; i++)
scanf("%d", &a[i]);
}
void pull()
{
for (int i = 1; i <= n; i++)
for (int j = 0; j < i; j++)//通过虚设一个零号元素,防止了某个元素最大不予赋值的情况,从而也免去了初始化为1的麻烦。
if (a[j] <= a[i])
dp[i] = max(dp[i], dp[j] + 1);
for (int i = 1; i <= n; i++)
printf("dp[%d] = %d\n", i, dp[i]);
}
void push()
{
for (int i = 0; i<= n; i++)
for (int j = i + 1; j <= n; j++)
if (a[i]<=a[j])
dp[j] = max(dp[j], dp[i] + 1);
for (int i = 1; i<=n ; i++)
printf("dp[%d] = %d\n", i, dp[i]);
}
int main()
{
inp();
pull();
return 0;
}
复杂度分析:
这个朴素算法的复杂度是
的。
第一轮循环枚举
,这个复杂度不能省掉。
第二轮是找出这些地方(前面的,且不小于x)的最大dp值。
优化
要找出满足条件处的最大dp值是很困难的。
二分思路:如果能很快地实现检验过程,那么我们也能快速找到答案。
如何实现这个check?x应该接在谁后面?
随着x的增大,我们维护一个数组
在已经走过的这些元素中,长度为i的LIS最小以什么数结尾。那么check§就很方便了:直接看p是否不超过a[x].
可二分性:LIS越长,结尾必然越大,r[i]单调上升。
int R[100005], ma;//在全局变量中声明的变量自动初始化为零
int bin_search(int x)//二分查找
{
int l = 0, r = ma, mid;
while (l != r)
{
mid = (l + r) /2 + 1;
if (R[mid] <= x) l = mid;
else r = mid - 1;
}
return l;
}
void opt()
{
int len;
for (int i = 1; i <= n; i++)
{
len = bin_search(a[i]) + 1; //求出以a[i]结尾的LIS最长能多长。
dp[i] = len;
ma = max(ma, len); // 维护当前LIS长度
R[len] = R[len] == 0? a[i]:min(R[len], a[i]); //维护R数组
}
}