停课后学习笔记
用于记录做题过程中的小收获、经验教训。
P.S. 习惯上,我们称一个 OIer 停课后的阶段为后竞赛时期。 —— Walski Schölder(沃茨基·硕德)
- UPD. 2023.9.4 优化文章架构。
5.12 周五 - 线段树
P4243 [JSOI2009] 等差数列
RE -> RE & WA -> AC
- 重申 构造函数 的重要性(
lazy
的初值) - 对于需要重载运算符的结构体,考虑 分离无关变量 撇出结构体以防止错误(
L
、R
的移出) - 重申 数据范围 的重要性(
lval
、rval
的long long
) - 没必要封装的,尽量不封装(
class segment_tree
的删除) - 对于占内存较大的变量,采用 全局声明(
node
型nd[N<<2]
的声明从类中移出,segment_tree
采用全局变量) - 考虑 特例 的出现(\(s=t\) 的情况)
- 将每一部分维护到位(差分 需要维护区间后一个元素)
- 将思路从板子灵活出来(四种区间不同计数
sum[4]
的运用,极大减少了pushup
的工作量)
5.13 周六 - 分治浅做
学习分治,很多时候依靠递归实现
1. Sand Fortress
WA -> AC
-
注意二分边界(范围 \([1,n)\))
-
注意大数乘法导致的溢出(#4
1000000000000000000 1000000000000000000
) -
巧妙判断(溢出的绝对偏大了,返回
LONG_LONG_MAX
)
2. Abracadabra
这道题有思路,而且是对的,但是去看了题解,发现大家的标程很精炼。
-
灵活运用
std::max
等函数(int solve
里的四种分治的max
和min
发挥了 拆分区间、排除非法、缩小范围 等多个职能。) -
把握分治的核心思想和大部分代码思路:基本都是 二分 和 递归。
3. P2218 [HAOI2007] 覆盖问题
dfs 不会写……
- 熟练朴素的模拟
4. Empty Rectangles
下位黑,但是自己写不好……
- 加强逻辑能力(构建和校正代码)
- 正确评估复杂度(分治到边界复杂度是正确的)
大概的把 ZR D 班的分治都练完了,大概的有那么几个通式,有折半的,有交替分治的……等等等等
5.15 周一 - 树剖
P4315 月下“毛景树”
- 写好
pushdown
! - 快读快写 别和 关闭流同步 一起用!!
- 树链剖分的第二个 dfs 要实现 重儿子优先遍历!!!
made……
5.19 周五 - 线段树进阶
TTM - To the moon
不算毒瘤的可持久化线段树,刚开始写 WA 了,以为是主席树的问题,于是就改了一个有 pushup
的版本,后来发现是这句话
rt[++T]=change(rt[T-1],l,r,d,1,n);
的问题,在一些编译器中会成为未定义行为,先执行左边还是右边不确定。所以下次还是别写这样的,还有,多听 -Wall
哥的话,有用捏。
所以写了有上传
void pushup(int x,int l,int mid,int r) { t[x].sum=t[t[x].ls].sum+t[t[x].rs].sum+(mid-l+1)*t[t[x].ls].lz+(r-mid)*t[t[x].rs].lz; }
和没有上传纯是
t[x].sum+=(r-l+1)*k;
的 版本。
P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并
总结了两个点 警示后人。不算很难吧也,但是思路有点绕。是权值线段树合并 + 树上差分。
5.20 周六 - 线段树分裂
今天是全国学生营养日,不是什么奇怪的 520 节,我们要积极发扬新发展理念,坚持协调发展,保证全国所有学生的营养充足。
P5494 【模板】线段树分裂
错误如下:
- 不开
long long
见祖宗。多练练怎么判断哪个变量该开朗朗。 - 分裂后为上传
pushup
节点。 - 查询
kth
到右儿子时没有给k
减去左儿子权值。 - 分函数传参两次解
rt
.能不能别这么马…… - 未对
kth
超出总数情况特判-1
.
Distinctification
噫!暴切黑题!爽!
就是
- 判断当前集合大小用的
fa[i]==i
不对嗷,应该是size[rt[get(i)]]==1
; - 连通的时候要考虑 \(x \geq N\) 或 \(y \geq N\) 的情况。因为到最后 连通块的大小和总数相当,导致 \(x\) 或 \(y\) 远远超出 \(n\) 了。这样下去会炸的!!!
5.22 周一 - 博弈论
[省选联考 2023] 过河卒
差点没死。
首先是码量大,然后调了一圈之后发现错的是 in_range
的判断写成了 y<=n
而非 y<=m
。
相似了。
然后那个 {1,0}
的初值,1
指的是当前出棋方必败,不是先手必败。
- 为什么拓扑排序能通过 有环但不平局 / 多种选择取最优 的情况?
我们观察我们的代码:
inline void toposort() {
while(!topo.empty()) {
auto u=topo.front(); topo.pop();
if(ws[u].fr==1) {
for(int p=head[u],v=E[p].to;p;p=E[p].next,v=E[p].to) {
if(ws[v].fr==inf) ws[v]={0,ws[u].sc+1};
if(!ws_vis[v]) ws_vis[v]=1,topo.push(v);
}
}
else {
for(int p=head[u],v=E[p].to;p;p=E[p].next,v=E[p].to) {
if(--ind[v]==0) {
if(ws[v].fr==inf) ws[v]={1,ws[u].sc+1};
if(!ws_vis[v]) ws_vis[v]=1,topo.push(v);
}
}
}
}
}
首先,在反图中,环在结果一边的出点 出度大于 1.
对于 ws[u].fr==1
的情况,证明当前是出棋者的必败状态,则 出点是出棋者的必胜状态。所以无所畏惧,我们将他的 所有出点 入栈,不考虑入度是否清零。即,即使有环,我照样赢。
对于 ws[u].fr==0
的情况,证明 如果出环,出棋者必输。所以我们考虑将 无入度的出点 入栈。即,我 不能让你进入环内,或者从起点方向说,我 不会选择出环。
这同样也是拓扑排序可以在多种不同的选择中做出最优选择的原理。与环不同的是,多个选择的决定点 出度大于 1. 但异曲同工的是,魔改后的拓扑排序 永远对将当前点判定为出棋者胜的点给予最高优先级。
5.25 周三 - 迭代加深搜索
P1763 埃及分数
dfs 再用结构体我是狗
dfs 再类型转换我是狗
1.20s TLE -> 1.10s AC -> 227ms AC
这俩 B 玩意是真扣时间
5.26 周五 - 树的直径
P3304 [SDOI2013]直径
手写栈开栈空间里,于是爆了。
static
关键字可以在函数中声明 静态空间 的变量,和 全局变量 一个效果。- 复制开销大的类函数建议传参使用
const T&
,使用 const 左值引用可以减少开销,且适用于绑定右值的场景。
5.29 周一 - 分治
平面最近点对(加强加强版)
及其优雅的分治,伟大的人类智慧。
本来是 5.26 的考试题,做的是 平面最近点对(加强版),当时大家提出了很多的想法:
-
暴力 \(37\ pts\)
-
随机化 \(37\ pts\)
-
初级人类智慧 \(\mathbf{90\ pts}\) - 我们充分发挥人类智慧,认为在 \(x,y\) 轴上投影距离不远的两点距离一定也不远。于是我们两次
std::sort
,遍历选取相邻两点求出距离。这样能够在 689ms 内跑到 \(90pts\) 的好成绩。于是……
-
初级人类智慧 \(+\)
1e6
随机化 \(\color{red}\mathbf{100 \ pts}\) - 非常的屌,用这种及其离谱的方法把这题草过了。当然,这不是正解。当时我提出了一个 正义 的(没错!初级人类智慧太不正义了!!!怒斥 agy!!!)最高分方法——
-
基于贪心的交替分治 \(\mathbf{60\ pts}\) - 这道题 zcq 讲过,当时只知道是分治,现在既然学了分治,当然照着正解做。本身是二维点题,基于 Empty Rectangles 的经验,选择 交替分治。但是众所周知,我的代码稳定性有待提高,ub 与日俱增。所以,为了更好地保证正确率,我没有完全横竖交替,而是加入了一点点的贪心,即每次选择 两侧距离分治线最近点距离最远的方向 进行分治。这样可以保证当时分治线上的最近点对后续被分在同一组,多少管点用。
(其实可能并没有)不过代码水平确实有提高,最后写完的时候基本上没有程序错误了,只是算法较劣,惨得 \(64\ pts\)。
甚至没超过 agy 的人类智慧。 -
人类智慧 - 最高赞题解这样写到……
当然,对于我来说可能有点难写,不太会用旋转,而且貌似这种方法局限性也挺大的,还是老老实实连分治吧。
-
优化后的 \(x\) 轴分治 \(\color{gold}\mathbf{100\ pts}\) - 这种做法是真真切切连加强加强版都能切的,当然了我从 \(16\) 改到 \(100\) 也经历了很多的改善,接下来介绍一下:
经过了很多修改,最后事实是 囧仙大佬题解 的 C++ 底层数据结构实现版。(qs,估计到考场上我也不怎么会用指针……)
-
首先是从交替分治上修改,将其完全修改为 \(x\) 轴分治。出现一个问题:可能有多个点 \(x\) 坐标相等;进行修正,在分治到 \([l,r) , r-l \leq 2\) 时直接枚举相邻点之间距离取 \(\min .\) 交上去几乎是全 T,在 \(n \geq 1000\) 后甚至明显被交替分治算法甩在身后,必须上优化。
-
改变思路,由于可能有多个点 \(x\) 坐标相等,考虑以
std::sort
后的点数组下表作为分治依据,不再使用 \(x\) 轴,即 \([l,r)\) 表示第 \(l\) 个到第 \(r-1\) 个点的分治,而非坐标。大部分还是 T,但是加强版过了。 -
发现是 合并分治区间 的问题,即枚举的范围过大。题解中使用
std::vector
记录了所有与分治线距离 \(\leq ans\) 的点,在其范围中进行枚举。由于本人倔,考虑到存点用的都是数组而不是std::vector
,干脆就别用了。所以最后选择用手写栈实现。又是 T,又是 T 啊!!! -
对比与题解的区别,修改了一下手写栈,之前把类指针直接当成编号用了……改回了一部分 WA;然后又把
long long
记录平方改成了long double
记录距离最后再平方回来,莫名其妙的快了,解决了一些 TLE;最后将分治内部重新以 \(y\) 轴为准排序的算法从std::sort
修改为和题解一样的归并排序std::inplace_merge
,TLE -> WA!历史性的突破!证明了归并排序在特定场景下的牛逼性! -
寻找 WA 的原因,感谢出题人让我的样例 #2 没过,一会儿就发现了错点,是
std::inplace_merge
的两个序列本身的有序性没有保证好,因为我写了一句if(r-l==2) return ans=std::min(ans,caldis(l,r-1)),void();
导致了没有保证含有两个元素的序列有序。
把这句删了,成功切掉。然后又优化了这个语句并加回来:
if(r-l==2) { if(p[l].y>p[r-1].y) std::swap(p[l],p[r-1]); return ans=std::min(ans,caldis(l,r-1)),void(); }
大抵是因为少递归一层,非常成功地将时间缩短了 0.35s.
-
最后,放上这个优美的代码,我真的太爱这道题的 \(AC \ CODE\) 了:
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define fsp(x) std::fixed<<std::setprecision(x)
const int N=4e5+5;
const ld inf=1e18+18;
int n;
int s[N],top=0;
ld ans=inf;
struct point { ll x,y; } p[N];
inline ld caldis(int a,int b) { return sqrt((p[a].x-p[b].x)*(p[a].x-p[b].x)+(p[a].y-p[b].y)*(p[a].y-p[b].y)); }
void solve(int l,int r) {
if(r-l<=1) return;
int mid=l+r>>1,cmpx=p[mid].x;
if(r-l==2) { if(p[l].y>p[r-1].y) std::swap(p[l],p[r-1]); return ans=std::min(ans,caldis(l,r-1)),void(); }
solve(l,mid),solve(mid,r);
std::inplace_merge(p+l,p+mid,p+r,[](const point &a,const point &b){ if(a.y==b.y) return a.x<b.x; return a.y<b.y; });
top=0;
for(int i=l;i<r;++i) if(llabs(p[i].x-cmpx)<=ans) s[++top]=i;
for(int dn=1,up=dn;dn<=top;++dn) {
while(up<=top && p[s[up]].y<=p[s[dn]].y+ans) up++;
for(int it=dn+1;it<up;++it) ans=std::min(ans,caldis(s[dn],s[it]));
}
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
std::cin>>n;
for(int i=1;i<=n;++i) std::cin>>p[i].x>>p[i].y;
std::sort(p+1,p+1+n,[](const point &a,const point &b){ if(a.x==b.x) return a.y<b.y; return a.x<b.x; });
solve(1,n+1);
std::cout<<fsp(0)<<ans*ans<<'\n';
return 0;
}
短短的 35 行,却是一部史诗。
6.4 周日 - 欧拉回路
海尔霍尔泽正确性检验 - Hierholzer
-
需要注意的是,由于点可以重复,所以 不打标,采用 \(hd_u\) 记录遍历边数 的方法确定是否遍历结束所有边。
-
如果需要最小字典序,那么就只能使用 一般邻接表(通常是
std::vector
),然后进行std::sort
。如果不需要字典序,可以考虑 前向星,速度会更快。 -
看起来很简单,只是在 判定确实存在欧拉路径 后 找合适的起点(根据性质,比如半欧拉图中无向图的 奇度点,有向图的 唯一出大于入点) 然后 一路 dfs 到底。但事实上,在一个节点的所有出边 遍历结束 后 入栈 是一个经过了严谨思考与证明的巧思。从正面讲,可以证明合理性;从反面讲,可以证明正确性。应该加深理解。
6.11 周日 - 可持久化数据结构
【模板】可持久化文艺平衡树
- 注意使用函数简化操作,增加正确性(增加
clone
函数) - 注意 FHQ - treap 分裂区间 ——
v
在左边!
6.12 周一 - 偏序 / cdq 分治
made,看不懂他们不加 \(\LaTeX\) 的 shaber 公式,我自己打吧……
- 偏序关系:集合内只有部分元素间在这个关系下是可以比较的
- 全序关系:集合内任何一对元素在这个关系下都是可以比较的
我们设 \(\operatorname{R}\) 是集合 \(A\) 上的一个二元关系,如果有:
- 自反性:对任意 \(x \in A,\) 有 \(x \operatorname{R} x;\)
- 反对称性:对任意 \(x,y \in A,\) 若 \(x \operatorname{R} y,\) 且 \(y \operatorname{R} x,\) 则 \(x=y;\)
- 传递性:对任意 \(x,y,z \in A,\) 若 \(x \operatorname{R} y,\) 且\(y \operatorname{R} z,\) 则 \(x \operatorname{R} z.\)
则称 \(\operatorname{R}\) 是 \(A\) 上的偏序关系,通常记作 \(\preccurlyeq.\)
1. 二维偏序与树状数组
一般来讲,二维偏序采用 树状数组 解决。将一维排序,另一维采用权值树状数组维护,按序查询 \([1,a_i-1]\) 后在 \(a_i\) 出插入 \(1\) 即可。可以看出,本质上这是 离线做法。以后用到的离线做法会越来越多。
之前做过的 逆序对 就是典型的二维偏序。最开始,我们是考虑采用线段树解决的;现在我们知道,对于 单点修改,区间查询,树状数组更加优秀(修改查询都是 \(O(\log n)\))。
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define fsp(x) std::fixed<<std::setprecision(x)
#define forE(u) for(int p=head[u],v=E[p].to;p;p=E[p].next,v=E[p].to)
const int N=5e5+5;
int n,cnt,a[N],o[N];
ll ans,t[N];
inline int lowbit(int x) { return x&(-x); }
inline void change(int x) { while(x<=cnt) t[x]+=1,x+=lowbit(x); }
inline int query(int x) {
ll ans=0;
while(x) ans+=t[x],x-=lowbit(x);
return ans;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
std::cin>>n;
for(int i=1;i<=n;++i) { std::cin>>a[i]; o[i]=a[i]; }
std::sort(o+1,o+n+1); cnt=std::unique(o+1,o+n+1)-o-1;
for(int i=1;i<=n;++i) a[i]=cnt-(std::lower_bound(o+1,o+cnt+1,a[i])-o)+1;
for(int i=1;i<=n;++i) ans+=query(a[i]-1),change(a[i]);
std::cout<<ans;
return 0;
}
什么?你想要二维偏序在线做法?有本事你树套树啊!
2. 三维偏序与 cdq 分治
接下来我们就要搞一些好玩的,三维偏序。
一般来讲,如果考虑在线,每一维都需要采用一个数据结构来维护,典型的两个数据结构维护两位就是 树套树;而现在我们要处理三维,不可能树套树套树对吧,给评测机整个二倍空间都费劲,更何况你手写这东西也是要疯的。所以就只能考虑 离线 了。
我们要先介绍一个新的算法:cdq 分治。顾名思义,这是由 陈丹琦 引入国内 OI 界的。其核心思想是对需要计算贡献的点先 二分递归计算子问题贡献,再 合并子问题计算跨子问题贡献。
cdq 分治和整体二分都是基于分治的思想,把复杂的问题拆分成许多可以简单求的解子问题。但是这两种算法必须离线处理,不能解决一些强制在线的题目。不过如果题目允许离线的话,这两种算法能把在线解法吊起来打(如树套树)。
——【洛谷日报#115】CDQ分治和整体二分
比如还是逆序对,我们就考虑将需要计算贡献的点 \([l,r)\) 分成 \([l,mid),[mid,r)\) 两个子问题,然后分别计算子问题贡献,最后计算 \(i \in [l,mid), j \in [mid,r)\) 的这种跨子问题贡献。
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define fsp(x) std::fixed<<std::setprecision(x)
#define forE(u) for(int p=head[u],v=E[p].to;p;p=E[p].next,v=E[p].to)
const int N=5e5+5;
int n,cnt,a[N],o[N],s[N],top;
ll cdq(int *A,int sz) {
if(sz==1) return 0;
int M=sz>>1,*ptr1=A,*ptr2=A+M;
ll ans=cdq(A,M)+cdq(A+M,sz-M); top=0;
while(ptr2<A+sz) {
while(ptr1<A+M && *ptr1>*ptr2) s[top++]=*ptr1++;
ans+=ptr1-A,s[top++]=*ptr2++;
}
while(ptr1<A+M) s[top++]=*ptr1++;
memcpy(A,s,sizeof(int)*top);
return ans;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
std::cin>>n;
for(int i=0;i<n;++i) { std::cin>>a[i]; o[i]=a[i]; }
std::sort(o,o+n); cnt=std::unique(o,o+n)-o;
for(int i=0;i<n;++i) a[i]=std::lower_bound(o,o+cnt,a[i])-o+1;
std::cout<<cdq(a,n);
return 0;
}
你可能会想起逆序对的两种招牌做法——归并排序 和 树状数组,然后发现 这道题的 cdq 分治做法就是前者。从这点看,可以说 cdq 分治是归并排序的拓展,归并排序是 cdq 分治解决二维时的简单情况。
从这点上理解 cdq 分治,刚开始,我们通常先按第一维作为第一关键字排序;而在子问题处理完毕后,便只需要考虑跨子问题的贡献,子区间内第一维的顺序已经无所谓了,则可以将两个子区间按照我们需要的第二维作为第一关键字进行排序,并使用双指针解决跨子问题贡献,这本质上就是一个归并排序。(真的是人类智慧啊,就那么几行搞完了 qwq)
所以现在,我们回到三维偏序。怎么解决三维偏序?记住一个核心思路:一个数据结构维护一维,离线可以省去一维。所以,我们考虑最外层离线,按一、二、三维为第一、二、三关键字来排序;接下来在第二维使用 cdq 分治,对子序列以第二、三维作为第一、二关键词排序,然后处理跨子问题贡献;在第三维,使用树状数组维护,像逆序对一样,但是别忘记在每次处理后将树状数组清零,cdq 分治的每一个子问题中树状数组维护的信息是无法继承的。
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define fsp(x) std::fixed<<std::setprecision(x)
#define forE(u) for(int p=head[u],v=E[p].to;p;p=E[p].next,v=E[p].to)
const int N=1e5+5;
int n,k,tot,t[N<<1],f[N];
struct data { int a,b,c,cnt; ll ans; } elem[N],u[N];
inline int lowbit(int x) { return x&-x; }
inline void modify(int x,int v) { while(x<=k) t[x]+=v,x+=lowbit(x); }
inline ll query(int x) { ll ans=0; while(x) ans+=t[x],x-=lowbit(x); return ans; }
void solve(int l,int r) {
if(l+1==r) return;
int mid=l+r>>1,ptr=l;
solve(l,mid),solve(mid,r);
std::sort(u+l,u+mid,[](const data &x,const data &y){ return x.b!=y.b?x.b<y.b:x.c<y.c; });
std::sort(u+mid,u+r,[](const data &x,const data &y){ return x.b!=y.b?x.b<y.b:x.c<y.c; });
for(int i=mid;i<r;++i) {
while(ptr<mid && u[ptr].b<=u[i].b) modify(u[ptr].c,u[ptr].cnt),++ptr;
u[i].ans+=query(u[i].c);
}
for(int i=l;i<ptr;++i) modify(u[i].c,-u[i].cnt);
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
std::cin>>n>>k;
for(int i=0;i<n;++i) std::cin>>elem[i].a>>elem[i].b>>elem[i].c;
std::sort(elem,elem+n,[](const data &x,const data &y){ return x.a!=y.a?x.a<y.a:x.b!=y.b?x.b<y.b:x.c<y.c; });
u[tot]=elem[0],++u[tot].cnt;
for(int i=1;i<n;++i) {
if(elem[i-1].a!=elem[i].a || elem[i-1].b!=elem[i].b || elem[i-1].c!=elem[i].c) u[++tot]=elem[i];
++u[tot].cnt;
}
solve(0,++tot);
for(int i=0;i<tot;++i) f[u[i].ans+u[i].cnt-1]+=u[i].cnt;
for(int i=0;i<n;++i) std::cout<<f[i]<<'\n';
return 0;
}
一般来讲,cdq 分治的复杂度计算主要依赖于层数,根据具体情况不同。一般来讲层数是 \(\log n.\)
6.13 周二 - cdq 分治
P4755 Beautiful Pair
不要总是写从 0 开始的数组这种你明显驾驭不了的东西。
在需要离散化的操作的时候,从 0 开始就体现出了极强的不稳定性。所以我制定如下标准:
- 主程序使用 从 1 开始的数组。
- 分治采用 左开右闭区间。
保留左开右闭区间是我的一个执念吧……而且有时候确实有优势。
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define fsp(x) std::fixed<<std::setprecision(x)
#define forE(u) for(int p=head[u],v=E[p].to;p;p=E[p].next,v=E[p].to)
const int N=1e5+5;
using std::upper_bound;
using std::lower_bound;
ll n,node,a[N],b[N],cnt,pre[N],suf[N],rev[N];
int root[N];
struct segment_tree { int ls,rs,sum; } t[N<<6];
void modify(int o,int &x,int l,int r,int p) {
t[x=++node]=t[o],++t[x].sum;
if(l==r) return;
int mid=l+r>>1;
if(p<=mid) modify(t[o].ls,t[x].ls,l,mid,p);
else modify(t[o].rs,t[x].rs,mid+1,r,p);
}
ll query(int L,int R,int l,int r,int p) {
if(l==r) return t[R].sum-t[L].sum;
int mid=l+r>>1;
if(p<=mid) return query(t[L].ls,t[R].ls,l,mid,p);
return t[t[R].ls].sum-t[t[L].ls].sum+query(t[L].rs,t[R].rs,mid+1,r,p);
}
ll solve(int l,int r) {
if(l+1==r) return b[a[l]]==1;
ll mid=l+r>>1,ans=solve(l,mid)+solve(mid,r);
for(int i=mid;i<r;++i) pre[i]=std::max(pre[i-1],a[i]);
for(int i=mid-1;i>=l;--i) rev[mid-i]=suf[i]=std::max(suf[i+1],a[i]);
for(int i=l;i<mid;++i) {
int ptr=upper_bound(pre+mid,pre+r,suf[i])-pre-1;
if(ptr>=mid) {
int val=upper_bound(b+1,b+cnt+1,b[suf[i]]/b[a[i]])-b-1;
if(val) ans+=query(root[mid-1],root[ptr],1,cnt,val);
}
}
for(int i=mid;i<r;++i) {
int ptr=mid-(lower_bound(rev+1,rev+mid-l+1,pre[i])-rev-1);
if(ptr<mid) {
int val=upper_bound(b+1,b+cnt+1,b[pre[i]]/b[a[i]])-b-1;
if(val) ans+=query(root[ptr-1],root[mid-1],1,cnt,val);
}
}
for(int i=l;i<r;++i) pre[i]=suf[i]=0;
return ans;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
std::cin>>n;
for(int i=1;i<=n;++i) { std::cin>>a[i]; b[i]=a[i]; }
std::sort(b+1,b+n+1); cnt=std::unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;++i) a[i]=lower_bound(b,b+cnt,a[i])-b,modify(root[i-1],root[i],1,cnt,a[i]);
std::cout<<solve(1,n+1)<<'\n';
return 0;
}
6.14 周三 起 - 网络流开机
由于网络流极其庞大,所以我暂时也不知道我应不应该写博客。我把大概纲要记了下来,叫 网络流入门手册,如果写博客的话我就按这个展开。(魏老师的博客真的很好 qaq)
6.28 周三 - 卡特兰数
卡特兰数可以认为是长度为 \(2n\) 的合法括号序列的个数,递推式如下:
其意义为枚举与最后一个右括号匹配的左括号的位置,从而将该括号序列拆成两个更小的括号序列。
假设对于一个长度为 \(2i\) 的合法括号序列,其与最后一个右括号匹配的左括号在第 \(2j+1\) 位,那么这个序列被拆成长度分别是 \(2j\) 和 \(2(i-j-1)\) 的子序列(后者被最后一个右括号和与其匹配的左括号套着,所以长度有 \(-1\))。利用乘法原理,该情况方案数为 \(C_j \cdot C_{i-j-1}.\)
7 - 8 月 正睿集训
非常刺激的 30 天。人类智慧已经汇聚在 ZROI Series 里了。8 月下旬回来后的小测,结题报告在 9 月份进行总结。
学习笔记到这里就结束了~ 九月份之后的笔记,可以去看 魔鬼冲刺学习笔记 哦~