动态规划总结

目录

1.算法解释

2.基本动态规划:一维

(1)70. Climbing Stairs (Easy)

(2)198打家劫舍

(3)413等差数列划分

3.基本动态规划:二维

(1)64最小路径和

 (2)542  0-1矩阵

(3)221最大正方形

4.分割问题

(1)279完全平方数

(2)解码方法

(3)139单词拆分(重点)

5. 子序列问题

(1)300最长递增子序列

(2)1143. 最长公共子序列

6.背包问题

01背包 

完全背包​编辑 ​编辑

 综合模板

(1)416分割等和子集 

(2)322零钱兑换

(3)一和零

7. 字符串编辑

(1)72编辑距离

(2)650只有两个键的键盘

(3)10正则匹配


1.算法解释

   还有一些前置理论,基础的思想以前写过:从递归到动态规划,详细解析_chy响当当的博客-CSDN博客_递归转动态规划

2.基本动态规划:一维

(1)70. Climbing Stairs (Easy)

 题解

这是十分经典的斐波那契数列题。定义一个数组 dp dp[i] 表示走到第 i 阶的方法数。因为
我们每次可以走一步或者两步,所以第 i 阶可以从第 i-1 i-2 阶到达。换句话说,走到第 i 阶的 方法数即为走到第 i-1 阶的方法数加上走到第 i-2 阶的方法数。这样我们就得到了状态转移方程 dp[i] = dp[i-1] + dp[i-2]。注意边界条件的处理。

int climbStairs(int n) {
	if (n <= 2) return n;
	vector<int> dp(n + 1, 1);
	for (int i = 2; i <= n; ++i) {
		dp[i] = dp[i - 1] + dp[i - 2];
	}
	return dp[n];
}

压缩空间的话:

int climbStairs(int n) {
if (n <= 2) return n;
int pre2 = 1, pre1 = 2, cur;
for (int i = 2; i < n; ++i) {
cur = pre1 + pre2;
pre2 = pre1;
pre1 = cur;
}
return cur;
}

(2)198打家劫舍

 

初始化dp[1]别忘了 

//,dp[i] 表示抢劫到第 i 个房子时,可以抢劫的最大数量
//本题的状态转移方程为 dp[i] = max(dp[i-1],nums[i - 1] + dp[i - 2])。
int rob(vector<int>& nums) {
	if (nums.empty()) return 0;
	int n = nums.size();
	vector<int> dp(n + 1, 0);
	dp[1] = nums[0];
	for (int i = 2; i <= n; ++i) {
		dp[i] = max(dp[i - 1], nums[i - 1] + dp[i - 2]);
	}
	return dp[n];
}

//压缩空间版:
int rob(vector<int>& nums) {
	if (nums.empty()) return 0;
	int n = nums.size();
	if (n == 1) return nums[0];
	int pre2 = 0, pre1 = 0, cur;
	for (int i = 0; i < n; ++i) {
		cur = max(pre2 + nums[i], pre1);
		pre2 = pre1;
		pre1 = cur;
	}
	return cur;
}

(3)413等差数列划分

 


//求和求和!
class Solution {
public:
    //dp[i]代表以i结尾的数组有多少个等差数列子数组
    //转移方程
    //而等差子数组可以在任意一个位置终结,因此此题在最后需要对 dp 数组求和。
    int numberOfArithmeticSlices(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n, 0);
        if (n <= 2) {
            return 0;
        }

        for (int i = 2; i <n; ++i) {
            if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) {
                dp[i] = dp[i - 1] + 1;
            }
        }
        return accumulate(dp.begin(), dp.end(), 0);
    }
};

//还有一个找规律
/*仔细观察,会发现当整个数组为(1, 2, 3, 4, 5, 6)的时候,我们先取出前三个,(1, 2, 3)的等差数列的个数为1,
(1, 2, 3, 4)的等差数列的个数为3,(1, 2, 3, 4, 5)的等差数列的个数为6,
(1, 2, 3, 4, 5, 6)的等差数列个数为10,
以此类推我们可以很容易的发现在一个等差数列中加入一个数字,如果还保持着等差数列的特性,
每次的增量都会加1,你看这个1是指1-》3加2,3-》6加3,6-》10加4

如果刚加进来的数字与原先的序列构不成等差数列,就将增量置为0,接下来继续循环,*/
/*
int numberOfArithmeticSlices(int []A) {
    if (A == NULL || A.length <= 2)
        return 0;
    int res = 0;
    int add = 0;
    for (int i = 2; i < A.length; i++)
        if (A[i - 1] - A[i] == A[i - 2] - A[i - 1])
            res += ++add;
        else
            add = 0;
    return res;
}*/

