题解:P4118 [Ynoi2018] 末日时在做什么?有没有空?可以来拯救吗?

· · 题解

我写这篇题解主要作用是帮助那些需要卡常的,如果需要学习主要做法的话可以参考 Sol1 大佬的这篇文章。

在这道题上我的代码拿到了最优解,总时间 9.51s(未开 O2),单点最慢 626ms,唯一一个跑进 10s 内的。
由于一些神秘的原因,开了 O2 反而变慢了,跑了个 9.68s。

1. 带权分块:

针对 80pts 被卡最后5个 Hack 数据的,能优化很多时间。
首先带权分块和普通分块的主要区别就是块长,普通分块固定块长对于随机数据跑得很快,但是对于 Hack 的构造数据会被卡飞,原因就是它的时间复杂度核心并不是普通的 块数 \times m,而是 \sum 散块的修改次数 \times 块长,如果某些位置被大量散块修改覆盖,固定块长会让这些热点块代价很高。带权分块就是先根据操作分布估计每个候选块 [L,R] 的代价,然后用 DP 选块边界。
然后就是代价模型,一般用的是 散块修改次数 \times 块长+整块查询次数 \times KK,其中 KK 是一个等块查询的等效常数,散块修改贵,因为会触发块内线段树 pull,重建凸包;整块查询相对便宜,因为可以离线排序后双指针扫。
实现时不需要枚举所有 L,R,否则 DP 本身太慢。一般只允许右端点是 D 的倍数,并限制最大块长 AX。针对本道题用的参数:D=100, AX=5000, KK=15

感谢 王熙文 在这篇中提供的参数,让我减少了很多调参的时间。

2. 块内凸包不要用 vector:

这个是较为一般的卡常方式了,但确实能节省很多时间,那我就来解释一下原因吧。
块内线段树的每个节点都要维护三类凸包:前缀最优值的凸包、后缀最优值的凸包、最大子段和的凸包。如果这些凸包使用动态容器保存,那么每次重建节点时可能发生清空、扩容、重新分配、析构、拷贝等操作。
显然这道题的数据中会出现大量这种操作,动态内存分配的成本会非常高,而且会破坏缓存局部性。
更快的方式是使用预先分配好的连续内存池。每个节点只记录自己在内存池中的起始位置和凸包长度。这样所有凸包点都放在连续数组里,避免堆分配,访问也更集中。

这种是针对运算较底层的优化了,一般只用使用就行了,不需要知道原理。

3.整块查询:

整块查询时,只需要查询块根节点上的凸包。如果每次查询都在凸包上二分,会多一个对数,而且常数不小。
更好的做法是逐块离线处理。处理某个块时,遇到整块查询先不立刻回答,而是记录当时这个块的整体加标记和询问编号。等到必须处理这些查询时,把它们按整体加标记排序。
排序后,凸包上的最优点会单调移动,可以用指针扫,而不是每次二分。也就是说,一批整块查询可以接近线性处理。

4. 排序优化:

用整数基数排序,原因不用多说。
但小批量事件用插入排序,如果某次积攒的整块查询事件很少,基数排序反而不划算。因为基数排序每次都要清空桶、扫描多轮,即使只有几十个元素,也要付固定成本。
小数组用插入排序更快。尤其这题里,事件的整体加标记经常局部接近有序,插入排序在这种情况下常数很低。

如果有序就跳过排序:

在积攒整块查询事件时,可以顺便判断当前整体加标记是否保持非降序。如果一直有序,那么处理时不需要排序,直接用指针扫凸包即可。
这个判断成本极低,只需要比较新加入的标记和上一个标记。对于某些数据,尤其是整体修改方向比较单一的数据,可以省掉大量排序时间。

这里也是针对构造数据的,随机数据基本没啥用。

5. 最大字段和优化:

一次区间查询最终要把很多块或很多线段树节点的结果合并起来。每个结果包含四个值:区间和、最大前缀和、最大后缀和、最大子段和。
如果每次合并都返回一个新的结构体,会产生大量临时对象。这个结构体一般有四个 64 位整数,大小不算小。频繁传值和返回会增加寄存器压力,也可能产生栈上的读写。
更快的方式是把右边结果直接合并进左边结果,也就是原地更新。这样可以减少临时对象和结构体拷贝。

