也是小良的博客,之前写过了就不写重复的了
动态规划2:从一道题讲最长上升子序列、最长公共子序列、最长公共子串题1:HDUOJ 1257 最少拦截系统(有坑)
算法分析:
这个题目我wa了3发……确实有点坑,我们来细品……
这个题目说,第一发炮弹可以达到任意的高度,只是后面的炮弹不得高于这个高度,问依次来n枚导弹,最少需要多少个拦截系统来拦截这些导弹
第一次误解:
我一开始以为,一个拦截系统,没有时效性,第一次的炮弹击落了第一个导弹之后,后面的导弹如果高度低于前一个,那么还可以继续用这个系统,但是如果高于前一个,则需要增加一个系统,那么根据样例:
8
389 207 155 300 299 170 158 65
我们需要两个拦截系统:
第一个系统:389 207 155
第二个系统:300 299 170 158 65
当时心想,这TM也忒简单了吧!
然后五行代码提交……wa的无地自容
正解:
构造出一个反例:
8
7 6 5 6 3 2 4 1
如果按照第一个思路,就应该输出3,如下:
第一个系统:7 6 5
第二个系统:6 3 2
第三个系统:4 1
但是,别人告诉我(哎,我又理解错题意了):这样应该输出2
因为导弹高度为4的可以被第一个系统击落,因为4小于5,然后1可以被第二个系统击落,因为1小于2……好坑啊
重新整理思路:
每次输入一个导弹的高速,我们都应该去扫一遍已知系统的当前高度,然后去寻找合适的一个系统去拦截,如果没有这样的系统,则需要重新增加一个系统。
那么什么系统是合适的系统呢?首先,必然这个系统的当前高度要不低于这个导弹的高度,不然就无法拦截,其次,如果有多个系统满足条件,我们应该怎么选取呢?
贪心策略:
再次看这个样例:
8
389 207 155 300 299 170 158 65
系统调度:
第一个系统:
389 207 155
第二个系统:
300 299 170 158
然后 65 这个导弹应该放到哪里去呢?显然这个导弹的高度,可以由系统2或者系统1去拦截,假设使用系统2拦截,则第二个系统的高度变成:65,第一个系统高度是155,如果此时再来一个导弹,高度是157,则无法实施拦截,只能再增加一个系统,但是如果是把65交给第一个系统拦截,则两个系统的高度为:65 158,这样就可以很好的拦截可能出现的:157高度的导弹,所以我们贪心的策略就是:
选择在满足可以拦截的系统中,当前高度最小的那一个,因为要维护最强拦截系统
AC代码:
#include <cstdio>
const int maxn = 1e4 + 5;
int n, h[maxn], boom;
int main(){
while(~scanf("%d", &n)){
int cnt = 1;
scanf("%d", &boom);
h[cnt] = boom; n--;
while(n--){
scanf("%d", &boom);
int pos = 0, val = 30005, isVis = 0;
for(int i = 1;i <= cnt;i++){
if(boom <= h[i]){
if(val > h[i]){
val = h[i];
pos = i;
isVis = 1;
}
}
}
if(isVis)
h[pos] = boom;
else
h[++cnt] = boom;
}
printf("%d\n", cnt);
}
return 0;
}
另一个思路:最长递增子序列
实际上是在玩最长不增子序列,是LIS的应用,再看那个样例:
8
389 207 155 300 299 170 158 65
首先设置一个标记数组:vis[] = {0};
然后跑一遍最长不增子序列:
序列1:389 300 299 170158 65,然后他们都标记成已经被访问
序列2:207 155
再看看那个坑例子:
8
7 6 5 6 3 2 4 1
序列1:7 6 5 3 2 1
序列2:6 4
我们发现,貌似最长不增子序列的方法更优秀!
但是,它的实现是比较困难的, 因为我们需要跑很多次最长不增子序列,还需要去检查,直到所有数据都被访问(所有导弹都被拦截)
新思路:这个比较难想,我也是看书知道的
观察样例中的第一个最长不增子序列:
X{389,300,299,170,158,65}
Y{207,155}
我们可以发现:
在Y中必然至少有一个数a比X中某个数大,否则Y中的所有数都比X中的数小,这样的话,Y中这个数a就应该在X中。那么如果顺着看,求最长递增子序列,每个不增子序列都能且只能提供一个元素,于是最长递增子序列的规模即是答案。
原因是:
对于任意两个最长不增子序列的集合,都满足我上述的性质,所以假设有两枚导弹 x,y 来自同一个拦截系统,则 x > y 于是与递增违背
AC代码:
#include <cstdio>
const int maxn = 1e4 + 5;
int n, h[maxn];
int LIS(){
int ans = 1, dp[maxn];
dp[1] = 1;
for(int i = 2;i <= n;i++){
int MaxVal = 0;
for(int j = 1;j < i;j++)
if(h[j] < h[i] && MaxVal < dp[j])
MaxVal = dp[j];
dp[i] = MaxVal + 1;
if(dp[i] > ans)
ans = dp[i];
}
return ans;
}
int main(){
while(~scanf("%d", &n)){
for(int i = 1;i <= n;i++)
scanf("%d", &h[i]);
printf("%d\n", LIS());
}
return 0;
}
分析最长递增子序列算法
如何求一个序列的最长递增子序列?
算法1:复用最长公共子序列
已知序列:389 207 155 300 299 170 158 65
排好序: 65 155 158 170 207 299 300 389
最长公共子序列:155 170 或 155 158 等等
所以得出结论:
排好序的序列和原序列的最长公共子序列就是最长递增子序列
求最长公共子序列状态转移方程:
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]);
LCS代码:
#include <iostream>
#include <algorithm>
using namespace std;
int LCS(int a[], int b[], int n){
int dp[100][100] = {0}, ans = 0;
for(int i = 1;i <= n;i++){
for(int j = 1;j <= n;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]);
if(dp[i][j] > ans)
ans = dp[i][j];
}
}
return ans;
}
int main(){
int n, a[100] = {0}, b[100] = {0};
cin >> n;
for(int i = 1;i <= n;i++){
cin >> a[i];
b[i] = a[i];
}
sort(b + 1, b + n + 1);
cout << LCS(a, b, n);
return 0;
}
算法二:求最长递增子序列
设状态:dp[i]是以第 i 个元素结尾的最长递增子序列
则状态转移:dp[i] = max(0, dp[j]) + 1; 1 <= j < i && a[j] < a[i];
意思就是,只要前面的序列有比你小的元素,下标为 j ,那么你当前的状态就可以由 dp[j]转移得到,然后遍历所有你之前的状态,取最优子结构作为你的上一个状态去转移即可。
核心LIS代码:
int LIS(){
int ans = 1, dp[maxn];
dp[1] = 1;
for(int i = 2;i <= n;i++){
int MaxVal = 0;
for(int j = 1;j < i;j++)
if(h[j] < h[i] && MaxVal < dp[j])
MaxVal = dp[j];
dp[i] = MaxVal + 1;
if(dp[i] > ans)
ans = dp[i];
}
return ans;
}
最长递增子序列再优化:单调栈+二分搜索
我们维护一个单调栈,但是与一般单调栈问题处理的方式不一样,我们遇到失去单调性的元素的时候,我们用二分法(因为栈内元素单调)去栈内查找最优的替换策略,什么替换策略是最优的呢?自然是替换在不破坏单调性的情况下,能够替换的最大的数,这个数就是在栈中,比它大的最小的数,这个大家细细品味。因为这样替换之后,整个当前序列的数值下降最大,只有当前数值减少,后面才能加入更多的数。
#include <iostream>
using namespace std;
const int maxn = 1e5 + 5;
int n, a, Stack[maxn], cnt;
int main(){
cin >> n;
for(int i = 1;i <= n;i++){
cin >> a;
if(a > Stack[cnt] || !cnt)
Stack[++cnt] = a;
else{
int l = 1, r = cnt;
while(l <= r){
int mid = (r + l) >> 1;
if(a <= Stack[mid]) // 搜索第一个出现的比 a 大的数,也就是比a大的最小的数
r = mid - 1;
else
l = mid + 1;
}
Stack[l] = a; // 用 a 替换!
}
}
cout << cnt << endl;
return 0;
}
最后讲一下最长公共子串
子串和子序列有什么区别???
子序列可以不连续,但是子串务必要连续!所以子串的状态转移方程就在子序列的基础上稍微修改即可:
if(a[i] == b[j])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = 0;