3.基本动态规划:二维

(1)64最小路径和

我们可以定义一个同样是二维的 dp 数组,其中 dp[i][j] 表示从左上角开始到 (i, j) 位置的最
优路径的数字和。因为每次只能向下或者向右移动,我们可以很容易得到
状态转移方程 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j],其中 grid 表示原数组
class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int r = grid.size();
        int c = grid[0].size();
        vector<vector<int>>dp(r, vector<int>(c, 0));
        for (int i = 0; i < r; ++i) {
            for(int j=0;j<c;++j){
                //边界特殊情况要考虑好哦
                if (i == 0 && j == 0) {
                    dp[i][j] = grid[i][j];
                }
                else if (i == 0) {
                    dp[i][j] = dp[i][j - 1] + grid[i][j];
                }
                else if (j == 0) {
                    dp[i][j] = dp[i - 1][j] + grid[i][j];
                }
                else {
                    dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
                }
            }
        }
        return dp[r - 1][c - 1];
    }

};

int minPathSum(vector<vector<int>>& grid) {
    int m = grid.size(), n = grid[0].size();
    vector<int> dp(n, 0);
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            if (i == 0 && j == 0) {
                dp[j] = grid[i][j];
            }
            else if (i == 0) {
                dp[j] = dp[j - 1] + grid[i][j];
            }
            else if (j == 0) {
                dp[j] = dp[j] + grid[i][j];
            }
            else {
                dp[j] = min(dp[j], dp[j - 1]) + grid[i][j];
            }
        }
    }
    return dp[n - 1];
}

 (2)542  0-1矩阵

法一,广度:


//第一反应,广搜
//48/50超时,优化一下应该可以过,
//优化方法一:就是把0的位置加入队列,然后从零开始向外搜1,而不是遍历矩阵,从1搜0
//方法二:visit防止重复访问,这个还是不太行
class Solution {
public:

    vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
        int r = mat.size();
        int c = mat[0].size();
        vector<vector<int>> rs(r, vector<int>(c, 0));
        vector<vector<bool>> visited(r, vector<bool>(c, 0));
        for(int i = 0; i < r; ++i) {
            for (int j = 0; j < c; ++j) {
                if (mat[i][j] == 0)continue;
                if (visited[i][j]) continue;
                visited[i][j] = true;
                queue<vector<int>> qq;
                qq.push({ i + 1,j });
                qq.push({ i - 1,j });
                qq.push({ i ,j - 1});
                qq.push({ i ,j+ 1 });
                int len = 0;
                while (!qq.empty()) {
                    int circle_num = qq.size();
                    ++len;
                    bool flag = false;
                    while (circle_num--) {
                        vector<int> front = qq.front();
                        qq.pop();
                        int x = front[0];
                        int y = front[1];
                        if (x < 0 || y < 0 || x >= r || y >= c) {
                            continue;
                        }
                        if (mat[x][y] == 0) {
                            flag = true;
                            break;
                        }
                        else {
                            qq.push({ x+1,y });
                            qq.push({ x ,y + 1});
                            qq.push({ x - 1,y });
                            qq.push({ x,y - 1 });
                        }

                    }
                    if (flag) {
                        break;
                    }
                }

                rs[i][j] = len;
            }
        }
        return rs;
    }

};

 法二,dp


vector<vector<int>> updateMatrix(vector<vector<int>>& matrix) {
    if (matrix.empty()) return {};
    int n = matrix.size(), m = matrix[0].size();
    vector<vector<int>> dp(n, vector<int>(m, INT_MAX - 1));
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            if (matrix[i][j] == 0) {
                dp[i][j] = 0;
            }
            else {
                if (j > 0) {
                    dp[i][j] = min(dp[i][j], dp[i][j - 1] + 1);
                }
                if (i > 0) {
                    dp[i][j] = min(dp[i][j], dp[i - 1][j] + 1);
                }
            }
        }
    }
    for (int i = n - 1; i >= 0; --i) {
        for (int j = m - 1; j >= 0; --j) {
            if (matrix[i][j] != 0) {
                if (j < m - 1) {
                    dp[i][j] = min(dp[i][j], dp[i][j + 1] + 1);
                }
                if (i < n - 1) {
                    dp[i][j] = min(dp[i][j], dp[i + 1][j] + 1);
                }
            }
        }
    }
    return dp;
}