简单来说就是如果你相信评测姬的实力就不用,实际 CCF 的评测姬跑得飞快,所以可以不用,但洛谷的评测姬的话……

6. 函数返回的优化:

块根查询和散块查询都会频繁返回四元信息。如果通过函数返回值返回,编译器虽然可能优化,但在递归和复杂控制流下不一定完全消掉。
解决方法是把结果对象传进去,让函数直接写入。这样减少返回大对象的开销,也降低寄存器分配压力。

用处不大,但很稳定,至少能保证对所有数据都有优化。

7. 快读快写:

优秀的快读快写可以极大的优化你的常数,感谢 Hootime 提供的代码,我也在这里放出来吧: :::info[Code]

#define bs (1<<20)
char buf[bs], *p1, *p2;
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,bs,stdin),p1==p2)?EOF:*p1++)
template<typename T>
inline void read(T& x){
    x = 0; int w = 1;
    char ch = gc();
    while(ch < '0' || ch > '9'){
        if(ch == '-') w = -w;
        ch = gc();
    }
    while(ch >= '0' && ch <= '9')
        x = (x<<3)+(x<<1)+(ch^48), ch = gc();
    x *= w;
}
template<typename T, typename... Args>
inline void read(T& x, Args&... y){
    return read(x), read(y...);
}

在本题中扔掉处理负数片段以获得更大优化。 :::

8. 操作数组扫描的优化:

一定要顺序扫描,不要盲目打包!!!

直觉上,把操作类型、左右端点、编号压到一个整数里,似乎能减少内存访问。但实际经常变慢。
每次处理操作时都需要解码,包括移位、掩码和类型判断。而且修改值仍然需要额外访问。相比之下,分开的数组虽然看起来多,但每一轮都是从头到尾顺序扫描,CPU 的预取效果很好。
另一种看似合理的优化是:提前把每个操作分发到它影响的块里,这样每个块不用扫所有操作。
但如果用链表或分散存储,会破坏顺序访问。CPU 很难预取链式跳转的数据,cache miss 会增加。而且一个长区间操作会被挂到很多块上,预处理数据量也会膨胀。
当前做法虽然每个块都顺序扫所有操作,但访问模式非常稳定,缓存友好。除非预分发做成非常紧凑的连续数组,否则容易变慢。

9. 凸包合并循环的优化:

散块修改最重的地方是重建线段树节点的信息。重建时要合并前缀凸包、后缀凸包、最大子段和凸包,还要做一次类似闵可夫斯基和的合并。
这些循环里会反复计算“某个点在当前整体加标记下的实际值”。如果每次都通过一个辅助函数计算,或者反复计算相邻两个点的实际值差,会浪费很多乘法和数组读取。
更好的方式是直接使用相邻点的差值:横坐标差乘标记,加上纵坐标差。这样能减少函数调用和重复乘法。

10. 关于 int128:

一般来说,人们会对超出 long long 范围的数值使用 __int128 存储,但你要知道,有些题目只是输入需要,并不是运算中也需要。
如果能证明数值不会溢出 64 位,就应该用 64 位整数。由于这题保证任意时刻单个元素绝对值不超过一定范围,块长又有限,很多地方的乘积实际不会超过 64 位范围。
但是这个优化有风险。如果证明不严谨,可能出现隐藏溢出导致错误答案。比较稳的做法是:只在最可能溢出的少数斜率比较处保留 128 位,其它地方用 64 位。
因为 64 位比 128 位快。

11.查询时要下传标记:

散块查询只读信息,不修改结构。查询递归时,可以把沿途经过的整体加标记累加成一个参数传下去。到达完整覆盖节点时,用这个累积标记去查询凸包。
这样查询不会改变线段树状态,也不会触发标记下传和重建。只有散块修改才需要下传标记,因为修改后要重新合并父节点信息。

12.合理安排批处理时机:

整块查询可以延迟批处理,但不能无限延迟。因为一旦出现散块修改,块内线段树结构会改变,之前积攒的查询必须在修改前处理掉。
这个貌似 O2 会帮你做(之前貌似看到过,但有些忘记了,如果写错了记得叫我),但自己做了肯定要好一点。
流程大概是这样: 遇到整块修改,只更新块的整体加标记。
遇到整块查询,记录当前整体加标记和询问编号。
遇到散块查询,直接在线段树上查询。
遇到散块修改,先处理之前积攒的整块查询,再执行散块修改。
如果过早处理整块查询,排序批次变多,常数变大。如果过晚处理,会因为线段树状态改变而答案错误。

