最长上升子序列LIS
这类问题的特点:
1.目标序列 是 原序列某些数拎出来组成的,不是原序列的某个连续的子序列。
题意理解:
给定一个数列,求最长严格递增子序列的长度。
//注意:严格递增不等同于非降序!严格递增中间不能出现等于的情况。
题解:
第一种方法:动规
算法思路:
1.考虑到输入的数据是一个一维数组,所以设置一个一维的dp表。
2.dp[i]:定义为考虑前 i 个元素,以第 i 个元素为结尾的最长上升子序列的长度。
3.状态转移方程为:
// 状态转移方程 基于 dp 的定义而来:
4.最终要解的答案 最长的上升子序列的长度 即为 dp[]数组中的最大值。
算法理解:
1.以i元素为结尾的最长上升子序列的长度,可以由以j元素为结尾的最长上升子序列长度转化而来。(递推)
2.答案(最长上升子序列长度)存在dp[i]中。
代码实现:
#include <bits/stdc++.h>
using namespace std;
const int N = 2550;
int n, nums[N], dp[N], Max = 0; // dp[i] 的 定义:在nums[1]~nums[i]中以nums[i]为结尾的最长上升子序列的长度
int main()
{
cin >> n; // 输入
for(int i = 1; i <= n; i++)
cin >> nums[i];
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++) // 遍历数组 nums[]
{
for(int j = i - 1; j >= 1; j--) // 遍历前i-1个数
{
if(nums[j] < nums[i])
dp[i] = max(dp[i], dp[j] + 1);
}
if(dp[i] == 0) // 细节:如果没有可作为nums[i]前驱的元素,则 dp[i] = 1;
dp[i] = 1;
Max = max(dp[i], Max); // Max 保存 dp[] 中的最大值
}
cout << Max << endl;
// for(int i = 1; i <= n; i++) // 调试语句
// cout << dp[i] << ' ';
// cout << endl;
return 0;
}
算法分析:
时间复杂度:O(n^2) // 该算法中有两层循环
空间复杂度:O(n) // 开了额外的辅助空间 dp 数组
第二种方法:动规+贪心
算法思路:
首先,我们把思考点焦距在我们要求解的东西:最长上升序列的长度。
基于这个目标,我们可以想到一种合理的说法:
如果我们要使上升子序列尽可能的长,那么我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
怎么去实现我们这一想法?
维护一个一维数组dp:dp[len]维护长度为len的最长上升子序列的末尾元素的最小值。
// 注:初始时,dp[1] = nums[1]。
// 注:易知,dp[len]是单调递增的。即 当len1 < len2,则 dp[len1] < dp[len2],故可考虑用高效的二分。
代码实现:
#include <bits/stdc++.h>
using namespace std;
const int N = 2550;
int n, nums[N], dp[N]; // dp[len]表示长度为len的最长上升子序列的末尾元素的最小值
int main()
{
cin >> n; // 输入
for(int i = 1; i <= n; i++)
cin >> nums[i];
dp[1] = nums[1];
int len = 1;
for(int i = 2; i <= n; i++) // 遍历 nums[2]~nums[n]
{
int p=lower_bound(dp+1,dp+len+1,nums[i])-dp; //查找大于nums[i]的第一个元素
if(p==len+1)
dp[++len]=nums[i]; // 增长序列长度
else
dp[p]=nums[i]; // 对长度为p的序列的末尾元素进行更新
}
cout << len << endl;
return 0;
}
算法分析:
时间复杂度:O(nlogn)
// lower_bound()是O(logn)的时间复杂度,再加最外层的for循环,总的时间复杂度是:O(nlongn)
空间复杂度:O(n)
// 用了额外的辅助空间 dp[]
对第二种方法再理解:
1.最终得到的 dp[] 序列不一定是一个 可行解(譬如对于数列:2 4 10 3 最终得到的 dp[] 序列是:2 3 10 这个不是一个可行解
2.dp[]数组的作用是放小元素,使得序列增得比较缓,譬如对于序列:2 9 10 3 9 10 正是有dp[]数组的存在,才可能得到序列:2 3 9 10。否则,可能得到 2 9 10 , 后面的元素舍弃。
// 虽然 3 被安插进来,看着很奇怪,但是,正是要有 3 被安插进来,才可能使得后面能排进 9 和 10
思考总结:
1.用动态规划算法,一定要用到dp表去记录不同阶段的信息,以此根据之前的信息实现状态转移。
2.所谓 动态规划 大概可以理解为在 动中规划(动:可以是指遍历数组的过程~~~规划 可以理解是 怎么处理 动 中的 每一个阶段)。
参考资料:
https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/
关于dp[]单调递增的证明:
假如dp[j] >= dp[i] 且 j <i
考虑从长度为 i 的最长上升子序列的末尾删除 i−j 个元素,那么这个序列长度变为 j ,且第 j 个元素 x(末尾元素)必然小于 dp[i]
根据dp[j] >= dp[i],所以也就小于 dp[j]
这样我们就找到了一个长度为 j 并且末尾元素比 dp[j] 小的最长上升子序列,从而产生了矛盾。(根据定义)
因此数组 dp 的单调性得证。
最长上升子序列LIS问题-同类题型:
HDU-1087 Super Jumping! Jumping! Jumping!
状态定义:
dp[i]:前i个元素,以第i个元素为结尾得到的分数的最大值
状态转移方程:
当a[j] < a[i]时,dp[i]=max(dp[j]+a[i],dp[i]);
参考代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e3+10;
ll n,a[N],dp[N];
int main()
{
while(cin >> n)
{
if(n==0) break;
memset(dp,0,sizeof dp);
for(int i=1;i<=n;++i) cin >> a[i];
dp[1]=a[1];
ll mx=dp[1];
for(int i=2;i<=n;++i)
{
dp[i]=a[i]; // WA了一发,这里忘记赋值
for(int j=1;j<i;++j)
{
if(a[j] < a[i])
dp[i]=max(dp[j]+a[i],dp[i]);
}
mx=max(mx,dp[i]);
}
cout << mx << endl;
}
}
这类问题的注意点:
1.当不满足条件时,dp[]也需要给他附上值,所以可以直接一开始时,附上初值。
2.它的解是在中间某个状态产生的,不是dp[n]。
(类似)最长上升子序列LIS问题-同类题型:
给定n种方块类型,(xi,yi,zi),每种类型的方块有无限块。现在要将若干个方块叠起来,要求在下面的方块的底面的长和宽都大于上面方块的底面的长和宽。问可以得到的最大高度。
状态定义:
dp[i]:前i个元素,以第i个元素为结尾得到的高度的最大值
。
状态转移方程:
if(v[j].fi.fi>v[i].fi.fi && v[j].fi.se>v[i].fi.se) dp[i]=max(dp[i],dp[j]+v[i].se);
参考代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define fi first
#define se second
ll idx,cnt,n,dp[200];
vector<pair<pair<ll,ll>,ll>> v;
void add(ll t1,ll t2,ll t3)
{
cnt++;
v.push_back({{t1,t2},t3});
}
bool cmp(pair<pair<ll,ll>,ll> x,pair<pair<ll,ll>,ll> y)
{
if(x.fi.fi==y.fi.fi)
{
if(x.fi.se==y.fi.se)
return x.se>y.se;
return x.fi.se>y.fi.se;
}
return x.fi>y.fi;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
while(cin >> n)
{
if(n==0) break;
cnt=0;
idx++;
memset(dp,0,sizeof dp);
v.clear();
for(int i=1;i<=n;++i)
{
long long t1,t2,t3;
cin >> t1 >> t2 >> t3;
add(t1,t2,t3); //这里也可以只放三组,需要排一下序
add(t1,t3,t2);
add(t2,t1,t3);
add(t2,t3,t1);
add(t3,t1,t2);
add(t3,t2,t1);
}
ll mx=0;
sort(v.begin(),v.end(),cmp);
for(int i=0;i<cnt;++i)
{
dp[i]=v[i].se;
for(int j=1;j<i;++j)
if(v[j].fi.fi>v[i].fi.fi && v[j].fi.se>v[i].fi.se)
dp[i]=max(dp[i],dp[j]+v[i].se);
mx=max(dp[i],mx);
}
printf("Case %d: maximum height = %lld\n",idx,mx);
}
return 0;
}
同样地:我们看到这道题也是:1.不符合条件也需要赋值。 2.最优解是在中间某个状态产生的。
最长公共子序列LCS问题:
这类问题的提法一般是:给定两个数组(或字符串),求这两个数组(或字符串)的最长公共子序列。
关于最长公共子序列问题有以下两种类型:
最长公共子序列(数字)
在网上看到有一篇博客讲这个讲得非常得好,分享:https://blog.csdn.net/hrn1216/article/details/51534607
一点说明:输入的两个数列分别用s1[]和s2[]存储,下标分别是从1 ~ n 和 1 ~ m
算法思路:
1.由于输入有两个序列,故考虑用二维数组dp[][]
存放动规过程的重要信息。定义dp[i][j]
数组的含义为:s1[1 ~ i]
和 s2[1 ~ j]
的最长公共子序列的长度。
2.对问题进行分析:
假设问题的最优解为 Z={z1, z2, ..., zk}
,若 zk = s1[i] = s2[j]
,那么 dp[i][j] = dp[i - 1][j - 1] + 1
;否则,dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])
。
故:
根据分析和定义可以得到如下的状态转移关系:
代码实现:
第一种方法:非递归实现:
#include <bits/stdc++.h>
using namespace std;
const int N = 1100;
int n, m, s1[N], s2[N], dp[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> s1[i];
for(int i = 1; i <= m; i++)
cin >> s2[i];
for(int i = 1; i <= n; i++) // 遍历s1[] // 非递归写法!
{
for(int j = 1; j <= m; j++) // 遍历s2[]
{
if(s1[i] == s2[j])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
cout << dp[n][m] << endl;
// 输出轨迹 (我是倒着输出的)
int i = n, j = m;
while(i >= 1 && j >= 1)
{
if(s1[i] == s2[j])
{
cout << s1[i] << ' ';
i--;
j--;
}
else
{
if(dp[i - 1][j] > dp[i][j - 1])
i--;
else
j--;
}
}
return 0;
}
算法分析:
时间复杂度:O(nm) // 两层循环
空间复杂度:O(n^2) // 开了一个二维的辅助数组 dp[][]
第二种方法:递归实现:
#include <bits/stdc++.h>
using namespace std;
const int N = 1100;
int n, m, s1[N], s2[N], dp[N][N];
int dg(int x, int y);
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> s1[i];
for(int i = 1; i <= m; i++)
cin >> s2[i];
dg(n, m);
cout << dp[n][m] << endl;
// 输出轨迹 (我是倒着输出的) // 可以通过某种方法把它正序输出,譬如先存在stack里,再最后输出
int i = n, j = m;
while(i >= 1 && j >= 1)
{
if(s1[i] == s2[j])
{
cout << s1[i] << ' ';
i--;
j--;
}
else
{
if(dp[i - 1][j] > dp[i][j - 1])
i--;
else
j--;
}
}
return 0;
}
int dg(int x, int y) // 递归写法
{
if(x == 0 || y == 0)
{
dp[x][y] = 0;
return 0;
}
if(dp[x][y] != 0) // 如果子问题之前计算过,直接返回就行,避免子问题重复计算!
return dp[x][y];
if(s1[x] == s2[y]) // 仍是围绕状态转移方程
dp[x][y] = dg(x - 1, y - 1) + 1;
else
dp[x][y] = max(dg(x - 1, y), dg(x, y - 1));
return dp[x][y];
}
算法分析:
时间复杂度:O(n) //还不会分析 // 但是,根据输出,递归实现的求解的子问题少了
空间复杂度:O(nm) // 开了一个二维数组dp[][]
最长公共子序列(字符串)
原题链接:https://leetcode-cn.com/problems/longest-common-subsequence/
一点说明:输入的两个字符串分别用s1[]和s2[]存储,下标分别是从 0 ~ (n-1) 和0 ~ (m-1)
算法思路:
// 最长公共子序列(字符串)的解题想法 和 最长公共子序列(数字)相似。
状态转移方程是一样的,如下:
代码实现:
第一种方法:二维数组实现
#include <bits/stdc++.h>
using namespace std;
const int N = 700;
int n, m;
string s1, s2;
int dp[N][N];
int main()
{
cin >> s1 >> s2;
n = s1.size() - 1;
m = s2.size() - 1;
memset(dp, 0, sizeof(dp));
for(int i = 0; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
if(s1[i] == s2[j])
{
if(i == 0 || j == 0)
dp[i][j] = 1;
else
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else
{
if(i > 0 && j > 0)
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
else if(i > 0)
dp[i][j] = dp[i - 1][j];
else if(j > 0)
dp[i][j] = dp[i][j - 1];
else;
}
}
}
cout << dp[n][m] << endl;
return 0;
}
算法分析:
时间复杂度:O(nm) // 两层循环
空间复杂度:O(nm) // 开了二维辅助空间 dp[][]
第二种方法:一维数组实现
#include <bits/stdc++.h>
using namespace std;
const int N = 700;
int n, m;
string s1, s2;
int main()
{
cin >> s1 >> s2;
n = s1.size() - 1;
m = s2.size() - 1;
int dp[N];
memset(dp, 0, sizeof(dp)); // 初始化赋值为 0
int t1 = 0, t2 = 0;
for(int i = 0; i <= n; i++) // 遍历s1
{
for(int j = 0; j <= m; j++) // 遍历s2
{
t1 = t2; // 记录 [i-1][j-1]
t2 = dp[j]; // 记录 [i-1][j]
if(s1[i] == s2[j])
{
if(i == 0 || j == 0)
dp[j] = 1;
else
dp[j] = t1 + 1; // [i-1][j-1] + 1
}
else
{
if(i > 0 && j > 0)
dp[j] = max(t2, dp[j - 1]);
else if(i > 0)
dp[j] = t2;
else if(j > 0)
dp[j] = dp[j - 1];
else;
}
}
}
cout << dp[m] << endl;
// for(int i = 0; i <= m; i++)
// cout << dp[i] << endl;
return 0;
}
算法分析:
时间复杂度:O(nm) // 两层循环
空间复杂度:O(n) // 开了一维辅助空间 dp[]
最长公共上升子序列LCIS
#include <bits/stdc++.h>
using namespace std;
#define fir(i,n) for(int i=1;i<=n;++i)
const int N=3e3+10;
int n,a[N],b[N],dp[N][N]; //dp[i][j]:1~i,1~j,并且以b[j]为结尾的最长公共上升子序列
int main()
{
cin >> n;
fir(i,n) cin >> a[i];
fir(i,n) cin >> b[i];
fir(i,n)
{
int mx=0;
fir(j,n)
{
dp[i][j]=dp[i-1][j]; //要把前面的值贴过来
if(a[i]>b[j]) mx=max(mx,dp[i-1][j]);
if(a[i]==b[j]) dp[i][j]=mx+1;
}
}
int ans=0;
fir(i,n)
ans=max(ans,dp[n][i]);
cout << ans << endl;
}
编辑距离问题
**问题描述: **
有两个字符串A和B,字符串转化的操作有三种,分别是:(1)删除一个字符; (2)插入一个字符; (3)将一个字符改为另一个字符,即替换。 求将字符串A变换为字符串B所用的最少字符操作数。
解法:
// 这道题其实很明显地说了串间转移方法,故本道题采用动态规划来解决。
定义状态:dp[i][j]
:表示 a[1~i]
、b[1~j]
的最小编辑距离。
// 原问题转化为:求 dp[n][m]
的值。// n 表示 A字符串 的长度;m 表示 B字符串 的长度。
定义状态转移方程:
如果 a[i] = b[j],则 c[i][j] = c[i-1][j-1]
;
如果 a[i] ≠ b[j],则 c[i][j] = max({c[i-1][j-1] + 1, c[i-1][j] + 1, c[i][j-1] + 1})
;
// 解释:c[i-1][j-1] + 1
表示修改操作的处理;c[i][j-1] + 1
表示插入操作的处理;c[i-1][j] + 1
表示删除操作的处理。
代码实现:
#include <bits/stdc++.h>
using namespace std;
const int N = 2005;
string s1 = " ", s2 = " ", t1, t2;
int c[N][N];
int main()
{
cin >> t1 >> t2;
s1 += t1;
s2 += t2;
int l1 = t1.size(), l2 = t2.size();
for(int i = 1; i <= l1; i++) //预处理
c[i][0] = i;
for(int i = 1; i <= l2; i++)
c[0][i] = i;
for(int i = 1; i <= l1; i++)
{
for(int j = 1; j <= l2; j++)
{
if(s1[i] == s2[j])
c[i][j] = c[i - 1][j - 1];
else
c[i][j] = min({c[i - 1][j - 1] + 1, c[i][j - 1] + 1, c[i - 1][j] + 1});
}
}
cout << c[l1][l2] << endl;
return 0;
}
算法复杂度分析:
时间复杂度:O(nm)
// n 表示 字符串 A 的大小;m 表示 字符串 B 的大小。算法需要两层循环,一层遍历字符串A,一层遍历字符串B。
空间复杂度:O(nm)
// 算法需要二维dp
数组保留子问题的解。
心得体会:
1.对于一些问题,要学会用递归的角度、思维去认识问题以及解决问题。
2.采用动态规划法,在代码实现时,通常要用到数组去保存子问题的解,通常要用到循环去推进子问题一步步扩展到原问题。
动态规划法的个人体会和思考:
1.采用动态规划解题时,非常关键的一步是要确定好 状态 以及 状态转移方程。
2.动态规划法本质上是避免子问题重复计算,从而降低时间复杂度。为了达到避免子问题重复计算,动态规划法通常先将原问题划分为独立的不同子问题(不同阶段),再通过某种策略逐步将子问题拓展到原问题,从而求解原问题。
数塔(数字三角形)
·从下往上递推,最终得到从顶部开始向下走到底部的路径的最大值。
特点:从一个点出发,线性逐层递推。
状态定义:
f[i][j]
表示从最后一行开始,走到位置 (i,j) 的最优解。并且要逐层走下来,不能跳着走。
容易想到 状态转移方程为:
f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
参考代码:
#include <bits/stdc++.h>
using namespace std;
int r,a[1100][1100],f[1100][1100];
int main()
{
cin >> r;
for(int i=1;i<=r;++i)
{
for(int j=1;j<=i;++j)
cin >> a[i][j];
}
for(int i=r;i>=1;--i)
{
for(int j=1;j<=i;++j)
f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
}
cout << f[1][1] << endl;
}
**时间复杂度:**O(N^2)
**空间复杂度:**O(N^2)
注意点:
从第r行开始遍历,f不需要初始化;从r-1行开始遍历,需要先把f初始化一下,即将输入的最后一行的值赋值给f。
数塔问题-同类题型:
HDU-1176 免费馅饼
状态定义:
dp[i][j]:第i秒走到第j位置拿到的馅饼数
状态转移方程:
dp[i][j]=max({dp[i+1][j-1],dp[i+1][j],dp[i+1][j+1]})+a[i][j];
因为它是从(0,5)位置(这一个点)开始走的,然后(按照数塔模型)我们反着推回去,(i,j)应该有(i+1,j-1/j/j+1)推得。
参考代码:
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,x,t,a[N][13],dp[N][13];
int main()
{
while(cin >> n)
{
if(n==0) break;
int tm=0;
memset(dp,0,sizeof dp);
memset(a,0,sizeof a);
for(int i=1;i<=n;++i){
cin >> x >> t;
a[t][x+1]++;
tm=max(t,tm); //找到最大值
}
for(int i=tm;i>=0;--i)
{
for(int j=1;j<=11;++j)
{
dp[i][j]=max({dp[i+1][j-1],dp[i+1][j],dp[i+1][j+1]})+a[i][j];
}
}
cout << dp[0][6] << endl;
}
}
学习一些好的代码写法:
这里在保存时,是写成a[t][x+1]++;
而不是a[t][x]++;
,并且数组也开得稍微大了些,意在于使得代码写起来更加简洁。在后面状态转移时会涉及 dp[i+1][j-1]
,如果前面从0开始保存,那么后面需要多写一些条件判断,从1开始保存,使得后面写起来更简洁。
我的另一种一开始的写法:(感觉好像有些奇怪,但是对了)
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,x,T,a[N][13],dp[N][13];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
while(cin >> n)
{
if(n==0) break;
memset(a,0,sizeof a);
memset(dp,0,sizeof dp);
int tm=1;
for(int i=1;i<=n;++i)
{
cin >> x >> T;
tm=max(tm,T);
a[T][x+1]++;
}
int mx=0;
for(int i=1;i<=tm;i++)
{
for(int j=1;j<=11;++j)
{
if(abs(j-6)<=i)
{
dp[i][j]=max({dp[i-1][j-1],dp[i-1][j],dp[i-1][j+1]})+a[i][j];
mx=max(dp[i][j],mx);
}
}
}
cout << mx << endl;
}
}
类似题目:
AtCoder Beginner Contest 266 D - Snuke Panic (1D)
最大字段和问题:
状态定义:
dp[j]:表示分为i组时,以j结尾的最大值
pre[j]:表示前j个数分为i-1组时的最大值
状态转移方程:
dp[j]=max(dp[j-1],pre[j-1])+s[j];
参考代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e6+10;
ll n,m,s[N],dp[N],pre[N];
int main()
{
while(cin >> m >> n)
{
for(int i=1;i<=n;++i) cin >> s[i];
memset(dp,0,sizeof dp);
memset(pre,0,sizeof pre);
ll mx=0;
for(int i=1;i<=m;++i)
{
/*
dp[j]:表示分为i组时,以j结尾的最大值
pre[j]:表示前j个数分为i-1组时的最大值
*/
mx=LLONG_MIN;
for(int j=i;j<=n;++j)
{
dp[j]=max(dp[j-1],pre[j-1])+s[j];
pre[j-1]=mx;
mx=max(mx,dp[j]);
}
}
cout << mx << endl; //这里不是写 dp[n] //是mx保留了分为i组时的最大值
}
}