最长上升子序列
给定一个长度为 nn 的数组 a1,a2,…,ana1,a2,…,an,问其中的最长上升子序列的长度。也就是说,我们要找到最大的 mm 以及数组 p1,p2,…,pmp1,p2,…,pm,满足 1≤p1<p2<⋯<pm≤n1≤p1<p2<⋯<pm≤n 并且 ap1<ap2<⋯<apmap1<ap2<⋯<apm。
输入格式
第一行一个数字 nn。
接下来一行 nn 个整数 a1,a2,…,ana1,a2,…,an。
输出格式
一个数,表示答案。
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
int a[1001];
int dp[1001];//定义dp表表示以a[i]为结尾的最长上升子序列
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
}
dp[1] = 1;//到1的时候最长上升子序列只有1个
for(int i = 2; i <= n; i++)
{
dp[i] = 1;//首先自己至少就是1
for(int j = 1; j < i; j++)
{
if(a[i] > a[j])
{
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
int res = 0;
for(int i = 1; i <= n; i++)
{
res = max(res, dp[i]);
}
cout << res;
return 0;
}
顺着这一道题,我们要深刻的理解一下根据无后效性来定义状态
无后效性最通俗易懂的方法就是:过去不影响未来,只要求出了过去的某一个值,那么根未来就没有关系了。这个就是无后效性。刚做这一道题的时候我想的状态转移方程是:到达i位置的时候的最长上升子序列。这个状态的定义是错误的,原因是我如果要到第i位置,那么我就必须知道前面有多少个数字比我现在要大,也就是说我到达i位置的时候我还必须回头看看过去的情况,看看以前有多少比我小,这个就是违反了无后效性的。所以正确的状态转移方程是:以a[i]结尾的最长上升子序列,为什么这个是对的,因为假如我以及求出了以a[i - 1]结尾的最长上升子序列的话,那么只需要再判断这个数字是否要小,就可以直接转移出i位置的,不需要了解过去的情况。
此题还有一个很关键的点把我卡住了,就是我到达i位置的时候要回溯搜索小于i位置的所有位置,这个套路很多题也会使用
最长上升字串
和上面一道题一模一样,只不过我们改变一下,把序列改成字串。
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
int a[1001];
int dp[1001];//定义dp表表示以i结尾的最长上升字串
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
}
int res = 0;//用一个变量来记录最大值
dp[1] = 1;//1位置最长上升字串就是它自己
for(int i = 2; i <= n; i++)
{
dp[i] = 1;//这里代表的是清0的过程
if(a[i] < a[i - 1])
{
res = max(res, dp[i - 1]);
}
else
{
dp[i] = dp[i - 1] + 1;
}
}
cout << res;
return 0;
}
这里的dp含义就可以定义为以i结尾了。
最长公共子序列
给定一个长度为 n 的数组 a1,a2,…,an 以及一个长度为 m 的数组 b1,b2,…,bm,问 a 和 b 的最长公共子序列的长度。
也就是说,我们要找到最大的 k 以及数组 p1,p2,…,pk,数组 l1,l2,…,lk 满足 1≤p1<p2<⋯<pk≤n 并且 1≤l1<l2<⋯<lk≤m 并且对于所有的 i(1≤i≤k) ,api=bli。
输入格式
第一行两个整数 n,m。
接下来一行 n 个整数,a1,a2,…,an。
接下来一行 m 个整数,b1,b2,…,bm。
输出格式
输出一个整数,表示答案。
先来看代码:
#include<iostream>
#include<algorithm>
using namespace std;
int a[1001];
int b[1001];
int dp[1001][1001];//定义dp表,表示前a数组前i的位置和b数组前j的位置的最大公共子序列
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> a[i];
for(int i = 1; i <= m; i++)
cin >> b[i];
//当i或者j有一个是0的时候,最长公共子序列根本不存在,所以从1开始可以很巧妙的避免补条件
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
if(a[i] == b[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];
return 0;
}
这道题的dp表的含义是:dp[i][j],表示a数组前i个和b数组前j个的最长公共子序列。
转移:如果a[i] == b[j]的话那么说明可以多一个公共子序列,否则的话就缩小范围继续查找。dp[i - 1] [j]或者dp[i] [j - 1]里面的最大值。这里我们定义的含义是以下标i和下标j为计数的。所以我们必须要从int i = 1的时候开始,0表示我这个时候没有序列,但是如果我们做leetcode里面的oj的话,那么我们必须从0开始,这个时候我们就要处理边界情况了,但是我不想处理边界情况,所以出现了下面这个很巧妙的写法:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int N = text1.size();//这里直接对应的就是最后一个字母的下标
int M = text2.size();
vector<vector<int>> dp(N + 1, vector<int>(M + 1));
for(int i = 1; i <= N; ++i)
{
for(int j = 1; j <= M; ++j)
{
if(text1[i - 1] == text2[j - 1])
{
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else
{
dp[i][j] = max(dp[i][j], max(dp[i - 1][j], dp[i][j - 1]));
}
}
}
return dp[N][M];
}
};
这里dp表的定义是长度为i和长度为j的最长公共子序列。所以我们比较的时候比较下标应该是tex1[i - 1] == text2[j - 1]最后返回dp[N] [M].这就是巧妙的地方。
最长回文字串
#include<iostream>
#include<algorithm>
using namespace std;
int n;
int a[1001];
int dp[1001][1001];//定义dp表的含义是(i, j)范围之内的最长回文字串的长度
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> a[i];
int res = 1;//res用来记录最长的回文字串的长度
for(int i = 1; i <= n; i++)
dp[i][i] = 1;//只有一个数字的时候最长回文字串就是它自己,为1
for(int i = n - 1; i >= 1; i--)
{
dp[i][i + 1] = a[i] == a[i + 1] ? 2 : 0;
for(int j = i + 2; j <= n; j++)
{
if(a[i] == a[j] && dp[i + 1][j - 1])
{
dp[i][j] = dp[i + 1][j - 1] + 2;
}
else
{
dp[i][j] = 0;
}
res = max(res, dp[i][j]);
}
}
cout << res;
return 0;
}
这道题应该话表格方便来理解,然后就是注意这里的条件判断。
最长回文子序列
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<vector<int>> dp(n + 1, vector<int>(n + 1));
for(int i = 0; i < n; i++)
{
dp[i][i] = 1;
dp[i][i + 1] = s[i] == s[i + 1] ? 2 : 1;
}
for(int i = n - 3; i >= 0; i--)
{
for(int j = i + 2; j < n; j++)
{
//第一种可能,
if(s[i] == s[j])
{
dp[i][j] = dp[i + 1][j - 1] + 2;
}
else
{
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}
};
回文问题与序列问题总结
对比上面的回文序列和子串问题可以发现:如果是子串问题的话,涉及到子串问题的题目都有一个清0和记录最大值的过程,如果是子序列问题的话就不需要了。