动态规划,是一种用来解决一类最优化问题的算法思想。动态规划的思想就是将一个大问题转化为若干个子问题,但是子问题必须具有重复子问题和无后效性,不然就是分治了;此外动态规划问题还必须满足最优子结构和重复子问题。
每求出一个子问题就把这个子问题的结果保存下来。由于动态规划具有重复子问题结构,所以下次求解这个子问题时直接就可以得到结果,避免了重复计算。
动态规划的核心问题是设计状态和状态转移方程
递归写法:是自上而下解决问题,如果当前这个问题并没有被求解出来,则向下递归,直到找到递归边界或达到一个已经求解过的子问题;
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];//返回F(n)的结果
}
}
递推写法:是自下而上解决问题,从小问题出发,根据状态转移方程解出大问题;
最优子结构:一个问题的最优解可以由其子问题的最优解有效地构造出来
分治与动态规划都是将一个大问题分解为若干个小的子问题,然后合并子问题的解得到最终解。
但二者的区别是:分治的每个子问题都是不重叠的,求解的问题不一定是最优化问题,一般用递归的方式的实现(自顶向下解决问题,达到递归边界返回);动态规划的子问题是重叠的,求解的问题一定是最优化问题,实现的方式可以用递归(一般是带返回值的递归)也可以用递推的写法(自下而上);
贪心一般是从上而下解决问题,并不等待子问题求解完毕再选择哪一个,而是通过一种策略直接选择一个子问题去求解,没背选择的子问题就不去求解了;
而动态规划对于那些没有考虑的问题在后期可能还会进行考虑,使其有机会成为全局最优的一部分;
状态的无后效性指:一旦当前状态确定,就不会再改变,且未来的决策只能在已有的一个或几个状态的基础上进行。
题目:给定一个数组,找到一个连续的子序列,使其和为最大
思路:
状态:以第i个元素为结尾的连续子序列(注意,第i个元素一定要被选择)
状态转移方程:以第i个元素结尾的最大连续子序列有两种情况:
1、以前一个元素结尾的最优连续子序列 + 第i个元素
2、只有第i个元素
dp[i] = max(dp[i - 1] + A[i], A[i]);
#include
#include
using namespace std;
const int maxn = 1e5 + 10;
//dp[i]代表以第i个元素为结尾时最大的连续子序列和(注意第i个元素一定要被选择)
int dp[maxn], A[maxn];
int main(){
int n, ans = 0;
scanf("%d", &n);
for(int i = 0; i < n; i ++){
scanf("%d", &A[i]);
}
dp[0] = A[0];
for(int i = 1; i < n; i ++){
//状态转移方程,以第i个元素结尾的最大连续子序列有两种情况:
//1、以前一个元素结尾的最优连续子序列 + 第i个元素
//2、只有第i个元素
dp[i] = max(dp[i - 1] + A[i], A[i]);
}
for(int i = 0; i < n; i ++){
ans = max(dp[i], ans);
}
printf("%d", ans);
system("pause");
return 0;
}
题目:给定一个数组,找到一个最长的不下降子序列,求其元素个数
思路:
状态:dp[i]存放以第i个元素结尾时最长的非递减子序列元素个数
状态转移方程:以第i个元素结尾的最长的非递减子序列元素个数:
依次检查前面已经解过的所有问题,如果A[j] <= A[i],说明出现了一种情况,在去判断dp[j] + 1 > dp[i],如果大于说明出现了更优的解;
dp[i] = max(dp[j] + 1, dp[i]);
代码
#include
#include
using namespace std;
const int maxn = 1e5 + 10;
//dp[i]存放以第i个元素结尾时最长的非递减子序列元素个数
int dp[maxn], A[maxn];
int main(){
int n, ans = 0;
scanf("%d", &n);
for(int i = 0; i < n; i ++){
scanf("%d", &A[i]);
}
for(int i = 0; i < n; i ++){
//初始时只有一个元素,本身
dp[i] = 1;
//求解以第i个元素为结尾的最长非递减子序列元素个数,需要去枚举前面已经求解过的所有可能,再取最大值
for(int j = 0; j < i; j ++){
if(A[j] <= A[i] && dp[j] + 1 > dp[i]){
dp[i] = dp[j] + 1;
}
}
ans = max(dp[i], ans);
}
printf("%d", ans);
system("pause");
return 0;
}
题目:给定两个字符串,找到最长的公共子序列
思路:
状态:dp[i][j]存放以 s1第i个元素结尾,s2第j个元素结尾时两个字符串最长的公共子序列元素个数
状态转移方程:如果s1[i] == s2[j], 说明 dp[i][j] = dp[i - 1][j - 1] + 1;
如果s1[i] != s2[j], 说明 dp[i][j] = max(dp[i ][j - 1] , dp[i - 1][j];
可以想象成是在一个二维数组不断地填数据,刚开始只知道第一行,第一列全是零,那么剩下的数据可以根据他左上角相邻的三个元素得出结果;
代码
#include
#include
#include
using namespace std;
const int maxn = 100;
int dp[maxn][maxn];
string s1, s2;
int main(){
cin >> s1 >> s2;
//把第一行赋值为零
for(int i = 0; i < s2.size(); i ++) dp[0][i] = 0;
//把第一列赋值为零
for(int i = 0; i < s1.size(); i ++) dp[i][0] = 0;
//开始填表,依次去填满每一行,再接着填下一行
for(int i = 0; i < s1.size(); i ++){
for(int j = 0; j < s2.size(); j ++){
if(s1[i] == s2[j]){
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else{
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
printf("%d", dp[s1.size() - 1][s2.size() - 1]);
system("pause");
return 0;
}
题目:给定一个字符串,找到最长的回文子串(子串是必须连续的,子序列不要求连续)
思路:
状态:dp[i][j]存放以第i个元素开始,第j个元素结尾时的字符串是否是回文子串,如果是用1表示,不是用0表示;
状态转移方程:先求出所有长度为2的字符串是否是回文子串。然后依次计算长度为3,4,,,直到整个字符串的长度;
如果s[i] == s[j],并且如果s[i + 1][j - 1]是回文子串,那么s[i][j]就是回文子串,回文子串的长度加一
可以想象成是在一个二维数组不断地填数据,刚开始只知道对角线上的元素值,即长度为一的字符串;然后是对角线向上向右平移一格线上的元素,即长度为二的字符串;接着就是对角线继续向右平移,注意这时候的结果就要依靠对角线上的结果了
代码
#include
#include
#include
#include
using namespace std;
const int maxn = 100;
int dp[maxn][maxn] = {0};
string s1, s2;
int main(){
cin >> s1;
int ans = 1;
//判断长度为2 的子串是否是回文串
for(int i = 0; i < s1.size(); i ++){
dp[i][i] = 1;
if(i < s1.size() - 1 && s1[i] == s1[i + 1]){
dp[i][i + 1] = 1;
ans = 2;
}
}
//从长度为3开始,判断所有长度为len的子串是否是回文串
for(int len = 3; len <= s1.size(); len ++){
for(int i = 0; i + len - 1 < s1.size(); i ++){
int j = i + len - 1;
//如果这个串的首元素等于尾元素,并且长度减二的子串也是回文串,说明这个字符串也是回文串
if(s1[i] == s1[j] && dp[i + 1][j - 1] == 1){
//更新长度
ans = len;
dp[i][j] = 1;
}
}
}
printf("%d", ans);
system("pause");
return 0;
}
理论 : 要求出从每个点出发能到达的最长路径,可以设置两个数组,dp[], choice[];
其中,dp[i]数组存放从 i 号节点出发,能到达的最长路径,choice[]存放最长路径上这个点的后驱节点;
求解一个点出发能达到的最长路径;注意到这次我们使用了递归来写这个动态规划;
你可以想象一个从左向右的有向无环图,要想求出左边一个点的最长路径,那么你必须知道与他相连的所有右边节点的结果,才能解出结果
非递归也很好写,那就是先把末位点都赋成零,但是得先找到没有出度的点,然后求出与他相连的点
代码
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 100;
int G[maxn][maxn];//以邻接矩阵存储图
int dp[maxn];//记录以某个节点开始能达到的最长路径
int choice[maxn];//记录最长路径上这个节点的后继
//求解一个点出发能达到的最长路径
//注意到这次我们使用了递归来写这个动态规划,
//你可以想象一个从左向右的有向无环图,要想求出左边一个点的最长路径,
//那么你必须知道与他相连的所有右边节点的结果,才能解出结果
//非递归也很好写,那就是先把末位点都赋成零,但是得先找到没有出度的点,然后写与他相连的点
int DP(int i){
if(dp[i] > 0)return dp[i];
//依次计算与这个点相连的点,最后如果到一个没有出度的节点,会直接返回0
for(int j = 0; j < n; j ++){
if(G[i][j] != inf){
int temp = DP[j] + G[i][j];//不要写到if中,防止递归两次
//如果从i出发,经过j的最长路径大于当前从i出发的最长路径,则更新路径,并记录其后继节点
if(temp > DP[i]){
dp[i] = temp;
choice[i] = j;//记录后继节点
}
}
}
return dp[i];
}
void printPath(int i){
printf("%d", i);
while(choice[i] != -1){
i = choice[i];
printf("%d", i);
}
}
int main(){
system("pause");
return 0;
}
理论 :将状态定义为以节点i为结尾时的最长路径
#include
#include
using namespace std;
const int maxn = 100;
const int inf = 1e9;
int G[maxn][maxn];
int dp[maxn] = {0};//记录以节点i结尾的最长路径
int main(){
fill(G[0], G[0] + maxn * maxn, inf);
int n, m, u, v, w;
scanf("%d%d", &n, &m);
for(int i = 0; i < m; i ++){
scanf("%d%d%d", &u, &v, &w);
G[u][v] = w;
}
//遍历每一个点,如果这个点的dp值为0,说明暂时没有节点可以到达此节点
for(int i = 0; i < n; i ++){
//更新与这个节点相连点的最长路径
for(int j = 0; j < n; j ++){
if(G[i][j] != inf){
dp[j] = max(dp[j], dp[i] + G[i][j]);
}
}
}
for(int i = 0; i < n; i ++)
printf("%d ", dp[i]);
system("pause");
return 0;
}
总结
注意到,当定义状态dp[i]为从节点 i 出发能到达的最长路径时用的是动态规划的递归写法,为什么会这样?
不妨这样想,假设节点 i 在节点 j 的左边,当我们想求出从节点 i 出发能到达的最长路径,首先得知道从节点 j 出发能达到的最长路径,求解dp[i] 和 dp[j] 的顺序是逆拓扑序的,递归结束的条件是出度为零的点,即整个有向无环图最右边的节点,实际在求解时,我们总是先求出这个边界点;
当定义状态dp[i]为以节点 i 为结尾时的最长路径用的是递归的递推写法,我们还假设节点 i 在节点 j 的右侧,要想求出dp[j] 必须首先知道 dp[i] 的值,求解dp[i] 和 dp[j] 的顺序是按拓扑序的。不同的是,这次的边界变成了那些入度为零的节点,如果我们按节点 id 大小遍历,最先遍历的节点一定是入度为零的节点。
题目 :给你一系列物品的重量和每件物品的价值,要求在不超过背包容量的情况下,使得背包内物品价值总和最大
思路 :我们为每件物品分别定义一个维度,此时的背包容量定义为另一维;
dp[i][v] 代表前i件物品恰好装入容量为 v 的背包中所能获得的最大价值;
对第 i 件物品,有两种策略:
1、不放第 i 件物品,那么问题转化为前 i- 1 件物品恰好装入容量为v 的背包中所能获得的最大价值;
2、放第 i件物品,那么问题转化为 前 i- 1件物品恰好装入容量为 v- w[ i ] 的背包中所能获得的最大价值;
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 100;//物品的上限
const int maxv = 1000;//每件物品价值的上限
int w[maxn], c[maxn], dp[maxv];//w是物品的重量,c是物品的价值
int main(){
int n, V;
scanf("%d%d", &n, &V);
for(int i = 0; i < n; i ++) scanf("%d", &w[i]);
for(int i = 0; i < n; i ++) scanf("%d", &c[i]);
//边界,第一行都是零;把零件物品放到背包中,价值肯定都为零
for(int v = 0; v <= V; v ++) dp[v] = 0;
for(int i = 1; i <= n; i ++){
for(int v = V; v >= w[i]; v--){
//状态转移方程,选择的时候只有两种可能,选当前物品,不选当前物品
//压缩空间的做法,逆序填,滚动数组
dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
//不压缩空间的做法
//dp[i][v] = max(dp[i - 1][v], dp[i - 1][v - w[i]] + w[i]);
}
}
//寻找dp[0,1,2,,,,]中的最大值
int ans = 0;
for(int v = 0; v <= V; v++){
ans = max(ans, dp[v]);
}
printf("%d", ans);
system("pause");
return 0;
}