目录
前言
动态规划(Dynamic Programming,简称DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼等人在研究多阶段决策过程的优化问题时,提出并创立。
动态规划(DP)通过循环做出每一步的最优解从而自底向上的得出对问题的整体最优解;这是它与分支算法的自顶向下求解和与贪心算法寻找局部最优解有本质的区别。
动态规划和分治思想:两者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小的子问题,然后将子问题的解合并,最终得到答案。
分治法将分解后的子问题看成相互独立的,通常用递归来做。动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做。
通俗的讲dp的核心就是记住已经解决过子问题的解,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。dp常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所消耗的时间往往远小于朴素解法。
动态规划的性质
一般题目需要求什么就表示成什么
最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理
无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
步骤
- 确定dp数组及其下标的含义;(找出状态表示)
- 确定递推公式;(确定状态转移)
- dp数组如何初始化;
- 确定遍历顺序;
- 举例推导dp数组。
经常适用的问题
最优解问题:数组中最大值型,比如:最长上升子序列,最大子数组,最长公共子序列等问题。
求可行性问题:如果有这样一个问题,让你判断是否存在一条总和为 x 的路径,或者让你判断能否找到一条符合某种条件的路径,那么这类问题都可以归纳为求可行性问题,并且可以使用动态规划来解。
求方案数问题:求方案总数也是比较常见的一类动态规划问题。比如说给定一个数据结构和限定条件,让你计算出一个方案的所有可能的路径,那么这种问题就属于求方案总数的问题
跳台阶问题
问题1:
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
抛开初始值n=0和n=1,这里稍微思考一下其中关系
台阶数 跳法 跳法数
n=2 11 2 2n=3 111 12 21 3
n=4 1111 112 121 211 22 5
每次最多跳两阶,有规律,为斐波那契数列
#include<iostream>
using namespace std;
int main()
{
int n; cin>>n;
int a[100]={0};
a[0]=a[1]=1;
for(int i=2;i<n;i++)
a[i]=a[i-1]+a[i-2];
cout<<a[n-1];
return 0;
}
问题2:
有 N 级台阶,你一开始在底部,每次可以向上迈 1∼K 级台阶,问到达第 N 级台阶有多少种不同方式。
两个正整数 N,K。
一个正整数 (mod100003)ans(mod100003),为到达第 N 级台阶的不同方式数。
输入
5 2
输出
8
- 对于 100%100% 的数据,1≤N≤100000,1≤K≤100。
第一种方法找规律用数学方法解决
k=2 : 1 2 3 5 8 13 21 34...
k=3 : 1 2 4 7 13 24 44 81...
k=4 : 1 2 4 8 15 29 56 108...
k=5 : 1 2 4 8 16 31 61 120...
#include<iostream>
#include<cstdio>
using namespace std;
const int mod=100003;
int n,k,a[1000000],ans=0;
int main()
{
cin>>n>>k;
a[0]=a[1]=1;
for(int i=2;i<=n;++i)
{
if(i<=k)
{
a[i]=(a[i-1]*2)%mod;
}
else
{
a[i]=(a[i-1]*2-a[i-k-1])%mod;
}
}
ans=(a[n]+mod)%mod;
cout<<ans;
return 0;
}
第二种方法用dp
#include<iostream>
#include<cstdio>
using namespace std;
const int mod=100003;
int n,k,dp[1000000];
int main()
{
cin>>n>>k;
dp[0]=dp[1]=1;
for(int i=2;i<=n;i++)
{
for(int j=1;j<=k;j++)
{
if(i>=j)
{
dp[i]=(dp[i]+dp[i-j])%mod;
}
}
}
cout<<dp[n]%mod;
return 0;
}
连续子数组的最大和
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。 要求时间复杂度为O(n)。 【示例】 输入: nums = [-2,1,-3,4,-1,2,1,-5,4] 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
分析:需要使用使用dp的方法,并且需要对整个数组进行遍历一遍,因为需要从底层开始确保每次都求得最优解,那么可以写成dp=max(dp+a[i],a[i]);然后再用mx=max(mx,dp);从而找到最大值。
具体的实现
#include<iostream>
using namespace std;
#include<algorithm>
int main()
{
int a[100]={0},dp,mx;
int n; cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
dp=mx=a[0];
for(int i=1;i<n;i++)
{
dp=max(dp+a[i],a[i]);
mx=max(mx,dp);
}
cout<<mx;
return 0;
}
数字三角形和矩形问题
矩形礼物最大值
同样的想法,通过循环做出每一步的最优解从而自底向上的得出对问题的整体最优解
“连续子数组的最大和” 里直接把初值第0个赋予了max和dp[i]
这里的初值什么? 第一反应是左上角的grid[0] [0] ,将它作为初始值赋予dp[0] [0]没有问题
但是同时要结合题干考虑,题干里明确说了每次只能 “向右 或者 向下” 走,那就明显还需要边界的初始值来约束路径
那边界初始值自然是dp中第0行和第0列了,求第0行和第0列两串初始
#include<iostream>
#include<algorithm>
int a[10000][10000], dp[10000][10000];
using namespace std;
int main()
{
int n, m, mx;
cin >> n >> m;
for (int i = 0; i < n; i++)
for (int o = 0; o < m; o++)
cin >> a[i][o];
mx = dp[0][0] = a[0][0];//对dp数组的两边以及dp[0][0]进行初始化
for (int i = 1; i < n; i++)
dp[i][0] = a[i][0] + dp[i - 1][0];
for (int i = 1; i < m; i++)
dp[0][i] = a[0][i] + dp[0][i - 1];
for (int i = 1; i < n; i++)//对整个数组进行遍历
for (int o = 1; o < m; o++)
{
dp[i][o] = max(dp[i - 1][o], dp[i][o - 1]) + a[i][o];//确保每次找到的都是最优解
mx = max(mx, dp[i][o]);
}
cout << mx;
return 0;
}
如果要求最小值的时候
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int a[N][N],dp[N][N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++) dp[i][1]=dp[i-1][1]+a[i][1];
for(int i=2;i<=n;i++) dp[1][i]=dp[1][i-1]+a[1][i];
for(int i=2;i<=n;i++)
for(int j=2;j<=n;j++)
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+a[i][j];
cout<<dp[n][n]<<endl;
return 0;
}
数字三角形模型
整型上下限
1.INT_MAX
由于int占据4个字节,一个字节八位,所以int占据32位
再根据二进制编码的规则,可得INT_MAX=2^32-1
2.INT_MIN
同理可得,INT_MIN=-INT_MAX
用处:
如果得到数组中的最小或者最大值,可使用INT_MAX或INT_MIN初始化最小值最大值C++中表示
正无穷是0x3f3f3f3f(1061109567),负无穷是0xc0c0c0c0(-1061109568)
注意,负无穷在printf语句中,可能有问题
#include<iostream>
#include<algorithm>
using namespace std;
int a[10000][10000] = { 0 }, dp[10000][10000] = { 0 };
int main()
{
int n; cin >> n;
for (int i = 0; i <= n; i++)
for (int o = 0; o <= n; o++)
dp[i][o] = INT_MIN;//把有联系的矩形空间先都设置为无穷小的量,防止输入负数的时候影响判断情况
for (int i = 1; i <= n; i++)
for (int o = 1; o <= i; o++)
cin >> a[i][o];
int mx = INT_MIN;
dp[1][1] = a[1][1];//先对dp[1][1]点进行初始化
for (int i = 2; i <= n; i++)
for (int o = 1; o <= i; o++)
{
dp[i][o] = max(dp[i - 1][o], dp[i - 1][o - 1]) + a[i][o];//确保每次都是最优解
if (i == n)
mx = max(dp[i][o], mx);
}
cout << mx;
return 0;
}
方格取数
#include<bits/stdc++.h>
using namespace std;
int n, i, j, l, k, x, y, s;
int d[55][55], f[55][55][55][55];
int main()
{
cin>>n;
while(cin>>x>>y>>s && x)
d[x][y] = s;
for(i = 1; i <= n; i++)
for(j = 1; j <= n; j++)
for(l = 1; l <= n; l++)
for(k = 1; k <= n; k++)
{
f[i][j][l][k] = max(max(f[i - 1][j][l - 1][k], f[i][j - 1][l][k-1]), max(f[i - 1][j][l][k - 1], f[i][j - 1][l - 1][k])) + d[i][j];
if(i != 1 && j != k) f[i][j][l][k] += d[l][k];
}
printf("%d", f[n][n][n][n]);
return 0;
}
本题有很多的方法, 可以参考洛谷P1004 [NOIP2000 提高组] 方格取数和https://blog.csdn.net/qq_51052824/article/details/113269729
最长子序列问题
模板
状态表示:f[i]表示以A[i]结尾的LIS的长度
转移方程:f[i] =max(f[j]+1,f[i]) (A[i]之前子序列的最大长度+1,不一定是以前一个结尾的子序列的最大值+1)
#include<iostream>
using namespace std;
#include<algorithm>
int main()
{
int a[10000] = { 0 }, dp[10000] = { 0 }, mx = 0;
int n; cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
for(int i=0;i<n;i++)//它需要从零开始的,因为第一项如果没赋值成1会影响它有联系的其它项
{
dp[i] = 1;//它刚开始的时候本身自己也是看成一个递增子序列
for (int o = 0; o < i; o++)
{
if (a[i] > a[o])
{
dp[i] = max(dp[o] + 1, dp[i]);
mx = max(mx, dp[i]);
}
}
}
for (int i = 0; i < n; i++)
cout << dp[i] << endl;
cout << mx;
return 0;
}
使用的样例
它只需要两次套用模板就可以了
#include<iostream>
using namespace std;
#include<algorithm>
int main()
{
int a[10000] = { 0 }, dp[10000] = { 0 }, mx = 0;
int n; cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
for(int i=0;i<n;i++)//它需要从零开始的,因为第一项如果没赋值成1会影响它有联系的其它项
{
dp[i] = 1;//它刚开始的时候本身自己也是看成一个递增子序列
for (int o = 0; o < i; o++)
{
if (a[i] < a[o])
{
dp[i] = max(dp[o] + 1, dp[i]);
mx = max(mx, dp[i]);
}
}
}
cout << mx << endl;
mx = 0;
for (int i = n - 1; i >= 0; i--)
{
dp[i] = 1;
for (int o = n - 1; o > i; o--)
{
if (a[i] < a[o])
dp[i] = max(dp[o] + 1, dp[i]);
}
mx = max(mx, dp[i]);
}
cout << mx;
return 0;
}
合唱队形题
最长公共子序列
状态表示:f[i][j]表示以a[i],b[j]结尾的字符串的最长公共子序列
状态转移:f[i][j] = max(f[i][j-1),f[i-1][j])
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
char a[N], b[N];
int f[N][N];
int n, m;
int main()
{
cin >> n >> m;
cin >> a + 1 >> b + 1;//从a[1],b[1]开始储存了
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
if (a[i] != b[j])
f[i][j] = max(f[i][j - 1], f[i - 1][j]);
//如果a[i]==b[j],那就是i-1,j-1结尾的最长公共子序列+1
else
f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
cout << f[n][m] << endl;
return 0;
}
最长公共上升子序列
更。。。。。。