动态规划(DP)
用来优化加速,时间复杂度从指数(Exponential) 加速到多项式时间(Polynomial)。
何时使用DP?
- 计数问题:求有多少种方法
- 优化问题:max和min问题
使用DP的要求
- 最优子结构
- 重复子问题:求解子问题时,会出现重复计算(overlapping)。如果没有重复计算,则退化成分治算法(divide and conquer)
- 无后效性:调用子问题最优解时,子问题的最优解不发生改变。
使用方法
- 带有记忆性的递归
举例:斐波那契序列 - DP
举例:LIS(最长上升子序列),LCS(最长公共子序列)
典型题目
斐波那契序列
分析
递推公式和边界条件已知。存在重复计算的问题,类似带有记忆性的递归,即使用数组保存先前的结果,减少重复计算。
开一个数组dp[]
for i in range(3,n)
dp[i]=dp[i-1]+dp[i-2]
仅需一个for loop
代码
int DP(int n)
{
int dp[n+1];
dp[1]=1;dp[2]=1;
for(int i=3;i<=n;i++)
dp[i]=dp[i-1]+dp[i-2];
return dp[n];
}
三角形数
题目
给定一个由行数字组成的数字三角形。试着设计一个算法,计算出从三角形的顶到底的一条路径,使得该路径经过的数字总和最大。
测试数据
1
2 3
20 5 6
7 8 9 10
最大值 31
思路1:
自底向上方向,MaxSum[ i ] [ j ]表示第i行第j列的数到最底下路径和的最大值。
MaxSum[ i] [ j] 和MaxSum[ i ][ j+1]中的较大者,作为第i-1行的转移项。
代码
#include<iostream>
using namespace std;
const int maxn=1e2+2;
int D[maxn][maxn],MaxSum[maxn][maxn];
int main()
{
int i,j;
int N;
cin>>N;//输入N行测试数据
for(i=1;i<=N;i++)
for(j=1;j<=i;j++)
cin>>D[i][j];
for(i=1;i<=N;i++)//最大权值和初始化为最后一列
MaxSum[N][i]=D[N][i];
for(i=N;i>1;i--)
for(j=1;j<i;j++)
{
if(MaxSum[i][j]>MaxSum[i][j+1])//比较左右两边数据大小
MaxSum[i-1][j]=D[i-1][j]+MaxSum[i][j];//更新上一行最大权值的和
else
MaxSum[i-1][j]=D[i-1][j]+MaxSum[i][j+1];
}
//最大值在MaxSum[1][1]
cout<<MaxSum[1][1]<<endl;
}
需要注意
注意数组的边界,数组不是从0开始,这里使用从1开始。
对于第i行,需要使用第i+1行的数据,对于for(i=N;i>1;i–)这里
思路2:自上到下,思路在下方代码上面。
下面这道题目可能包含负数。
输入格式
第一行包含整数n,表示数字三角形的层数。
接下来n行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30
/*
数字三角形思路分析;
状态表示f[i,j]
集合:所有从起点到(i,j)的路径
属性:max
状态计算
f[i,j]可以来自左上和右上两种情况:
左上:f[i-1][j-1]+a[i][j]
右上:f[i-1][j]+a[i][j]
*/
/*
dp问题的时间复杂度分析: 状态数量 × 转移的计算量
本题:状态数量n^2 ,转移计算量O(1),所以本题时间复杂度O(n^2)
*/
#include<bits/stdc++.h>
using namespace std;
const int N=510,INF=1e9;
int n;
int f[N][N];
int a[N][N];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++)
cin>>a[i][j];
}
//初始化多初始化两侧两列:因为计算 f[i][1]需要用到f[i-1][0],这里第0列需要初始化
for(int i=1;i<=n;i++){
for(int j=0;j<=i+1;j++)
f[i][j]=-INF;
}
int res=-INF;
f[1][1]=a[1][1];
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++)
{
f[i][j]= max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
}
}
//结果在最后一行取
for(int i=1;i<=n;i++) res=max(res,f[n][i]);
cout<<res<<endl;
}
最小路径和
Leetcode 64
题目
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
ac代码
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m=grid.size();
int n=grid[0].size();
vector<vector<int>> f(m,vector<int>(n,0));//m*n的两维vector,全0
//状态
f[0][0]=grid[0][0];
for(int i=1;i<n;i++)//第一行
f[0][i]+=grid[0][i]+f[0][i-1];
f[0][0]=grid[0][0];
for(int i=1;i<m;i++)//第一列
f[i][0]+=grid[i][0]+f[i-1][0];
//转移
for(int i=1;i<m;i++)
for(int j=1;j<n;j++)
f[i][j]=grid[i][j]+min(f[i-1][j],f[i][j-1]);
return f[m-1][n-1];
}
};
最长上升子序列(LIS)
题目链接:Acwing895. 最长上升子序列
问题描述
一个数的序列bi,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8)。你的任务,就是对于给定的序列,求出最长上升子序列的长度。
输入数据
输入的第一行是序列的长度N(1<=N<=1000)。第二行给出的序列中的N个整数,这些整数的取值范围都是0-10000。
输出要求
最长上升子序列的长度。
输入样例
7
1 7 3 5 9 4 8
输出样例
4
分析
求 a
1
_1
1,a
2
_2
2,…,a
k
_k
k,…,a
n
_n
n 的最长上升子序列
子问题是求 a
1
_1
1,a
2
_2
2,…,a
k
_k
k的最长上升子序列
dp[i]表示终点是a
i
_i
i 的最长上升子序列的长度
动态规划
- 状态转移 : dp[ i ] = max { dp[ j ]+1 , dp[ i ] } , 对于所有的 j< i ,并且 f
j
_j
j<f
i
_i
i
含义: 以a j _j j为终点的最长子序列长度加上 a i _i i本身 就是 以 a i _i i结尾的最长子序列的长度, 即 dp[ j ]+1 - base cases :dp[ i ] =1 ,i ∈ [0,n)
含义:初始时每个位置的最长上升子序列长度都为1,即它本身
时间复杂度 O(n 2 ^2 2)
代码
#include<iostream>
using namespace std;
const int maxn=1010;
int f[maxn],dp[maxn];
//dp[i]表示终点是i的最长上升子序列的长度
int ans=1;//保存结果
int main()
{
int N;
cin>>N;
for(int i=1;i<=N;i++)
{
cin>>f[i];
dp[i]=1;//边界处理
}
//转移
for(int i=1;i<=N;i++)
{
for(int j=1;j<i;j++)
{
if(f[j]<f[i]) dp[i]=max(dp[i],dp[j]+1);
}
ans=max(ans,dp[i]);
}
cout<<ans;
}
补充最长下降子序列(LDS)和最长上升子序列(LIS)分装成函数:
#include<iostream>
using namespace std;
const int maxn=1010;
long long a[maxn],dp[maxn];
//最长下降子序列
long long LDS( long long a[],int N)
{
long long temp=0;
for(int i=1;i<=N;i++)
dp[i]=1;
for(int i=1;i<=N;i++)
{
for(int j=1;j<i;j++)
{
if(a[j]>a[i])
dp[i]=max(dp[j]+1,dp[i]);
}
temp=max(dp[i],temp);
}
return temp;
}
//最长上升子序列
long long LIS( long long a[],int N)
{
long long temp=0;
for(int i=1;i<=N;i++)
dp[i]=1;
for(int i=1;i<=N;i++)
{
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
dp[i]=max(dp[j]+1,dp[i]);
}
temp=max(dp[i],temp);
}
return temp;
}
int main()
{
int N;
cin>>N;
for(int i=1;i<=N;i++)
{
cin>>a[i];
}
cout<<LDS(a,N)<<endl;
cout<<LIS(a,N)<<endl;
}
最长公共子序列(LCS)
Leetcode1143. 最长公共子序列
问题描述
输入两个字符串, 要你求出两个字符串的最长公共子序列长度。
输入
输入两行不超过200的字符串。
输出
给出两个字符串的最大公共字符串的长度。
样例输入
abcfbc
abfcab
样例输出
4
分析
状态转移
我们需要看字符串 s1 第i个字母 和 字符串 s2 第j个字母的关系
if s1
i
_i
i == s2
j
_j
j
同时去掉各自最后的字母,看s1
i
_i
i-
1
_1
1 和 s2
j
_j
j-
1
_1
1
得到 dp[ i ] [ j ] = dp [ i-1 ] [ j -1 ]+1
if s1
i
_i
i != s2
j
_j
j
若去掉 s1
i
_i
i ,看 s1
i
_i
i-
1
_1
1 和 s2
j
_j
j 的公共子序列 与
若去掉 s2
j
_j
j,看s1
i
_i
i和 s2
j
_j
j-
1
_1
1的公共子序列
取两者最大值 dp[ i ] [ j ] = max (dp [ i ] [ j-1 ],dp [ i-1] [ j ] );
时间复杂度 O ( m × n ) O(m \times n) O(m×n),两个字符串的长度。
代码
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m=text1.size();
int n=text2.size();
int dp[m+1][n+1];
memset(dp,0,sizeof(dp));//置零
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
{
if(text1[i-1]!=text2[j-1])
dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
if(text1[i-1]==text2[j-1])
dp[i][j]=dp[i-1][j-1]+1;
}
return dp[m][n];
}
};
板子题目链接:Acwing897. 最长公共子序列
ac代码
#include<bits/stdc++.h>
using namespace std;
const int N=1000;
int n,m;
int dp[N+1][N+1];
int main(){
string t1,t2;
cin>>n>>m;
cin>>t1>>t2;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(t1[i-1]==t2[j-1])
dp[i][j]=dp[i-1][j-1]+1;
else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
cout<<dp[n][m]<<endl;
}
将字符串翻转成单增
Leetcode 926
如果一个由 ‘0’ 和 ‘1’ 组成的字符串,是以一些 ‘0’(可能没有 ‘0’)后面跟着一些 ‘1’(也可能没有 ‘1’)的形式组成的,那么该字符串是单调递增的。
我们给出一个由字符 ‘0’ 和 ‘1’ 组成的字符串 S,我们可以将任何 ‘0’ 翻转为 ‘1’ 或者将 ‘1’ 翻转为 ‘0’。
返回使 S 单调递增的最小翻转次数。
示例 1:
输入:“00110”
输出:1
解释:我们翻转最后一位得到 00111.
示例 2:
输入:“010110”
输出:2
解释:我们翻转得到 011111,或者是 000111。
示例 3:
输入:“00011000”
输出:2
解释:我们翻转得到 00000000。
提示:
1 <= S.length <= 20000
S 中只包含字符 ‘0’ 和 ‘1’
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/flip-string-to-monotone-increasing
分析
求最长不降子序列(LIS),字符串长度减去LIS 即可
使用 DP算法求解LIS超时。
超时代码(思路是对的)
时间复杂度O(
n
2
n^2
n2)
class Solution {
public:
int minFlipsMonoIncr(string S) {
int len=S.size();
int dp[len+1];
for(int i=0;i<=len;i++)
dp[i]=1;
int ans=0;
for(int i=0;i<len;i++)
{
for(int j=0;j<i;j++)
{
if(S[j]<=S[i])
dp[i]=max(dp[i],dp[j]+1);
}
ans=max(dp[i],ans);
}
return len-ans;
}
};
优化代码(AC)
维护单调数列,使用二分法。
采用upper_bound()函数,返回第一个大于x的位置这样的话,遇到相等的元素,则一直会添加到数组中来
时间复杂度O(nlogn)
空间复杂度O(n)
class Solution {
public:
int minFlipsMonoIncr(string S) {
int len=S.size();
vector<char> vec;
for(int i=0;i<len;i++)
{
//p是大于S[i]的下标
int p = upper_bound(vec.begin(),vec.end(),S[i])-vec.begin();
if(vec.size()==p) //新来的是最大的
vec.push_back(S[i]);//加入数组
else//新来的比原位置的小
vec[p]=S[i];替换掉
}
return len-vec.size();//返回字符串长度-LIS
}
};
迷雾森林
补充一道简单的dp题
链接:迷雾森林
来源:牛客网
赛时提示:保证出发点和终点都是空地
帕秋莉掌握了一种木属性魔法
这种魔法可以生成一片森林(类似于迷阵),但一次实验时,帕秋莉不小心将自己困入了森林
帕秋莉处于地图的左下角,出口在地图右上角,她只能够向上或者向右行走
现在给你森林的地图,保证可以到达出口,请问有多少种不同的方案
答案对2333取模
输入描述:
第一行两个整数m , n表示森林是m行n列
接下来m行,每行n个数,描述了地图
0 - 空地
1 - 树(无法通过)
输出描述:
一个整数表示答案
此题需要在计算过程中%2333,同时在处理边界的时候,出现1的话后面的都不能通过;这里处理是多用了一行的一个格,省掉了一行和一列的边界处理。
#include<bits/stdc++.h>
using namespace std;
const int maxn=3010;
int a[maxn][maxn];
int n,m;
long long dp[maxn][maxn];
template<class T>inline void read(T &res)
{
char c;T flag=1;
while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;res=c-'0';
while((c=getchar())>='0'&&c<='9')res=res*10+c-'0';res*=flag;
}
int main(){
cin>>m>>n;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++)
read(a[i][j]);
}
memset(dp,0,sizeof(dp));
dp[m+1][1]=1;
for(int i=m;i>=1;i--){
for(int j=1;j<=n;j++){
if(a[i][j]!=1)
dp[i][j]=(dp[i+1][j]+dp[i][j-1])%2333;
}
}
cout<<dp[1][n]<<endl;
// cout<<endl;
// for(int i=1;i<=m;i++){
// for(int j=1;j<=n;j++)
// cout<<dp[i][j]<<" ";
// cout<<endl;
// }
}
/*
3 5
0 1 0 0 0
0 0 0 0 0
0 0 1 0 0
*/
感谢您阅读到最后,点击下面链接可轻松跳转。
祝您阅读愉快。