子序列:可以通过删除原序列中一些元素获得的序列。
子串:原序列中连续的一段。
子序列和子串的区别:子序列不需要连续,子串是连续的。如abcdef中acf是子序列,bcd是子串。显然子串都是子序列。
参考:Longest increasing subsequence
n^2做法
POJ-2533 Longest Ordered Subsequence
求最长上升 子序列,数据范围N<=1000,n^2复杂度可过。
假设f[i]表示以a[i]结尾的最长上升子序列长度,对每个f[i],在前面找一个比a[i]小的数a[j],更新f[i] = max(f[i], f[j] + 1),最终最大的f[i]即为答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> #include <algorithm> using namespace std ;const int N = 1005 ;int a[N], f[N];int main () { int n; cin >> n; for (int i = 1 ; i <= n; i++) cin >> a[i]; int ans = 1 ; for (int i = 1 ; i <= n; i++) { f[i] = 1 ; for (int j = 1 ; j < i; j++) { if (a[j] < a[i] && f[j] + 1 > f[i]) { f[i] = f[j] + 1 ; } } ans = max(ans, f[i]); } cout << ans << '\n' ; return 0 ; }
nlgn做法
POJ-3903 Stock Exchange
题意一样,数据范围变成了10^5,显然n^2算法不能过了,需要使用nlogn算法。
假设f[i]表示长度为i的上升子序列的末尾元素的最小值。ans表示目前最长上升子序列的长度。初始值ans=1,f[1]=a[1],依次考虑每个元素i,如果$a[i]>f[ans]$ ,说明a[i]可以接在目前最长的上升子序列的末尾,答案+1。否则,答案不会更优。但是a[i]可以替换掉f数组中第一个大于等于 a[i]的值所代表上升子序列的末尾元素,因为a[i]比他更小,所以更有可能使得答案变大。注意这个位置是a[i]唯一能替换的元素,小于a[i]的,替换后只会使答案可能变小,不是第一个大于等于a[i]的,不符合上升序列的定义。注意到f数组单调递增,因此可以使用二分查找。
参考:最长不下降子序列nlogn算法详解
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 #include <iostream> #include <algorithm> using namespace std ;const int N = 1e5 + 5 ;int a[N], f[N];int main () { ios::sync_with_stdio(false );cin .tie(0 ); int n; while (cin >> n) { for (int i = 1 ; i <= n; i++) { cin >> a[i]; } int ans = 1 ; f[1 ] = a[1 ]; for (int i = 2 ; i <= n; i++) { if (a[i] > f[ans]) { ans++; f[ans] = a[i]; } else { f[lower_bound(f + 1 , f + 1 + ans, a[i]) - f] = a[i]; } } cout << ans << '\n' ; } return 0 ; }
最长公共子序列(LCS)
HDU-1159 Common Subsequence
求两个串的最长公共子序列,数据范围小于1000,n^2算法可过。
假设两个串为a、b,f[i][j]表示a串前i个字符与b串前j个字符的最长公共子序列长度。依次枚举i、j,若a[i] = b[j],则f[i][j] = f[i - 1][j - 1] + 1,否则,f[i][j] = max(f[i - 1][j], f[i][j - 1])。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <bits/stdc++.h> using namespace std ;const int N = 1005 ;char a[N], b[N];int f[N][N];int main () { while (cin >> a + 1 >> b + 1 ) { int la = strlen (a + 1 ), lb = strlen (b + 1 ); for (int i = 1 ; i <= la; i++) { for (int j = 1 ; j <= lb; j++) { if (a[i] == b[j]) { f[i][j] = f[i - 1 ][j - 1 ] + 1 ; } else { f[i][j] = max(f[i][j - 1 ], f[i - 1 ][j]); } } } cout << f[la][lb] << '\n' ; } return 0 ; }
LCS转LIS
Luogu-1439 【模板】最长公共子序列
数据范围10^5,因此不能用n^2的算法。
假设两个序列为a、b,构造一个新的序列c,c[i]表示b[i]在a[i]中出现的位置,即b[i] = a[c[i]],则c序列的LIS即为a、b的LCS。例如:a = [3, 1, 2, 5, 4],b = [4, 2, 3, 1, 5],则c = [5, 3, 1, 2, 4],c的LIS为[1, 2, 4]对应a,b的LCS为[3, 1, 5]。
注意:这种转换有特殊条件,只有在b中的元素能匹配较少的a中的元素时才有效,否则时间反而更慢。
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 #include <bits/stdc++.h> using namespace std ;const int N = 1e5 + 5 ;int a[N], b[N], f[N];int main () { int n; cin >> n; for (int i = 1 ; i <= n; i++) { int x; cin >> x; a[x] = i; } for (int i = 1 ; i <= n; i++) { int x; cin >> x; b[i] = a[x]; } int ans = 1 ; f[1 ] = b[1 ]; for (int i = 2 ; i <= n; i++) { if (b[i] > f[ans]) { ans++; f[ans] = b[i]; } else { f[lower_bound(f + 1 , f + 1 + ans, b[i]) - f] = b[i]; } } cout << ans << '\n' ; return 0 ; }
参考:LIS 问题与 LCS 问题可以互相转换
把最长公共子序列LCS问题转化为最长上升子序列LIS问题
Junior Dynamic Programming——动态规划初步·各种子序列问题
LCS转为LIS
UVA-10635 Prince and Princess
和Luogu-1439差不多,代码改改就能过了。
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 #include <bits/stdc++.h> using namespace std ;const int N = 250 * 250 + 5 ;int a[N], b[N], f[N];int main () { int T; cin >> T; for (int t = 1 ; t <= T; t++) { int n, p, q; cin >> n >> p >> q; for (int i = 1 ; i <= p + 1 ; i++) { int x; cin >> x; a[x] = i; } n = 0 ; for (int i = 1 ; i <= q + 1 ; i++) { int x; cin >> x; if (a[x]) b[++n] = a[x]; } int ans = 1 ; f[1 ] = b[1 ]; for (int i = 2 ; i <= n; i++) { if (b[i] > f[ans]) { ans++; f[ans] = b[i]; } else { f[lower_bound(f + 1 , f + 1 + ans, b[i]) - f] = b[i]; } } cout << "Case " << t << ": " << ans << '\n' ; } return 0 ; }
Luogu-4303 【AHOI2006】基因匹配
和上一题同样的思路,构造新序列b,数组a用来记录第一个序列中的元素出现的位置。关键在代码第19行,for (int j = c[x] - 1; j >= 0; j–) ,这里是逆序构造的。至于为什么要逆序,可以参考最长公共子序列是否存在低于 O(n^2) 的算法? 这个答案。简单说一下就是:之前构造的新序列c中的每一个元素对应一个b中的元素,如果有多个对应,则这些元素就形成了一个组。即每一组只能选一个元素 ,为了方便实现,直接将每一组逆序。
参考:【AHOI2006】基因匹配 Match
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 #include <bits/stdc++.h> using namespace std ;const int N = 1e5 + 5 ;int a[N][5 ], c[N], b[N * 5 ], f[N * 5 ];int main () { int n; cin >> n; n *= 5 ; for (int i = 1 ; i <= n; i++) { int x; cin >> x; a[x][c[x]] = i; c[x]++; } int bn = 0 ; for (int i = 1 ; i <= n; i++) { int x; cin >> x; for (int j = c[x] - 1 ; j >= 0 ; j--) { bn++; b[bn] = a[x][j]; } } int ans = 1 ; f[1 ] = b[1 ]; for (int i = 2 ; i <= bn; i++) { if (b[i] > f[ans]) { ans++; f[ans] = b[i]; } else { f[lower_bound(f + 1 , f + 1 + ans, b[i]) - f] = b[i]; } } cout << ans << '\n' ; return 0 ; }
加权LIS
计蒜客 - A1288 The Heaviest Non-decreasing Subsequence Problem
在LIS的基础上引入权值,要求权值最大。
这种问题如果权值非常小的话,有一种简单的解决办法:
权值小于等于0的数不要选,即使权值全部为负数,也可以一个都不选,这样总权值为0反而更大。
将权值为n(正整数)的数拆分成n个数,构造一个新序列
求新序列的LIS即为原序列的最大权值
注意是优先长度还是优先权值
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 ;template <typename T, typename F>int LIS (T first, T last, F cmp) { if (first == last) return 0 ; int ans = 1 ; vector <int > f (1 , *first) ; for (T i = first + 1 ; i != last; i++) { if (cmp(f.back(), *i)) f.push_back(*i); else *lower_bound(f.begin(), f.end(), *i, cmp) = *i; } return f.size(); } int main () { vector <int > a; int x; while (cin >> x) { if (x < 0 ) continue ; if (x >= 10000 ) { x -= 10000 ; vector <int > t (5 , x) ; a.insert(a.end(), t.begin(), t.end()); } else { a.push_back(x); } } cout << LIS(a.begin(), a.end(), less_equal<int >()); return 0 ; }
二维
滑雪
就是求一个二维的LIS,假设f[i][j]表示以(i,j)结尾的LIS长度,显然f[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 #include <iostream> #include <vector> #include <algorithm> using namespace std ;const int N = 105 ;int h[N][N], f[N][N];const int dx[]{-1 , 0 , 1 , 0 }, dy[]{0 , 1 , 0 , -1 };struct node { int x, y; }; bool cmp (node u, node v) { return h[u.x][u.y] < h[v.x][v.y]; } int main () { int r, c; cin >> r >> c; vector <node> a; for (int i = 1 ; i <= r; i++) { for (int j = 1 ; j <= c; j++) { cin >> h[i][j]; node t; t.x = i; t.y = j; a.push_back(t); } } sort(a.begin(), a.end(), cmp); int ans = 1 ; for (int i = 0 ; i < a.size(); i++) { int x = a[i].x, y = a[i].y; f[x][y] = 1 ; for (int j = 0 ; j < 4 ; j++) { int xx = x + dx[j], yy = y + dy[j]; if (h[xx][yy] < h[x][y] && f[x][y] < f[xx][yy] + 1 ) { f[x][y] = f[xx][yy] + 1 ; if (f[x][y] > ans) ans = f[x][y]; } } } cout << ans << '\n' ; return 0 ; }
模板
将该算法抽象成一个模板函数,不依赖外部状态,同时支持数组与容器,支持不下降(传入less_equal<int>()
即可)。
上面部分题目用到了封装好的算法。
1 2 3 4 5 6 7 8 9 10 11 template <typename T, typename F>int LIS (T first, T last, F cmp) { if (first == last) return 0 ; int ans = 1 ; vector <int > f (1 , *first) ; for (T i = first + 1 ; i != last; i++) { if (cmp(f.back(), *i)) f.push_back(*i); else *lower_bound(f.begin(), f.end(), *i, cmp) = *i; } return f.size(); }
使用示例:
1 2 3 4 vector <int > a{1 , 4 , 2 , 2 , 3 };LIS(a.begin(), a.end(), less<int >()); int b[]{1 , 4 , 2 , 2 , 3 };LIS(b, b + 5 , less_equal<int >());
其他
LCS+位压缩 HDU-2253 Longest Common Subsequence Again
用之前的办法做了一下,MLE了,先留个坑在这里。
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 #include <bits/stdc++.h> using namespace std ;const int N = 3e4 + 5 ;char a[N], b[N];int main () { while (cin >> a >> b) { int la = strlen (a), lb = strlen (b); vector <int > fa[26 ]; for (int i = 0 ; i < la; i++) { fa[a[i] - 'A' ].push_back(i + 1 ); } vector <int > c (1 ) ; for (int i = 0 ; i < lb; i++) { auto &fb = fa[b[i] - 'A' ]; for (auto j = fb.rbegin(); j != fb.rend(); j++) { c.push_back(*j); } } int ans = 1 , n = c.size() - 1 ; vector <int > f (n + 1 ) ; f[1 ] = c[1 ]; for (int i = 2 ; i <= n; i++) { if (c[i] > f[ans]) { f[++ans] = c[i]; } else { f[lower_bound(f.begin() + 1 , f.begin() + 1 + ans, c[i]) - f.begin()] = c[i]; } } cout << ans << '\n' ; } return 0 ; }
贪心 LIS
先增后减子序列 CodeChef-GMB04 Mountain Engineering
这题更大的难点是需要统计这种序列的个数。