题目信息
题目来源:CCF 山东省选 2005;
在线评测地址:Luogu#2439;
运行限制:时间不超过 (1.00 extrm{s}),空间不超过 (128 extrm{MiB})。
题目描述
我们现有许多演讲要在阶梯教室中举行。每一个演讲都可以用唯一的起始和终止时间来确定,如果两个演讲时间有部分或全部重复,那么它们是无法同时在阶级教室中举行的。现在我们想要尽最大可能的利用这个教室,也就是说,我们需要在这些演讲中选择一些不重复的演讲来举行使得他们用的总时间尽可能的长。我们假设在某一演讲结束的瞬间我们就可以立即开始另一个演讲。
请写一个程序:
读入所有演讲的起始和终止时间,计算最大的可能演讲总时间。
输入格式
第一行包括一个正整数 (n),为所有的演讲的数目。
以下的 (n) 行每行含有两个由空格隔开的整数 (p) 和 (k)。这样的一对整数表示一个演讲由时间 (p) 开始到时间 (k) 结束。
输出格式
输出唯一的一个整数,为最长的演讲总时间。
数据规模与约定
(1le nle 10^4),(1le p<kle 3 imes 10^4)。
分析
这道题有两种 DP 方式,各有各的优点和缺点,这里都介绍一下:
Solution 1
考虑以时间刻为单位 DP:令 (f_i) 为到第 (i) 分钟为止的最长完整演讲时间。则 (f_i=max{f_{i-1},f_{p_j}+i-p_j\,( extrm{When }i=k_j)})。
一个可喜的性质是 (f_i) 是单调的,所以我们可以利用单调性优化。
使用邻接表,记录下每个 (i) 作为右端点时的对应左端点。这样复杂度是 (mathcal{O}(n+max k)),可以通过此题。
Solution 2
考虑以演讲次为单位,则定义 (f_i) 为讲完第 (i) 次演讲的最长时间,则转移是 (f_i=max{f_j\,( ext{When }k_jle p_i)}+k_i-p_i)。
但是需要注意的一点是这时 (f_i) 不是单调的,每一次都要算一次最小值。这时算法是 (mathcal{O}(n^2)) 的。
优化 1
我们注意到,这个 DP 不依赖于演讲的顺序,所以如果将所有演讲按照结束时间排序,那么计算最小值的就是一个区间了。
同时,每一次取最小值都是一段从 (1) 开始的区间(显然),我们直接可以进行前缀和最小值优化。定义 (g_i=min_{jle i} f_j),则 (g_i=min{g_{i-1},f_i}),可以顺路维护。
为了找到 RMQ 的右端点,我们需要二分,所以单次转移的复杂度是一个 log,整体的复杂度就是 (mathcal{O}(nlog n))。
优化 2
这是我自己想出的一个玄学的优化
还是按照右端点排序。然后就向前遍历求最大值。但是,我在遍历时记录已经遍历区间的最大左端点,如果当前区间的右端点已经小于等于这个值了就停止转移。
这个优化正确吗?显然。如果继续往前取最大值,显然没有当前的答案大,因为之前的区间必然已经被已经转移的区间统计过了,是不如那个答案的。
不懂?看图:
这个优化极度玄学,以至于我都无法分析复杂度。反正能过。如果有一组形如 1 2、1 3、1 4 之类的估计会被卡飞。
注意事项
在 Solution 2 里,(f_i) 是不单调的,所以别忘了最后要全部取 max。
Code
通过了以后可以尝试加强版。
Solution 2 + 基本优化
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int max_n = 10000;
struct elem
{
int l, r;
};
int dp[max_n+1], pref[max_n+1];
elem rang[max_n];
inline int my_max(int a, int b) { return (a > b)? a:b; }
inline bool cmp(const elem& a, const elem& b) { return a.r < b.r; }
inline int read()
{
int ch = getchar(), t = 1, n = 0;
while (isspace(ch)) { ch = getchar(); }
if (ch == '-') { t = -1, ch = getchar(); }
while (isdigit(ch)) { n = n * 10 + ch - '0', ch = getchar(); }
return n * t;
}
int ser(int val, int lim) // 二分搜索
{
int lb = 0, ub = lim, mid, ans = -1;
while (lb < ub)
{
mid = (lb + ub) >> 1;
if (rang[mid].r <= val) // 符合
ans = mid, lb = mid + 1; // 尽可能找更大的
else // 否则
ub = mid; // 还不够小
}
return ans;
}
int main()
{
int n = read(), ans = 0;
for (int i = 0; i < n; i++)
rang[i].l = read(), rang[i].r = read(); // 输入
sort(rang, rang + n, cmp); // 按照右端点排序
dp[0] = pref[0] = 0; // 同时维护前缀最小值和 DP 数组
for (int i = 1; i <= n; i++)
{
dp[i] = pref[ser(rang[i-1].l, i)+1] + rang[i-1].r - rang[i-1].l; // DP
pref[i] = my_max(pref[i-1], dp[i]); // 前缀最小值
}
for (int i = 0; i <= n; i++) // 别忘了全部取 max
if (ans < dp[i])
ans = dp[i];
printf("%d
", ans); // 输出
return 0; // 然后就 AC 了、
}
Solution 2 + 玄学优化
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int max_n = 10000;
struct elem
{
int l, r;
};
int dp[max_n+1];
elem rang[max_n];
inline bool cmp(const elem& a, const elem& b) { return a.r < b.r; }
inline void upd(int& a, const int b) { if (b > a) { a = b; } }
inline int read()
{
int ch = getchar(), t = 1, n = 0;
while (isspace(ch)) { ch = getchar(); }
if (ch == '-') { t = -1, ch = getchar(); }
while (isdigit(ch)) { n = n * 10 + ch - '0', ch = getchar(); }
return n * t;
}
int main()
{
int n = read(), minl, ans = 0;
for (int i = 0; i < n; i++)
rang[i].l = read(), rang[i].r = read(); // 输入
sort(rang, rang + n, cmp); // 按照右端点排序
dp[0] = 0;
for (int i = 1; i <= n; i++) // DP
{
minl = -1;
dp[i] = rang[i-1].r - rang[i-1].l; // 基本数
for (int j = i - 1; j > 0; j--)
{
if (rang[j-1].r < minl) // 超出范围了
break;
if (rang[j-1].r > rang[i-1].l) // 还没有符合
continue;
upd(dp[i], dp[j] + rang[i-1].r - rang[i-1].l); // 更新
upd(minl, rang[j-1].l);
}
}
for (int i = 0; i <= n; i++) // 别忘了要把所有数取 max
if (ans < dp[i])
ans = dp[i];
printf("%d
", ans); // 输出
return 0; // 然后就 AC 了、
}