动态规划(dynamic programming)是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。动态规划算法通常用于求解具有某种最优性质的问题,在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划的理论性和实践性都较强,需要理解“状态”、“状态转移”、“最优子结构”等概念并根据题目灵活设计算法。
动态规划经典问题:
不同路径 ( 题目链接:https://leetcode-cn.com/problems/unique-paths )
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向右 -> 向下
- 向右 -> 向下 -> 向右
- 向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
提示:
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 10 ^ 9
问题分析
如图所示,从原点出发,计算走到每个网格路径个数,递推计算出到达终点的路径个数。图中网格位置可用一个二维数组表示,到达每个网格的路径数等于左边和上边两个方格路径数目之和,即a[i][j]=a[i-1][j]+a[i][j-1].其中第一排和第一列每个方格的路径数为1,这样通过遍历二维数组,最终求解。
代码实现:
int uniquePaths(int m, int n)
{
if(m==0||n==0)
return 0;
int i,j;
int a[105][105];
for(i=1;i<=m;i++)
a[i][1]=1;
for(j=1;j<=n;j++)
a[1][j]=1;
for(i=2;i<=m;i++)
{
for(j=2;j<=n;j++)
{
a[i][j]=a[i-1][j]+a[i][j-1];
}
}
return a[m][n];
}
01背包问题 ( 题目链接 https://www.acwing.com/problem/content/2/ )
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例
8
问题分析
首先考虑用蛮力枚举的方法,一次可以选i(1<=i<=N)件物品,共有2^N-1种可能,再从中选出满足条件的最大价值。但这种方法包含大量重复计算,算法复杂度是O( 2^n ),于是考虑优化算法,通过构造带备忘录的数组,减少重复计算。
用dp[i][j]表示只考虑前i件物品,背包容积是j的情况下所装物品的最大价值。接下来有两种情况,放第i件物品和不放第i件物品,dp[i][j]取这两种情况的最大值,即dp[i][j]=max{dp[i][j], dp[i-1][j-v[i]]+w[i]},v[i]和w[i]分别表示第i件物品的体积和价值。那么就可以采用递推的方法,对二维数组dp[i][j]的各个元素赋值,最终dp[N][V]就是最大价值。
C++代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1010;
int n,m;
int dp[N][N];
int v[N],w[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
if(j>=v[i])
dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
}
}
cout<<dp[n][m]<<endl;
return 0;
}
最长公共子序列问题 ( 题目链接http://poj.org/problem?id=1458 )
Common Subsequence
Description
A subsequence of a given sequence is the given sequence with some elements (possible none) left out. Given a sequence X = < x1, x2, …, xm > another sequence Z = < z1, z2, …, zk > is a subsequence of X if there exists a strictly increasing sequence < i1, i2, …, ik > of indices of X such that for all j = 1,2,…,k, xij = zj. For example, Z = < a, b, f, c > is a subsequence of X = < a, b, c, f, b, c > with index sequence < 1, 2, 4, 6 >. Given two sequences X and Y the problem is to find the length of the maximum-length common subsequence of X and Y.
Input
The program input is from the std input. Each data set in the input contains two strings representing the given sequences. The sequences are separated by any number of white spaces. The input data are correct.
Output
For each set of data the program prints on the standard output the length of the maximum-length common subsequence from the beginning of a separate line.
Sample Input
abcfbc abfcab
programming contest
abcd mnp
Sample Output
4
2
0
问题分析
子序列:将给定序列中零个或多个元素(如字符)去掉后所得结果(例如ABCD的子序列可以是AB,ABC,AC,ABD,ABCD,可以是连续或不连续)。用𝑪[𝒊,𝒋]表示𝑿[𝟏. .𝒊]和𝒀[𝟏. .𝒋]的最长公共子序列长度,分两种情况:1.𝒙𝒊 ≠ 𝒚𝒋,此时𝑪[𝒊,𝒋] = 𝐦𝐚𝐱 {𝑪[𝒊 − 𝟏,𝒋], 𝑪[𝒊,𝒋 − 𝟏]} 2.𝒙𝒊 = 𝒚𝒋时,𝑪[𝒊,𝒋] = 𝑪[𝒊 − 𝟏,𝒋 − 𝟏] + 𝟏
代码实现:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1000;
char a[N];
char b[N];
int c[N][N];
int main()
{
int i,j;
while(cin>>a>>b)
{
int m=strlen(a);
int n=strlen(b);
for(i=1;i<=m;i++)
{
for(j=1;j<=n;j++)
{
if(a[i-1]==b[j-1])
c[i][j]=c[i-1][j-1]+1;
else
c[i][j]=max(c[i-1][j],c[i][j-1]);
}
}
printf("%d\n",c[m][n]);
}
return 0;
}
总结
动态规化解题的一般思路:
- 将原问题分解为子问题
把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。 - 确定状态
在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓某个“状态”下的“值”,就是这个“状态”所对应的子问题的解。 - 确定状态转移方程
定义出什么是“状态”,以及在该 “状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的“状态”,求出另一个“状态”的“值”。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。
动态规划的核心是状态和状态转移方程。