13.一些关于负优化的:

  1. 首先是关于三目运算符,不要以为它和位运算长得像就以为它要快一点。
    现代 CPU 对稳定分支预测很强。如果一个条件的分布比较稳定,普通条件语句往往已经很好。
    三目表达式如果写得复杂,可能导致重复计算。特别是在凸包合并里,如果一个三目表达式中重复计算多个点的实际值,反而会变慢。
  2. 不要把点结构强行压缩对齐,凸包上的每个点包含一个长度和一个对应的最优值。长度是较小整数,值是 64 位整数。按正常结构体存储时,中间可能有填充字节,看起来浪费空间。
    但如果强行压缩结构体,64 位整数可能变成非对齐访问。非对齐访问虽然在某些 CPU 上可以运行,但通常更慢,而且可能跨缓存行。
    这题凸包点的 64 位值是热路径中高频读取的数据,所以应保持自然对齐,不要强行压缩!
  3. 不要给所有大数组强行缓存行对齐。缓存行对齐对某些小而热的数据有用,比如排序桶。但如果给大量大数组都强行对齐,可能扩大内存布局,增加页表和缓存压力。
    大数组的访问模式更重要。顺序访问的大数组,即使没有手动对齐,也能被 CPU 预取很好地处理。乱加对齐不一定有收益,甚至可能变慢。

    4. O2 可能对你的代码负优化!!!非常重要!!!

14. 调参:

不说了好吧,一杯茶,一包烟,一个块长调一天。 嗯……还是写一下吧: :::info[调块长的技巧] 分块调参的方法不是取根号,而是先写出这份分块的真实代价模型,然后按模型求一个理论块长,再围绕这个值实测微调。不同分块的最优块长差别很大,\sqrt n 只是最常见的一种特例。
设块长为 B,块数大约是 n/B
很多分块操作的代价可以拆成两类:
一种是散块代价,也就是区间左右两端不完整块的处理。它通常和 B 成正比。
另一种是整块代价,也就是中间完整块的处理。它通常和 n/B 成正比。
所以最常见模型是:单次操作代价 \approx C_1 \times B + C_2 \times n / B
如果两边常数差不多,最优块长就是:B=\sqrt n;
但实际常数经常不一样,所以更准确是:B = \sqrt {\frac{C_2 \times n} {C_1}}
也就是说,如果散块处理特别贵,就应该减小块长;如果整块处理特别贵,就应该增大块长。
有些分块里,散块只是暴力扫元素,而整块查询只读一个预处理值。这时散块常数大,整块常数小,块长应该偏小。
如果整块处理要做二分、排序、合并结构,整块常数大,块长应该偏大,减少块数。 经验上:

带修改的分块经常会出现“散块修改后重构整个块”的情况。
如果一次散块修改要重构一个块,重构代价是 O(B \log B)O(B),那么散块修改会很贵。此时块长不能太大。
模型可能变成:

$查询代价 \approx \frac{C_2 \times n}{B}$。 如果修改次数和查询次数差不多,块长仍然接近根号,但会比普通根号小一些,因为重构常数通常很高。 如果修改远多于查询,块长应继续减小。 如果查询远多于修改,块长可以增大。 有些分块维护块内所有答案,重构一个块需要 $O(B^2)$。 这时模型大概是:$总代价 \approx 修改次数 \times B^2 + \frac{查询次数 \times n}{B}$; 平衡之后,块长不是根号,而接近:$B \approx cube\_root(\frac{查询次数 \times n}{修改次数})$。 所以凡是块内维护二维信息、所有子区间信息、块内 DP 表之类的分块,块长一般不能取太大。 有些分块里整块不是 $O(1)$,而是: - $O(\log B)$; - $O(\log 值域)$; - $O(二分次数)$; - $O(小数据结构查询)$; 那么中间完整块代价是:$\frac{n}{B} \times \log B$; 模型变成:$C_1\times B + \frac{C_2\times n}{B}\times \log B$。 这时最优块长一般会比普通根号略大。因为块长增大虽然让散块更贵,但能减少完整块数量,而完整块每个还有对数常数。 有些分块中,散块不是纯暴力扫,而是在线段树、平衡结构、凸包上查询。散块代价可能是: - $\log B

