简单回顾一下动态规划,整理一下刷题顺序
动态规划解题流程:
第一步:确定状态数组
这一步是最难的,一维数组来说比较简单。对于二维来说,直观的机器人走方格这种是比较容易想到的,但许多题目需要把抽象的关系映射到二维表格上,这是非常难想到的,只能多做题了。
第二步:找递推关系式
找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]…..dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。
注意使用动态规划的条件:最优子结构!
第三步:初始化条件
递推式的起始点,dp[0]、dp[1]或者dp[0][0]或者dp[i][0]、dp[0][j],视具体情况而定。
南大的一位老师讲的dp还是挺不错的,有时间可以去听一下
由易到难,来做题吧!
一、一维动态规划
爬楼梯
力扣https://leetcode-cn.com/problems/climbing-stairs/
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路:
爬到第i层 可以是从i-1层爬上来的,也可以是从i-2层爬上来的
所以爬到第i层的方法数为爬到i-1层 和 爬到i-2层 方法数之和,使用一维dp数组,意义为 到达第i层的方法数。
状态转移方程:
dp[i] = dp[i-1] + dp[i-2]
初始化:第0层不用爬只有一种方法,第1层只有第0层到第1层一种方法
dp[0] = 1;
dp[1] = 1;
结果:dp[n]
代码:
#include<iostream>
using namespace std;
int main(){
int n;
cin >> n;
int dp[n+1];
dp[0]=1;
dp[1]=1;
for(int i = 2; i <= n; i++){
dp[i]=dp[i-1]+dp[i-2];
}
cout << dp[n];
}
下面是一些一维的题目,有的题目在力扣有各种详解,只做一下链接传送
1.买卖股票的最佳时机
力扣https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
2.打家劫舍
力扣https://leetcode-cn.com/problems/house-robber/
dp[k] = max(dp[k-1], nums[k-1] + dp[k-2]);
偷前k间房子能获得的最大利益 = 偷第k-1间最大值而不偷第k间 与 偷第k-2间最大值而不偷第k-1间
3.打家劫舍2
力扣https://leetcode-cn.com/problems/house-robber-ii/
我的思路是,把打家劫舍1里的函数抽出来,然后把环首尾断开,去掉首部进行一次抢劫dp,去掉尾部进行一次抢劫dp,两者取最大值
class Solution {
public:
//抢劫函数
int get(vector<int>& nums){
if (nums.size() == 0) {
return 0;
}
int N = nums.size();
vector<int> dp(N+1, 0);
dp[0] = 0;
dp[1] = nums[0];
//状态转移方程
for (int k = 2; k <= N; k++) {
dp[k] = max(dp[k-1], nums[k-1] + dp[k-2]);
}
return dp[N];
}
int rob(vector<int>& nums) {
if(nums.size()<=2)return *max_element(nums.begin(),nums.end());
vector<int> first;
vector<int> second;
//首尾断开,两次dp
first.assign(nums.begin(),nums.end()-1);
second.assign(nums.begin()+1,nums.end());
return max(get(first),get(second));
}
};
4.最大子数组和
力扣https://leetcode-cn.com/problems/maximum-subarray/
注意是子数组,不是子序列(后面有子序列的题)
子数组比较简单:
dp[i] = max(dp[i-1] + nums[i], nums[i]);
dp[i]代表以i下标为结尾的子数组的最大值 = dp[i-1] + 当前值 与 当前值
很好理解的,如果以i-1为结尾的子数组最大值是负数,那肯定不如nums[i]自己大
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size());
dp[0] = nums[0];
for(int i = 1; i < nums.size(); i++){
dp[i] = max(dp[i-1] + nums[i], nums[i]);
}
return *max_element(dp.begin(),dp.end());
}
};
5.接雨水
比较难的一个一维dp,官方题解给的很详细,一看就懂
力扣https://leetcode-cn.com/problems/trapping-rain-water/
6.最长上升子序列
最长上升子序列(Longest Increasing Subsequence),简称LIS,也有些情况求的是最长非降序子序列,二者区别就是序列中是否可以有相等的数。
假设我们有一个序列 b i,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们也可以从中得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N,但必须按照从前到后的顺序。
比如,对于序列(1, 7, 3, 5, 9, 4, 8),我们就会得到一些上升的子序列,如(1, 7, 9), (3, 4, 8), (1, 3, 5, 8)等等,而这些子序列中最长的(如子序列(1, 3, 5, 8) ),它的长度为4,因此该序列的最长上升子序列长度为4。
我们都知道,动态规划的一个特点就是当前解可以由上一个阶段的解推出, 由此,把我们要求的问题简化成一个更小的子问题。子问题具有相同的求解方式,只不过是规模小了而已。最长上升子序列就符合这一特性。我们要求n个数的最长上升子序列,可以求前n-1个数的最长上升子序列,再跟第n个数进行判断。求前n-1个数的最长上升子序列,可以通过求前n-2个数的最长上升子序列……直到求前1个数的最长上升子序列,此时LIS当然为1。
让我们举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前 i 个数以A[ i ]结尾的最长上升子序列长度。
前1个数 d(1)=1 子序列为2;
前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7
前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1
前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5
前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6
前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4
前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3
前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8
前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9
d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5
总结一下,d(i) 就是找以A[ i ]结尾的,在A[ i ]之前的最长上升子序列+1,即前 i 个数的 LIS 长度 + 1。当A[ i ]之前没有比A[ i ]更小的数时,d(i) = 1。所有的d(i)里面最大的那个就是最长上升子序列。其实说的通俗点,就是每次都向前找比它小的数和比它大的数的位置,将第一个比它大的替换掉,这样操作虽然LIS序列的具体数字可能会变,但是很明显LIS长度还是不变的,因为只是把数替换掉了,并没有改变增加或者减少长度。
状态设计:F [ i ] 代表以 A [ i ] 结尾的 LIS 的长度
//这个状态转移有难度,需要有双重for循环实现
状态转移:F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])
边界处理:F [ i ] = 1 (1 <= i <= n)
时间复杂度:O (n^2)
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 103;
int main(){
int n;
vector<int> a(maxn, 0);
vector<int> f(maxn, 0);
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
//f[i]最最最最初始为1,意为以单独i下标的为一个序列
f[i] = 1;
}
for(int i = 1; i <= n; i++){
for (int j = 1; j < i; j++){
if(a[j] < a[i])
//f[i]最最最最初始为1,意为以单独i下标的为一个序列,在for循环中不断更新
f[i] = max(f[i], f[j] + 1);
}
}
int ans = *max_element(f.begin(),f.end() );
cout << ans;
}
7.递增子序列的最大值
参考上一题,趁热打铁
有N个数字构成的序列,求最大递增子段和,即递增子序列和的最大值,
思路就是定义dp[i],表示以a[i]结尾的最大递增子段和,双重for循环
每次求出以a[i]结尾的最大递增子段和。Sample Input:
3 1 3 2
4 1 2 3 4
4 3 3 2 1
0Sample Output:
4
10
3
与上一题的具体区别在代码中!一定详细反复看!!关键处给了注释
#include<iostream>
#include<stdio.h>
#include<vector>
#include<cstring>
#include<algorithm>
/*
题意是有N个数字构成的序列,求最大递增子段和,即递增子序列和的最大值,
思路就是定义dp[i],表示以a[i]结尾的最大递增子段和,双重for循环
每次求出以a[i]结尾的最大递增子段和。
*/
using namespace std;
int main(){
int a[1005], dp[1005], n, max1;
vector<int> res;
while(scanf("%d", &n) && n){
max1 = 0;
//dp[i]初始值是0,意为可以以i为结尾,但是子序列为空的
memset(dp, 0, sizeof(dp));
for(int i = 0; i <= n-1; i++)
scanf("%d", &a[i]);
dp[0] = a[0];
for(int i = 1; i <= n-1; i++){
for(int j = 0; j <= i-1; j++){
if(a[i] > a[j])
//注意这里一次修改dp,dp[i]初始值是0,意为可以以i为结尾,但是子序列为空的
dp[i] = max(dp[j] + a[i], dp[i]);
}
//这里还有一次修改dp数组,因为有负数情况
dp[i] = max(dp[i], a[i]);
}
max1 = dp[0];
for(int i = 0; i <= n-1; i++)
max1 = max(dp[i], max1);
res.push_back(max1);
}
for(int i = 0; i<res.size();i++){
cout << res[i];
if(i!=res.size()-1) cout << endl;
}
return 0;
}
8.最少拦截系统
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能超过前一发的高度.某天,雷达捕捉到敌国的导弹来袭.由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹.
怎么办呢?多搞几套系统呗!你说说倒蛮容易,成本呢?成本是个大问题啊.所以俺就到这里来求救了,请帮助计算一下最少需要多少套拦截系统.
Input
输入若干组数据.每组数据包括:导弹总个数(正整数),导弹依此飞来的高度(雷达给出的高度数据是不大于30000的正整数,用空格分隔)
Output
对应每组数据输出拦截所有导弹最少要配备多少套这种导弹拦截系统.
Sample Input:
8 389 207 155 300 299 170 158 65
Sample Output:
2
这个题可以用dp数组做,但可以结合dp的思想用容器来更方便的解决-----set
对于当前的导弹高度,查找set里是否有能达到这个高度的系统
如果没有:
插入一个系统,它能达到的最大高度是目前的导弹高度。
如果有:
找到最低满足要求的导弹系统,更新它的值为当前导弹高度,
运用set的原因:
set自动有序、lower_bound()函数
//lower_bound()--返回指向大于(或等于)某值的第一个元素的迭代器 cout<<"lower_buond 3 "<<*s.lower_bound (3)<<endl; //upper_bound()--返回大于某个值元素的迭代器 cout<<"upper_bound 3 "<<*s.upper_bound (3)<<endl; //find()--返回一个指向被查找到元素的迭代器 cout<<"find() 3 "<<*s.find (3)<<endl;//erase() -- 删除当前迭代器指针对着的元素
#include<iostream>
#include<stdio.h>
#include<vector>
#include<set>
#include<cstring>
#include<algorithm>
using namespace std;
int leastSys(vector<int>& nums){
int len = nums.size();
//特判0,1个导弹
if(len == 0) return 0;
if(len == 1) return 1;
//当前每个系统能打到的最高高度,使用set使其自动有序
set<int> curSys;
curSys.insert(nums[0]);
for(int i = 1; i < len; i++){
//找那个最低的能打到的
set<int>::iterator it = curSys.lower_bound(nums[i]);
//有就把它删了更新值
if(it != curSys.end()) curSys.erase(it);
curSys.insert(nums[i]);
}
return curSys.size();
}
int main(){
int n;
vector<int> res;
while(scanf("%d",&n) != EOF){
vector<int> nums;
for(int i = 0; i < n; i++){
int temp;
cin >> temp;
nums.push_back(temp);
}
res.push_back(leastSys(nums));
}
for(int i = 0; i < res.size(); i++ ){
cout << res[i];
if(i < res.size()-1) cout<<endl;
}
}
9.解码方法
力扣https://leetcode-cn.com/problems/decode-ways/比较有意思的一个一维dp
二、二维dp
1.不同路径
问题描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
力扣https://leetcode-cn.com/problems/unique-paths/
这是最最最直观的二维dp题目,现在觉得二维dp应当从此题入手
因为二维数组dp完全和网格一一对应
步骤一、定义数组元素的含义
由于我们的目的是从左上角到右下角一共有多少种路径,那我们就定义 dp[i] [j]的含义为:当机器人从左上角走到(i, j) 这个位置时,一共有 dp[i] [j] 种路径。那么,dp[m-1] [n-1] 就是我们要的答案了。
注意,这个网格相当于一个二维数组,数组是从下标为 0 开始算起的,所以 右下角的位置是 (m-1, n - 1),所以 dp[m-1] [n-1] 就是我们要找的答案。
步骤二:找出关系数组元素间的关系式
想象以下,机器人要怎么样才能到达 (i, j) 这个位置?由于机器人可以向下走或者向右走,所以有两种方式到达
一种是从 (i-1, j) 这个位置走一步到达
一种是从(i, j - 1) 这个位置走一步到达
因为是计算所有可能的步骤,所以是把所有可能走的路径都加起来,所以关系式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。
步骤三、找出初始值
显然,当 dp[i] [j] 中,如果 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,因为这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。这个还是非常容易计算的,相当于计算机图中的最上面一行和左边一列。因此初始值如下:
dp[0] [0….n-1] = 1; // 相当于最上面一行,机器人只能一直往左走
dp[0…m-1] [0] = 1; // 相当于最左面一列,机器人只能一直往下走
#include<iostream>
#include<vector>
using namespace std;
int main(){
int m,n;
cin >> m >> n;
vector<vector<int>> dp = vector<vector<int>>(m,vector<int>(n,0));
// 初始化
for(int i = 0; i < m; i++){
dp[i][0] = 1;
}
for(int i = 0; i < n; i++){
dp[0][i] = 1;
}
//根据状态转移方程填表
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
cout << dp[m-1][n-1];
return 0;
}
2.最小路径和
64. 最小路径和https://leetcode-cn.com/problems/minimum-path-sum/
难度中等1153
给定一个包含非负整数的 m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]] 输出:12
与上题类似
步骤一、定义数组元素的含义
由于我们的目的是从左上角到右下角,最小路径和是多少,那我们就定义 dp[i] [j]的含义为:当机器人从左上角走到(i, j) 这个位置时,最下的路径和是 dp[i] [j]。那么,dp[m-1] [n-1] 就是我们要的答案了。
注意,这个网格相当于一个二维数组,数组是从下标为 0 开始算起的,所以 由下角的位置是 (m-1, n - 1),所以 dp[m-1] [n-1] 就是我们要走的答案。
步骤二:找出关系数组元素间的关系式
想象以下,机器人要怎么样才能到达 (i, j) 这个位置?由于机器人可以向下走或者向右走,所以有两种方式到达
一种是从 (i-1, j) 这个位置走一步到达
一种是从(i, j - 1) 这个位置走一步到达
不过这次不是计算所有可能路径,而是计算哪一个路径和是最小的,那么我们要从这两种方式中,选择一种,使得dp[i] [j] 的值是最小的,显然有
dp[i] [j] = min(dp[i-1][j],dp[i][j-1]) + arr[i][j];// arr[i][j] 表示网格种的值
步骤三、找出初始值
显然,当 dp[i] [j] 中,如果 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,因为这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。这个还是非常容易计算的,相当于计算机图中的最上面一行和左边一列。因此初始值如下:
dp[0] [j] = arr[0] [j] + dp[0] [j-1]; // 相当于最上面一行,机器人只能一直往左走
dp[i] [0] = arr[i] [0] + dp[i] [0]; // 相当于最左面一列,机器人只能一直往下走
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0) {
return 0;
}
int rows = grid.size(), columns = grid[0].size();
auto dp = vector < vector <int> > (rows, vector <int> (columns));
dp[0][0] = grid[0][0];
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][columns - 1];
}
};
3.背包问题
实际应用抽象出来与二维dp数组对应的必经之路(哭死)
【动态规划】01背包问题(通俗易懂,超基础讲解)_Yngz_Miao的博客-CSDN博客_动态规划解决01背包问题
看这个博主写的文章!
看这个B站讲解视频!
这两个应该是讲的最清楚的了,看完好好琢磨,突然有一瞬间你就懂了
4.2021复旦机考第三题
题目描述:给定一个非负整数序列x1,x2,x3...xn,可以给每一个整数取负数或者取原值,求有多少种取法使得这些整数的和等于期望值E。请写出程序,并解释解题思路。
输入:1, 1, 1, 1, 1, 3
输出:5
样例解释:
-1+1+1+1+1 = 3
1-1+1+1+1 = 3
1+1-1+1+1 = 3
1+1+1-1+1 = 3
1+1+1+1-1 = 3
初始化结构:
需要一个二维数组
联想背包问题,前i个数,能组合得到的值 v
由于和背包问题不同,v可以为负值,考虑用 vector<map<int,int>>进行解决
由于全是输入数组nums整数,其和值为sum,则v的取值在-sum和sum区间之内
所以二维结构为 0 <= i <= nums.size() -sum <= v <= sum 的一个表格
初始化填表:
前 i=0 个值能得到的任何 非0 的v的方法数都是0 ,因为没有数可选,得不到非0 的v
前 i=0 个值能得到的 v = 0 的方法数 是 1, 因为没有数可选 只有一种方法 得到 v=0
综上,初始化为dp[0][0] = 1; 其余都先填上0(其实先把dp[0][v] (v!=0)填上0 即可,写代码方便起见全填0)
状态转移方程:
dp[i][v] = dp[i-1][v-nums[i-1]] + dp[i-1][v+nums[i-1]]
即前i个值取到v的方法数 = 前i-1个数取到 v-nums[i-1]的方法数 + 前i-1个数取到 v+nums[i-1]的方法数 (注意nums[i-1]是第i个数)
结果:
dp[nums.size()][E]
#include<iostream>
#include<vector>
#include<map>
#include<numeric>
using namespace std;
int main(){
cout<<"输入序列(-1为结束符):"<<endl;
vector<int> nums;
while(1){
int temp;
cin >> temp;
if(temp == -1) break;
nums.push_back(temp);
}
int sum = accumulate(nums.begin(),nums.end(),0);
cout << "请输入期望值E:"<<endl;
int E;
cin >> E;
//初始化
vector<map<int,int>> dp(nums.size()+1);
for(int i = 0; i <= nums.size(); i++ ){
for(int v = -sum; v <= sum; v++){
if(i == 0 && v == 0){
dp[i][v] = 1;
}
else{
dp[i][v] = 0;
}
}
}
//状态转移 填表
for(int i = 1; i <= nums.size(); i++){
for(int v = -sum; v <= sum; v++){
//temp1 和 temp2分别为两种方法数 curdata为此时的第i个数
int temp1 = 0, temp2 = 0, curdata = nums[i-1];
//判断 v - curdata 是否是在-sum ~ sum 内的 ,由于curdata是大于0的,因此v-curdata只可能小于-sum, v+curdata 同理
if(-sum <= v - curdata) temp1 = dp[i-1][v - curdata];
if(sum >= v + curdata) temp2 = dp[i-1][v + curdata];
//两种方法数和为dp[i][v]结果
dp[i][v] = temp1 + temp2;
}
}
//得到最后结果
cout << dp[nums.size()][E];
}
4.最长回文子串
力扣https://leetcode-cn.com/problems/longest-palindromic-substring/
5.分割回文串
6.超级丑数
7.编辑距离
力扣https://leetcode-cn.com/problems/edit-distance/solution/edit-distance-by-ikaruga/这篇题解把字符串对应dp讲的太清楚了
。。。半个月就刷了这几个题,绷不住了
溜了