动态规划专题
动态规划的递归写法和递推写法
递归写法
Fibonacci(斐波那契数列)
int F(int n)
{
if(n == 0 || n == 1) return 1;
else return F(n-1) + F(n-2);
}
-
缺点:重复计算,例如当n==5时,F(5) = F(4) + F(3),而F(4) = F(3) + F(2),多计算了一次F(3)。
-
改进:用空间换时间,一维数组dp记录F(n)的结果,dp[i] = -1时表示未计算过
int dp[MAXN];
int F(int n)
{
if(n == 0 || n == 1) return 1;
if(dp[n] != -1) return dp[n]; //已经计算过结果返回
else
{
dp[n] = F(n-1) + F(n-2);
return dp[n]; //计算完结果返回
}
}
- 时间复杂度:O(2n)->O(n)
- 空间复杂度:O(1)->O(n)
递推写法
数塔问题
- 状态转移方程
dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + f[i][j]
- 状态:dp[i][j]
- 决策:走位置[i][j]的左下还是右下
- 边界:可以直接确定其结果的部分
边界dp出在最底层各位置,不断往上求出每一层的dp值,最后会得到dp[1][1]
总结
- 递推写法 自顶向上
- 递归写法 自顶向下
一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划去解决
最大连续子列和
- 状态转移方程
d[i] = max(A[i], dp[i-1] + A[i])
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10010;
int A[MAXN], dp[MAXN];
int main()
{
int n;
cin>>n;
for(int i = 0; i < n; ++i)
cin>>A[i];
dp[0] = A[0];
for(int i = 1; i < n; ++i)
dp[i] = max(A[i], dp[i - 1] + A[i]);
//dp[i]中存放以A[i]结尾的连续序列的最大和
int k = 0;
for(int i = 1; i < n; ++i)
if(dp[i] > dp[k])
k = i;
cout<<dp[k];
return 0;
}
状态的无后效性:当前状态记录了历史信息,一旦当前状态确定,就不会再改变,历史信息只能通过已有的状态去影响未来的决策
动态规划核心
- 状态转移方程
- 边界状态
最长不下降子序列
- 状态转移方程
dp[i] = max(1, dp[j] + 1)
(j = 1,2,...,i-1 && A[j] < A[i])
for(int i = 1; i <= n; ++i)
{
dp[i] = 1; //边界初始条件,即每个元素自成一个序列
for(int j = 1; j < i; ++j)
if(A[i] > A[j] && dp[j] + 1 > dp[i]) //状态转移条件,即更新A[i]的条件
dp[i] = dp[j] + 1;
ans = max(ans, dp[i]); //ans为最大的dp[i]
}
//1.A[i]>A[j],能够插入到以A[j]为末尾的队伍中
//2.dp[j] + 1,以A[j]为末尾的队伍的长度+1,即并入A[i]
最长公共子序列(子序列可以不连续)
- 状态转移方程
d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 , A [ i ] = = B [ j ] m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) , A [ i ] ! = B [ j ] dp[i][j]=\left\{ \begin{aligned} & dp[i - 1][j - 1] + 1, A[i] == B[j] \\ & max(dp[i - 1][j], dp[i][j - 1]), A[i] != B[j] \\ \end{aligned} \right. dp[i][j]={dp[i−1][j−1]+1,A[i]==B[j]max(dp[i−1][j],dp[i][j−1]),A[i]!=B[j] - 边界
d p [ i ] [ j ] = 0 dp[i][j] = 0 dp[i][j]=0
//字符串从下标为1读入
//边界全部设置为0,即无公共子序列,从dp[1][1]开始
for(int i = 1; i <= lenA; ++i)
{
for(int j = 1; j <= lenB; ++j)
{
if(A[i] == B[i])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i][j-1], dp[i-1][j]);
}
}
cout<<dp[lenA][lenB];
最长回文子串
- 状态转移方程
d p [ i ] [ j ] = { d p [ i + 1 ] [ j − 1 ] , S [ i ] = = S [ j ] 0 , S [ i ] ! = S [ j ] dp[i][j]=\left\{ \begin{aligned} & dp[i + 1][j - 1] , S[i] == S[j] \\ & 0, S[i] != S[j] \\ \end{aligned} \right. dp[i][j]={dp[i+1][j−1],S[i]==S[j]0,S[i]!=S[j] - 边界
d p [ i ] [ j ] = 1 , d p [ i ] [ i + 1 ] = ( S [ i ] = = S [ i + 1 ] ) ? 1 : 0 dp[i][j] = 1, dp[i][i + 1] = (S[i] == S[i+1])?1:0 dp[i][j]=1,dp[i][i+1]=(S[i]==S[i+1])?1:0
//边界
for(int i = 0; i < len; ++i)
{
dp[i][i] = 1;
if(i < len - 1)
{
if(S[i] == S[i + 1])
{
dp[i][i+1] = 1;
ans = 2;
}
}
}
//状态转移方程
for(int L = 3; L <= len; ++L)
{
for(int i = 0; i + L - 1 < len; ++i)
{
int j = i + L -1;
if(S[i] == S[j] && dp[i+1][j-1] == 1)
{
dp[i][j] = 1;
ans = L;
}
}
}
DAG最长路
DAG:有向无环图
- 求整个DAG中的最长路径(即不固定起点跟终点)
- 固定重点,求DAG的最长路径
问题:给定一个有向无环图,怎么样求解整个图的所有路径中权值之和最大的那条
背包问题
多阶段动态规划问题
背包问题和完全背包问题
总结
- 最大连续子列和
令 d p [ i ] 表 示 以 A [ i ] 作 为 末 尾 的 连 续 序 列 的 最 大 和 令dp[i]表示以A[i]作为末尾的连续序列的最大和 令dp[i]表示以A[i]作为末尾的连续序列的最大和 - 最长不下降子序列(LIS)
令 d p [ i ] 表 示 以 A [ i ] 结 尾 的 最 长 不 下 降 子 序 列 长 度 令dp[i]表示以A[i]结尾的最长不下降子序列长度 令dp[i]表示以A[i]结尾的最长不下降子序列长度 - 最长公共子序列(LCS)
令 d p [ i ] [ j ] 表 示 字 符 串 A 的 i 号 位 和 字 符 串 B 的 j 号 位 之 前 的 L C S 长 度 令dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度 令dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度 - 最长回文子串
令 d p [ i ] [ j ] 表 示 S [ i ] 至 S [ j ] 所 表 示 的 字 串 是 否 是 回 文 子 串 令dp[i][j]表示S[i]至S[j]所表示的字串是否是回文子串 令dp[i][j]表示S[i]至S[j]所表示的字串是否是回文子串 - 数塔DP
令 d p [ i ] [ j ] 表 示 从 第 i 行 第 j 个 数 字 出 发 的 到 达 最 底 层 的 所 有 路 径 上 所 能 够 得 到 的 最 大 和 令dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径上所能够得到的最大和 令dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径上所能够得到的最大和 - DAG最长路
令 d p [ i ] [ j ] 表 示 从 i 号 顶 点 出 发 能 获 得 的 最 长 路 径 长 度 令dp[i][j]表示从i号顶点出发能获得的最长路径长度 令dp[i][j]表示从i号顶点出发能获得的最长路径长度 - 01背包
令 d p [ i ] [ v ] 表 示 令dp[i][v]表示 令dp[i][v]表示 - 完全背包
- $$$$