(3)221最大正方形

 


/*当我们判断以某个点为正方形右下角时最大的正方形时,那它的上方,左方和左上方三个点也一定是某个正方形的右下角,
否则该点为右下角的正方形最大就是它自己了。这是定性的判断,那具体的最大正方形边长呢?
我们知道,该点为右下角的正方形的最大边长,最多比它的上方,左方和左上方为右下角的正方形的边长多1,
最好的情况是是它的上方,左方和左上方为右下角的正方形的大小都一样的,这样加上该点就可以构成一个更大的正方形。
但如果它的上方,左方和左上方为右下角的正方形的大小不一样,合起来就会缺了某个角落,
这时候只能取那三个正方形中最小的正方形的边长加1了。

假设dpi表示以i,j为右下角的正方形的最大边长,则有 dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1*/

class Solution {
public:
    int maximalSquare(vector<vector<char>>& a) {
        int r = a.size();
        int c = a[0].size();
        vector < vector<int>> dp(r+1, vector<int>(c+1,0));//初始化
        if (a.empty())return  0;
        int max_size = 0;
//
        for (int i = 1; i <= r; ++i) {
            for (int j = 1; j <= c; ++j) {
                if (a[i-1][j-1] == '1') {
                    dp[i][j] = 1 + min(dp[i - 1][j - 1], min(dp[i][j - 1], dp[i - 1][j]));//    这个min好好体会一下
                }
                max_size = max(max_size, dp[i][j]);//dpi表示以i,j为右下角的正方形的最大边长!!!!!!!!
            }
        }
        //这里是求边长哦

        return max_size * max_size;

    }
};

/*
动态规划的小小技巧吧:

默念一句话“动态规划每一步填表都建立在之前已经填完的表上”;
表的值的意义和你return的值的意义一样(不要多想)!!!
不要考虑初始化边缘(千万不要从dp[0][0]想思路!!!),
从中间取一个点,想这个点的值怎么根据它的左边、上边、左上边、左下边的值计算出来!!!*/

4.分割问题

(1)279完全平方数

/* dp[i] 表示数字 i 最少可以由几个完全平方数相加
构成。在本题中,位置 i 只依赖 i - k^2 的位置,如 i - 1、i - 4、i - 9 等等,才能满足完全平方分割
的条件。因此 dp[i] 可以取的最小值即为 1 + min(dp[i-1], dp[i-4], dp[i-9] · · · )。*/
class Solution {
public:

	int numSquares(int n) {
		vector<int> dp(n + 1, INT_MAX);
		dp[0] = 0;
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j * j <= i; ++j) {//牛逼啊   时间复杂度O(nlogn),空间复杂度O(n)
				dp[i] = min(dp[i], dp[i - j * j] + 1);
			}
		}
		return dp[n];
	}
};



class Solution {
public:

