线性dp
线性DP,是较常见的一类动态规划问题,其是在线性结构上进行状态转移,这类问题不像背包问题、区间DP等有固定的模板
线性动态规划的目标函数为特定变量的线性函数,约束是这些变量的线性不等式或等式,目的是求目标函数的最大值或最小值
LCS问题——最长公共子序列
子序列
: 指的是字符串中不一定连续 但先后顺序一致的n个字符字符子串
:指的是字符串中连续的n个字符最长公共子序列
,英文缩写为LCS(Longest Common Subsequence)。其定义是: 一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。
问题描述:给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。
状态表示
集合表示:所有 $ A[1,n] $和 $ B[1,m] $ 的公共子序列的集合
dp[i][j]代表以s1[i],s2[j]结尾的LCS的长度
属性:公共子序列长度的最大值
状态计算
找集合中所有情况的共同点和不同点来划分集合
该公共子序列分为四种情况:包括a[n] , b[m] ,不包括a[n] ,包括b[m] ,不包括a[n] , b[m] ,包括a[n] ,不包括b[m] (可以用二进制0 1来表示)
$ f[i][j] $:A前i个字符,B前j个字符的公共子序列 的集合 属性:maxlen
集合划分情况(假定)
(1) $ f[i-1][j-1] + 1 $ 同时包括 a[n] 和 b[m] (前提是a[n] == a[m])
(2) $ f[i-1][j] $ 不包括a[n] ,包括b[m]
(3) $ f[i][j-1] $ 包括a[n] ,不包括b[m]
(4) $ f[i-1][j-1] $ a[n] 和 b[m] 都不包括
集合划分情况(实际)
f[i-1][j-1]+1 可以表示情况1 –> a
f[i][j-1]=max(情况2,情况4) –> b
f[i-1][j]=max(情况3,情况4) –> c 所以我们最终只需要 求 max(a,b,c) 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 \\最长公共子序列#include <bits/stdc++.h> using namespace std;const int N = 1010 ;char a[N],b[N]; int n,m,f[N][N];int main () { cin >> n >> m; for (int i=1 ;i<=n;i++){ cin >> a[i]; } for (int i=1 ;i<=m;i++){ cin >> b[i]; } for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=m;j++){ f[i][j] = max (f[i][j-1 ],f[i-1 ][j]); if (a[i] == b[j]) f[i][j]=max (f[i][j],f[i-1 ][j-1 ]+1 ); } } cout << f[n][m]; }
LIS问题——最长上升子序列
最长上升子序列
(Longest Increasing Subsequence),简称LIS,也有些情况求的是最长非降序子序列,二者区别就是序列中是否可以有相等的数,对于固定的数组,虽然LIS序列不一定唯一,但LIS的长度是唯一的
状态设计:$dp[i]$ 代表以 $a[i]$ 结尾的LIS的长度
状态转移:$dp[i]=max{dp[j]+1,dp[i]} (1<=j< i,a[j]<a[i])$
边界处理:$dp[i]=1(1<=i<=n)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> #include <algorithm> using namespace std;const int N = 1010 ;int a[N],n;int f[N]; int main () { int ans = 0 ; cin >> n; for (int i =1 ;i<=n;i++){ cin >> a[i]; f[i] = 1 ; } for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=i-1 ;j++){ if (a[j] < a[i]) f[i] = max (f[i],f[j]+1 ); } ans = max (ans,f[i]); } cout << ans; return 0 ; }
求最长子序列的路径 核心思想:使用g数组存储序列中每个元素下标的上一个元素的下标g[i] = j;
: 最长子序列中下标为 i 的元素 的 上一个元素下标为 j
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <iostream> #include <algorithm> using namespace std;const int N = 1e3 +10 ;int n,a[N],g[N],f[N]; int main () { cin >> n; for (int i=1 ;i<=n;i++){ cin >> a[i]; } for (int i=1 ;i<=n;i++){ f[i] = 1 ; g[i] = 0 ; for (int j=1 ;j<=i-1 ;j++){ if (a[i] > a[j]){ f[i] = max (f[i],f[j]+1 ); if (f[i] == f[j]+1 ){ g[i] = j; } } } } int res = -1 ; int k; for (int i=1 ;i<=n;i++){ if (f[i] > res){ res = f[i]; k = i; } } for (int t=0 ;t<res;t++){ cout << a[k] << " " ; k = g[k]; } return 0 ; }
最长回文子序列
题目链接:密码脱落
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <bits/stdc++.h> using namespace std;const int N = 1010 ;int f[N][N];char s[N];int main () { ios::sync_with_stdio (0 ); cin.tie (0 ); cout.tie (0 ); cin >> s; int n = strlen (s); for (int len=1 ;len<=n;len++){ for (int l=0 ; len+l-1 < n; l++){ int r = len+l-1 ; if (len == 1 ){ f[l][r] = 1 ; } else { f[l][r] = max (f[l][r-1 ],f[l+1 ][r]); if (s[l] == s[r]) f[l][r] = max (f[l][r],f[l+1 ][r-1 ] + 2 ); } } } cout << n - f[0 ][n-1 ]; return 0 ; }
得到具体的回文字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution {public : string longestPalindrome (string s) { int n = s.size (); if (n == 0 ) return "" ; bool f[n+1 ][n+1 ]; for (int i = 0 ; i < n; ++i) { f[i][i] = true ; } int start = 0 , maxLen = 1 ; for (int len = 2 ; len <= n; ++len) { for (int i = 0 ; i + len - 1 < n; ++i) { int j = i + len - 1 ; if (s[i] == s[j] && (len < 3 || f[i + 1 ][j - 1 ])) { f[i][j] = true ; maxLen = len; start = i; } else { f[i][j] = false ; } } } return s.substr (start, maxLen); } };