【题目链接】
ybt 4150:【GESP2509七级】⾦币收集
洛谷 P14078 [GESP202509 七级] 金币收集
【题目难度】:B
【题目考点】
1. 线性动规:求最长上升子序列
- 动规求最长上升子序列,时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 贪心+二分求最长上升子序列,时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
相关概念及方法见:ybt 1281:最长上升子序列
【解题思路】
随着时间 t t t的增大,角色只能向位置 x x x不变或更大的位置走。 t i t_i ti时刻到达 x i x_i xi才能获得一个金币。
为了方便理解,可以把时间 t t t当做直角坐标系的x轴,把位置 x x x当做y轴,每个可以获得金币的时刻和位置 ( t i , x i ) (t_i, x_i) (ti,xi)就是平面直角坐标系中的一个给定的点。角色从原点 ( 0 , 0 ) (0,0) (0,0)出发,时间 t t t每增加1,角色只能选择让位置 x x x不变或增加1,也就是只能向右移动一个位置( t t t增加1, x x x不变),或向右上方移动一个位置( t t t增加1, x x x增加1)。本题求从 ( 0 , 0 ) (0,0) (0,0)出发按照上述规则移动,可以经过最多的给定点的数量。

如果一个点 ( t , x ) (t,x) (t,x)满足 t < x t<x t<x,那么基本从 ( 0 , 0 ) (0,0) (0,0)出发每次都向右上移动,当横坐标等于 t t t时,纵坐标也小于 x x x,也就是无论如何都无法到达这样的点。因此在输入时,只保留满足 t ≤ x t\le x t≤x的点。
经过点的过程中,假设先经过第 j j j点,再经过第 i i i点。首先要满足 x i ≥ x j x_i\ge x_j xi≥xj。从第 j j j点走到第 i i i点所用的时间为 t i − t j t_i-t_j ti−tj,经过的路程为 x i − x j x_i-x_j xi−xj。每个单位时间最多走一个单位距离,因此 t i − t j t_i-t_j ti−tj时间内最多走过的路程为 t i − t j t_i-t_j ti−tj。因此实际走过的路程 x i − x j x_i-x_j xi−xj必须满足 x i − x j ≤ t i − t j x_i-x_j\le t_i-t_j xi−xj≤ti−tj,也可以写为 t i − x i ≥ t j − x j t_i-x_i\ge t_j-x_j ti−xi≥tj−xj。
因此,选择经过的点中如果第 i i i点是第 j j j点的下一个点,必须满足 t j ≤ t i , x i ≥ x j , t i − x i ≥ t j − x j t_j\le t_i, x_i\ge x_j, t_i-x_i\ge t_j-x_j tj≤ti,xi≥xj,ti−xi≥tj−xj
接下来要按顺序遍历各个点,看最多可以经过哪些点。问题在于:按什么顺序遍历各个点。可选的排序方法有两种:按 x x x升序顺序遍历各点,或按 t t t升序顺序遍历各点。
-
如果按 t t t升序遍历各点,先访问并选择了第 j j j点,再访问第 i i i点,看是否可以选择第 i i i点。由于各点关于 t t t是升序的,因此直接满足了 t j ≤ t i t_j\le t_i tj≤ti,还需要再判断如果第 i i i点满足 x i ≥ x j x_i\ge x_j xi≥xj与 t i − x i ≥ t j − x j t_i-x_i\ge t_j-x_j ti−xi≥tj−xj,那么才可以选择第 i i i点,使选择点的序列中第 i i i点作为 j j j点的下一个点。
-
如果按 x x x升序遍历各点,如果两个点的 x x x相同,也可以先经过 t t t较小的点,再经过 t t t较大的点,因此当 x x x相同时,要按 t t t从小到大排序。先访问并选择了第 j j j点,而后再访问第 i i i点,看是否可以选择第 i i i点。由于各点关于 x x x是升序的,因此已经满足 x i ≥ x j x_i\ge x_j xi≥xj,接下来只要判断满足 t i − x i ≥ t j − x j t_i-x_i\ge t_j-x_j ti−xi≥tj−xj,自然就满足了 t i ≥ t j t_i\ge t_j ti≥tj,因此那么就可以选择点 i i i作为选点序列中点 j j j的下一个点。
已知 x i ≥ x j x_i\ge x_j xi≥xj,对于 t i − x i ≥ t j − x j t_i-x_i\ge t_j-x_j ti−xi≥tj−xj,不等式坐标加上较大的 x i x_i xi,右边加上较小的 x j x_j xj,不等式符号不变,得到 t i ≥ t j t_i\ge t_j ti≥tj。
这种排序方法在判断能否选择点 i i i时减少了一个判断条件,更加简洁。因此选择对所有的点按 x x x升序排序, x x x相等时按 t t t升序排序。
对于排序后的点序列,要找到最长的子序列,子序列中相邻两点前面的第 j j j点和后面的第 i i i点要满足 t j − x j ≤ t i − x i t_j-x_j\le t_i-x_i tj−xj≤ti−xi。想要求选择点序列的最大长度
相当于存在序列 b b b, b i = t i − x i b_i=t_i-x_i bi=ti−xi,求 b b b序列的最长不降子序列的长度。
其方法等同于求最长上升子序列,相关概念及方法参考:ybt 1281:最长上升子序列,以下不再详细解释方法的原理。
解法1(非正解):动态规划求最长不降子序列
状态定义: d p i dp_i dpi表示 b b b序列前 i i i元素中最长不降子序列的长度。
状态转移方程: d p i = max { d p j + 1 } , b j ≤ b i , 1 ≤ j < i dp_i =\max\{dp_j+1\}, b_j\le b_i, 1\le j<i dpi=max{dpj+1},bj≤bi,1≤j<i
求 d p dp dp数组的最大值,即为本题结果。
时间复杂度为 O ( n 2 ) O(n^2) O(n2)
解法2:贪心+二分
设 d d d序列, d i d_i di表示 b b b序列长为 i i i的所有不降子序列中,末尾元素最小的不降子序列的末尾元素。 d d d序列长为 l e n len len
- 如果 b i ≥ d l e n b_i\ge d_{len} bi≥dlen,那么可以在长为 l e n len len的不降子序列末尾增加 b i b_i bi,得到长为 l e n + 1 len+1 len+1的不降子序列,末尾元素为 b i b_i bi, l e n len len增加1。
- 如果 b i < d l e n b_i < d_{len} bi<dlen,由于 d d d序列是单调递增的,可以在 d d d序列中通过二分找到满足 d l − 1 ≤ b i < d l d_{l-1}\le b_i<d_l dl−1≤bi<dl位置 l l l,即位置 l l l是满足 d l > b i d_l>b_i dl>bi的最小下标。可以在长为 l − 1 l-1 l−1的末尾元素为 d l − 1 d_{l-1} dl−1的序列末尾添加 b i b_i bi,得到长为 l l l的末尾元素为 b i b_i bi的不降子序列,将 d l d_l dl更新为 b i b_i bi。
最后 l e n len len即为 b b b序列的最长不降子序列的长度。
时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)
解法2:离散化+树状数组
对于动规过程:
状态定义: d p i dp_i dpi表示 b b b序列前 i i i元素中最长不降子序列的长度。
状态转移方程: d p i = max { d p j + 1 } , b j ≤ b i , 1 ≤ j < i dp_i =\max\{dp_j+1\}, b_j\le b_i, 1\le j<i dpi=max{dpj+1},bj≤bi,1≤j<i
先对 b b b序列做离散化(注意离散化后的最小值为1,不能为0。因为树状数组的下标最小为1)。而后设 c c c数组, c x c_x cx表示以 b b b数组值为 x x x的元素为结尾的最长上升子序列的长度。那么 = max { d p j } , b j ≤ b i =\max\{dp_j\}, b_j\le b_i =max{dpj},bj≤bi,就是 c c c数组区间 [ 1 , b i ] [1,b_i] [1,bi]的最大值,该值加1就是 d p i dp_i dpi的值,而后更新 c b i = m a x ( c b i , d p i ) c_{b_i}=max(c_{b_i}, dp_i) cbi=max(cbi,dpi)。可以使用树状数组维护序列的区间最大值,只能求前缀最大值。
时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)
【题解代码】
解法1(非正解):动态规划求最长上升子序列 56pt
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
struct Pos
{
int x, t;
bool operator < (const Pos &b) const
{
return x < b.x || x == b.x && t < b.t;
}
} a[N];
int an, n, b[N], dp[N], ans;//dp[i]:前i个点中选点包含第i点的最大选点数量
int main()
{
int x, t;
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> x >> t;
if(t >= x)//只保留可能走到的点
a[++an] = Pos{x, t};
}
sort(a+1, a+1+an);
for(int i = 1; i <= an; ++i)
b[i] = a[i].t-a[i].x;
for(int i = 1; i <= an; ++i)
{
dp[i] = 1;
for(int j = 1; j < i; ++j) if(b[j] <= b[i])
dp[i] = max(dp[i], dp[j]+1);
ans = max(ans, dp[i]);
}
cout << ans;
return 0;
}
解法2:贪心+二分
- 写法1:手写二分
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
struct Pos
{
int x, t;
bool operator < (const Pos &b) const
{
return x < b.x || x == b.x && t < b.t;
}
} a[N];
int an, n, b[N], d[N], len, ans;//d[i]:b序列长为i的不降子序列的最小末尾元素
int main()
{
int x, t;
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> x >> t;
if(t >= x)//只保留可能走到的点
a[++an] = Pos{x, t};
}
sort(a+1, a+1+an);
for(int i = 1; i <= an; ++i)
b[i] = a[i].t-a[i].x;
d[++len] = b[1];
for(int i = 2; i <= an; ++i)
{
if(b[i] >= d[len])
d[++len] = b[i];
else
{
int l = 1, r = len;
while(l <= r)
{
int mid = (l+r)/2;
if(d[mid] > b[i])
r = mid-1;
else
l = mid+1;
}
d[l] = b[i];
}
}
cout << len;
return 0;
}
- 写法2:使用STL upper_bound
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
struct Pos
{
int x, t;
bool operator < (const Pos &b) const
{
return x < b.x || x == b.x && t < b.t;
}
} a[N];
int an, n, b[N], d[N], len, ans;//d[i]:b序列长为i的不降子序列的最小末尾元素
int main()
{
int x, t;
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> x >> t;
if(t >= x)//只保留可能走到的点
a[++an] = Pos{x, t};
}
sort(a+1, a+1+an);
for(int i = 1; i <= an; ++i)
b[i] = a[i].t-a[i].x;
d[++len] = b[1];
for(int i = 2; i <= an; ++i)
{
if(b[i] >= d[len])
d[++len] = b[i];
else
{
int l = upper_bound(d+1, d+1+len, b[i])-d;
d[l] = b[i];
}
}
cout << len;
return 0;
}
解法3:离散化+树状数组
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
struct Pos
{
int x, t;
bool operator < (const Pos &b) const
{
return x < b.x || x == b.x && t < b.t;
}
} a[N];
vector<int> t;
int an, n, b[N], ans, tree[N];//c[x]:表示以b数组值为x的元素为结尾的最长上升子序列的长度。tree是c的树状数组,维护区间最大值。
void discretization()
{
for(int i = 1; i <= an; ++i)
t.push_back(b[i]);
sort(t.begin(), t.end());
t.erase(unique(t.begin(), t.end()), t.end());
for(int i = 1; i <= an; ++i)
b[i] = upper_bound(t.begin(), t.end(), b[i])-t.begin();//保证值>=1,因为要作为树状数组的下标
}
int lowbit(int x)
{
return x & -x;
}
void update(int i, int v)//c[i] = v
{
for(int x = i; x <= an; x += lowbit(x))
tree[x] = max(tree[x], v);
}
int maxVal(int i)//max{c[1]...c[i]}
{
int res = 0;
for(int x = i; x > 0; x -= lowbit(x))
res = max(res, tree[x]);
return res;
}
int main()
{
int x, t;
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> x >> t;
if(t >= x)//只保留可能走到的点
a[++an] = Pos{x, t};
}
sort(a+1, a+1+an);
for(int i = 1; i <= an; ++i)
b[i] = a[i].t-a[i].x;
discretization();
for(int i = 1; i <= an; ++i)
{
int dpVal = maxVal(b[i])+1;
ans = max(ans, dpVal);
update(b[i], dpVal);
}
cout << ans;
return 0;
}