2025.6 陈诺 数据结构

CF1609E William The Oblivious

给你一个长为 n 的字符串,只包含 abc 三种字符。q 次操作,每次把一个位置的字符改成给定字符,询问当前串至少修改几次满足不包含子序列 abc。修改指把一个位置的字符修改成 a、b、c 三种字符之一。

1≤n,q≤105。


用线段树维护六个东西:

区间 a,b,c 的数量 a,b,c
区间不存在 ab,bc,abc 最少需要改变几个位置 ab,bc,abc

合并:

ak​=al​+ar​
bk​=bl​+br​
ck​=cl​+cr​
abk​=min(al​+abr​,abl​+br​)
bck​=min(bl​+bcr​,bcl​+cr​)
abck​=min(abcl​+cr​,abl​+bcr​,al​+abcr​)

每次查询 abcrt​ 就是答案。


五彩斑斓的世界

一个长为 nn 的序列 aa,有 mm 次操作

把区间 [l,r][l,r] 中大于 xx 的数减去 xx。
查询区间 [l,r][l,r] 中 xx 的出现次数。

考虑只有整体修改。

我们令 k 表示一个数列中的可能最大值。

若 2x≥k,则我们令大于 x 的数减去 x 之后,就没有比 x 大的数了,则 k 在操作后至少减少 k−x。

若 2x<k,则我们令小于等于 x 的数加上 x,就没有比 x 小的数了,然后。打全局减的标记,则 k 在操作后至少减少 x。

所以k是不增的,我们用并查集(或值域链表)维护每个值有多少数,这样均摊每次操作是O(1)的把修改前对应值域上的点的并查集父亲设为修改后的并查集即可。

我们对序列进行分块。

对于一个块的全局修改、全局查询,我们直接按照上述方式去做即可,总时间复杂度 O(n sqrt n)

对于一个块的部分修改,我们先暴力把每个位置的实际值还原,然后对块进行重构即可。单次 O(n)

发现空间会爆炸,但是每个块独立,所以离线询问用块挨个处理即可


P7447 [Ynoi2007] rgxsxrs

给定一个长为 n 的序列 a,需要实现 m 次操作:

1 l r x:表示将区间 [l,r] 中所有 >x 的元素减去 x。

2 l r:表示询问区间 [l,r] 的和,最小值,最大值。

值域分块

按照值域分为logV个块,分别为21-22 2^2 - 2^3 ......

然后每个块维护线段树

对于查询,直接log^2查询即可

对于修改。

我们考虑一个区间

如果最小值大于x

那就直接整体打标记,然后跌落,跌落是log * 当前值域块元素数量

如果最大值小于x,什么都不会发生

如果夹在中间,直接暴力即可。

证明,每次跌落是log的,一共n个数,每个数最多跌落log次,所以是nlog方的。

