题解:洛谷 P14078 金币收集
【题目来源】
洛谷:P14078 [GESP202509 七级] 金币收集 - 洛谷
【题目描述】
小 A 正在游玩收集金币的游戏。具体来说,在数轴上将会出现 \(n\) 枚金币,其中第 \(i\) 枚(\(1≤i≤n\))金币将会在时刻 \(t_i\) 出现在数轴上坐标为 \(x_i\) 的位置。小 A 必须在时刻 \(t_i\) 恰好位于坐标 \(x_i\),才可以获得第 \(i\) 枚金币。
游戏开始时为时刻 \(0\),此时小 A 的坐标为 \(0\)。正常来说,小 A 可以按游戏机的按键在数轴上左右移动,但不幸的是游戏机的左方向键失灵了。小 A 每个时刻只能选择保持不动,或是向右移动一个单位。换言之,如果小 A 在时刻 \(t\) 的坐标为 \(x\),那么他在时刻 \(t+1\) 的坐标只能是 \(x\) 或是 \(x+1\) 二者之一,分别对应保持不动和向右移动。
小 A 想知道他最多能收集多少枚金币。你能帮他收集最多的金币吗?
【输入】
第一行,一个正整数 \(n\),表示金币的数量。
接下来 \(n\) 行,每行两个正整数 \(x_i,t_i\),分别表示金币出现的坐标与时刻。
【输出】
输出一行,一个整数,表示小 A 最多能收集的金币数量。
【输入样例】
3
1 6
3 7
2 4
【输出样例】
2
【核心思想】
-
问题分析:小 A 在数轴上从时刻 \(0\)、坐标 \(0\) 出发,每个时刻只能保持不动或向右移动一个单位。金币 \(i\) 在时刻 \(t_i\) 出现在坐标 \(x_i\),必须在恰好时刻 \(t_i\) 位于 \(x_i\) 才能收集。求最多能收集的金币数量。由于只能向右移动,小 A 的坐标随时间单调不减,且从 \((x_i, t_i)\) 能到达 \((x_j, t_j)\)(\(t_i \le t_j\))的条件是 \(x_j - x_i \le t_j - t_i\),即 \(x_j - t_j \le x_i - t_i\)。此外,从原点出发能到达金币 \(i\) 的前提是 \(x_i \le t_i\)。
-
算法选择:
- 线性 DP + 二分(类似 LIS):将问题转化为在筛选后的金币序列中,寻找最长的子序列,使得相邻两个金币满足 \(x_j \ge x_i\) 且 \(x_j - t_j \le x_i - t_i\)。这类似于最长递增子序列(LIS),但比较条件变为 \(x - t\) 的非递增性。使用二分维护当前最优序列,对于每个新金币,若它能接在序列末尾则直接加入;否则找到第一个满足 \(x - t > x_{mid} - t_{mid}\) 的位置并替换,保证序列始终最优。
- 排序预处理:先按 \(x\) 升序排序,\(x\) 相同时按 \(t\) 升序,保证处理顺序满足坐标单调性。
-
关键步骤:
- 筛选有效金币:读入 \(n\) 个金币,只保留满足 \(x_i \le t_i\) 的金币(否则从原点无法到达),存入数组 \(a\)。
- 排序:对有效金币按 \(x\) 升序、\(t\) 升序排序。
- 初始化最长序列:用数组 \(b\) 维护当前最优序列,\(b[1] = a[1]\),长度 \(len = 1\)。
- 遍历与更新:对于 \(i\) 从 \(2\) 到 \(cur\):
- 若 \(a[i].x - b[len].x \le a[i].t - b[len].t\)(即可接在末尾),则 \(b[++len] = a[i]\)。
- 否则,二分查找第一个满足 \(a[i].x - b[mid].x > a[i].t - b[mid].t\) 的位置 \(j\),用 \(a[i]\) 替换 \(b[j]\)。
- 输出答案:\(len\) 即为最多能收集的金币数。
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log n)\),排序 \(O(n \log n)\),每个元素二分 \(O(\log n)\)。
- 空间复杂度:\(O(n)\),存储筛选后的金币和最优序列。
-
线性 DP + 二分的核心思想:
- 可达性条件转化:从 \((x_i, t_i)\) 到 \((x_j, t_j)\) 的可达性条件 \(x_j - x_i \le t_j - t_i\) 可转化为 \(x_j - t_j \le x_i - t_i\),这使得比较变得简洁。
- LIS 思想的推广:问题本质是在二维偏序下求最长链,通过排序固定一维(\(x\)),另一维转化为 \(x - t\) 的非递增序列,用二分维护最长序列。
- 贪心替换的合理性:当新元素无法接在末尾时,用它替换第一个不满足条件的位置,不会使后续可接的元素变少,这是 LIS 二分解法的经典性质。
- 适用场景:适用于二维偏序下的最长链问题,如导弹拦截、任务调度等需要同时满足两个维度约束的场景。
【算法标签】
普及+ #线性DP-一维
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100005; // 定义最大数据量
int n; // 输入的数据对数
int maxn; // 未使用的变量(可删除)
int len; // 记录当前最长序列长度
// 定义节点结构体,存储x和t值
struct Node
{
int x, t;
} a[N], b[N]; // a数组存储原始数据,b数组存储最长序列
/**
* 比较函数,用于排序
* @param x 第一个节点
* @param y 第二个节点
* @return 先按x升序,x相同按t升序
*/
bool cmp(Node x, Node y)
{
if (x.x != y.x)
return x.x < y.x;
return x.t < y.t;
}
/**
* 二分查找函数,寻找插入位置
* @param x 要查找的节点
* @return 合适的插入位置
*/
int find(Node x)
{
int L = 1, R = len, mid;
while (L < R)
{
mid = (L + R) / 2;
if (x.x - b[mid].x > x.t - b[mid].t)
R = mid;
else
L = mid + 1;
}
return L;
}
int main()
{
cin >> n;
int cur = 0; // 记录有效数据数量
// 输入并筛选有效数据(x <= t)
for (int i = 1; i <= n; i++)
{
int x, t;
cin >> x >> t;
if (x <= t)
{
a[++cur].x = x;
a[cur].t = t;
}
}
// 对有效数据进行排序
sort(a + 1, a + cur + 1, cmp);
// 初始化最长序列
len = 1;
b[1] = a[1];
// 构建最长满足条件的序列
for (int i = 2; i <= cur; i++)
{
// 如果当前元素可以直接加入序列
if (a[i].x - b[len].x <= a[i].t - b[len].t)
{
b[++len] = a[i];
}
// 否则找到合适的位置替换
else
{
int j = find(a[i]);
b[j] = a[i];
}
}
// 输出最长序列长度
cout << len << endl;
return 0;
}
【运行结果】
3
1 6
3 7
2 4
2
浙公网安备 33010602011771号