    int numSquares(int n) {
        vector<int> dp(n + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 1; i <= sqrt(n) + 1; ++i) {
            for (int j = 0; j <= n; ++j) {
                if (j - i * i >= 0) {
                    dp[j] = min(dp[j], dp[j - i * i] + 1);
                }
            }

        }
        return dp[n];
    }
};
//还可以用背包
java版本
class Solution {
    public int numSquares(int n) {
         完全背包
        int max = Integer.MAX_VALUE;
        int[] dp = new int[n + 1];
        for (int i = 1; i < dp.length; ++i) dp[i] = max;
         先遍历物品,后遍历背包
         for (int i = 1; i * i <= n; ++i) {
             for (int j = 1; j <= n; ++j) {
                 if (j >= i * i) {
                     dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
                 }
             }
         }
         先遍历背包,再遍历物品
        for (int j = 1; j <= n; ++j) {
            for (int i = 1; i * i <= j; ++i) {
                dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
}

(2)解码方法

题解:
    上楼梯的复杂版?
    如果连续的两位数符合条件,就相当于一个上楼梯的题目,可以有两种选法:
        1.一位数决定一个字母
        2.两位数决定一个字母
        就相当于dp(i) = dp[i-1] + dp[i-2];
    如果不符合条件,又有两种情况
        1.当前数字是0:
            不好意思,这阶楼梯不能单独走,
            dp[i] = dp[i-2]
        2.当前数字不是0
            不好意思,这阶楼梯太宽,走两步容易扯着步子,只能一个一个走
            dp[i] = dp[i-1];


//怎么类比楼梯呢?你想想如果s[i]是要单独算的,那就是上一阶台阶 dp[i]+=dp[i-1]
//如果s[i]可以作为两位数的末位,那就是上两阶台阶 dp[i]+=dp[i-2]

//太牛了好好体会一下
class Solution {
public:
    int numDecodings(string s) {
        int n = s.size();
        
        if (n == 0) {
            return 0;
        }
        int prev = s[0] - '0';
        if (!prev) return 0;//防止0开头
        if (n == 1) return 1;
        vector<int> dp(n+1,0);

        dp[0] = 1; dp[1] = 1;//初始化,想想看为什么
        //在把特殊情况排除之后,剩下的都是正常的,你for里面又是从2开始,那dp[1]必须得为1,那dp0为什么为1呢?
        //因为进去之后要用到dp[2-2],那也为1才行

        for (int i = 2; i <= n; ++i) {
            int cur = s[i - 1] - '0';
            if ((prev == 0 || prev > 2) && cur == 0) {
                return 0;//防止02 37这两种情况
            }
            if ((prev < 2 && prev > 0) || prev == 2 && cur < 7) {
                if (cur) {
                    dp[i] = dp[i - 2] + dp[i - 1];
                }
                else {
                    dp[i] = dp[i - 2];//cur=0必须跟在后面一起算
                }
            }
            else {
                dp[i] = dp[i - 1];//只能单个算
            }
            prev = cur;//cur到pre,真的妙
        }
        return dp[n];
    }
};

(3)139单词拆分(重点)

类似于完全平方数分割问题,这道题的分割条件由集合内的字符串决定,因此在考虑每个分
割位置时,需要遍历字符串集合,以确定当前位置是否可以成功分割。注意对于位置 0 ,需要初始化值为真

其实这个算背包问题,完全背包

//标答2,这个好理解  递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<bool> dp(s.size() + 1, false);
        dp[0] = true;//0一定要为1,不然后面都是false了
        //i j 一个从 一个从0,i是因为要dp1-n,而且截取长度也是从1开始而不是从0,j就简单了是正常0开始的,反正要小心一点
        for (int i = 1; i <= s.size(); i++) {   // 遍历背包
            for (int j = 0; j < i; j++) {       // 遍历物品
                string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
                if (wordSet.find(word) != wordSet.end() && dp[j]) {
                    dp[i] = true;
                }
            }
        }
        return dp[s.size()];
    }
};
//标答1
bool wordBreak(string s, vector<string>& wordDict) {
    int n = s.length();
    vector<bool> dp(n + 1, false);
    dp[0] = true;
    unordered_set<string> m(wordDict.begin(), wordDict.end());//去重的
    for (int i = 1; i <= n; ++i) {
        for (const string& word : m) {//加快速度
            int len = word.length();
            if (i >= len && s.substr(i - len, len) == word) {
                dp[i] = dp[i] || dp[i - len];
            }
        }
    }
    return dp[n];
}

5. 子序列问题

(1)300最长递增子序列

注意这里是不连续的 

class Solution {
public:
    //n^2
    //递推没想明白
    /*对于每一个位置 i,如果其之前的某
个位置 j 所对应的数字小于位置 i 所对应的数字,则我们可以获得一个以 i 结尾的、长度为 dp[j]
+ 1 的子序列*/
    int lengthOfLIS(vector<int>& nums) {
        int max_length = 0, n = nums.size();
        if (n <= 1) return n;
        vector<int> dp(n, 1);
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (nums[i] > nums[j]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            max_length = max(max_length, dp[i]);
        }
        return max_length;
    }
};


//法二 nlogn
//其实是在动态规划的基础上用了贪心
/**
        dp[i]: 所有长度为i+1的递增子序列中, 最小的那个序列尾数.
        由定义知dp数组必然是一个递增数组, 可以用 maxL 来表示最长递增子序列的长度.
        对数组进行迭代, 依次判断每个数num将其插入dp数组相应的位置:
        1. num > dp[maxL], 表示num比所有已知递增序列的尾数都大, 将num添加入dp
           数组尾部, 并将最长递增序列长度maxL加1
        2. dp[i-1] < num <= dp[i], 只更新相应的dp[i]
**/

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        int max_size = 0;
        vector<int>dp(n, 0);
        for (const int &num : nums) {
            //二分查找
            int low = 0;
            int high = max_size;
            while (low < high) {
                int mid = (low + high) / 2;
                if (dp[mid] < num) {
                    low = mid + 1;
                }
                else {
                    high = mid;
                }
            }


            if (low == max_size) {//说明比dp里面的都大
                dp[max_size++] = num;
            }
            else {//说明有一个可以替换
                dp[low] = num;
            }
        }
        return max_size;
    }
};

