动态规划算法常用于解决的问题:
这部分内容【动态规划算法常用于解决的问题】是因为后面在做题的时候经常分析不出一道题是否符合DP算法,所以问了chatgpt,下面内容大家自行判断,我这里只是用于自己整理思路用的,其余部分是自己整理的
- 重叠子问题:如果一个问题可以被分解为多个重叠的子问题,且这些子问题之间有重复计算的部分,那么动态规划是一个很好的选择。例如,在计算斐波那契数列时,fib(n) 的值依赖于 fib(n-1) 和 fib(n-2),而这两个值会在计算过程中多次被用到。
- 最优子结构:如果一个问题的最优解包含了其子问题的最优解,这通常意味着动态规划可以应用。典型的例子是背包问题,其中最优解可以通过解决更小的子问题(如剩余容量的背包问题)获得。
- 状态转移:问题的解可以通过已知状态(子问题的解)转移得到新的状态。例如,在字符串分割(word break)问题中,dp[i] 的值是基于 dp[j] (0<j<=i)的状态转移而来。
- 多种选择的组合:如果问题涉及多个选择的组合,并且需要找到唯一的最佳或可行解,这也通常意味着动态规划可能适用。例如,路径选择问题需要在多个路径中找到最短或最优路径。
问题对应的内容:
分解问题:尝试将问题分解为更小的子问题,看看是否存在重叠的子问题。如果是,那么动态规划可能适用。
是否需要记忆化:如果在递归解决方案中,你发现你需要记忆化某些中间结果来避免重复计算,这也是动态规划的标志。
递归与子问题的关系:尝试用递归解决问题,并观察是否可以通过递归关系来定义问题。如果能通过子问题的解推导出整个问题的解,这表明动态规划可能是一种合适的方法。
解题思路 使用数组解决 vector vector<vector>
- 首先确定要求的东西是什么(题目中要求return的内容),含义,以及对应的数组形状;
- 找出关系式,也就是将大问题拆分成的小问题是怎样的
- 初始条件,把限制边界全部设定好,也就是这些问题的起点,初始,也是关系式可以有实际数值的起源
509 斐波那契数列
思路: 思路:这个好像是有什么特殊的算法的,好像是交换,但这里我先暴力拆解??首先确定循环次数,然后设置好n0 和 n1的交换,每次循环fib = n0+n1。前提是n !=0 && n!=1。【自写】
class Solution {
public:
int count(int n){
int n0 = 0, n1 =1, fib =0;
n-=1;
while(n--){
fib = n0 + n1;
n1 = fib;
n0 = n1 - n0;
}
return fib;
}
int fib(int n) {
if(n == 0 )
return 0;
else if(n == 1)
return 1;
else
return count(n);
}
};
破案了,是动态规划,但总感觉没触及灵魂,本来是想用上面动图的思路,但是感觉没办法提取出来通用方法,先用最上面的思路写
class Solution {
public:
int fib(int n) {
if (n <= 1) {
return n;
}
// 找出问题所求,第n个fib数
vector<int> f(n + 1); // 初始化大小为 n+1 的 vector
// 初始条件
f[0] = 0;
f[1] = 1;
// 关系式, 第n个 = 第n-1 + n-2个
for (int i = 2; i <= n; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
};
70 爬楼梯
思路:我想着就分析最后一次可以是1 也可以是2,然后就爬,倒着减 1 or 2,当爬完了就ans++,但是对于测试用例44他就跑不通了,看了题解还是和上一题一样的情况???这样也可以???好像答案都没怎么改???
class Solution {
public:
int ans = 0;
int climbStairs(int n) {
if(n>=1)
climbStairs(n-1);
if(n>=2)
climbStairs(n-2);
return n == 0? ans++: ans;
}
};
通法代码
class Solution {
public:
int climbStairs(int n) {
if(n <= 2){
return n;
}
//找出问题所求, 爬到n层楼的方法数
vector<int> way(n+1);
//初始条件, 爬到1层 1种方式 2层 2种方式 3层
way[0] = 1 , way[1] = 2 ;
//关系式, 爬到最后一层的方法数 = 爬到倒数第二层和倒数第一层的方法数之和
for(int i = 2; i<n ; i++){
way[i] = way[i-1] + way[i-2];
}
return way[n-1];
}
};
62 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
class Solution {
public:
int uniquePaths(int m, int n) {
if(m == 1 || n== 1)
return 1;
//找出问题所求, 走到第m行第n列的路径数目,注意这里数组存储的元素是到mn的路径数目,不是步数啥的
vector<vector<int>> way(m+1,vector<int>(n+1,0));
//初始条件, 开始只能向右或者向下,最左边[i][0]的只能从上边过来,最上边[0][i]的只能从右边过来
way[0][0] = 0, way[0][1] = 1, way[1][0] = 1;
for(int i = 2;i<m;i++){
way[i][0] = way[i-1][0];
}
for(int i = 2;i<n;i++){
way[0][i] = way[0][i-1];
}
//关系式, [m][n]需要从上或者左过来 = [m-1][n] + [m][n-1]
for(int i = 1; i<m ; i++){
for(int j = 1;j<n;j++){
way[i][j] = way[i-1][j] + way[i][j-1];
}
}
return way[m-1][n-1];
}
};
c++学习【二维数组初始化】
vector<vector<int>> double_matrix(m,vector<int>(n,0));
63 不同路径 II 之 障碍物
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size() , n = m>0?obstacleGrid[0].size():0;
if(m == 1 || n== 1){
for(int i = 0 ;i<m;i++){
for(int j = 0 ; j <n ;j++){
if(obstacleGrid[i][j] == 1)
return 0;
}
}
return 1;
}
if(obstacleGrid[0][0] == 1 || obstacleGrid[m-1][n-1] == 1)
return 0;
//找出问题所求, 走到第m行第n列的路径数目
vector<vector<int>> way(m+1,vector<int>(n+1,0));
//初始条件, 开始只能向右或者向下,最左边[i][0]的只能从上边过来,最上边[0][i]的只能从右边过来
//多增加的障碍物,导致了障碍物周围的路径发生了变化,主要是它本身这个位置的路径为0
//这里注意,只要在设定way数组元素的时候进行置零就好,它的影响表示为:这条路不能走了,对于周边的元素来说就是这条路是到不了自己的位置的,所以在计算路径数目的时候+0就可以了
way[0][0] = 1;
for(int i = 1;i<m;i++){
if(obstacleGrid[i][0] == 1){
way[i][0] = 0;
}else{
way[i][0] = way[i-1][0];
}
}
for(int i = 1;i<n;i++){
if(obstacleGrid[0][i] == 1){
way[0][i] = 0;
}else{
way[0][i] = way[0][i-1];
}
}
//关系式, [m][n]需要从上或者左过来 = [m-1][n] + [m][n-1]
for(int i = 1; i<m ; i++){
for(int j = 1;j<n;j++){
if(obstacleGrid[i][j] == 1)
way[i][j] = 0;
else
way[i][j] = way[i-1][j] + way[i][j-1];
}
}
return way[m-1][n-1];
}
};
64 最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size() , n = grid[0].size();
int mans = 0;
if(m == 1 || n== 1){
for(int i = 0 ;i<m;i++){
for(int j = 0 ; j <n ;j++){
mans += grid[i][j];
}
}
return mans;
}
//找出问题所求, 走到第m行第n列的最短路径
vector<vector<int>> way(m+1,vector<int>(n+1,0));
//初始条件, 开始只能向右或者向下,最左边[i][0]的只能从上边过来,最上边[0][i]的只能从右边过来
//唯一需要注意的是,way不是路径数目,是最短路径,即最短距离,也就是只包含了一条路,不要惯性思维了
way[0][0] = grid[0][0];
for(int i = 1;i<m;i++){
way[i][0] = way[i-1][0] + grid[i][0];
}
for(int i = 1;i<n;i++){
way[0][i] = way[0][i-1] + grid[0][i];
}
//关系式, [m][n]需要从上或者左过来 那么就取从上面过来或者从左边过来的较小的值 [m-1][n] > [m][n-1]?[m][n-1]:[m-1][n]
for(int i = 1; i<m ; i++){
for(int j = 1;j<n;j++){
if(way[i-1][j] < way[i][j-1]){
way[i][j] = way[i-1][j] + grid[i][j];
}else{
way[i][j] = way[i][j-1] + grid[i][j];
}
}
}
return way[m-1][n-1];
}
};
8.15 139 Word Break
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//动态规划
//要做的事情是确定s中前i位是否可以被分割 dp[i]
vector<bool> dp(s.size()+1,false);
unordered_set<string> wordSet(wordDict.begin() , wordDict.end());
//初始条件dp[0] = true;
dp[0] = true;
//遍历string s找合适的切割组合
for(int i = 1 ; i <= s.size() ; i++){
//判定是否可以被分割
for(int j = 0 ; j < i ; j++){
//判定条件,关系式:wordSet.find(s.substr(j , i-j)) != wordSet.end()
if(dp[j] && wordSet.find(s.substr(j , i-j)) != wordSet.end()){
//可以被分割
dp[i] = true;
break;
}
}
}
return dp[s.size()];
}
};
8.15 review 467 Unique Substrings in Wraparound String
class Solution {
public:
int findSubstringInWraproundString(string s) {
//不同的非空的子串数目 不同+非空怎么判定-->记录数组dp 仅记录以26字母为结尾的
//要找的是 且是不同的 存在于base中的子串 所以判定dp[i]为以i = ch - 'a'为索引,以ch为结尾的最长子串
vector<int> dp(26,0);
int maxLen = 0;
//base的判定前者-后者为1 或者前者为z后者为a,记录长度
for(int i = 0 ; i < s.size() ; i++){
char ch = s[i];
if(i>0 && (s[i] - s[i-1] == 1 || (s[i-1] == 'z' && s[i] == 'a'))){
maxLen++;
}else{
//初始条件
maxLen = 1;
}
dp[ch-'a'] = std::max(maxLen,dp[ch-'a']);
}
int ans = 0;
for(int len : dp){
ans += len;
}
return ans;
}
};