最长公共子序列:
如abcdefg acqweg,最长公共子序列就是aceg,长度为4。子序列是不需要连续的。
详情可看HDU1159。
我们可以根据递推的方式得到最长的长度,如果想得到子序列也只需要记忆化一下每个点是从哪个点递推得到的即可。
我还是习惯写正向递推,显然dp[i][j] = max( dp[i - 1][j - 1] + (str1[i] == str2[j] ?1:0), max(dp[i-1][j], dp[i][j - 1]) )。
写出这个递推后再想,就能明白:1.这个dp[i][j]统计的是str1的前i个和str2的前j个的最长公共长度是多少。
2.可以由不使用第i个和第j个字符的最大情况推出,就是dp[i - 1][j - 1]。
3.也可以是只使用了第i个字符或者只使用了第j个字符的最大情况推出。
所以代码就是这样子的:
1.这里我习惯写成了滚动数组,其实第i层只需要第i-1层的状态就可以推出了,写滚动数组可以节省空间。
2.我自己检测这种递推式的方法就是把矩阵输出出来,这样有利于分析bug。
3.如果写成了滚动数组就没有办法找路径输出最长子序列啦。必须开n*n的矩阵才能存。
#includeusing namespace std; const int maxn = 1e5 + 5; char str1[maxn], str2[maxn]; int dp[2][maxn]; int main() { while(scanf("%s%s", str1, str2) != EOF) { int key = 0, len1 = strlen(str1), len2 = strlen(str2); for(int i = 1; i <= len2; ++i) { if(str1[0] == str2[i - 1]) dp[1][i] = 1; else dp[1][i] = dp[1][i - 1]; } for(int i = 2; i <= len1; ++i) { for(int j = 1; j <= len2; ++j) { dp[key][j] = max(dp[!key][j - 1] + (str1[i - 1] == str2[j - 1] ? 1 : 0), max(dp[!key][j], dp[key][j - 1])); // cout << dp[key][j] << ' '; } // cout << '\n'; key = !key; } printf("%d\n", dp[!key][len2]); } return 0; }
最长递增子序列:
比如1 7 5 6 8 2 3 9,那么最长递增子序列就是1 5 6 9或者1 2 3 9,长度为4.
详情可看POJ2533。
方法一:
与上面题目不同的是,此时只在一个数组里面寻找递增,而不是两个数组。
这时候我们可以创造一个数组出来,先复制一下原来的数组,然后排一下序,这就有对比了,然后再跑LCS。
如果我们把1 7 5 6 8 2 3 9和(排序后)1 2 3 5 6 7 8 9进行最长公共子序列的对比,那么就会得到1 5 6 9或者1 2 3 9.
但是要注意是否严格递增,如果严格递增,则需要去重再比较,否则对1 5 3 3 3和1 3 3 3 5进行LCS会识别出1 3 3 3这种情况导致答案不是严格递增的。
可以对比一下和上面代码的区别:
#include#include #include #include using namespace std; const int maxn = 1e5 + 5; int n; int a[maxn], b[maxn]; int dp[2][maxn]; int main() { while(scanf("%d", &n) != EOF) { int key = 0; memset(dp, 0, sizeof(dp)); for(int i = 0; i < n; ++i) { scanf("%d", &a[i]); b[i] = a[i]; } sort(b, b + n); int m = unique(b, b + n) - b; for(int i = 1; i <= n; ++i) { for(int j = 1; j <= m; ++j) { dp[key][j] = max(dp[!key][j - 1] + (a[i - 1] == b[j - 1] ? 1 : 0), max(dp[!key][j], dp[key][j - 1])); // cout << dp[key][j] << ' '; } // cout << '\n'; key = !key; } printf("%d\n", dp[!key][m]); } return 0; }
但是这种方法时间复杂度依然是O(n*m+nlogn)的,复杂度并不稳定,更多的情况是n*m的。
方法二:
实际上,最长递增子序列LIS是有稳定的O(nlogn)解法的。
LIS跟LCS最大的不同在于,LIS找的是数字递增的最大序列。
一个递增的序列,有序,常见的log的来源就是二分了。
所以方案为,开一个长度为n的数组,第i个元素代表长度为i时第i个位置最小值是多少。第i个位置的最小值越小,后面增长的可能性才越大。
比如,对于1 3 2 9 3 7:
我们在判断前两个时候,第一个位置最小值肯定是1,第二个位置最小值是3。
此时加入2,二分发现大于等于2对应的位置是3。也就是说在1 3 2里面长度为2的第二个元素最小值是2.
二分9,得到的位置是2的后面,所以直接插入,此时最长序列为1 2 9。
二分3,得到的位置还是2的后面,而且比9小,所以更新为1 2 3。
此时二分7,得到的位置是3的后面,所以直接插入,得到1 2 3 7。
那么就得到了最长序列长度为4。
代码如下:
#include#include #include #include using namespace std; const int maxn = 1e5 + 5; int n, idx; int a[maxn]; int dp[maxn]; int main() { while(scanf("%d", &n) != EOF) { memset(dp, 0, sizeof(dp)); idx = 1; for(int i = 0; i < n; ++i) { scanf("%d", &a[i]); } dp[0] = a[0]; for(int i = 1; i < n; ++i) { int p = lower_bound(dp, dp + idx, a[i]) - dp; if(p == idx) { dp[idx++] = a[i]; } else { dp[p] = a[i]; } } printf("%d\n", idx); } return 0; }
那么如果要求的不是严格递增的序列,也就是1 2 2 2 5也就可行的,那么上面要改哪里呢?
答案就在lower_bound,lower_bound(起始位置,终止位置,查找的数值)返回的是大于等于查找的数值的第一个位置,这样就会导致如果当前是1 4,那么后面查找4,他依然指向第二个位置,把原来的4替换成新的4,这样就会保证严格递增。
STL库里面还有一个函数叫upper_bound,返回的是大于查找的数值的第一个位置,如果当前是1 4 X,然后再查找4,那么他就会指向X的位置,直接塞进去即可。