(2)1143. 最长公共子序列

class Solution {
public:
    /* dp[i][j] 表示到第一个字符串位置 i 为止、到
第二个字符串位置 j 为止、最长的公共子序列长度。*/

    int longestCommonSubsequence(string t1, string t2) {
        int n = t1.length();
        int m = t2.length();
        vector<vector<int>> dp(n+1, vector<int>(m+1, 0));
      /*  if (n == 0 || m == 0) {
            return 0;
        }
        if (n == 1) {
            if (t2.find(t1) != t2.npos) {
                return 1;
            }
            return 0;
        }
        if (m == 1) {
            if (t1.find(t1) != t1.npos) {
                return 1;
            }
            return 0;
        }  这些判断不用写,想想看为什么
*/
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= m; ++j) {
                if (t1[i - 1] == t2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else {
                    dp[i][j] = max(dp[i-1][j], dp[i ][j - 1]);//这个很关键我写错了、
                    /* dp[i][j] = Math.max(dp[i - 1][j], dp[i][j]);
                dp[i][j] = Math.max(dp[i][j - 1], dp[i][j]);    或者这样写更理解*/
                }
            }
        }
        return dp[n][m];

    }


};

6.背包问题

01背包 

 

完全背包 

同样的,我们也可以利用空间压缩将时间复杂度降低为 O ( W ) 。这里要注意我们在遍历每一
行的时候必须 正向遍历 ,因为我们需要利用当前物品在第 j-w 列的信息

 

 综合模板

1 0/1 背包:外循环 nums, 内循环 target,target 倒序且target>=nums[i];
2 、完全背包(组合):外循环 nums, 内循环 target ,target正序且 target>=nums[i];
3 、完全背包(排列):外循环 target, 内循环 nums ,target正序且 target>=nums[i

(1)416分割等和子集 


/*本题等价于 0-1 背包问题,设所有数字和为 sum,我们的目标是选取一部分物品,使得它们
的总和为 sum/2。这道题不需要考虑价值,因此我们只需要通过一个布尔值矩阵来表示状态转移
矩阵。注意边界条件的处理。*/

//不会啊啊
class Solution {
public:
    //和是容量
    //w是nums【i]
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum & 1) return false;
        int target = sum / 2, n = nums.size();
        vector<vector<bool>> dp(n + 1, vector<bool>(target + 1, false));
        for (int i = 0; i <= n; ++i) {
            dp[i][0] = true;
        }
        for (int i = 1; i <= n; ++i) {
            for (int j = 0; j <= target; ++j) {
                if (j - nums[i - 1] >= 0) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];//这里max其实就是转化为||了
                }
                else {
                    dp[i][j] = dp[i - 1][j];
                }

            }
        }
       

        return dp[n][target];
    }

    //空间压缩
    bool canPartition2(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum & 1) return false;
        int target = sum / 2, n = nums.size();
        vector<bool> dp(target + 1, false);
        

        //关键:
        dp[0] = true;//不然进行不下去的

        for (int i = 1; i <= n; ++i) {
            for (int j = target; j >=0; --j) {//逆向
                if (j - nums[i - 1] >= 0) {
                    dp[j] = dp[j] || dp[j - nums[i - 1]];//这里max其实就是转化为||了
                }
                else {
                    dp[j] = dp[j];
                }

            }
        }


        return dp[target];
    }
};

(2)322零钱兑换


