《算法笔记》动态规划

目录

        • 概念
          • 什么是动态规划
          • 动态规划的递归写法和递推写法
          • 最优子结构
          • 分治与动态规划的区别
          • 贪心与动态规划的区别
          • 无后效性
        • 区间动态规划
          • 最大连续子序列和
          • 最长不下降子序列
          • 最长公共子序列
          • 最长回文子串
        • 序列型动态规划(DAG(有向无环图)最长路)
          • 整个有向无环图的最长路径
          • 固定终点,求最长路径
        • 背包问题(多阶段动态规划)
          • 0-1背包问题

概念

什么是动态规划

动态规划,是一种用来解决一类最优化问题的算法思想。动态规划的思想就是将一个大问题转化为若干个子问题,但是子问题必须具有重复子问题和无后效性,不然就是分治了;此外动态规划问题还必须满足最优子结构和重复子问题。
每求出一个子问题就把这个子问题的结果保存下来。由于动态规划具有重复子问题结构,所以下次求解这个子问题时直接就可以得到结果,避免了重复计算。
动态规划的核心问题是设计状态和状态转移方程

动态规划的递归写法和递推写法

递归写法:是自上而下解决问题,如果当前这个问题并没有被求解出来,则向下递归,直到找到递归边界或达到一个已经求解过的子问题;

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;
}

序列型动态规划(DAG(有向无环图)最长路)

整个有向无环图的最长路径

理论 : 要求出从每个点出发能到达的最长路径,可以设置两个数组,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 大小遍历,最先遍历的节点一定是入度为零的节点。

背包问题(多阶段动态规划)

0-1背包问题

题目 :给你一系列物品的重量和每件物品的价值,要求在不超过背包容量的情况下,使得背包内物品价值总和最大
思路 :我们为每件物品分别定义一个维度,此时的背包容量定义为另一维;
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;
}

你可能感兴趣的:(#,《算法笔记》胡凡)