数据结构口胡记录

114514天没写博客了(悲)

Bear and Bad Powers of 42

tag:线段树,势能分析

原问题不好直接做,考虑转化维护信息

首先可以发现42的幂次并不多,所以每次操作3到停止的次数并不多,因此可以用线段树多次打区间加标记。

问题转化为看一个区间内是否存在42的倍数,因为区间不存在42的倍数等价于区间每个数与它到下一个42次幂的差的最小值不为0,考虑维护这个差值即可。

每次打完标记后若有小于零的差值,就暴力递归到这些叶子节点,修改其到下一个幂次的差值,实际上因为42幂次分布稀疏,差值跨过0的次数并不多,所以复杂度正确。

注意到一个有区间推平标记的节点可以等价于叶子节点一样修改,不用往下递归,就避免推平可能造成一个区间的差值反复横跳。

Naive Operations思路类似,简单很多。

前进四

tag:吉司机线段树,离线处理

nlogn2的方法显然,但是过不了。

发现修改的本质是对于前缀取min,需要维护时间和位置两个维度,因此考虑离线询问,按照位置排序,建一棵维护当前位置每个时间后缀最小值的Sgt,支持区间取min和询问单点被取min时被修改次数,直接吉司机即可。

Tree Generator™

tag:线段树

首先括号序列有一个性质:一个连续的括号序列去掉所有匹配的括号对后剩下的就是树上两点之间的链长。

因此题意转化为求出原序列的一个子区间使其非匹配括号数量最多。将(转化为1,)转化为-1,可以直接用线段树以类似GSS的方式维护,记录强制/非强制选择左右端点和的最大值分类讨论pushup即可。

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

tag:李超线段树,数据结构合并

李超树合并板子题,记住有这个东西就行了。

[NOI2022] 众数

tag:线段树,数据结构

当位置和值域询问分离的时候,考虑用位置有关的基本数据结构维护。

deque空间大,不要随便乱开,可以用list替代,listsplice可以O(1)插入。

V

tag:吉司机线段树,维护函数

转化标记的思想比较神仙。

首先正常的带区间加的吉司机线段树是nlogn2的。

设标记(val,mx)表示先加val再对mxmax,三个操作可以分别表示为(x,INF),(x,0),(INF,x),而两个标记是直接可以合并成(a.val+b.val,max(a.mx+b.val,b.mx))的。

f(x)为值x经过标记后的值,不难发现是一个先平行于x轴再斜率为1的折线,取两条折线的最大值仍然是一条折线的形式,因此可以比max维护区间标记历史最值。

直接用线段树维护标记即可。

同时这种方式也可以推广,感觉比吉司机树好写还更快

[BJOI2014] 大融合

tagLCT

LCT维护子树信息。

因为Splay中虚边是认父不认子的,考虑对于每个节点定义siz[x]为其子树大小,siz2[x]为其虚儿子子树大小之和,那么pushup的时候有siz[x]=siz[ls]+siz[rs]+siz2[x]+1

其中siz2[x]应该在断开或者加入一条虚边时得到更新,发现只有accesslink中会出现加虚边,然后就做完了。

[NOI2021] 轻重边

tag:树链剖分,线段树

修改时不好直接维护于链相连的边,因此考虑从点入手。我们可以将每次链的操作转化为对链上点的操作,每次对链上的点区间赋一种新的颜色,那么那么两个点之间是重边等价于两个点的颜色相同,写一棵支持区间赋值和询问区间相邻元素相同的个数线段树就行了。

注意询问时答案与合并的顺序有关,两个节点向上爬时需分别维护两条链上的答案,最后再合并起来,以及注意两条链合并起来时应该是判断lval相同与否。

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] 树点涂色

tagLCT

首先因为每次加入的都是一种新的颜色,并且是直接将x到根节点染色,所以有每种颜色的点在树上构成一条链,且深度单调递增。

再由于每次修改都是一个点到根节点,可以发现如果直接用LCT维护原树,每次修改一直接access到根,那么某个点的答案就是向上跳到根时所经过的虚边的数量。

考虑魔改LCT,用LCT每条实链维护一个颜色段,那么在access的时候,ch[x][1]所在子树由于断边答案全部加一,y所在子树答案全部加一。由于已经用LCT维护了颜色段,还需要其它数据结构维护答案,直接树剖+区间加区间最大值线段树即可。

对于询问2答案即为ans(x)+ans(y)2ans(lca(x,y))+1

P7739 [NOI2021] 密码箱

tag:线段树,矩阵乘法,动态dp

首先考虑f,假设前面几项得到的答案分数形式xy(x,y),那么下一项应该是(y,y×ai+x),可以用矩阵乘法加速。

那么有

[xy]×[011ai]=[yy×ai+x]