代码毒瘤。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = (l); i <= (r); ++i)
#define per(i, r, l) for (int i = (r); i >= (l); --i)
#define ls (num << 1)
#define rs (num << 1 | 1)
const int inf = INT_MAX, df = 2e4 + 7, N = 5e5 + 7, B = 16, K = 32 ;/*相对较好的参数*/
int i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z;
long long ma[9][df << 1];
long long tag[9][df << 1];
int mx[9][df << 1], mn[9][df << 1];
int a[N];
long long sum[9][df << 1];
long long lb[9], rb[9]; /*这里是倍增分块的上界和下界*/
/*
ma:区间有几个数
sum:区间和
mx:区间最大值
mn:区间最小值
tag:懒标记
*/
void pushup(int blk, int num)
{
	ma[blk][num] = ma[blk][ls] + ma[blk][rs];
	sum[blk][num] = sum[blk][ls] + sum[blk][rs];
	mx[blk][num] = max(mx[blk][ls], mx[blk][rs]);
	mn[blk][num] = min(mn[blk][ls], mn[blk][rs]);
	return;
}
/*线段树上传信息*/
void pushdown(int blk, int num)
{
	if (!tag[blk][num])
		return;
	tag[blk][ls] += tag[blk][num] * (ma[blk][ls] != 0), tag[blk][rs] += tag[blk][num] * (ma[blk][rs] != 0);
	if (ma[blk][ls])
		sum[blk][ls] -= tag[blk][num] * ma[blk][ls], mx[blk][ls] -= tag[blk][num], mn[blk][ls] -= tag[blk][num];
	if (ma[blk][rs])
		sum[blk][rs] -= tag[blk][num] * ma[blk][rs], mx[blk][rs] -= tag[blk][num], mn[blk][rs] -= tag[blk][num];
	tag[blk][num] = 0;
	return;
}
/*线段树下传标记*/
int getblk(int x)
{
	int now = 1;
	while (x > rb[now])
		now++;
	return now;
}
/*x在第几个值域块*/
void blk_pushdown(int blk, int le, int rig, long long &x)
{
	if (!x)
		return;
	rep(i, le, rig) if (a[i] >= lb[blk] && a[i] <= rb[blk]) a[i] -= x;
	x = 0;
	return;
}
/*线段树的叶子节点向序列下传标记*/
void blk_pushup(int blk, int le, int rig, int num)
{
	ma[blk][num] = 0, sum[blk][num] = 0, mx[blk][num] = 0, mn[blk][num] = inf;
	rep(i, le, rig)
	{
		if (a[i] >= lb[blk] && a[i] <= rb[blk])
		{
			ma[blk][num]++;
			sum[blk][num] += a[i];
			mx[blk][num] = max(mx[blk][num], a[i]);
			mn[blk][num] = min(mn[blk][num], a[i]);
		}
	}
	return;
}
/*把序列的信息上传到线段树的叶子节点上*/
void insert(int blk, int num, int le, int rig, int p, int x)
{
	if (rig - le + 1 <= K)
	{
		blk_pushdown(blk, le, rig, tag[blk][num]);
		a[p] = x;
		blk_pushup(blk, le, rig, num);
		return;
	}
	int mid = (le + rig) >> 1;
	pushdown(blk, num);
	if (p <= mid)
		insert(blk, ls, le, mid, p, x);
	else
		insert(blk, rs, mid + 1, rig, p, x);
	pushup(blk, num);
	return;
}
/*线段树插入节点*/
void blk_upd(int blk, int le, int rig, int x)
{
	rep(i, le, rig) if (a[i] >= lb[blk] && a[i] <= rb[blk] && a[i] > x)
	{
		a[i] -= x;
		if (a[i] < lb[blk])
		{
			insert(getblk(a[i]), 1, 1, n, i, a[i]);
		} /*插入另一个块的线段树*/
	}
	return;
}
/*块内暴力执行1号操作*/
void blk_qry(int blk, long long &ans, int &maxn, int &minn, int le, int rig, int l, int r)
{
	rep(i, max(le, l), min(rig, r))
	{
		if (a[i] >= lb[blk] && a[i] <= rb[blk])
		{
			ans += a[i], maxn = max(maxn, a[i]), minn = min(minn, a[i]);
		}
	}
	return;
}
/*块内暴力执行查询*/
void upd(int blk, int num, int le, int rig, int l, int r, int x)
{
	if (mx[blk][num] <= x)
		return; /*小于就跳过*/
	if (rig - le + 1 <= K)
	{
		blk_pushdown(blk, le, rig, tag[blk][num]);
		l = max(l, le), r = min(r, rig);
		blk_upd(blk, l, r, x);
		blk_pushup(blk, le, rig, num);
		return;
	} /*到达叶子暴力修改*/
	if (le >= l && rig <= r && mn[blk][num] - lb[blk] >= x)
	{
		sum[blk][num] -= x * ma[blk][num], mn[blk][num] -= x, mx[blk][num] -= x; /*直接打懒标记*/
		tag[blk][num] += x;
		return;
	}
	int mid = (le + rig) >> 1;
	pushdown(blk, num);
	if (l <= mid)
		upd(blk, ls, le, mid, l, r, x);
	if (r > mid)
		upd(blk, rs, mid + 1, rig, l, r, x);
	pushup(blk, num);
	return;
}
/*线段树上执行操作*/
void qry(long long &ans, int &maxn, int &minn, int blk, int num, int le, int rig, int l, int r)
{
	if (le >= l && rig <= r)
	{
		ans += sum[blk][num], maxn = max(maxn, mx[blk][num]), minn = min(minn, mn[blk][num]);
		return;
	} /*叶子节点暴力查询*/
	if (rig - le + 1 <= K)
	{ /*完全包含,直接统计*/
		blk_pushdown(blk, le, rig, tag[blk][num]);
		blk_qry(blk, ans, maxn, minn, le, rig, l, r);
		blk_pushup(blk, le, rig, num);
		return;
	}
	int mid = (le + rig) >> 1;
	pushdown(blk, num); /*注意及时下传和上传*/
	if (l <= mid)
		qry(ans, maxn, minn, blk, ls, le, mid, l, r);
	if (r > mid)
		qry(ans, maxn, minn, blk, rs, mid + 1, rig, l, r);
	pushup(blk, num);
	return;
}
inline int read()
{
	int x = 0, y = 1;
	char ch = getchar();
	while (ch > '9' || ch < '0')
		y = (ch == '-') ? -1 : 1, ch = getchar();
	while (ch >= '0' && ch <= '9')
		x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
	return x * y;
}
void fwrite(int x)
{
	if (x > 9)
		fwrite(x / 10);
	putchar(x % 10 + '0');
	return;
} /*最好加上快读快输*/
void llfwrite(long long x)
{
	if (x > 9)
		llfwrite(x / 10);
	putchar(x % 10 + '0');
	return;
}
int main()
{
	rep(j, 1, 8) rep(i, 0, df * 2 - 2) mn[j][i] = inf, mx[j][i] = 0;/*初始化*/
	n = read(), q = read();
	rep(i, 1, 8) lb[i] = rb[i - 1] + 1, rb[i] = lb[i] * B - 1;/*预处理*/
	rep(i, 1, n) a[i] = x = read(), insert(getblk(x), 1, 1, n, i, x);
	int op = 0, l = 0, r = 0, x = 0, lastans = 0, maxn = 0, minn = 0;
	long long ans = 0;
	while (q--)
	{
		op = read(), l = read() ^ lastans, r = read() ^ lastans;
		if (op == 1)
		{
			x = read() ^ lastans;/*强制在线*/
			rep(i, 1, 8) if (rb[i] >= x) upd(i, 1, 1, n, l, r, x);
		}
		else
		{
			ans = 0, maxn = 0, minn = inf;
			rep(i, 1, 8) qry(ans, maxn, minn, i, 1, 1, n, l, r);
			llfwrite(ans), putchar(' '), fwrite(minn), putchar(' '), fwrite(maxn), putchar('\n');
			lastans = ans % 1048576;
		}
	}
}