如果散块已经不那么贵,那么可以适当增大块长来减少块数。
现在这类题就是典型例子:散块查询不算特别重,真正重的是散块修改造成的凸包重构。因此调块长时不能只看查询,要重点看修改分布。

普通莫队常见块长是 \sqrt n,但更准确的模型是:

所以最优块长大约是:B \approx \frac{n}{\sqrt{查询数}}
如果查询数和 n 同阶,就是 \sqrt n
如果查询数远大于 n,块长应该更小;
如果查询数远小于 n,块长应该更大。

带修改莫队有三个维度:左端点、右端点、时间。块长一般和 n^{\frac{2}{3}} 有关。
粗略经验是:

$时间块长 \approx n^{\frac{2}{3}}$。 但具体还要看修改代价和增删代价。如果修改应用/撤销比普通移动贵,时间块应该调大,减少时间维移动次数。 这类题块长尤其要实测,理论值只能作为起点。 位置分块的块长通常围绕 $\sqrt n$ 调。 值域分块要看值域大小 $V$,而不是数组长度 $n$。如果按值域分块,块长应该根据 $V$、操作数量、单次桶处理代价来定。 例如值域统计中: $散值处理 \approx B$; $整桶处理 \approx \frac{V}{B}$。 那么块长接近:$\sqrt V$; 如果值域经过离散化,应该用离散化后的大小,而不是原始值域。 如果分块里使用 bitset 或手写位集,块长最好和机器字长相关。 因为 bitset 的基本处理单位通常是 64 位。块长取 64 的倍数、128 的倍数、256 的倍数,常数会更稳定。 原因是按 64 位对齐后,循环次数、内存访问和尾部处理都更规整。 固定块长假设操作分布比较均匀。但 hack 数据往往会集中攻击某些位置。 如果某一段被大量散块修改,固定块长会很吃亏。 带权分块的做法是让热点区域块短一点,让冷门区域块长一点。这样能降低热点块的重构代价,同时不让总块数太多。 适用场景:操作分布不均匀、最后几个点是 hack、散块修改代价很高、块长对时间非常敏感…… 带权分块的参数怎么调? 带权分块一般有三个参数。 第一个是候选断点间隔。间隔越小,DP 分块越精细,但预处理更慢。一般从 $100$ 左右试。 第二个是最大块长。最大块长太大,单块数据结构会变重,散块修改变慢;太小,块数太多,每块扫操作和整块查询变慢。可以从 $3000$ 到 $7000$ 测。 第三个是整块查询权重。这个值越大,DP 越倾向于减少整块查询代价;越小,越倾向于减少散块修改代价。一般从 $10$ 到 $25$ 试。 一些神秘知识:块长太大时,单块内部数据结构可能放不进 L2 cache,甚至频繁打到 L3。虽然理论操作次数少了,但 cache miss 会变多。 块长太小时,块数增加,每次操作扫描块或处理块元信息的次数变多。 最后,一定要多侧块长,不要光测测数值,测得越多对你调参更友好,希望大家不要在被困在调块长的深渊里了。 ::: 最后放一下我的完整代码吧: :::info[CODE] ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; typedef unsigned long long ull; #define BS (1<<20) #define IL inline __attribute__((always_inline)) const int N=100000+5,D=100,AX=5000,K=AX+5,M=AX*4+20,Q=85000,PN=N*20,KK=15; const ll OF=5000000000LL,INF=4000000000000000000LL; char ib[BS],*p1=ib,*p2=ib,ob[BS]; int oop; #define gc() (p1==p2&&(p2=(p1=ib)+fread(ib,1,BS,stdin),p1==p2)?EOF:*p1++) IL int ru(){ int x=0;char c=gc(); while(c<'0'||c>'9')c=gc(); while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=gc(); return x; } IL ll rs(){ ll x=0;int w=1;char c=gc(); while(c!='-'&&(c<'0'||c>'9'))c=gc(); if(c=='-')w=-1,c=gc(); while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=gc(); return x*w; } IL void pc(char c){ if(oop==BS)fwrite(ob,1,oop,stdout),oop=0; ob[oop++]=c; } IL void wt(ll x){ char s[24];int n=0; if(!x){pc('0');return;} if(x<0)pc('-'),x=-x; while(x)s[n++]=x%10+48,x/=10; while(n--)pc(s[n]); } struct P{int x;ll y;}; struct V{ll s,l,r,a;}; IL ll gv(P p,ll t){return p.y+(ll)p.x*t;} IL bool bad(P a,P b,P c){ return (b.y-a.y)*(ll)(c.x-b.x)<=(c.y-b.y)*(ll)(b.x-a.x); } IL void hp(P *h,int &n,P p){ while(n&&h[n-1].x==p.x){ if(h[n-1].y>=p.y)return; n--; } while(n>=2&&bad(h[n-2],h[n-1],p))n--; h[n++]=p; } IL ll ah(P *h,int n,ll t){ int l=0,r=n-1; while(l<r){ int m=(l+r)>>1; ll x=h[m].y+(ll)h[m].x*t,y=h[m+1].y+(ll)h[m+1].x*t; if(x<=y)l=m+1; else r=m; } return h[l].y+(ll)h[l].x*t; } IL ll askp(P *h,int n,ll t,int &p){ while(p+1<n&&h[p].y+(ll)h[p].x*t<=h[p+1].y+(ll)h[p+1].x*t)p++; return h[p].y+(ll)h[p].x*t; } IL void mgto(V &a,const V &b){ ll s=a.s+b.s; ll l=a.l,x=a.s+b.l; if(x>l)l=x; ll r=b.r; x=b.s+a.r; if(x>r)r=x; ll z=a.a>b.a?a.a:b.a; x=a.r+b.l; if(x>z)z=x; a.s=s;a.l=l;a.r=r;a.a=z; } struct B{ int n,tp,ln[M],cl[M],cr[M],ca[M],of[M]; ll d[K],sm[M],tg[M]; P lp[Q],rp[Q],apol[Q],cy[K*2]; IL P* gl(int o){return lp+of[o];} IL P* gr(int o){return rp+of[o];} IL P* ga(int o){return apol+of[o];} IL void pd(int o){ ll z=tg[o]; if(!z)return; tg[o<<1]+=z; tg[o<<1|1]+=z; tg[o]=0; } void mk(P *a,int na,ll ta,P *b,int nb,ll tb,P *c,int &nc){ nc=0; if(!na||!nb)return; int i=0,j=0,o; P p; p.x=a[0].x+b[0].x; p.y=a[0].y+(ll)a[0].x*ta+b[0].y+(ll)b[0].x*tb; hp(c,nc,p); while(i+1<na||j+1<nb){ if(j+1==nb)o=1; else if(i+1==na)o=2; else{ int ax=a[i+1].x-a[i].x,bx=b[j+1].x-b[j].x; ll ay=a[i+1].y-a[i].y+(ll)ax*ta; ll by=b[j+1].y-b[j].y+(ll)bx*tb; o=(ay*bx>=by*ax)?1:2; } if(o==1){ int dx=a[i+1].x-a[i].x; p.x+=dx; p.y+=a[i+1].y-a[i].y+(ll)dx*ta; i++; }else{ int dx=b[j+1].x-b[j].x; p.x+=dx; p.y+=b[j+1].y-b[j].y+(ll)dx*tb; j++; } hp(c,nc,p); } } void m3(P *a,int na,ll ta,P *b,int nb,ll tb,P *c,int nc,P *d,int &nd){ nd=0; int i=0,j=0,k=0; while(i<na||j<nb||k<nc){ int x=INT_MAX; if(i<na&&a[i].x<x)x=a[i].x; if(j<nb&&b[j].x<x)x=b[j].x; if(k<nc&&c[k].x<x)x=c[k].x; ll y=-(1LL<<60),v; while(i<na&&a[i].x==x){ v=a[i].y+(ll)a[i].x*ta; if(v>y)y=v; i++; } while(j<nb&&b[j].x==x){ v=b[j].y+(ll)b[j].x*tb; if(v>y)y=v; j++; } while(k<nc&&c[k].x==x){ if(c[k].y>y)y=c[k].y; k++; } hp(d,nd,(P){x,y}); } } void init(int m,ll *s){ n=m;tp=0; for(int i=1;i<=n;i++)d[i]=s[i]; bd(1,1,n); } void bd(int o,int l,int r){ of[o]=tp; tp+=r-l+2; tg[o]=0; ln[o]=r-l+1; if(l==r){ sm[o]=d[l]; cl[o]=cr[o]=ca[o]=0; P p; p.x=0;p.y=0; hp(gl(o),cl[o],p); hp(gr(o),cr[o],p); hp(ga(o),ca[o],p); p.x=1;p.y=d[l]; hp(gl(o),cl[o],p); hp(gr(o),cr[o],p); hp(ga(o),ca[o],p); return; } int m=(l+r)>>1; bd(o<<1,l,m); bd(o<<1|1,m+1,r); pu(o); } void pu(int o){ int a=o<<1,b=o<<1|1; ll ta=tg[a],tb=tg[b],ls=sm[a]+ta*ln[a],rs=sm[b]+tb*ln[b]; int nl=ln[a],nr=ln[b]; P *ol=gl(o),*orx=gr(o),*oa=ga(o); P *al=gl(a),*ar=gr(a),*aa=ga(a); P *bl=gl(b),*br=gr(b),*ba=ga(b); ln[o]=nl+nr; sm[o]=ls+rs; tg[o]=0; cl[o]=0; for(int i=0,ed=cl[a];i<ed;i++)hp(ol,cl[o],(P){al[i].x,al[i].y+(ll)al[i].x*ta}); for(int i=0,ed=cl[b];i<ed;i++)hp(ol,cl[o],(P){bl[i].x+nl,bl[i].y+(ll)bl[i].x*tb+ls}); cr[o]=0; for(int i=0,ed=cr[b];i<ed;i++)hp(orx,cr[o],(P){br[i].x,br[i].y+(ll)br[i].x*tb}); for(int i=0,ed=cr[a];i<ed;i++)hp(orx,cr[o],(P){ar[i].x+nr,ar[i].y+(ll)ar[i].x*ta+rs}); int cn; mk(ar,cr[a],ta,bl,cl[b],tb,cy,cn); m3(aa,ca[a],ta,ba,ca[b],tb,cy,cn,oa,ca[o]); } void add(int o,int l,int r,int ql,int qr,ll k){ if(ql<=l&&r<=qr){tg[o]+=k;return;} pd(o); int m=(l+r)>>1; if(ql<=m)add(o<<1,l,m,ql,qr,k); if(qr>m)add(o<<1|1,m+1,r,ql,qr,k); pu(o); } IL void add(int l,int r,ll k){add(1,1,n,l,r,k);} IL void all(ll k){tg[1]+=k;} IL ll gt(){return tg[1];} IL void va(int o,ll t,V &v){ t+=tg[o]; v.s=sm[o]+t*ln[o]; v.l=ah(gl(o),cl[o],t); v.r=ah(gr(o),cr[o],t); v.a=ah(ga(o),ca[o],t); } IL void rt(ll t,int &p,int &q,int &r,V &v){ v.s=sm[1]+t*ln[1]; v.l=askp(gl(1),cl[1],t,p); v.r=askp(gr(1),cr[1],t,q); v.a=askp(ga(1),ca[1],t,r); } void qry(int o,int l,int r,int ql,int qr,ll t,V &v){ if(ql<=l&&r<=qr){ V z;va(o,t,z);mgto(v,z);return; } t+=tg[o]; int m=(l+r)>>1; if(ql<=m)qry(o<<1,l,m,ql,qr,t,v); if(qr>m)qry(o<<1|1,m+1,r,ql,qr,t,v); } IL void qry(int l,int r,V &v){v.s=v.l=v.r=v.a=0;qry(1,1,n,l,r,0,v);} }ds; int ls[PN],rsn[PN],su[PN],rtp[N],pcn; int up(int p,int l,int r,int x){ int q=++pcn; ls[q]=ls[p];rsn[q]=rsn[p];su[q]=su[p]+1; if(l!=r){ int m=(l+r)>>1; if(x<=m)ls[q]=up(ls[p],l,m,x); else rsn[q]=up(rsn[p],m+1,r,x); } return q; } int qu(int p,int l,int r,int ql){ if(!p||r<ql)return 0; if(ql<=l)return su[p]; int m=(l+r)>>1; return qu(ls[p],l,m,ql)+qu(rsn[p],m+1,r,ql); } IL ull ky(ll x){return (ull)(x+OF);} ll et[N],bt[N]; int ei[N],bi[N],ct[256]; void sr(int n){ if(n<=1)return; if(n<80){ for(int i=1;i<n;i++){ ll t=et[i];int id=ei[i],j=i-1; while(j>=0&&et[j]>t){et[j+1]=et[j];ei[j+1]=ei[j];j--;} et[j+1]=t;ei[j+1]=id; } return; } ll *pa=et,*pb=bt,*pt; int *ia=ei,*ib=bi,*it; for(int sh=0;sh<40;sh+=8){ memset(ct,0,1024); for(int i=0;i<n;i++)ct[(ky(pa[i])>>sh)&255]++; int p=0; for(int i=0;i<256;i++){int v=ct[i];ct[i]=p;p+=v;} for(int i=0;i<n;i++){ int v=(ky(pa[i])>>sh)&255,p=ct[v]++; pb[p]=pa[i];ib[p]=ia[i]; } pt=pa;pa=pb;pb=pt; it=ia;ia=ib;ib=it; } if(pa!=et){memcpy(et,pa,n<<3);memcpy(ei,ia,n<<2);} } ll a[N],ta[K],dp[2005]; int tp[N],lq[N],rq[N],idq[N]; ll xq[N]; V an[N]; int hd[N],nq[N],qr[N],ul[N],ur[N],sl[N],srp[N],en[2005],fr[2005],bl[2005],br[2005]; int main(){ int n=ru(),m=ru(); for(int i=1;i<=n;i++)a[i]=rs(); int qc=0; for(int i=1;i<=m;i++){ tp[i]=ru();lq[i]=ru();rq[i]=ru(); if(tp[i]==1){xq[i]=rs();ul[lq[i]]++;ur[rq[i]]++;} else{idq[i]=++qc;qr[qc]=rq[i];nq[qc]=hd[lq[i]];hd[lq[i]]=qc;} } for(int i=1;i<=n;i++){ sl[i]=sl[i-1]+ul[i]; srp[i]=srp[i-1]+ur[i]; rtp[i]=rtp[i-1]; for(int j=hd[i];j;j=nq[j])rtp[i]=up(rtp[i],1,n,qr[j]); } int ec=0;en[0]=0; for(int i=D;i<n;i+=D)en[++ec]=i; if(en[ec]!=n)en[++ec]=n; dp[0]=0;fr[0]=0; for(int i=1;i<=ec;i++){ dp[i]=INF;fr[i]=i-1; for(int j=i-1;j>=0&&en[i]-en[j]<=AX;j--){ int L=en[j]+1,R=en[i],le=R-L+1; ll c=(ll)(sl[R]-sl[L])+(ll)(srp[R-1]-srp[L-1]); ll q=qu(rtp[L],1,n,R); ll v=dp[j]+c*le+q*KK; if(v<dp[i])dp[i]=v,fr[i]=j; } } int bc=0; for(int i=ec;i;i=fr[i]){bl[++bc]=en[fr[i]]+1;br[bc]=en[i];} for(int i=1,j=bc;i<j;i++,j--){swap(bl[i],bl[j]);swap(br[i],br[j]);} for(int i=1;i<=qc;i++)an[i]=(V){0,0,0,0}; for(int b=1;b<=bc;b++){ int L=bl[b],R=br[b],le=R-L+1,lm=L-1,tc=0,ok=1; for(int i=1;i<=le;i++)ta[i]=a[lm+i]; ds.init(le,ta); #define FL() if(tc){if(!ok)sr(tc);int p=0,q=0,r=0;V vv;for(int ii=0,ed=tc;ii<ed;ii++){ds.rt(et[ii],p,q,r,vv);mgto(an[ei[ii]],vv);}tc=0;ok=1;} for(int i=1;i<=m;i++){ int rr=rq[i]; if(rr<L)continue; int llx=lq[i]; if(llx>R)continue; int l=(llx<L)?1:(llx-lm); int r=(rr>R)?le:(rr-lm); if(tp[i]==1){ if(l==1&&r==le)ds.all(xq[i]); else{FL();ds.add(l,r,xq[i]);} }else{ int id=idq[i]; if(l==1&&r==le){ ll z=ds.gt(); if(tc&&et[tc-1]>z)ok=0; et[tc]=z;ei[tc++]=id; }else{ V vv; ds.qry(l,r,vv); mgto(an[id],vv); } } } FL(); #undef FL } for(int i=1;i<=qc;i++){wt(an[i].a);pc('\n');} fwrite(ob,1,oop,stdout); return 0; } ``` :::