1.拦截导弹
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于 30000 30000 30000的正整数,导弹数不超 1000 1000 1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
共一行,输入导弹依次飞来的高度。
输出格式
第一行包含一个整数,表示最多能拦截的导弹数。
第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。
数据范围
雷达给出的高度数据是不大于
30000
30000
30000的正整数,导弹数不超过
1000
1000
1000。
输入样例:
389 207 155 300 299 170 158 65
输出样例:
6
2
题解
Dilworth 定理
狄尔沃斯定理(Dilworth’s theorem)亦称偏序集分解定理,该定理断言:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。此定理的对偶形式亦真,它断言:对于任意有限偏序集,其最长链中元素的数目必等于其最小反链划分中反链的数目
该定理在子序列问题上可表述为:把序列分成不升子序列的最少个数,等于序列的最长上升子序列长度。把序列分成不降子序列的最少个数,等于序列的最长下降子序列长度
该定理在二分图上等价于柯尼希定理:二分图最小点覆盖的点数等于最大匹配数
定理法
给定正整数序列,求最长不升子序列长度,以及能覆盖整个序列的不升子序列的最少个数
问题一套用 LIS模型即可求解。由 Dilworth 定理,问题二等价于 LIS长度
#include <cstdio>
#define max(a, b) ((a) > (b) ? (a) : (b))
#define rep(i, s, e) for (int i = s; i <= e; i ++)
#define N 1010
int a[N], f[N], g[N];
int main() {
int n = 1; while (~scanf("%d", a + n)) n ++; n --;
rep(i, 1, n) rep(j, 0, i - 1) {
bool down = a[j] >= a[i];
f[i] = max(f[i], down * f[j] + 1);
g[i] = max(g[i], !down * g[j] + 1);
}
int r1 = 0, r2 = 0;
rep(i, 1, n) r1 = max(r1, f[i]), r2 = max(r2, g[i]);
printf("%d\n%d\n", r1, r2);
return 0;
}
贪心法
对于每个数,既可以把它接到已有子序列后面,也可以建立一个新序列。要使子序列数最少,应尽量不建立新序列。此外,应让每个子序列的末尾尽可能大,这样能接的数更多。因为一个数若能接到小数后面,必然能接到大数后面,反之则不成立。根据这些想法,可总结出如下贪心流程:
从前往后扫描每个数,对于当前数
- 若现有子序列的结尾都小于它,则创建新子序列
- 否则,将它放到结尾大于等于它的最小数后面
证明
记 A A A为贪心解, B B B为最优解
- 贪心解能覆盖所有数,且形成的都是不升序列,因此合法。由定义, B ≤ A B≤A B≤A
- 假设最优解对应的方案和贪心方案不同,从前往后找到第一个不在同一序列的数 x x x。假设贪心解中 x x x前面的数是 a a a,最优解中 x x x 前面的数是 b b b, a a a 后面的数是 y y y,由于贪心会让当前数接到大于等于它的最小数后面,所以 x , y ≤ a ≤ b x,y≤a≤b x,y≤a≤b,此时,在最优解中,把 x x x 一直到序列末尾,和 y y y 一直到序列末尾交换位置,这样做不影响正确性,也不增加序列个数,但会使 x x x 在最优解和贪心解中所处的位置相同。由于序列中的数是有限的,只要一直做下去,一定能使最优解变为贪心解。因此 A ≤ B A≤B A≤B
综上 A = B A=B A=B
实现
用
g
g
g保存每条不升子序列的末尾,可用归纳法证明
g
g
g是单调上升的。初始时,
g
g
g为空满足条件。假设
g
g
g已经单调上升,现在要加入数
x
x
x,设
g
[
i
]
g[i]
g[i]是大于等于
x
x
x的最小数,则
g
[
i
−
1
]
<
x
≤
g
[
i
]
≤
g
[
i
+
1
]
g[i−1]<x≤g[i]≤g[i+1]
g[i−1]<x≤g[i]≤g[i+1]。由于
x
x
x要放到
g
[
i
]
g[i]
g[i] 后面,因此
g
[
i
]
g[i]
g[i] 会被更新成
x
x
x,之后满足
g
[
i
−
1
]
<
g
[
i
]
≤
g
[
i
+
1
]
g[i−1]<g[i]≤g[i+1]
g[i−1]<g[i]≤g[i+1],其它元素不受影响,整个序列仍单调上升。由此得证
可发现上述贪心实现,和最长上升子序列的贪心解法代码相同,这也印证了 Dilworth 定理
#include <cstdio>
#define max(a, b) ((a) > (b) ? (a) : (b))
#define rep(i, s, e) for (int i = s; i <= e; i ++)
#define N 1010
int a[N], f[N], g[N];
int main() {
int n = 1; while (~scanf("%d", a + n)) n ++; n --;
rep(i, 1, n) rep(j, 0, i - 1)
f[i] = max(f[i], (a[j] >= a[i]) * f[j] + 1);
int r = 0; rep(i, 1, n) r = max(r, f[i]);
printf("%d\n", r);
r = 0;
rep(i, 1, n) {
int j = 0; while (j < r && g[j] < a[i]) j ++;
g[j] = a[i]; r += j == r;
}
printf("%d\n", r);
return 0;
}
y总做法
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n;
int q[N];
int f[N],g[N];
int main()
{
while(cin >> q[n]) n++;
int res = 0;
for(int i = 0;i < n;i++)
{
f[i] = 1;
for(int j = 0;j < i;j++)
if(q[j] >= q[i])
f[i] = max(f[i],f[j] + 1);
res = max(res,f[i]);
}
cout<<res<<endl;
//第二问
int cnt = 0;//表示当前子序列的个数
for(int i = 0;i < n;i++)
{
int k = 0;//k是从前往后找的序列
while(k < cnt && g[k] < q[i]) k++;
g[k] = q[i];
if(k >= cnt)cnt++;
}
cout<<cnt<<endl;
return 0;
}
2.导弹防御系统
为了对抗附近恶意国家的威胁,R国更新了他们的导弹防御系统。
一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。
例如,一套系统先后拦截了高度为 3 3 3和高度为 4 4 4的两发导弹,那么接下来该系统就只能拦截高度大于 4 4 4的导弹。
给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。
输入格式
输入包含多组测试用例。
对于每个测试用例,第一行包含整数 n n n,表示来袭导弹数量。
第二行包含 n n n个不同的整数,表示每个导弹的高度。
当输入测试用例 n = 0 n=0 n=0 时,表示输入终止,且该用例无需处理。
输出格式
对于每个测试用例,输出一个占据一行的整数,表示所需的防御系统数量。
数据范围
1
≤
n
≤
50
1≤n≤50
1≤n≤50
输入样例:
5
3 5 2 4 1
0
输出样例:
2
样例解释
对于给出样例,最少需要两套防御系统。
一套击落高度为 3 , 4 3,4 3,4 的导弹,另一套击落高度为 5 , 2 , 1 5,2,1 5,2,1 的导弹。
题解
- 概述
该题也是一个典型的最长上升子序列的问题。
条件: 导弹拦截高度要么一直上升要么一直下降。
有的导弹可以选择上升,有的可以选择下降,不是单纯地问所存在的序列可以划分为多少组上升子序列的问题,所以不能用之前的方法解。
怎么做?
当找不到办法时,考虑使用枚举法
如何做?
从问题的解出发,最终问题的答案是有许多单调上升子序列和许多单调下降子序列,那么实际就是对于每个数,来思考将该数放到上升序列中还是下降序列中。
- 题解
注意顺序性
-
依次枚举每个数
先枚举将该数作为单调上升的序列,还是单调下降的序列中- 如果该数被放到了单调上升的序列中,则枚举将该数放到哪个单调上升的序列后面
- 如果该数被放到了单调下降的序列中,则枚举将该数放到哪个单调下降的序列后面。
- 或者该数成为一个单独的单调序列
实际上来看这就是一个建模问题,首先建模为初始状态,初始状态可以经过很多离散步骤到达一个新的状态,最终不断地到达目的状态。
暴力,考虑搜索空间,首先如果搜索的序列是有序的,那么就可以使用二分法。每一步搜索空间是50,那么这样下去,就是5050…, 最终是指数级别
up[]
存储当前所有上升子序列的末尾元素
down[]
存储当前所有下降子序列的末尾元素
所以这里实际上用到了上一题导弹防御的结果来做,对于多个单调上升(下降)子序列只需要对末尾元素进行考虑即可。
这一点真厉害!!!
然后这里又可以进行优化
使用贪心的思想
x
x
x 尽可能覆盖一个末尾元素大的序列
因此是一个很强的优化,所以每一步实际上只有两种选择,要么上升,要么下降。
up本身是单调的,所以一方面找到即停止,另一方面可以直接考虑用二分
如何进行搜素
- 使用BFS, BFS是宽度优先,需要进行存储(空间太多,不太好剪枝),而且代码写起来比较麻烦
- 使用DFS, 一种想法是使用全局变量,第二种方式是迭代加深
代码实现
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 55;
int n;
int q[N];
int up[N],down[N];//一个表示上升子序列结尾,另外一个表示下降子序列结尾
int ans;
//su表示当前上升子序列个数,sd表示当前下降子序列个数
void dfs(int u,int su,int sd)
{
if(su + sd >= ans) return;
if(u == n)
{
ans = su + sd;
return;
}
//情况1:将当前数放到上升子序列中
int k = 0;
while(k <= su && up[k] >= q[u]) k++;
int t = up[k];
up[k] = q[u];
if(k < su) dfs(u + 1,su,sd);
else dfs(u + 1,su + 1,sd);
up[k] = t;
//情况2:将当前数放到下降子序列中
k = 0;
while(k < sd && down[k] <= q[u]) k++;
t = down[k];
down[k] = q[u];
if(k < sd) dfs(u + 1,su,sd);
else dfs(u + 1,su,sd + 1);
down[k] = t;
}
int main()
{
while(cin >> n,n)
{
for(int i = 0;i < n;i++) cin >> q[i];
ans = n;
dfs(0,0,0);
cout << ans << endl;
}
return 0;
}
3.最长公共上升子序列
熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。
小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。
小沐沐说,对于两个数列 A A A 和 B B B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。
奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。
不过,只要告诉奶牛它的长度就可以了。
数列 A A A 和 B B B的长度均不超过 3000 3000 3000。
输入格式
第一行包含一个整数
N
N
N,表示数列
A
,
B
A,B
A,B 的长度。
第二行包含 N N N 个整数,表示数列 A A A。
第三行包含 N N N 个整数,表示数列 B B B。
输出格式
输出一个整数,表示最长公共上升子序列的长度。
数据范围
1
≤
N
≤
3000
1≤N≤3000
1≤N≤3000,序列中的数字均不超过
2
31
−
1
2^{31}−1
231−1。
输入样例:
4
2 2 1 3
2 1 2 3
输出样例:
2
题解(此题不是很懂,比较综合)
(DP,线性DP,前缀和) O ( n 2 ) O(n^{2}) O(n2)
这道题目是AcWing 895. 最长上升子序列和AcWing 897. 最长公共子序列的结合版,在状态表示和状态计算上都是融合了这两道题目的方法。此两道题在之前博客均有叙述,同时可以在Acwing官网查看(算法提高课)
状态表示:
f[i][j]
代表所有a[1 ~ i]
和b[1 ~ j]
中以b[j]
结尾的公共上升子序列的集合;f[i][j]
的值等于该集合的子序列中长度的最大值;
状态计算(对应集合划分):
首先依据公共子序列中是否包含a[i]
,将f[i][j]
所代表的集合划分成两个不重不漏的子集:
- 不包含a[i]的子集,最大值是
f[i - 1][j]
; - 包含
a[i]
的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]
中是哪个数:- 子序列只包含
b[j]
一个数,长度是1; - 子序列的倒数第二个数是
b[1]
的集合,最大长度是f[i - 1][1] + 1
; - …
- 子序列的倒数第二个数是
b[j - 1]
的集合,最大长度是f[i - 1][j - 1] + 1
;
- 子序列只包含
如果直接按上述思路实现,需要三重循环:
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= n; j ++ )
{
f[i][j] = f[i - 1][j];
if (a[i] == b[j])
{
int maxv = 1;
for (int k = 1; k < j; k ++ )
if (a[i] > b[k])
maxv = max(maxv, f[i - 1][k] + 1);
f[i][j] = max(f[i][j], maxv);
}
}
}
然后我们发现每次循环求得的maxv
是满足a[i] > b[k]
的f[i - 1][k] + 1
的前缀最大值。
因此可以直接将maxv
提到第一层循环外面,减少重复计算,此时只剩下两重循环。
最终答案枚举子序列结尾取最大值即可。
C++ 代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 3010;
int n;
int a[N], b[N];
int f[N][N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for (int i = 1; i <= n; i ++ ) scanf("%d", &b[i]);
for (int i = 1; i <= n; i ++ )
{
int maxv = 1;
for (int j = 1; j <= n; j ++ )
{
f[i][j] = f[i - 1][j];
if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
if (a[i] > b[j]) maxv = max(maxv, f[i - 1][j] + 1);
}
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
printf("%d\n", res);
return 0;
}