P8421 [THUPC 2022 决赛] rsraogps

给序列 a1​,…,an​,b1​,…,bn​,c1​,…,cn​,

定义区间 [l,r] 的价值为 al​,…,ar​ 按位与,bl​,…,br​ 按位或,cl​,…,cr​ 的最大公因数,这三者的乘积;

m 次查询,每次查询给出区间 [l,r],查询满足 l≤l′≤r′≤r 的 [l′,r′] 的价值之和。


  1. 观察性质

or and gcd这三种运算在不断累积时有一个很好的性质,都有单调性。

也就是说or的1一定会越来越多,and的0一定会越来越多,gcd一定会越来越小。

然后考虑暴力差分扫描线,每次更新都是O(n)的,总复杂度n^2。
但是根据刚才那个性质,只会更新log V次

所以我们需要排除非更新计算。

然后考虑 r 向右移 1,这个时候 si​ 的变化量,如果 Ai​,Bi​,Ci​ 都没有发生变化,那么 si​ 的变化量也和上次右移的时候一致,所以我们可以把每个 si​ 写成 pi​r+qi​ 的形式,那么需要修改 pi​,qi​ 的 i 就只有 Ai​,Bi​,Ci​ 发生变化的部分,由于 Ai​,Bi​,Ci​ 都只会发生 O(logv) 次修改(v 是值域),所以总变化次数只有 O(nlogv) 次,暴力修改即可。

具体地,

我们维护 si​ 为左端点 ≤i,右端点 ≤r 的区间价值总和,对于 L,R 的询问,答案就是 sR​−sL−1​。

考虑 r 的一次右移对 a′i​,b′i​,c′i​ 的影响,进而考虑对 si​ 的影响(其实就是 [i,r+1],[i+1,r+1],⋯,[r+1,r+1] 的价值和)。

根据上面的性质,我们发现:对于 a′i​,b′i​,c′i​ 而言,在 i 很靠近 r 时影响很大,而每远离一点影响就比前一次小。到达一个位置 p 之后就再也没有影响了。

并且,有影响的位置每次都是 O(logn) 个,因此可以考虑对于每次移动 r,都暴力遍历找到 p,复杂度是 O(nlogn) 的。然后对于 p,⋯,r 的位置,暴力计算新价值和。

那 1,⋯,p−1 的位置的答案怎么办呢?我们发现这一轮没有影响他们的答案,所以他们的答案更新其实和上一轮修改的答案更新是一样的,不需要再计算。

于是我们给每个位置都打上标记,代表它上一次修改增加的价值和 w,同时多维护一个上一轮修改的时间 t,那么 r 时刻总体答案可以理解为 si​+(r−t)×w。

当然暴力计算答案这一步要先把之前打的标记的贡献算到 si​ 里面去,再把新的贡献打在标记里面。