//完全背包问题
//标答1,它这个外层背包,内层硬币
int coinChange(vector<int>& coins, int amount) {
    if (coins.empty()) return -1;
    vector<int> dp(amount + 1, amount + 2);
    dp[0] = 0;
    for (int i = 1; i <= amount; ++i) {
        for (const int& coin : coins) {
            if (i >= coin) {
                dp[i] = min(dp[i], dp[i - coin] + 1);
            }
        }
    }
    return dp[amount] == amount + 2 ? -1 : dp[amount];
}

//标答2
//由于这里不强调排列和组合,所以外层硬币也可以
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 0; i < coins.size(); i++) { // 遍历物品
            for (int j = coins[i]; j <= amount; j++) { // 遍历背包
                if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过
                    dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
                }
            }
        }
        if (dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
};


//我的
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        //容量是amount
        //w是conis[i]
        //value其实不需要,跟416一样
        int n = coins.size();
        vector<vector<int>> dp(n+1, vector<int>(amount+1, amount + 2));//因为要MIn所以要初始化最大值
        for (int i = 0; i <= n; ++i) {
            dp[i][0] = 0;//这个0很重要,不然全部不执行。
        }

        //经验:初始化真的要做好
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= amount; ++j) {//这样写一定要从1开始
                if (j >= coins[i-1]) {
                    dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
                }
                else {
                    dp[i][j] = dp[i - 1][j];
                }

            }

        }

        return dp[n][amount] == amount + 2 ? -1 : dp[n][amount];


    }
};

//按标答2的优化,循环的起始,循环体内部,真的暂时只能体会,我还没水平说出所以然
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        //容量是amount
        //w是conis[i]
        //value其实不需要,跟416一样
        int n = coins.size();
        vector<int> dp(amount + 1, amount + 2);//因为要MIn所以要初始化最大值

        dp[0] = 0;//这个0很重要,不然全部不执行。


    //经验:初始化真的要做好
        for (int i = 1; i <= n; ++i) {
            for (int j = coins[i - 1]; j <= amount; ++j) {
                if (dp[j - coins[i - 1]] != amount + 2) {

                    dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);
                }
            }

        }
        return dp[amount] == amount + 2 ? -1 : dp[amount];
    }
};

//按标答1的优化
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        //容量是amount
        //w是conis[i]
        //value其实不需要,跟416一样
        int n = coins.size();
        vector<int> dp(amount + 1, amount + 2);//因为要MIn所以要初始化最大值

        dp[0] = 0;//这个0很重要,不然全部不执行。
    //经验:初始化真的要做好
        for (int i = 1; i <= n; ++i) {
            for (int j = coins[i - 1]; j <= amount; ++j) {
                if (j >= coins[i - 1]) {

                    dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);
                }
                else {
                    dp[j] = dp[j - 1];
                }
            }
        }
        return dp[amount] == amount + 2 ? -1 : dp[amount];


    }
};

(3)一和零

//这个通过模仿416终于会做了
class Solution {
public:
    //len个物品
    //容量为m个0 n个1,好像要三维?
    //所以i-1这一条直接就是压缩掉
    int findMaxForm(vector<string>& strs, int m, int n) {
        int len = strs.size();
        
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));

        for (int i = 0; i < len; ++i) {
            int one = 0;
            int zero = strs[i].size();
            while (strs[i].find("1") != strs[i].npos) {
                ++one;
                --zero;
                int pos = strs[i].find("1");
                strs[i].erase(pos, 1);
            }

            //01背包,压缩后逆序
            for (int j = m; j >= 0; --j) {
                for (int k = n; k >= 0; --k) {
                    if (j >= zero && k >= one) {
                        dp[j][k] = max(dp[j][k], dp[j - zero][k - one] + 1);
                    }
                    /*
                    //这一段是错的,你一个字符串要么都算要么就都不算,不能拆开来的
                    else if (j >= zero) {
                        dp[j][k] = max(dp[j][k], dp[j - zero][k] + 1);
                    }
                    else if (k >= one) {
                        dp[j][k] = max(dp[j][k], dp[j][k - one] + 1);
                    }*/
                    else {
                        dp[j][k] = dp[j][k];
                    }

                }
            }
        }
        return dp[m][n];
    }
};

7. 字符串编辑

(1)72编辑距离

 题解

