P4203 [NOI2008] 糖果雨 - 洛谷 (luogu.com.cn)
我宣布这道题的故事背景是我第二喜欢的!第一喜欢的是四叶草魔杖。
本文参考:题解:P4203 [NOI2008] 糖果雨 - 洛谷专栏 (luogu.com.cn)
0.思考
总结题意:
(1)每朵云都有颜色,云只会下这个颜色的糖果雨。
(2)云的运动范围是天空长度 [0, len],在边界中往返运动。
当云朵的左界碰到天空的左界,它会改变方向向右运动,
当云朵完全移出了天空的右界,它会改变方向向左运动。
(3)每一个单位时间云朵向左或向右运动一个单位。
(4)口袋的范围只要有云经过(不用完全覆盖,闭区间),就能接到那片云的糖果。
(5)任何时候天空中所有的云朵颜色都不相同(也就是说输出的是给定时刻和范围的云朵数量)
看数据范围,时间复杂度应该是线性 * 对数级。
最根本的操作就是查询出现时间在当前时间及之前的云朵,是否在给定范围。
让每片云朵一直动肯定是不可能的,考虑在线处理,直接改变查询范围。
1.进一步分析
由于云朵的运动周期为 2 * len(即每 2 * len 个单位时间云朵回到原位置),
所以在输入的时候直接将 。
然后结合其他信息,云朵的左端点(因为左端点范围是 [0, len])与云朵长度,
但左端点是会变的不好存储(需要不变且完整的信息),
而左端点第一次到达左区间 0 的时间就很好(不变),
这样还顺便解决了左右移动方向的问题,两全其美。
(实际上在同一个坐标往左往右,都可以看做是 2 * len 周期里不同的时间段,
本质是一样的,只和到达 0 的时间有关系,所以下文统一看作第一次离开 0 点后云往右移动)
于是,一片 的云朵这么记录成一个二元结构体:
t %= (2 * len);
int x = (t + 2 * len - d * l) % (2 * len); // 时间
// 如果一开始就往右移动,d = 1,那么要移动到 0 增加时间为 2 * len - l
// 如果一开始往左移动,d = -1,那么要移动到 0 增加时间为 l
// t 是出现在 l 的时间,需要加上
int y = r - l; // 长度,为了计算方便这么写
然后我们考虑一个询问 ,如果之前出现的云
能被接到。
首先 ,定义询问区间
和云朵此时区间
,
根据 和
的大小关系,和云朵运动趋势左右分为以下几类:
(以下 和
均在 [0, len] 内,
和
在 [0, 2* len) )
(1)
这时云朵正在向右。
满足 ,而
。
转换得到 。
将含 的项单独提出,得到:
再次整理:
(2)
这时云朵已经走过最右边界,正在向左。
同样满足 ,
而 。
转换得到 。
推导:
将含 的项单独提出,得到:
再次整理:
(3)
这时云朵还没走到最左边界,正在向左。
满足 ,而
。
转换得到 。
将含 的项单独提出,得到:
再次整理:
(4)
这时云朵还没走到最右边界,正在向右。
同样满足 ,
而 。
转换得到 。
推导:
将含 的项单独提出,得到:
再次整理:
只要求出上面四种范围云朵的数量,再加在一起,就是答案。
2.流程
总结上面四种情况,发现是在寻找符合范围的 和
的对数。
很标准的动态二维数点问题,至此我们确定做法:二维树状数组 / CDQ分治 / 树状数组套线段树。
这里我选择定义两个二维树状数组,时间复杂度 ,
空间复杂度 ,可以通过。
要开三倍(y - x + 2 * len)及其他细节见代码:
#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 2e5 + 10;
const int L = 3e3 + 5; // 最大是 3 * len,再开大点就 MLE 了
const int M = 1e6 + 10;
int n, len;
PII mp[M]; // 根据颜色下标存储每片云朵的信息
struct twoDFT {
int tr_d[L][L];
void add(int x, int y, int d) {
x ++; y ++; // 这两个都有可能是 0
// 但树状数组不能以 0 作下标
for (int i = x; i <= L - 5; i += i & -i) {
// 注意这里一定不能直接修改 x 和 y !!因为是双层循环
// 修改了 y 变量,导致外层循环的后续迭代无法正确进行
// 错误写法: for (; y <= L - 10; y += y & -y)
for (int j = y; j <= L - 5; j += j & -j) {
tr_d[i][j] += d;
}
}
}
int query(int x, int y) {
x ++; y ++;
if (x <= 0 || y <= 0) { // 边界
return 0;
}
int res = 0;
for (int i = x; i >= 1; i -= i & -i) {
for (int j = y; j >= 1; j -= j & -j) {
res += tr_d[i][j];
}
}
return res;
}
int get_squ(int lx, int rx, int ly, int ry) { // 注意传参的顺序
if (lx > rx || ly > ry) { // 防负数
return 0;
}
lx = max(lx, 0); ly = max(ly, 0);
rx = min(rx, L - 5); ry = min(ry, L - 5);
return query(rx, ry) - query(rx, ly - 1) -
query(lx - 1, ry) + query(lx - 1, ly - 1);
// 二维前缀和
}
} tra, trb;
void ins(int t, int c, int l, int r, int d) {
t %= (2 * len);
int x = (t + 2 * len - d * l) % (2 * len); // 时间
// 如果一开始就往右移动,d = 1,那么要移动到 0 增加时间为 2 * len - l
// 如果一开始往左移动,d = -1,那么要移动到 0 增加时间为 l
// t 是出现在 l 的时间,需要加上
int y = r - l; // 长度,为了计算方便这么写
mp[c] = {x, y};
tra.add(x, (y - x) + 2 * len, 1); // 统一加上 2 * len,避免负数
trb.add(x, x + y, 1);
}
void del(int c) {
int x = mp[c].first;
int y = mp[c].second;
tra.add(x, (y - x) + 2 * len, -1);
trb.add(x, x + y, -1);
}
int ask(int t, int l, int r) { // 把四种情况推出的公式范围里的云加起来
t %= (2 * len);
int ansa = tra.get_squ(t - r, t, l - t + 2 * len, L - 5); // L - 5 是上界
// y - x 的坐标统一加上 2 * len
// 这里 t - r 可能是负数,但 get_squ 函数处理了这种情况
int ansb = trb.get_squ(0, min(t - len - 1, r - 2 * len + t), l - 2 * len + t, L - 5);
int ansc = trb.get_squ(t + 1, t + r, l + t, L - 5);
int ansd = tra.get_squ(max(len + t + 1, 2 * len + t - r), L - 5, l - t, L - 5);
return ansa + ansb + ansc + ansd;
}
int main () {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> len;
memset(tra.tr_d, 0, sizeof(tra.tr_d));
memset(trb.tr_d, 0, sizeof(trb.tr_d));
for (int i = 1; i <= n; i ++) {
int opt;
cin >> opt;
if (opt == 1) {
int t, c, l, r, d;
cin >> t >> c >> l >> r >> d;
ins(t, c, l, r, d);
}
else if (opt == 2) {
int t, l, r;
cin >> t >> l >> r;
cout << ask(t, l, r) << "\n";
}
else {
int t, c;
cin >> t >> c;
del(c);
}
}
return 0;
}
1029

被折叠的 条评论
为什么被折叠?



