异想之旅:本人原创博客完全手敲,绝对非搬运,全网不可能有重复;本人无团队,仅为技术爱好者进行分享,所有内容不牵扯广告。本人所有文章仅在CSDN、掘金和个人博客(一定是异想之旅域名)发布,除此之外全部是盗文!
洛谷 P1233 木棍加工(最长上升子序列)
题目描述
一堆木头棍子共有n根,每根棍子的长度和宽度都是已知的。棍子可以被一台机器一个接一个地加工。机器处理一根棍子之前需要准备时间。准备时间是这样定义的:
第一根棍子的准备时间为1分钟;
如果刚处理完长度为L,宽度为W的棍子,那么如果下一个棍子长度为Li,宽度为Wi,并且满足L>=Li,W>=Wi,这个棍子就不需要准备时间,否则需要1分钟的准备时间;
计算处理完n根棍子所需要的最短准备时间。比如,你有5根棍子,长度和宽度分别为(4, 9),(5, 2),(2, 1),(3, 5),(1, 4),最短准备时间为2(按(4, 9)、(3, 5)、(1, 4)、(5, 2)、(2, 1)的次序进行加工)。
输入格式
第一行是一个整数n(n<=5000),第2行是2n个整数,分别是L1,W1,L2,w2,…,Ln,Wn。L和W的值均不超过10000,相邻两数之间用空格分开。
输出格式
仅一行,一个整数,所需要的最短准备时间。
输入输出样例
In 1:
5
4 9 5 2 2 1 3 5 1 4
Out 1:
2
题解
解这道题有两个步骤:首先推出这道题的答案即最长下降子序列,然后求解即可。
第一步严谨的证明十分复杂,我就用一个符合直觉的描述来说服大家就好。信竞题没有必要全部严格证明。
我们先来看第一步:构造数据 ( 1 , 1 ) , ( 2 , 2 ) , ( 3 , 3 ) , ( 4 , 4 ) , ( 5 , 5 ) (1,1),(2,2),(3,3),(4,4),(5,5) (1,1),(2,2),(3,3),(4,4),(5,5),尝试按照柱状图的思路画出来他们(有点丑不要介意)
显然这些是可以一趟处理完的。进而不难发现,如果一些木棍可以一次处理完,当且仅当木棍在二维平面上表示成上面这样的状态后,在右边的木棍都比在左边的高(也就是排成一个顺序,对于任意 i < j i<j i<j,有 L i < L j L_i<L_j Li<Lj 且 W i < W j W_i<W_j Wi<Wj)
再看一种情况:
答案显而易见为 2 2 2 。
我们发现,事实上对于任意序列,答案就是我们根据 L i L_i Li 对 W i W_i Wi 进行排序后,所有 W i W_i Wi 组成的序列中的最长下降子序列长度。
由第一个例子得到的结论我们知道, L i < L j L_i<L_j Li<Lj 且 W i > W j W_i>W_j Wi>Wj 的两个木棍不可能同批次加工,定然要分两组,因此这个结论满足必要性,也就是答案无论如何不可能比他小了。
下面证明充分性:
- 对于子序列中的项右边的 ( L x , W x ) (L_x, W_x) (Lx,Wx)(本例中的 ( 5 , 5 ) (5,5) (5,5)),定然存在 i i i 在选出的子序列中,使 L x > L i L_x>L_i Lx>Li 且 W x > W i W_x>W_i Wx>Wi ,所以在实际操作中把它放到 i i i 前方即可。
- 如果这样的 x x x 多于 i i i ,则最长下降子序列应由所有的 x x x 组成,而非当前的 i i i
- 事实上,对于 i i i 右边的 x x x ,都一定有一个 i i i 使它满足上述条件,否则这个 x x x 理应存在于选出的序列中
能不能看懂都就这样吧。证明真的比较麻烦。
下面来说如何求解最长下降子序列:
首先,对于复杂度为 n 2 n^2 n2 的算法不难想,就是对于每一个元素,寻找在此之前的比它大的元素中,已经组成的最长下降子序列的最大值,再加 1 保存为当前点结尾的最长下降子序列长度。
此处直接贴代码(求最长下降子序列的模板代码,非本题 AC 代码):
#include <bits/stdc++.h>
using namespace std;
const int N = 100001;
int n, a[N], dp[N];
int main() {
cin >> n; // 序列长度
for (int i = 1; i <= n; i++) cin >> a[i]; // 输入的序列
for (int i = 1; i <= n; i++) {
dp[i] = 1; // 初始值
for (int j = 1; j < i; j++) {
if (a[j] < a[i] && dp[j] + 1 > dp[i]) dp[i] = dp[j] + 1;
}
}
cout << *max_element(dp + 1, dp + n + 1) << endl; // 输出最大值
}
事实上,这个算法就能水过这道题
我们还有一种解法,大概思路就是将上面的方法中每次要重新遍历寻找的“比它大的元素中,已经组成的最长下降子序列的最大值”维护出来,具体看代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 100001;
int n, a[N];
int q[N]; // q[i]=a 表示长度为 i 的最长子序列的最后一个元素的值最小是 a
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int len = 0; // 最开始找到的最长子序列长度为 0
q[0] = INT_MIN;
for (int i = 1; i <= n; i++) { // 遍历每个元素
int l = 0, r = len;
while (l < r) {
// 二分寻找满足 q[j] < a[i] 的 j 的最大值
int mid = (l + r + 1) >> 1; // 玄学问题需要 +1,知道的话帮忙解释下
if (a[i] > q[mid])
l = mid;
else
r = mid - 1;
}
// q[r] < a[i],所以 r + 1 是新找到的最长子序列的长度
// 又因为 q[r + 1] >= a[i],所以此处直接更新 q[r + 1]
len = max(len, r + 1);
q[r + 1] = a[i];
}
cout << len << endl;
}
这个思路看不懂的可以试试食用这篇文章:AcWing 896. 最长上升子序列 II - AcWing
这两个模板的测试数据:输入 10 -5 -1 8 -4 3 -2 -5 -6 2 -6
,输出 4
最后是本题的 AC 代码:
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
const int N = 5001;
int n, dp[N];
pii a[N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i].first >> a[i].second;
sort(a + 1, a + n + 1); // 默认 first 为第一关键字
for (int i = 1; i <= n; i++) {
dp[i] = 1;
for (int j = 1; j < i; j++) {
if (a[j].second > a[i].second && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
}
}
}
cout << *max_element(dp + 1, dp + n + 1) << endl;
return 0;
}