数据结构口胡记录
114514天没写博客了(悲)
Bear and Bad Powers of 42
:线段树,势能分析
原问题不好直接做,考虑转化维护信息
首先可以发现42的幂次并不多,所以每次操作3到停止的次数并不多,因此可以用线段树多次打区间加标记。
问题转化为看一个区间内是否存在42的倍数,因为区间不存在42的倍数等价于区间每个数与它到下一个42次幂的差的最小值不为0,考虑维护这个差值即可。
每次打完标记后若有小于零的差值,就暴力递归到这些叶子节点,修改其到下一个幂次的差值,实际上因为42幂次分布稀疏,差值跨过0的次数并不多,所以复杂度正确。
注意到一个有区间推平标记的节点可以等价于叶子节点一样修改,不用往下递归,就避免推平可能造成一个区间的差值反复横跳。
Naive Operations思路类似,简单很多。
前进四
:吉司机线段树,离线处理
的方法显然,但是过不了。
发现修改的本质是对于前缀取,需要维护时间和位置两个维度,因此考虑离线询问,按照位置排序,建一棵维护当前位置每个时间后缀最小值的,支持区间取和询问单点被取时被修改次数,直接吉司机即可。
Tree Generator™
:线段树
首先括号序列有一个性质:一个连续的括号序列去掉所有匹配的括号对后剩下的就是树上两点之间的链长。
因此题意转化为求出原序列的一个子区间使其非匹配括号数量最多。将(转化为1,)转化为-1,可以直接用线段树以类似GSS的方式维护,记录强制/非强制选择左右端点和的最大值分类讨论即可。
struct Node{ int l,r; int sum,lmx,rmn,ans,lmd,rmd,md; }t[MAXN<<2]; void pushup(int p){ t[p].sum=t[p<<1].sum+t[p<<1|1].sum; t[p].lmx=max(t[p<<1].lmx,t[p<<1].sum+t[p<<1|1].lmx); t[p].rmn=min(t[p<<1|1].rmn,t[p<<1|1].sum+t[p<<1].rmn); t[p].lmd=max(t[p<<1].lmd,max(t[p<<1|1].lmd-t[p<<1].sum,t[p<<1].md+t[p<<1|1].lmx)); t[p].rmd=max(t[p<<1|1].rmd,max(t[p<<1].rmd+t[p<<1|1].sum,t[p<<1|1].md-t[p<<1].rmn)); t[p].md=max(t[p<<1].md+t[p<<1|1].sum,t[p<<1|1].md-t[p<<1].sum); t[p].ans=max(max(t[p<<1].ans,t[p<<1|1].ans),max(t[p<<1|1].lmd-t[p<<1].rmn,t[p<<1].rmd+t[p<<1|1].lmx)); }
Escape Through Leaf
:李超线段树,数据结构合并
李超树合并板子题,记住有这个东西就行了。
[NOI2022] 众数
:线段树,数据结构
当位置和值域询问分离的时候,考虑用位置有关的基本数据结构维护。
空间大,不要随便乱开,可以用替代,的可以插入。
V
:吉司机线段树,维护函数
转化标记的思想比较神仙。
首先正常的带区间加的吉司机线段树是的。
设标记表示先加再对取,三个操作可以分别表示为,,,而两个标记是直接可以合并成的。
为值经过标记后的值,不难发现是一个先平行于轴再斜率为1的折线,取两条折线的最大值仍然是一条折线的形式,因此可以比维护区间标记历史最值。
直接用线段树维护标记即可。
同时这种方式也可以推广,感觉比吉司机树好写还更快。
[BJOI2014] 大融合
:
维护子树信息。
因为中虚边是认父不认子的,考虑对于每个节点定义为其子树大小,为其虚儿子子树大小之和,那么的时候有。
其中应该在断开或者加入一条虚边时得到更新,发现只有和中会出现加虚边,然后就做完了。
[NOI2021] 轻重边
:树链剖分,线段树
修改时不好直接维护于链相连的边,因此考虑从点入手。我们可以将每次链的操作转化为对链上点的操作,每次对链上的点区间赋一种新的颜色,那么那么两个点之间是重边等价于两个点的颜色相同,写一棵支持区间赋值和询问区间相邻元素相同的个数线段树就行了。
注意询问时答案与合并的顺序有关,两个节点向上爬时需分别维护两条链上的答案,最后再合并起来,以及注意两条链合并起来时应该是判断相同与否。
struct Node{ int l,r,ans; int lval,rval,tag; void pushup(Node x,Node y){ ans=x.ans+y.ans; if (x.rval==y.lval) ans++; lval=x.lval;rval=y.rval; } }t[MAXN<<2]; int query(int x,int y){ Sgt::Node ansx,ansy; ansx.ans=ansy.ans=0; ansx.lval=-114514;ansx.rval=-114515; ansy.lval=-114516;ansy.rval=-114517; while(top[x]!=top[y]){ if (dep[top[x]]>dep[top[y]]){ ansx.pushup(Sgt::query(1,dfn[top[x]],dfn[x]),ansx); x=fa[top[x]]; } else{ ansy.pushup(Sgt::query(1,dfn[top[y]],dfn[y]),ansy); y=fa[top[y]]; } } if (dfn[x]>dfn[y]) ansx.pushup(Sgt::query(1,dfn[y],dfn[x]),ansx); else ansy.pushup(Sgt::query(1,dfn[x],dfn[y]),ansy); int res=ansx.ans+ansy.ans; if (ansx.lval==ansy.lval) res++; return res; }
同时,维护链及其相邻节点还可以用毛毛虫剖分来做,以后有时间再来补。
[SDOI2017] 树点涂色
:
首先因为每次加入的都是一种新的颜色,并且是直接将到根节点染色,所以有每种颜色的点在树上构成一条链,且深度单调递增。
再由于每次修改都是一个点到根节点,可以发现如果直接用维护原树,每次修改一直接到根,那么某个点的答案就是向上跳到根时所经过的虚边的数量。
考虑魔改,用每条实链维护一个颜色段,那么在的时候,所在子树由于断边答案全部加一,所在子树答案全部加一。由于已经用维护了颜色段,还需要其它数据结构维护答案,直接树剖+区间加区间最大值线段树即可。
对于询问2答案即为。
P7739 [NOI2021] 密码箱
:线段树,矩阵乘法,动态dp
首先考虑,假设前面几项得到的答案分数形式 为,那么下一项应该是,可以用矩阵乘法加速。
那么有
再尝试将两种操作用矩乘形式表示出来
很简单:
再考虑,按照最后一位的值分类讨论。
若最后一位不是1,那么有
在考虑最后一位为1的情况,发现此时序列操作后为,与最后一位不为1的序列经过计算后结果都为,故二者转移矩阵是相同的。
那么考虑用平衡树维护构成的操作序列,发现值取反和位置翻转标记维护思想可以采用[HNOI2011] 括号修复 / [JSOI2011]括号序列的思想,对于一个节点维护其在当前,值取反,位置翻转,值和位置同时翻转的矩阵子树乘积。的时候直接打标记,将当前值和对应取反操作的矩阵交换,另外两个矩阵也交换即可。
Little Pony and Lord Tirek
:分块,颜色段均摊
由颜色段均摊可以得到,我们在一个区间没有被推平标记时可以直接暴力修改后打上,有时考虑计算答案再更新标记即可,复杂度是正确的。
对于这道题,考虑分块,难点在于如何快速计算答案。可以预处理出表示第个块在被推平为后第秒的和,显然只用处理到。单独考虑块内每个值在每个时刻的贡献,设发现时间在这个数的贡献为,在时刻贡献为,此后的贡献都为0。发现每次修改时贡献的二次差分只会修改3个位置。最后块内两次前缀和就可以求出
再维护每个块一个上次修改的时间,时块内下传完推平标记后再暴力将每个修改即可。
void build(){ len=sqrt(n);block=n/len; if (len*block!=n) block++; for (int i=1;i<=block;i++){ L[i]=R[i-1]+1; R[i]=min(i*len,n); for (int j=L[i];j<=R[i];j++) belong[j]=i; memset(delta,0,sizeof delta); for (int j=L[i];j<=R[i];j++){ if (!r[j]) continue; int t=m[j]/r[j],val=m[j]%r[j]; delta[1]+=r[j];delta[t+1]-=r[j]-val;delta[t+2]-=val; } for (int j=1;j<MAXN;j++) delta[j]+=delta[j-1]; for (int j=1;j<MAXN;j++) delta[j]+=delta[j-1]; for (int j=1;j<MAXN;j++) sum[i][j]=delta[j]; } } void pushdown(int p,int t){ if (tag[p]){ for (int i=L[p];i<=R[p];i++) a[i]=0; tag[p]=0; } int d=t-tim[p]; for (int i=L[p];i<=R[p];i++){ a[i]=min((ll)m[i],a[i]+1ll*d*r[i]); } tim[p]=t; } ll query(int t,int l,int r){ int q=belong[l],p=belong[r]; ll ans=0; if (q==p){ pushdown(q,t); for (int i=l;i<=r;i++){ ans+=a[i]; a[i]=0; } return ans; } ans+=query(t,l,R[q]); ans+=query(t,L[p],r); for (int i=q+1;i<p;i++){ if (tag[i]){ int d=min(100000,t-tim[i]); ans+=sum[i][d]; } else ans+=query(t,L[i],R[i]); tag[i]=1;tim[i]=t; } return ans; }
Tree Queries
:结论,小清新
考场上一直在想数据结构,最后还是没做出来,我是不是学傻了?
的数据范围应该是要级别的算法。每次加入一个点时暴力更新的总复杂度是的,考虑优化这个过程。
我们可以先以第一个加入点为根,设为,一遍预处理出每个点到的路径最小值记为。
接下来考虑加入一个点时对所有点答案的影响, 显然子树内所有点答案不会变化。对于剩下的点,可以将它们分成两种情况:在上以及不在即在的另外一个儿子的子树中。
对于情况一,多出了这条路径上的最小值,即这个点的答案变成了和的路径并上的点权值最小值即
。
对于情况二,同理可得当前点答案变成了和的最小值。
发现对于子树外的点每次更新实质都是对取最小值,而子树内的点又有,所以直接维护全局除外被加入的点的答案最小值,每次询问点答案时就是即可,无需任何高级数据结构。
数据结构千万不要学太死了!
Yuezheng Ling and Dynamic Tree
和[Ynoi2007] rfplca是重题。
用数据结构不好维护,考虑根号算法。
因为此题有,可以类比弹飞绵羊,设表示这个点跳出这个点所在块后到达的点,可以预处理。
对于求,考虑每次若两个点不在同一个块内就把所在块编号最大的往上跳,在同一个块内则讨论是否相同,若相同则暴力每次一步一步往上跳,否则两个点都向上跳一次。
对于修改,对于散块当然可以暴力修改并重构,对于整块,如果只打的话难以块内维护,但是发现对于每个整块至多修改次后这些点一定都是在块外面的,就是,无需重构。
然后就做完了。
跳每次,每个块最多被重构次,总时间复杂度
点击查看代码
#include<bits/stdc++.h> using namespace std; template <class T> void read(T &x){ x=0;char c=getchar();bool f=0; while(!isdigit(c)) f=c=='-',c=getchar(); while(isdigit(c)) x=x*10+c-'0',c=getchar(); x=f? (-x):x; } const int MAXN=1e5+5; const int MAXM=325; int n,m,fa[MAXN]; int len,block; int L[MAXM],R[MAXM],belong[MAXN],nxt[MAXN],tag[MAXN]; void build(){ len=sqrt(n);block=n/len; if (len*block!=n) block++; for (int i=1;i<=block;i++){ L[i]=R[i-1]+1; R[i]=min(i*len,n); for (int j=L[i];j<=R[i];j++) belong[j]=i; } } void rebuild(int p){ for (int i=L[p];i<=R[p];i++){ fa[i]=max(1,fa[i]-tag[p]); } tag[p]=0; for (int i=L[p];i<=R[p];i++){ if (belong[fa[i]]!=p) nxt[i]=fa[i]; else nxt[i]=nxt[fa[i]]; } } int find_fa(int x){ return max(1,fa[x]-tag[belong[x]]); } int find_nxt(int x){ return max(1,nxt[x]-tag[belong[x]]); } int cnt[MAXM]; void update(int l,int r,int x){ int q=belong[l],p=belong[r]; if (q==p){ for (int i=l;i<=r;i++) fa[i]=max(1,fa[i]-x); rebuild(p); return; } for (int i=l;i<=R[q];i++) fa[i]=max(1,fa[i]-x); rebuild(q); for (int i=L[p];i<=r;i++) fa[i]=max(1,fa[i]-x); rebuild(p); for (int i=q+1;i<p;i++){ tag[i]+=x;tag[i]=min(tag[i],n); ++cnt[i]; if (cnt[i]<=MAXM) rebuild(i); } } int query(int x,int y){ while(x!=y){ if (belong[x]<belong[y]) swap(x,y); if (belong[x]!=belong[y]) x=find_nxt(x); else{ if (find_nxt(x)==find_nxt(y)){ while(x!=y){ if (x<y) swap(x,y); x=find_fa(x); } } else{ x=find_nxt(x);y=find_nxt(y); } } } return x; } int main(){ read(n);read(m); build(); for (int i=2;i<=n;i++) read(fa[i]); for (int i=1;i<=block;i++) rebuild(i); while(m--){ int op,l,r,x; read(op);read(l);read(r); if (op==1){ read(x); update(l,r,x); } else{ printf("%d\n",query(l,r)); } } return 0; }
Pudding Monsters
问题是二维的,不好入手,考虑转化为一维
发现每个点的两维坐标都是排列,双射一一对应,可以直接 转化成一维
那么原问题就可以转化为求有多少对,满足 并且 。因为原条件等价于转化后的序列相邻两项之差小于1,而本身是排列,所以不会出现 的情况,只会是从一直到,所以是等价的。
考虑枚举右端点,移项后设,显然,那么就是要维护使为0的左端点个数,直接维护区间最小值 + 出现次数即可。
具体来说每次右端点挪动时都会使所有减小1,同时维护两个最大/最小值的单调栈,每次撤销上一段最小值/最大值的贡献并用当前点更新单调栈即可。复杂度均摊。
好像这个玩意叫析合树?会的大佬可以普及一下,我太菜了不会 /kk。
点击查看代码
#include <bits/stdc++.h> const int MAXN = 3e5 + 5; void solve() { int n; std::cin >> n; std::vector <int> a(n + 1); for (int i = 1; i <= n; i++) { int x, y; std::cin >> x >> y; a[x] = y; } struct Sgt { struct Node { int l, r; int mn, cnt, tag; }; std::vector <Node> t; void pushup(int p) { if (t[p << 1].mn < t[p << 1 | 1].mn) { t[p].mn = t[p << 1].mn; t[p].cnt = t[p << 1].cnt; } else if (t[p << 1].mn > t[p << 1 | 1].mn) { t[p].mn = t[p << 1 | 1].mn; t[p].cnt = t[p << 1 | 1].cnt; } else { t[p].mn = t[p << 1].mn; t[p].cnt = t[p << 1].cnt + t[p << 1 | 1].cnt; } } void build(int p, int l, int r) { t[p].l = l; t[p].r = r; if (l == r) { t[p].mn = l; t[p].cnt = 1; return; } int mid = (l + r) >> 1; build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r); pushup(p); } void init(int n) { t.resize((n + 1) * 4); build(1, 1, n); } void pushtag(int p, int tg) { t[p].tag += tg; t[p].mn += tg; } void pushdown(int p) { if (t[p].tag) { pushtag(p << 1, t[p].tag); pushtag(p << 1 | 1, t[p].tag); t[p].tag = 0; } } void update(int p, int l, int r, int delta) { if (l <= t[p].l && t[p].r <= r) { pushtag(p, delta); return; } pushdown(p); int mid = (t[p].l + t[p].r) >> 1; if (l <= mid) update(p << 1, l, r, delta); if (r > mid) update(p << 1 | 1, l, r, delta); pushup(p); } }t; #define ll long long t.init(n); static int s1[MAXN], s2[MAXN]; int top1 = 0, top2 = 0; ll ans = 0; //s1 -> max, s2 -> min for (int i = 1; i <= n; i++) { t.update(1, 1, n, -1); while (top1 && a[s1[top1]] < a[i]) { t.update(1, s1[top1 - 1] + 1, s1[top1], -a[s1[top1]]); t.update(1, s1[top1 - 1] + 1, s1[top1], a[i]); top1--; } s1[++top1] = i; while (top1 && a[s2[top2]] > a[i]) { t.update(1, s2[top2 - 1] + 1, s2[top2], a[s2[top2]]); t.update(1, s2[top2 - 1] + 1, s2[top2], -a[i]); top2--; } s2[++top2] = i; assert(t.t[1].mn == 0); ans += t.t[1].cnt; } std::cout << ans << "\n"; } int main() { std::ios::sync_with_stdio(0); std::cin.tie(0); std::cout.tie(0); int t = 1; while (t--) { solve(); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通