再尝试将W,E两种操作用矩乘形式表示出来

W很简单:

[011ai]×[1101]=[011ai+1]

再考虑E,按照最后一位的值分类讨论。

若最后一位不是1,那么有

[011ai]×[1101]×[1101]×[1101]=[011ai]×[0112]

在考虑最后一位为1的情况,发现此时序列操作后为(x+1,1),与最后一位不为1的序列(x,0,1,1)经过f计算后结果都为x+1x+2,故二者转移矩阵是相同的。

那么考虑用平衡树维护W,E构成的操作序列,发现值取反和位置翻转标记维护思想可以采用[HNOI2011] 括号修复 / [JSOI2011]括号序列的思想,对于一个节点维护其在当前,值取反,位置翻转,值和位置同时翻转的矩阵子树乘积。pushtag的时候直接打标记,将当前值和对应取反操作的矩阵交换,另外两个矩阵也交换即可。

Little Pony and Lord Tirek

tag:分块,颜色段均摊

由颜色段均摊可以得到,我们在一个区间没有被推平标记tag时可以直接暴力修改后打上tag,有tag时考虑O(1)计算答案再更新标记即可,复杂度是正确的。

对于这道题,考虑分块,难点在于如何快速计算答案。可以预处理出sump,t表示第p个块在被推平为0后第t秒的和,显然t只用处理到1e5。单独考虑块内每个值在每个时刻的贡献,设ki=miri发现时间在[1,ki]这个数的贡献为ri,在ki+1时刻贡献为mi%ri,此后的贡献都为0。发现每次修改时贡献的二次差分只会修改3个位置。最后块内两次前缀和就可以求出sump,t

再维护每个块一个上次修改的时间timp,pushdown时块内下传完推平标记后再暴力将每个ai修改即可。

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

tag:结论,小清新

考场上一直在想polylog数据结构,最后还是没做出来,我是不是DS学傻了?

1e6的数据范围应该是要O(n)级别的算法。每次加入一个点时暴力更新的总复杂度是O(n2)的,考虑优化这个过程。

我们可以先以第一个加入点为根,设为rtdfs一遍预处理出每个点irt的路径最小值记为ansi

接下来考虑加入一个点x时对所有点答案的影响, 显然x子树内所有点答案不会变化。对于剩下的点u,可以将它们分成两种情况:在rt>x上以及不在rt>x即在rt的另外一个儿子的子树中。

对于情况一,多出了u>x这条路径上的最小值,即这个点的答案变成了rt>uu>x的路径并上的点权值最小值即ansx

对于情况二,同理可得当前点答案变成了ansuansx的最小值。

发现对于子树外的点每次更新实质都是对ansx取最小值,而子树内的点又有ansuansx,所以直接维护全局除rt外被加入的点的答案最小值mn,每次询问点u答案时就是min(a[u],mn)即可,无需任何高级数据结构。

数据结构千万不要学太死了!

Yuezheng Ling and Dynamic Tree

[Ynoi2007] rfplca是重题。

polylog数据结构不好维护,考虑根号算法。

因为此题有fai<i,可以类比弹飞绵羊,设nxti表示i这个点跳出这个点所在块后到达的点,可以O(n)预处理。

对于求lca,考虑每次若两个点不在同一个块内就把所在块编号最大的往上跳nxt,在同一个块内则讨论nxt是否相同,若相同则暴力每次一步一步往上跳,否则两个点都向上跳一次nxt

对于修改,对于散块当然可以暴力修改并重构,对于整块,如果只打tag的话难以块内维护nxt,但是发现对于每个整块至多修改nxt次后这些点nxt一定都是在块外面的,nxti就是fai,无需重构。

然后就做完了。

Lca每次O(n),每个块最多被重构n次,总时间复杂度O(nn)

点击查看代码
#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

问题是二维的,不好入手,考虑转化为一维

发现每个点的两维坐标都是排列,双射一一对应,可以直接 ax=y 转化成一维

那么原问题就可以转化为求有多少对(l,r),满足 lr并且 maxi=lraimini=lrai=rl。因为原条件等价于转化后的a序列相邻两项之差小于1,而a本身是排列,所以不会出现 x>x±1>x的情况,只会是从x一直到x±k,所以是等价的。

考虑枚举右端点r,移项后设M=maxminr+l,显然M0,那么就是要维护使M为0的左端点个数,直接维护区间最小值 + 出现次数即可。

具体来说每次右端点挪动时都会使所有M减小1,同时维护两个最大/最小值的单调栈,每次撤销上一段最小值/最大值的贡献并用当前点更新单调栈即可。复杂度均摊O(nlogn)

好像这个玩意叫析合树?会的大佬可以普及一下,我太菜了不会 /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;
}
posted @   cqbzlzh  阅读(18)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示