分析一下时间。我们枚举 p 这步就 O(nlogn) 了,按位与和按位或作为位运算是 O(1) 的。gcd 看起来复杂度不太对,但是总共只会有 O(logn) 个因子,你用辗转相除求 gcd 即使求 logn 次也只有 O(logn) 的计算量。


P9061 [Ynoi2002] Optimal Ordered Problem Solver

题目描述

给定 \(n\) 个点 \((x_i,y_i)_{i=1}^n\),你需要按顺序处理 \(m\) 次操作。每次操作给出 \(o,x,y,X,Y\)

  • 首先进行修改:
    • \(o=1\) 则将满足 \(x_i\le x,\;y_i\le y\) 的点的 \(y_i\) 修改为 \(y\)
    • \(o=2\) 则将满足 \(x_i\le x,\;y_i\le y\) 的点的 \(x_i\) 修改为 \(x\)
  • 然后进行查询,询问满足 \(x_i\le X,\;y_i\le Y\) 的点数。

输入格式

第一行两个整数 \(n,m\)

接下来 \(n\) 行每行两个整数 \(x_i,y_i\)

接下来 \(m\) 行每行五个整数 \(o,x,y,X,Y\),表示一次操作。

输出格式

\(m\) 行,每行一个整数,依次表示每次操作进行的查询的答案。

输入输出样例 #1

输入 #1

5 6
1 2
3 1
5 1
3 5
4 4
1 4 2 5 4
1 4 3 5 3
2 3 5 1 3
2 2 3 1 4
1 3 3 1 4
2 5 5 2 1

输出 #1

4
3
0
0
0
0

说明/提示

Idea:ccz181078,Solution:ccz181078,Code:ccz181078,Data:ccz181078

对于所有数据,\(1 \le n,m \le 10^6\)\(1\le x_i,y_i,x,y,X,Y\le n\)


考虑如何计算答案。对于一次询问 (X,Y),如果它在阶梯左下方不用管它,否则考虑容斥,答案即为 x≥X,y≥Y 的点数,加上询问时不在阶梯上且 x≤X 的点数,加上询问时不在阶梯上且 y≤Y 的点数,加上阶梯上的所有点数,再加上阶梯在 (X,Y) 左下方的点数。

不难发现这样计算会导致所求的被统计了 2 次,不为所求的被统计了 1 次。所以再减去总点数 n 就是答案。

发现 x≥X,y≥Y 的点数与时间维无关,因此是二维数点;若能处理出每个点进入阶梯的时刻(不难发现这个也是二维数点),那么询问时 x≤X 和 y≤Y 的点数也是二维数点。

剩下的问题是维护阶梯。

使用一颗平衡树维护所有右上角凸出的点,还需要两颗线段树维护点数,就做完了。

总结一下,你一共需要5个二维数点:

  1. 计算X,Y左上角点数(不计算删除)
  2. 对删除矩形做扫描线,对每个点标记删除时间
  3. 根据每个点的删除时间,动态删点,二维数点,计算包含删除,x<X的点
  4. 同3,对Y再做一遍。
  5. 使用平衡树维护右上角凸出点来维护轮廓线,并动态加入。

同时,你还需要一颗单点修改,前缀查询的树状数组,用于1,3,4。
你还需要一颗区间取min,单点查询的线段树,用于2。
你需要一颗可以在logn内,添加点(x,y),删除点(X,Y)左下角所有点(numlogn),查询第一个X坐标大于X的点的Y坐标,查询第一个Y坐标大于Y的点的X坐标的平衡树,用于5.
你还需要两颗区间修改,区间查询的线段树,用于维护5中竖向和横向的轮廓线上的点。

总结一下
5个二位数点
2个线段树
1个树状数组
1-2个平衡树

你这样写,QOJ就能过了,但是由于LXL认为CTT场上过的都是暴力,于是加强了luogu的数据并开小了时限,所以你过不了。


发现你这么写常数会爆炸
然后使用常数更小的FHQ-Therp
5操作平衡树只用一颗
然后开始合并二维数点
1可以和任意的合并
3,4合并
1,2合并
然后再将5,1,2合并。
极为难写,但是常数除了个2.
还是过不了,怎么办?

线段树常数太大,5操作的2个线段树改为1个大的。
然后再卡常。

二维数点每次预处理需要排序,怎么办?
一次排序n log n 巨慢无比,尤其是随机数据。

那就少排,不用排的二维数点一块连着做,
输出要排序,怎么办?


其实std::sort并不是最快的,可以去隔壁贺一个Timsort。

然后卡卡常就过了。
posted @ 2025-06-19 07:29  Dreamers_Seve  阅读(21)  评论(0)    收藏  举报