【题解】洛谷 P4203 [NOI2008] 糖果雨 [思维+ 二维树状数组]

P4203 [NOI2008] 糖果雨 - 洛谷 (luogu.com.cn)

我宣布这道题的故事背景是我第二喜欢的!第一喜欢的是四叶草魔杖

本文参考:题解:P4203 [NOI2008] 糖果雨 - 洛谷专栏 (luogu.com.cn)


0.思考

总结题意:

(1)每朵云都有颜色,云只会下这个颜色的糖果雨。

(2)云的运动范围是天空长度 [0, len],在边界中往返运动。

         当云朵的左界碰到天空的左界,它会改变方向向右运动,

        当云朵完全移出了天空的右界,它会改变方向向左运动。

(3)每一个单位时间云朵向左或向右运动一个单位。

(4)口袋的范围只要有云经过(不用完全覆盖,闭区间),就能接到那片云的糖果。

(5)任何时候天空中所有的云朵颜色都不相同(也就是说输出的是给定时刻和范围的云朵数量

看数据范围,时间复杂度应该是线性 * 对数级。

最根本的操作就是查询出现时间在当前时间及之前的云朵,是否在给定范围

让每片云朵一直动肯定是不可能的,考虑在线处理,直接改变查询范围。

1.进一步分析

由于云朵的运动周期为 2 * len(即每 2 * len 个单位时间云朵回到原位置),

所以在输入的时候直接将 t_i\ mod\ 2*len。 

然后结合其他信息,云朵的左端点(因为左端点范围是 [0, len])与云朵长度

但左端点是会变的不好存储(需要不变且完整的信息),

左端点第一次到达左区间 0 的时间就很好(不变), 

这样还顺便解决了左右移动方向的问题,两全其美。

(实际上在同一个坐标往左往右,都可以看做是 2 * len 周期里不同的时间段,

   本质是一样的,只和到达 0 的时间有关系,所以下文统一看作第一次离开 0 点后云往右移动

于是,一片 \left \{ t_i,c_i,l_i,r_i,d_i \right \} 的云朵这么记录成一个二元结构体:

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;                         // 长度,为了计算方便这么写

然后我们考虑一个询问 \left \{ t_i,l_i,r_i \right \},如果之前出现的云 \left \{ x,y \right \} 能被接到。

首先 t_i\ mod\ 2*len,定义询问区间 [l_i,r_i] 和云朵此时区间 [cl,cr]

根据 t_i 和 x 的大小关系,和云朵运动趋势左右分为以下几类:

(以下 [l_i,r_i] 和 [cl,cr] 均在 [0, len] 内,t_i 和 x 在 [0, 2* len) )

(1)t_i\geq x,t_i-x\leqslant len

         t_i 这时云朵正在向右。

         满足 cl\leq r_i\ AND\ cr\geq l_i,而 cl=t_i-x,cr=t_i-x+y

         转换得到 t_i-x\leq r_i\ AND\ t_i-x+y\geq l_i

         将含 x,y 的项单独提出,得到:

         max(0,t_i-len,t_i-r_i)\leq x\leq t_i

         y-x\geq l_i-t_i

         再次整理:

t_i-r_i\leq x\leq t_i

l_i-t_i\leq y-x

(2)t_i\geq x,t_i-x > len

         t_i 这时云朵已经走过最右边界,正在向左。

         同样满足 cl\leq r_i\ AND\ cr\geq l_i

         而 cl=2*len-(t_i-x),cr=2*len-(t_i-x)+y

         转换得到 2*len-(t_i-x)\leq r_i\ AND\ 2*len-(t_i-x)+y\geq l_i

         推导:

         x\leq r_i-2*len+t\ AND\ x+y\geq l_i-2*len+t

         x\leq t_i,\ x<t_i-len,\ x \leq t_i-len-1

         将含 x,y 的项单独提出,得到:

         0\leq x\leq min(2*len,t_i,t_i-len-1,r_i-2*len+t_i)

         x+y\geq l_i-2*len+t_i

         再次整理:

0\leq x\leq min(t_i-len-1,r_i-2*len+t_i)

l_i-2*len+t_i\leq x+y

(3)t_i< x,x-t_i\leqslant len

         t_i 这时云朵还没走到最左边界,正在向左。

         满足 cl\leq r_i\ AND\ cr\geq l_i,而 cl=x-t_i,cr=x-t_i+y

         转换得到 x-t_i\leq r_i\ AND\ x-t_i+y\geq l_i

         将含 x,y 的项单独提出,得到:

         max(0, t_i +1,t_i-len)\leq x\leq min(t_i+len,t_i+r_i)

         x+y\geq l_i+t_i

         再次整理:

t_i +1 \leq x\leq t_i+r_i

l_i+t_i\leq x+y

(4)t_i< x,x-t_i> len

         t_i 这时云朵还没走到最右边界,正在向右。

         同样满足 cl\leq r_i\ AND\ cr\geq l_i

         而 cl=2*len-(x-t_i),cr=2*len-(x-t_i)+y

         转换得到 2*len-(x-t_i)\leq r_i\ AND\ 2*len-(x-t_i)+y\geq l_i

         推导:

         2*len+t_i-r_i\leq x\ AND\ y-x\geq l_i-2*len-t_i

         t_i< x,x> len+t_i

         将含 x,y 的项单独提出,得到:

         max(0,t_i+1,len+t_i+1,2*len+t_i-r_i)\leq x

         y-x\geq l_i-2*len-t_i

         再次整理:

max(len+t_i+1,2*len+t_i-r_i)\leq x

l_i-2*len-t_i\leq y-x

只要求出上面四种范围云朵的数量,再加在一起,就是答案。

2.流程

总结上面四种情况,发现是在寻找符合范围的 \left \{ x,y-x \right \} 和 \left \{ x,x+y \right \} 的对数。

很标准的动态二维数点问题,至此我们确定做法:二维树状数组 / CDQ分治 / 树状数组套线段树。

这里我选择定义两个二维树状数组,时间复杂度 O(Nlog^2\ (3*LEN))

空间复杂度 O((3*LEN)^2),可以通过。

要开三倍(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;
} 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值