类似于题目 1143 ,我们使用一个二维数组 dp[i][j] ,表示将第一个字符串到位置 i 为止,和第
二个字符串到位置 j 为止,最多需要几步编辑。当第 i 位和第 j 位对应的字符相同时, dp[i][j] 等于 dp[i-1][j-1] ;当二者对应的字符不同时,修改的消耗是 dp[i-1][j-1]+1 ,插入 i 位置 / 删除 j 位置的消耗是 dp[i][j-1] + 1 ,插入 j 位置 / 删除 i 位置的消耗是 dp[i-1][j] + 1

/*,我们使用一个二维数组 dp[i][j],表示将第一个字符串到位置 i 为止,和第
二个字符串到位置 j 为止,最多需要几步编辑。当第 i 位和第 j 位对应的字符相同时,
dp[i][j] 等 于 dp[i-1][j-1];当二者对应的字符不同时,修改的消耗是 dp[i-1][j-1]+1,
插入 i 位置/删除 j 位置的消耗是 dp[i][j-1] + 1,插入 j 位置/删除 i 位置的消耗是 dp[i-1][j] + 1。*/
class Solution {
public:
	int minDistance(string word1, string word2) {
		int m = word1.length(), n = word2.length();
		vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
		for (int i = 0; i <= m; ++i) {
			for (int j = 0; j <= n; ++j) {
				if (i == 0) {
					dp[i][j] = j;//就是不断插入
				}
				else if (j == 0) {
					dp[i][j] = i;
				}
				else {
					//这一步增删改的min真的太牛逼了,好好体会一下,确实我们是从我w1转化为w2,
					//但是w1删其实就相当于w2增,操作数都是一样的
					dp[i][j] = min(
						dp[i - 1][j - 1] + ((word1[i - 1] == word2[j - 1]) ? 0 : 1),
						min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
				}
			}
		}
		return dp[m][n];
	}
};

(2)650只有两个键的键盘

题解
不同于以往通过加减实现的动态规划,这里需要乘除法来计算位置,因为粘贴操作是倍数增
加的。我们使用一个一维数组 dp ,其中位置 i 表示延展到长度 i 的最少操作次数。对于每个位置 j,如果 j 可以被 i 整除,那么长度 i 就可以由长度 j 操作得到,其操作次数等价于把一个长度为 1 的 A 延展到长度为 i/j 。因此我们可以得到递推公式 dp[i] = dp[j] + dp[i/j]
这里为什么你不 是i/j而是dp[i/ j]你可以想, 比如i/j=8,那 么真的是加八吗 ?这个八完全可 以通过变成4再 乘以2得到,这 样次数就比8小 了。
class Solution {
public:
	int minSteps(int n) {
		vector<int> dp(n + 1);
		int h = sqrt(n);
		for (int i = 2; i <= n; ++i) {
			dp[i] = i;
			for (int j = 2; j <= h; ++j) {
				if (i % j == 0) {
					dp[i] = dp[j] + dp[i / j];//这个dp[i/j]很关键!!!
					break;
				}
			}
		}
		return dp[n];
	}
};

(3)10正则匹配

 题解

我们可以使用一个二维数组 dp ,其中 dp[i][j] 表示以 i 截止的字符串是否可以被以 j 截止的
正则表达式匹配。根据正则表达式的不同情况,即字符、星号,点号,我们可以分情况讨论来更 新 dp 数组

class Solution {
public:

	bool isMatch(string s, string p) {
		int m = s.size(), n = p.size();
		vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
		dp[0][0] = true;//初始化很重要
		for (int i = 1; i < n + 1; ++i) {
			if (p[i - 1] ==  '*') {
				dp[0][i] = dp[0][i - 2];
			}
		}
		for (int i = 1; i < m + 1; ++i) {
			for (int j = 1; j < n + 1; ++j) {
				if (p[j - 1] == '.') {
					dp[i][j] = dp[i - 1][j - 1];
				}
				else if (p[j - 1] != '*') {
					dp[i][j] = dp[i - 1][j - 1] && p[j - 1] == s[i - 1];
				}
				else if (p[j - 2] != s[i - 1] && p[j - 2] != '.') {
					dp[i][j] = dp[i][j - 2];
				}
				else {
					dp[i][j] = dp[i-1][j - 2] || dp[i - 1][j] || dp[i][j - 2];
				}
			}
		}
		return dp[m][n];
	}

};

力扣

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值