参考《算法竞赛进阶指南》 、AcWing题库
题目描述
给定一个长度为 N N N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N N N 。
第二行包含 N N N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1 ≤ N ≤ 1000 1 \le N \le 1000 1 ≤ N ≤ 1000 ,
− 1 0 9 ≤ 数列中的数 ≤ 1 0 9 -10^9 \le 数列中的数 \le 10^9 − 1 0 9 ≤ 数列中的数 ≤ 1 0 9
输入样例:
输出样例 :
算法分析
贪心写法见 1010.拦截导弹
Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 1010 ;int a[N], f[N];int main () { int n ; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> a[i]; int ans = 0 ; for (int i = 1 ; i <= n; ++i) { f[i] = 1 ; for (int j = 1 ; j < i; ++j) { if (a[j] < a[i]) f[i] = max (f[i], f[j] + 1 ); } ans = max (ans, f[i]); } cout << ans; }
题目描述
怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。
而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。
有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。
不得已,怪盗基德只能操作受损的滑翔翼逃脱。
假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。
初始时,怪盗基德可以在任何一幢建筑的顶端。
他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。
因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。
他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。
请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?
输入格式
输入数据第一行是一个整数K,代表有K组测试数据。
每组测试数据包含两行:第一行是一个整数N,代表有N幢建筑。第二行包含N个不同的整数,每一个对应一幢建筑的高度h,按照建筑的排列顺序给出。
输出格式
对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。
数据范围
1 ≤ K ≤ 100 1 \le K \le 100 1 ≤ K ≤ 100 ,
1 ≤ N ≤ 100 1 \le N \le 100 1 ≤ N ≤ 100 ,
0 < h < 10000 0 < h < 10000 0 < h < 10000
输入样例 :
1 2 3 4 5 6 7 3 8 300 207 155 299 298 170 158 65 8 65 158 170 298 299 155 207 300 10 2 1 3 4 5 6 7 8 9 10
输出样例 :
算法分析
这题选比较裸的一道题,我们先来分析一下题目
首先,求一个高度递减的楼房子序列长度最大 ,其实就是求一个最长下降子序列
然后,这个怪盗基德老哥可以选择任意 楼房作为起始位置 ,接着选择一个方向 飞到尽头
于是,我们可以画出如下三种情况:
对于左边界情况 来说,其实就是中间位置 的左侧序列长度为 0 0 0 的情况;右边界情况 同理
所以,我们只需讨论中间情况 即可(两侧边界情况是该情况的子集)
于是,对于任意位置 x x x ,我们分别需要求出以他为右端点 的最长上升子序列 ,以及作为左端点 的最长下降子序列
而DP 中经典的最长上升子序列 模型f[i]
的状态表示 就是以i
为端点的最长上升子序列
由此我们通过线性DP ,可以求出任一点的左侧最长上升 和右侧最长下降
两者取一个 M a x \mathrm{Max} Max ,就是以该点作为起点 的最佳飞行方向 的最大长度
然后再枚举 所有点取一个 M a x \mathrm{Max} Max ,就是最佳起点 的最大长度 ,便是本题的答案
这题的DP模型就是经典的最长上升子序列
{ 状态表示 f i { 集合 : 以第 i 个位置作为最长上升子序列的右端点的方案 属性 : 方案的子序列长度最大 M a x 状态转移 f i = m a x { 1 , m a x { ∑ j = 1 i − 1 f j } + 1 } ( a i > a j ) \begin{cases} 状态表示f_i \begin{cases} 集合: 以第i个位置作为最长上升子序列的右端点的方案 \\ 属性: 方案的子序列长度最大 Max \end{cases} \\ 状态转移 f_i = max\{1, max\{\sum_{j=1}^{i-1}f_j\}+1\} \quad (a_i > a_j) \end{cases}
⎩ ⎨ ⎧ 状态表示 f i { 集合 : 以第 i 个位置作为最长上升子序列的右端点的方案 属性 : 方案的子序列长度最大 M a x 状态转移 f i = ma x { 1 , ma x { ∑ j = 1 i − 1 f j } + 1 } ( a i > a j )
Solution
正向上升 + 反向下降
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 <iostream> using namespace std;const int N = 110 ;int h[N], f[N];int main () { int k; cin >> k; while (k--) { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> h[i]; int ans = 0 ; for (int i = 1 ; i <= n; ++i) { f[i] = 1 ; for (int j = 1 ; j < i; ++j) { if (h[j] < h[i]) f[i] = max (f[i], f[j] + 1 ); } ans = max (ans, f[i]); } for (int i = n; i >= 1 ; --i) { f[i] = 1 ; for (int j = n; j > i; --j) { if (h[j] < h[i]) f[i] = max (f[i], f[j] + 1 ); } ans = max (ans, f[i]); } cout << ans << endl; } }
正向上升 + 正向下降
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> #include <algorithm> using namespace std;const int N = 110 ;int h[N], fUp[N], fDown[N];int main () { int k; cin >> k; while (k--) { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> h[i]; int ans = 0 ; for (int i = 1 ; i <= n; ++i) { fUp[i] = fDown[i] = 1 ; for (int j = 1 ; j < i; ++j) { if (h[j] < h[i]) fUp[i] = max (fUp[i], fUp[j] + 1 ); if (h[j] > h[i]) fDown[i] = max (fDown[i], fDown[j] + 1 ); } ans = max ({ans, fUp[i], fDown[i]}); } cout << ans << endl; } }
题目描述
五一到了,ACM队组织大家去登山观光,队员们发现山上一共有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。
同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。
队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?
输入格式
第一行包含整数N,表示景点数量。
第二行包含N个整数,表示每个景点的海拔。
输出格式
输出一个整数,表示最多能浏览的景点数。
数据范围
2 ≤ N ≤ 1000 2 \le N \le 1000 2 ≤ N ≤ 1000
输入样例 :
1 2 8 186 186 150 200 160 130 197 220
输出样例 :
算法分析
此题和上题怪盗基德的滑翔翼 有着异曲同工之妙
上题求的是从一个点开始,朝向一个方向求一个最长下降子序列
本题则是让我们求一个先上升后下降的最长子序列 ,具体如下图所示
当然也和上题一样存在边界情况 ,即最优解 答案可以是一个单调下降子序列 或单调上升子序列
这种边界情况 一样是先上升后下降的最长子序列 的子集 ,因此不用额外讨论
常规做法
一种做法是和上题一样,先做一遍最长上升 ,再做一遍最长下降
然后根据状态表示 的特性 ,将两个状态的值相加,取一个 M a x \mathrm{Max} Max 就是 先上升后下降的最长子序列 的长度
状态机模型DP
利用状态机模型DP 解决最长xxx子序列模型 的方法
xxx可以是先上升后下降,或者先上升后下降再上升,或者先上升后下降再上升再下降 ···
回到本题 ,我们就可以先利用状态机模型 进行分析,具体如下:
对于本题来说,当前状态 如果是上升状态 ,则他下一个阶段可以维持上升状态 ,或者变成下降状态
而对于已经处于下降状态 来说的状态 ,下一个阶段只能继续维持下降状态
{ 状态表示 f i , j { 集合 : 以第 i 个位置作为当前子序列的右端点,且当前状态为 j 属性 : 方案的子序列长度最大 M a x 状态转移 { f i , 0 = m a x { ∑ f k , 0 } + 1 f i , 1 = m a x { ∑ f k , 0 , ∑ f k , 1 } + 1 \begin{cases} 状态表示f_{i,j} \begin{cases} 集合: 以第i个位置作为当前子序列的右端点,且当前状态为j \\ 属性: 方案的子序列长度最大 Max \end{cases} \\ 状态转移 \begin{cases} f_{i,0} = max\{\sum f_{k,0}\}+1 \\ f_{i,1} = max\{\sum f_{k,0}, \sum f_{k,1}\} + 1 \end{cases} \end{cases}
⎩ ⎨ ⎧ 状态表示 f i , j { 集合 : 以第 i 个位置作为当前子序列的右端点,且当前状态为 j 属性 : 方案的子序列长度最大 M a x 状态转移 { f i , 0 = ma x { ∑ f k , 0 } + 1 f i , 1 = ma x { ∑ f k , 0 , ∑ f k , 1 } + 1
初始状态: f[0][0]
和f[0][1]
目标状态: f[i][0]
和f[i][1]
更近一步了解这类模型的话,可以做一下这道题 AcWing 3549. 最长非递减子序列
Solution
状态机模型DP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> #include <algorithm> using namespace std;const int N = 1010 ;int h[N], f[N][2 ];int main () { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> h[i]; int ans = 0 ; for (int i = 1 ; i <= n; ++i) { f[i][0 ] = f[i][1 ] = 1 ; for (int j = 1 ; j < i; ++j) { if (h[j] < h[i]) f[i][0 ] = max (f[i][0 ], f[j][0 ] + 1 ); if (h[j] > h[i]) f[i][1 ] = max ({f[i][1 ], f[j][0 ] + 1 , f[j][1 ] + 1 }); } ans = max ({ans, f[i][0 ], f[i][1 ]}); } cout << ans; }
常规LIS动态规划
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> using namespace std;const int N = 1010 ;int h[N], fU[N], fD[N];int main () { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> h[i]; for (int i = 1 ; i <= n; ++i) { fU[i] = 1 ; for (int j = 1 ; j < i; ++j) if (h[j] < h[i]) fU[i] = max (fU[i], fU[j] + 1 ); } for (int i = n; i >= 1 ; --i) { fD[i] = 1 ; for (int j = n; j > i; --j) if (h[j] < h[i]) fD[i] = max (fD[i], fD[j] + 1 ); } int ans = 0 ; for (int i = 1 ; i <= n; ++i) ans = max (ans, fU[i] + fD[i] - 1 ); cout << ans; }
题目描述
N N N 位同学站成一排,音乐老师要请其中的 ( N − K ) (N-K) ( N − K ) 位同学出列,使得剩下的 K K K 位同学排成合唱队形。
合唱队形是指这样的一种队形:设 K K K 位同学从左到右依次编号为 1 , 2 … , K 1,2…,K 1 , 2 … , K ,他们的身高分别为 T 1 , T 2 , … , T K T_1,T_2,…,T_K T 1 , T 2 , … , T K , 则他们的身高满足 T 1 < … < T i > T i + 1 > … > T K ( 1 ≤ i ≤ K ) T_1 < … < T_i > T_{i+1} > … > T_K(1 \le i \le K) T 1 < … < T i > T i + 1 > … > T K ( 1 ≤ i ≤ K ) 。
你的任务是,已知所有 N N N 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入格式
输入的第一行是一个整数 N N N ,表示同学的总数。
第二行有 N N N 个整数,用空格分隔,第 i i i 个整数 T i T_i T i 是第 i i i 位同学的身高(厘米)。
输出格式
输出包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
数据范围
2 ≤ N ≤ 100 2 \le N \le 100 2 ≤ N ≤ 100 ,
130 ≤ T i ≤ 230 130 \le T_i \le 230 130 ≤ T i ≤ 230
输入样例 :
1 2 8 186 186 150 200 160 130 197 220
输出样例 :
算法分析
本题的要求是,我们对原数组删去尽量少 的数字,构成该类型序列
我们知道,一个序列的子序列 ,就是通过删去原序列中部分的元素 后构成的
因此,找到满足该类型序列 ,最少需要删除的元素数量 ≡ \equiv ≡ 找到满足该类型序列 ,最长子序列的长度
通过这个等价转换,本题就完全转换成了上一题了
Solution
状态机模型DP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> #include <algorithm> using namespace std;const int N = 110 ;int h[N], f[N][2 ];int main () { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> h[i]; int ans = 0 ; for (int i = 1 ; i <= n; ++i) { f[i][0 ] = f[i][1 ] = 1 ; for (int j = 1 ; j < i; ++j) { if (h[j] < h[i]) f[i][0 ] = max (f[i][0 ], f[j][0 ] + 1 ); if (h[j] > h[i]) f[i][1 ] = max ({f[i][1 ], f[j][0 ] + 1 , f[j][1 ] + 1 }); } ans = max ({ans, f[i][0 ], f[i][1 ]}); } cout << n - ans; }
常规最长上升子序列DP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> #include <algorithm> using namespace std;const int N = 110 ;int h[N], f[N][2 ];int main () { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> h[i]; int ans = 0 ; for (int i = 1 ; i <= n; ++i) { f[i][0 ] = f[n - i + 1 ][1 ] = 1 ; for (int j = 1 ; j < i; ++j) { if (h[j] < h[i]) f[i][0 ] = max (f[i][0 ], f[j][0 ] + 1 ); if (h[n - j + 1 ] < h[n - i + 1 ]) f[n - i + 1 ][1 ] = max (f[n - i + 1 ][1 ], f[n - j + 1 ][1 ] + 1 ); } } for (int i = 1 ; i <= n; ++i) ans = max (ans, f[i][0 ] + f[i][1 ] - 1 ); cout << n - ans; }
题目描述
Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。
北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。
每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。
输入格式
第1行,一个整数N,表示城市数。
第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。
输出格式
仅一行,输出一个整数,表示政府所能批准的最多申请数。
数据范围
1 ≤ N ≤ 5000 1 \le N \le 5000 1 ≤ N ≤ 5000 ,
0 ≤ x i ≤ 10000 0 \le x_i \le 10000 0 ≤ x i ≤ 10000
输入样例 :
1 2 3 4 5 6 7 8 7 22 4 2 6 10 3 15 12 9 8 17 17 4 2
输出样例 :
算法分析
本题的背景是一个造桥项目,初始给定我们 n n n 座打算建造的桥
每座桥有两个参数 x 1 x_1 x 1 和 x 2 x_2 x 2 ,表示该桥一头连接在上岸 坐标为x 1 x_1 x 1 的地方,一头连在下岸 坐标为x 2 x_2 x 2 的地方
题目要求我们找出一种建桥方案 ,使得在所有建造的桥不相交 的前提下,建造尽可能多 的桥
求出该方案的造桥数量
具体的情况用文字可能不太好解释,这里画了一张图方便大家理解
红色虚线 表示初始提供的打算建造的桥
我们要找出的方案 是在不相交 的前提下的最大建桥数量
也就是上面的绿线连接而成的方案
这就是本题的大致意思
我们先想一下暴力 怎么做
很容易想到,我们可以枚举所有的方案 ,然后检查方案是否合法 ,如果合法就考虑是否能够更新最大值答案
这么做的时间复杂度是 O ( 2 n ) O(2^n) O ( 2 n ) ,而 n n n 的数据范围是 5000 5000 5000 ,毫无疑问会超时。
所以,我们就需要找出一些性质 进行优化
既然是要暴力枚举 ,我们可以考虑一个枚举方案 ,按照上岸 的坐标从小到大 来枚举
然后我们只需关心下岸 的坐标 之间有何关系即可
于是,可以轻易发现,上坐标从小到大 枚举选择到的桥,其对应下坐标 也必然是从小到大 的
具体见下图:
蓝色 表示该方案按照上坐标从小到大先选出来的桥
红色 表示该方案的下一座桥的下坐标不是从小到大 的,绿色 表示是从小到大 的
因此,该方案中,在上坐标排序 的情况下,下坐标 次序不是从小到大 的,则必然不合法 (会有相交)
于是,这题就变成了:桥以上坐标从小到大 排序后,找出下坐标 的最长上升子序列 长度
我们可以用pair
或者struct
先把所有的桥存下来,然后按照上坐标排序
再然后,下坐标 就构成了一个序列 ,我们只需找出该序列 的最长上升子序列 的长度 ,就求出本题的答案 了
Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> #include <algorithm> using namespace std;const int N = 5010 ;pair<int ,int > city[N]; int f[N];int main () { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> city[i].first >> city[i].second; sort (city + 1 , city + 1 + n); int ans = 0 ; for (int i = 1 ; i <= n; ++i) { f[i] = 1 ; for (int j = 1 ; j < i; ++j) { if (city[j].second < city[i].second) f[i] = max (f[i], f[j] + 1 ); } ans = max (ans, f[i]); } cout << ans; }
题目描述
一个数的序列 b i b_i b i ,当 b 1 < b 2 < … < b S b_1<b_2<…<b_S b 1 < b 2 < … < b S 的时候,我们称这个序列是上升的。
对于给定的一个序列(a 1 , a 2 , … , a N a_1,a_2,…,a_N a 1 , a 2 , … , a N ),我们可以得到一些上升的子序列(a i 1 , a i 2 , … , a i K a_{i_1},a_{i_2},…,a_{i_K} a i 1 , a i 2 , … , a i K ),这里1 ≤ i 1 < i 2 < … < i K ≤ N 1≤i_1<i_2<…<i_K≤N 1 ≤ i 1 < i 2 < … < i K ≤ N 。
比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。
这些子序列中和最大为18,为子序列(1,3,5,9)的和。
你的任务,就是对于给定的序列,求出最大上升子序列和。
注意,最长的上升子序列的和不一定是最大的,比如序列(100,1,2,3)的最大上升子序列和为100,而最长上升子序列为(1,2,3)。
输入格式
输入的第一行是序列的长度N。
第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。
输出格式
输出一个整数,表示最大上升子序列和。
数据范围
1 ≤ N ≤ 1000 1 \le N \le 1000 1 ≤ N ≤ 1000
输入样例 :
输出样例 :
算法分析
Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> using namespace std;const int N = 1010 ;int a[N], f[N];int main () { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> a[i]; int ans = 0 ; for (int i = 1 ; i <= n; ++i) { f[i] = a[i]; for (int j = 1 ; j < i; ++j) { if (a[j] < a[i]) f[i] = max (f[i], f[j] + a[i]); } ans = max (ans, f[i]); } cout << ans; }
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
共一行,输入导弹依次飞来的高度。
输出格式
第一行包含一个整数,表示最多能拦截的导弹数。
第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。
数据范围
雷达给出的高度数据是不大于 30000 30000 30000 的正整数,导弹数不超过 1000 1000 1000 。
输入样例 :
1 389 207 155 300 299 170 158 65
输出样例 :
算法分析
Dilworth定理转化
Dilworth定理:对于一个偏序集,最少链划分等于最长反链长度
Dilworth定理的对偶定理:对于一个偏序集,其最少反链划分数等于其最长链的长度
通俗解释: 把一个数列划分成最少的最长不升子序列的数目就等于这个数列的最长上升子序列的长度(LIS)。
最长上升子序列的长度 == 最少的非升子序列的划分数
最长非升子序列的长度 == 最少的上升子序列的划分数
最长下降子序列的长度 == 最少的非降子序列的划分数
最长非降子序列的长度 == 最少的下降子序列的划分数
求最少划分数的一种贪心
贪心思路:较大 的数作为末尾元素 的子序列比较小 的数作为末尾元素 的子序列更好,更容易接上
贪心步骤:
开一个last[]
记录所有下降子序列末尾元素,last[]
一开始是空集,长度为0
遍历原序列中的每个数,对于当前这个数x
:
情况1
:在last[]
中若找不到 ≥大于等于
当前数x
的数,接不上,需要新开非升序列,即扩大last[]
长度并记录当前数x
,因为last[]
单调上升,即对应x > last[最后数的下标]
情况2
:在last[]
中可以找到 ≥大于等于
当前数x
的最小的数 last[idx]
,则将当前数x
接在数last[idx]
作为末尾元素的子序列,因为last[idx]
记录的是该子序列的末尾元素,故将last[idx]
更新为x
,即last[idx] = x
最终last[]
数组的长度即为最少划分数,且last[]
中存储的是原序列的最长上升子序列
可以证明last[]
必定是一个单调上升 的数组
一开始last[]
是空集,空集也是单调的一种
对于每个数x
要记录到last[]
时,last[]
中有不等式a > b > c
,假设b
是所有大于等于≥ 数x
的那个最小的数,则有不等式a > b ≥ x > c
覆盖之后就会变成a > x > c
,并不改变last[]
的单调性,且last[]
是每次用一个较小的数覆盖一个较大的数 作为末尾元素更新到last[]
。如果无法覆盖更新,必须扩大last[]
,也就表示此时新增x
比所有末尾元素都大 。故last[]
随下标增大元素值也越大,因此last[]
是单调上升 的数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int lengthOfLIS (vector<int >& nums) { int n = nums.size (); vector<int > last_of_each_LNIS; for (auto x : nums) { if (last_of_each_LNIS.empty () || x > last_of_each_LNIS.back ()) last_of_each_LNIS.push_back (x); else *lower_bound (last_of_each_LNIS.begin (), last_of_each_LNIS.end (), x) = x; } return last_of_each_LNIS.size (); } };
求最长上升子序列的一种贪心
贪心思路:对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长
贪心步骤:
维护数组last[]
,last[i]
存的是所有长度 为i
的上升子序列中,末尾 元素最小 的子序列的末尾元素
last[]
一开始是空集,长度为0
遍历原序列中的每个数x
,对于当前数x
* 如果数x
大于 当前最长上升子序列的末尾元素,即x > last[当前最长LIS的长度]
,就把数x
接到当前最长的LIS后面,即last[++当前最长LIS的长度] = x
* 否则,利用数x
更新last[]
数组:在last[]
数组中找到大于等于 x
的最小的数last[idx]
,也即首个大于等于 x
的数,更新last[idx] = x
,肯定能找到,起码last[当前最长LIS的长度]
就满足
可以证明last[]
必定是一个严格单调上升 的数组
一开始last[]
是空集,空集也是单调的一种
对于每个数x
要记录到last[]
时,last[]
中有不等式a < b < c
,c
是last[]
数组最后的一个数
如果当前数x > c
,将数x
加入last[]
数组末尾,则有a < b < c < x
,仍然单调上升
如果当前数x = c
,则c
就是大于等于x
的最小的数,更新后有a < b < x
,仍然单调上升
如果当前数x < c
,找到大于等于x
的最小的数,因为last[]
是单调上升的,首个大于等于x
的数即为最小的数,假设为b
,更新后有a < x < c
,仍然单调上升
故last[]
始终单调上升,且是严格上升
查找[begin, end)
范围里首个大于等于x的数可以用lower_bound(begin, end, x)
,找到则返回指向目标的迭代器,否则返回end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int lengthOfLIS (vector<int >& nums) { int n = nums.size (); vector<int > last_of_each_len_lis; for (auto x : nums) { if (last_of_each_len_lis.empty () || x > last_of_each_len_lis.back ()) last_of_each_len_lis.push_back (x); else *lower_bound (last_of_each_len_lis.begin (), last_of_each_len_lis.end (), x) = x; } return last_of_each_len_lis.size (); } };
注意两种贪心数组的区别
最少划分数贪心是记录每个子序列末尾的数,last 中每个数都代表划分的一个子序列的末尾元素,一共划分了last.size()个子序列
最长子序列长度贪心是记录每种长度的子序列的末尾最小值,last[i] = 所有长度为 i 的上升子序列中,最小的末尾元素
Solution
运用Dilworth定理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> using namespace std;const int N = 1010 ;int h[N], f[N][2 ];int main () { int n = 0 ; while (cin >> h[n]) ++n; int ans1 = 0 , ans2 = 0 ; for (int i = 0 ; i < n; ++i) { f[i][0 ] = f[i][1 ] = 1 ; for (int j = 0 ; j < i; ++j) { if (h[j] >= h[i]) f[i][0 ] = max (f[i][0 ], f[j][0 ] + 1 ); if (h[j] < h[i]) f[i][1 ] = max (f[i][1 ], f[j][1 ] + 1 ); } ans1 = max (ans1, f[i][0 ]); ans2 = max (ans2, f[i][1 ]); } cout << ans1 << endl << ans2; }
两种贪心:长度 + 划分数
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 h[N], last1[N], last2[N], idx1, idx2;int main () { int n = 0 ; while (cin >> h[n]) ++n; for (int i = 0 ; i < n; ++i) { if (i == 0 || h[i] <= last1[idx1 - 1 ]) last1[idx1++] = h[i]; else *upper_bound (last1, last1 + idx1, h[i], greater<int >()) = h[i]; } cout << idx1 << endl; for (int i = 0 ; i < n; ++i) { if (i == 0 || h[i] > last2[idx2 - 1 ]) last2[idx2++] = h[i]; else *lower_bound (last2, last2 + idx2, h[i]) = h[i]; } cout << idx2 << endl; }
题目描述
为了对抗附近恶意国家的威胁,R R 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 \le n \le 50 1 ≤ n ≤ 50
输入样例 :
输出样例 :
样例解释
对于给出样例,最少需要两套防御系统。
一套击落高度为 3 , 4 3,4 3 , 4 的导弹,另一套击落高度为 5 , 2 , 1 5,2,1 5 , 2 , 1 的导弹。
算法分析
搜索顺序分为两个阶段:
Solution
迭代加深
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 45 46 47 48 49 50 51 52 53 54 55 56 #include <iostream> using namespace std;const int N = 55 ;int h[N], up[N], down[N];int n;bool dfs (int depth, int u, int su, int sd) { if (su + sd > depth) return false ; if (u == n) return true ; int l = 0 , r = su; while (l < r) { int mid = l + r >> 1 ; if (up[mid] < h[u]) r = mid; else l = mid + 1 ; } int t = up[r]; if (r == su) { up[r] = h[u]; if (dfs (depth, u + 1 , su + 1 , sd)) return true ; } else { up[r] = h[u]; if (dfs (depth, u + 1 , su, sd)) return true ; } up[r] = t; l = 0 , r = sd; while (l < r) { int mid = l + r >> 1 ; if (down[mid] > h[u]) r = mid; else l = mid + 1 ; } t = down[r]; if (r == sd) { down[r] = h[u]; if (dfs (depth, u + 1 , su, sd + 1 )) return true ; } else { down[r] = h[u]; if (dfs (depth, u + 1 , su, sd)) return true ; down[r] = t; } return false ; } int main () { while (cin >> n, n) { for (int i = 0 ; i < n; ++i) cin >> h[i]; int depth = 0 ; while (dfs (depth, 0 , 0 , 0 ) == false ) ++depth; cout << depth << endl; } }
最优性剪枝:维护全局最小值
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 45 46 47 48 49 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 55 ;int n;int h[N], lastu[N], lastd[N];int ans;void dfs (int u, int su, int sd) { if (su + sd >= ans) return ; if (u == n) { ans = su + sd; return ; } int p = upper_bound (lastu, lastu + su, h[u], greater<int >()) - lastu; int t = lastu[p]; if (p == su) { lastu[su] = h[u]; dfs (u + 1 , su + 1 , sd); } else { lastu[p] = h[u]; dfs (u + 1 , su, sd); } lastu[p] = t; p = upper_bound (lastd, lastd + sd, h[u]) - lastd; t = lastd[p]; if (p == sd) { lastd[sd] = h[u]; dfs (u + 1 , su, sd + 1 ); } else { lastd[p] = h[u]; dfs (u + 1 ,su ,sd); } lastd[p] = t; } int main () { while (cin >> n, n) { for (int i = 0 ; i < n; ++i) cin >> h[i]; ans = n; dfs (0 , 0 , 0 ); cout << ans << endl; } }
题目描述
给定两个长度分别为 N N N 和 M M M 的字符串 A A A 和 B B B ,求既是 A A A 的子序列又是 B B B 的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N N N 和 M M M 。
第二行包含一个长度为 N N N 的字符串,表示字符串 A A A 。
第三行包含一个长度为 M M M 的字符串,表示字符串 B B B 。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1 ≤ N , M ≤ 1000 1 \le N,M \le 1000 1 ≤ N , M ≤ 1000
输入样例 :
输出样例 :
算法分析
Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> #include <string> using namespace std;const int N = 1010 ;int f[N][N];char a[N], b[N];int main () { int n, m; 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) { if (a[i] == b[j]) f[i][j] = f[i - 1 ][j - 1 ] + 1 ; else f[i][j] = max (f[i - 1 ][j], f[i][j - 1 ]); } } cout << f[n][m]; }
题目描述
熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。
小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。
小沐沐说,对于两个数列 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 \le N \le 3000 1 ≤ N ≤ 3000 ,序列中的数字均不超过 2 31 − 1 2^{31}-1 2 31 − 1 。
输入样例 :
输出样例 :
算法分析
L C S LCS L CS
f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + 1 ( i , j > 0 , a [ i ] = b [ j ] ) f[i][j]=f[i-1][j-1]+1\;(i,j>0,a[i]=b[j])
f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + 1 ( i , j > 0 , a [ i ] = b [ j ])
f [ i ] [ j ] = m a x ( f [ i ] [ j − 1 ] , f [ i − 1 ] [ j ] ) ( i , j > 0 , a [ i ] ≠ b [ j ] ) f[i][j]=max(f[i][j-1],f[i-1][j])\;(i,j>0,a[i]\not=b[j])
f [ i ] [ j ] = ma x ( f [ i ] [ j − 1 ] , f [ i − 1 ] [ j ]) ( i , j > 0 , a [ i ] = b [ j ])
其中,f[i][j]
为a
序列前i
个元素与b
序列前j
个元素的L C S LCS L CS 长度
L I S LIS L I S
f [ i ] = m a x f [ j ] + 1 ( j < i , a [ j ] < a [ i ] ) f[i]=max~f[j]+1~(j<i,a[j]<a[i])
f [ i ] = ma x f [ j ] + 1 ( j < i , a [ j ] < a [ i ])
f[i]
为以第i
个元素结尾的L I S LIS L I S 长度。
这道题目是 LIS 与 LCS 的综合。请读者回顾在木节开头部分列举的 LIS 与 LCS 问题的动态规划状态表示,把二者相结合,容易想到以下解法:
F [ i , j ] F[i, j] F [ i , j ] 表示 A 1 ∼ A i A_{1} \sim A_{i} A 1 ∼ A i 与 B 1 ∼ B j B_{1} \sim B_{j} B 1 ∼ B j 可以构成的以 B j B_{j} B j 为结尾的 LCIS 的长度。
当 A i ≠ B j A_{i} \neq B_{j} A i = B j 时,有 F [ i , j ] = F [ i − 1 , j ] F[i, j]=F[i-1, j] F [ i , j ] = F [ i − 1 , j ]
当 A i = B j A_{i}=B_{j} A i = B j 时,有
F [ i , j ] = max 1 ≤ k < j , B k < B j { F [ i − 1 , k ] } + 1 = max 1 ≤ k < j , B k < A i { F [ i − 1 , k ] } + 1 F[i, j]=\max _{1 \leq k<j, B_{k}<B_{j}}\{F[i-1, k]\}+1=\max _{1 \leq k<j, B_{k}<A_{i}}\{F[i-1, k]\}+1
F [ i , j ] = 1 ≤ k < j , B k < B j max { F [ i − 1 , k ]} + 1 = 1 ≤ k < j , B k < A i max { F [ i − 1 , k ]} + 1
显然,上面的状态转移方程可以直接用三重循环的程序计算(初始化部分省略)。
1 2 3 4 5 6 7 8 9 for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) if (a[i] == b[j]) { f[i][j] = 1 ; for (int k = 1 ; k < j; k++) if (b[k] < a[i]) f[i][j] = max (f[i][j], f[i - 1 ][k] + 1 ); } else { f[i][j] = f[i - 1 ][j]; }
在转移过程中,我们把满足 1 ≤ k < j , B k < A i 1 \leq k<j, B_{k}<A_{i} 1 ≤ k < j , B k < A i 的 k k k 构成的集合称为 F [ i , j ] F[i, j] F [ i , j ] 进行状态转移时的决策集合,记为 S ( i , j ) S(i, j) S ( i , j ) 。注意到,在第二层循环 j j j 从 1 增加到 m m m 时,第一层循环 i i i 是一个定值,这使得条件 B k < A i B_{k}<A_{i} B k < A i 是固定的。因此,当变量 j j j 增加 1 时,k k k 的取值范围从 1 ≤ k < j 1 \leq k<j 1 ≤ k < j 变为 0 ≤ k < j + 1 0 \leq k<j+1 0 ≤ k < j + 1 ,即整数 j j j 可能会进入新的决策集合。也就是说,我们只需要 O ( 1 ) O(1) O ( 1 ) 地检查条件 B j < A i B_{j}<A_{i} B j < A i 是否满足,已经在决策集合中 的数则一定不会被去除:
S ( i , j + 1 ) = { S ( i , j ) B j ≥ A i S ( i , j ) ∪ { j } B j < A i S(i, j+1)=\left\{\begin{array}{cc}
S(i, j) & B_{j} \geq A_{i} \\
S(i, j) \cup\{j\} & B_{j}<A_{i}
\end{array}\right.
S ( i , j + 1 ) = { S ( i , j ) S ( i , j ) ∪ { j } B j ≥ A i B j < A i
所以上面的状态转移方程只需要两重循环即可求解。最终的目标是 max 1 ≤ j ≤ m F [ n , j ] \max _{1 \leq j \leq m} F[n, j] max 1 ≤ j ≤ m F [ n , j ] 。
1 2 3 4 5 6 7 8 9 10 for (int i = 1 ; i <= n; i++) { int val = 0 ; for (int j = 1 ; j <= m; j++) { if (a[i] == b[j]) f[i][j] = val + 1 ; else f[i][j] = f[i - 1 ][j]; if (b[j] < a[i]) val = max (val, f[i - 1 ][j]); } }
这道题转移部分的优化告诉我们,在实现状态转移方程时,要注意观察决策集合的范围随着状态的变化情况。对于 “决策集合中的元素只增多不减少” 的情景,就可以像 本题一样维护一个变量来记录决策集合的当前信息,避免重复扫描,把转移的复杂度降低一个量级。
Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> using namespace std;const int N = 3010 ;int a[N], b[N], f[N][N];int main () { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> a[i]; for (int i = 1 ; i <= n; ++i) cin >> b[i]; for (int i = 1 ; i <= n; ++i) { int val = 0 ; for (int j = 1 ; j <= n; ++j) { f[i][j] = a[i] == b[j] ? val + 1 : f[i - 1 ][j]; if (b[j] < a[i]) val = max (val, f[i - 1 ][j]); } } int ans = 0 ; for (int j = 1 ; j <= n; ++j) ans = max (ans, f[n][j]); cout << ans; }