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′] 的价值之和。
- 观察性质
or and gcd这三种运算在不断累积时有一个很好的性质,都有单调性。
也就是说or的1一定会越来越多,and的0一定会越来越多,gcd一定会越来越小。
然后考虑暴力差分扫描线,每次更新都是O(n)的,总复杂度n^2。
但是根据刚才那个性质,只会更新log V次
所以我们需要排除非更新计算。
然后考虑 r 向右移 1,这个时候 si 的变化量,如果 Ai,Bi,Ci 都没有发生变化,那么 si 的变化量也和上次右移的时候一致,所以我们可以把每个 si 写成 pir+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个二维数点:
- 计算X,Y左上角点数(不计算删除)
- 对删除矩形做扫描线,对每个点标记删除时间
- 根据每个点的删除时间,动态删点,二维数点,计算包含删除,x<X的点
- 同3,对Y再做一遍。
- 使用平衡树维护右上角凸出点来维护轮廓线,并动态加入。
同时,你还需要一颗单点修改,前缀查询的树状数组,用于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。
然后卡卡常就过了。
浙公网安备 33010602011771号