F. Removing Robots
题目:
给了 n 个机器人的坐标 xi 和移动距离 di, 每个机器人可以被主动激活向正轴移动 di 距离, 如果途中碰到别的机器人也会激活别的机器人, 以此递归进行。主动激活的次数不限制,机器人被激活后移动完成就会消失,询问最后机器人的剩余的不同情况数。答案 mod 998244353 。
数据范围:
1 ≤ N ≤ 2 ∗
− ≤
≤
1 ≤ ≤
思路:
DP过程:
状态表示:f[i] 表示考虑关于 [i,n] 机器人的方案数。
集合划分:以对第 i 个机器人的状态为依据划分为三类:主动激活/被动激活/未被激活——
1.主动激活:那么后面的一系列影响范围内的机器人都会被波及,不考虑前面机器人的局面时,继承从第1个不被波及的机器人开始(这里的不被波及不仅仅是第i个机器人的范围内,而且包括后面被第i个机器人波及到的而导致连锁反应后,最终不被波及的第一个机器人),记为right,从dp[right]处继承;
2.被动激活:不考虑前面机器人的局面,同主动激活
3.未被激活:不考虑前面机器人的局面,从下一个机器人继承,即继承f[i+1]
所以状态转移方程:f[i] = (f[i+1] + f[right]) % mod。最终答案:f[1]
实现过程:
1.dp是从后往前考虑机器人的方案数的,所以将机器人按坐标从小到大排序,为了方便找第一个不被机器人i波及到的机器人 right ,记录每个机器人的最远波及位置 xi + di ;
2.要找出第一个不被波及的机器人 right ,用栈模拟实现————
因为不同坐标的机器人与机器人之间是通过波及范围有关联的,在确定每个 i 对应的 right 时某种情况是存在单调性的。我们从后向前处理机器人,坐标从大到小,我们要找的第i个机器人对应的 right 是不受第i个机器人及其被波及到的机器人造成的连锁反应下的第一个不被波及的机器人。
假设第i个机器人激活后波及的范围(包括连锁反应)是 [i+1,j-1] ,所以第i个机器人的 right = j;对于第 i-1 个机器人:
(1)假设它的波及范围包括 i ,那么波及范围也就包括第 i 个机器人的波及范围,也就是说,第i-1个机器人的right一定不会在 [i+1,j-1] 这个在考虑第i个机器人时就不满足的范围,所以第i-1个机器人的right>=j;
(2)假设第 i-1 个机器人的波及范围不包括第 i 个机器人,那么 right = i,同样不会是在 [i+1,j-1] 这个在考虑第 i 个机器人时就不满足的范围。
所以,区间 [i+1,j-1] 在之后判断第 0 ~ i-1 个机器人的 right 时都不会用到,它可以在考虑第 i 个机器人时就淘汰掉。所以存在单调性,可以用栈实现。
同时,还要注意对栈的预处理:int top = 0; stk[++top] = n + 1; v.push_back({ 2e9 + 10,0 });
考虑当第 i 个机器人激活后,后面不存在未被波及的机器人,无需继承时。有两种情况:
1.第 n 个机器人一定符合该情况;
2.可能存在的符合该情况第 i 个机器人。
这时,第 i 个机器人被激活也是一种方案,只是不需要继承,right = n+1,f[n+1] = 1。将 n+1 入栈就是考虑第2种情况,当找不到符合实际意义的 right 时的 right 就是 n+1 。
Code:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
#define int long long
#define x first
#define y second
//typedef long long LL;
typedef pair<int, int>PII;
const int mod = 998244353;
int n;
void solve()
{
cin >> n;
vector<PII>v(n + 1);
for (int i = 1; i <= n; i++)
{
cin >> v[i].x >> v[i].y; //输入n个机器人的坐标及移动距离
v[i].y += v[i].x; //将移动距离更新为最远波及坐标
}
sort(v.begin() + 1, v.end()); //将n个机器人按坐标从小到大排序
vector<int>f(n + 2, 0); //定义dp数组f
f[n + 1] = 1; //因为当考虑第n个机器人时,它是被激活/未被激活,之后没有机器人受到波及,只看它自己即可,各自都算1个方案
vector<int>stk(n + 2, 0); //定义数组栈
int top = 0; //栈顶为top
stk[++top] = n + 1; //预处理将第n+1个不存在的机器人入栈,坐标为+无穷,最远波及为0
v.push_back({ 2e9 + 10,0 }); //将坐标设为最远距离max(Xi+Di)
for (int i = n; i; i--) //dp过程是从后往前处理
{
while (top && v[stk[top]].x < v[i].y)top--; //将此时栈中所有满足坐标v[stk[top]].x在第i个机器人(包括连锁反应)的波及范围v[i].y内的机器人出栈。
int right = max(stk[top], i); //因为栈stk的栈顶为stk[1]=n+1,而v[stk[1]].x=2e9,所以一定会因为&&后者判断语句跳出while。如果跳出while是当top=1时,那么right=n+1,说明第i个机器人波及到了剩余的所有机器人,这与处理第n个机器人相同,方案为f[n+1]=1;如果是因为top还未减到1时而跳出的循环,那么一定有 stk[top] > i(因为是从后往前入栈地,已在栈中的肯定比i大),所以right = stk[top]
stk[++top] = i; //将i入栈
f[i] = (f[i + 1] + f[right]) % mod; //dp,关于机器人i~n方案数 = 不激活方案f[i+1] + 已激活方案f[right]
}
cout << f[1] << endl; //最终结果为关于机器人1~n的方案数f[1]
}
signed main()
{
int t = 1;
//cin >> t;
while (t--)
{
solve();
}
return 0;
}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
吐槽:该题不仅考查dp思维,还巧妙地运用栈进一步解决优化,Orz大佬tpl,看着大佬的题解不禁思绪乱飘,犹记学dp模板时那抓耳挠腮的苦恼,一写这题感觉dp学得仅仅是皮毛啊,还有很长的路要探索。咳,题目感想:
1.dp的题写得不多,这题dp的集合定义,划分处理等等都让我感觉新鲜又合理。
2.在学习滑动窗口时了解到栈可以对于单调性的模型处理,但是实际应用,代码实现时,感觉将抽象的模型提取出出单调性进而想到用栈是真神奇啊。实现dp和栈时很多细节处理感觉很考验思维。