Mars的学习笔记(更新中)
前言
这篇文章主要用来总结我之前所学的算法,以更好地复习。希望大家在看时能理解我所说的话(包括未来的我)
算法大典
KMP
KMP是一个强大的字符串搜索算法,可以在线性的复杂度下将所需要的子串位置精确的找出。它的最大特点就是搜索是不会回溯。
朴素思想
给出两个字符串
abababab bab
要求找出第二个字串在第一个字串第一次出现的位置。朴素思想就是直接爆搜,从第一个字符开始搜索,没搜索到就重新跳到第二个字符。代码如下:
#include<bits/stdc++.h> using namespace std; string a,b; int main() { cin>>a>>b; for(int i=0;i<a.size();i++) { for(int j=i;j<b.size();j++) { if(a[j]!=b[j-i])break; else { cout<<i+1; return 0; } } } return 0; }
浅显易懂,但代价是复杂度来到了
KMP思想
考虑优化。
不难发现,在搜索时我们完全可以跳过一些已知的字符,从而继续匹配,这样子既不用回溯浪费时间,也不用一个个的匹配字符
拿两个字符串举例:
abbcabba abba
当我们匹配到
前后缀的求法十分巧妙,大家可以先看以下代码:
void get_next() { int t1=0,t2=-1; next1[0]=-1; while(t1<len2) { if(t2==-1 || s2[t2]==s2[t1]) next1[++t1]=++t2; else t2=next1[t2]; } }
我们可以将t1想象成一直往前开的火车,t2为一节车厢,当目前的前串(t2)与后串(t1)相等时,t2就能向前一步,反之就要倒退。
当你理解透了以上代码,你就很容易理解匹配代码了:
void KMP() { int t1=0,t2=0; while(t1<len1) { if(t2==-1 || s1[t1]==s2[t2]) t1++,t2++; else t2=next1[t2]; if(t2==len2)printf("%d\n",t1-t2+1),t2=next1[t2]; } }
原理是相似的。依旧建议反复观看理解。
贴上完整代码:
#include<bits/stdc++.h> using namespace std; int n,k,len1,len2; int next1[1000005]; char s1[1000005]; char s2[1000005]; void get_next() { int t1=0,t2=-1; next1[0]=-1; while(t1<len2) { if(t2==-1 || s2[t2]==s2[t1]) next1[++t1]=++t2; else t2=next1[t2]; } } void KMP() { int t1=0,t2=0; while(t1<len1) { if(t2==-1 || s1[t1]==s2[t2]) t1++,t2++; else t2=next1[t2]; if(t2==len2)printf("%d\n",t1-t2+1),t2=next1[t2]; } } int main() { scanf("%s",s1); scanf("%s",s2); len1=strlen(s1); len2=strlen(s2); get_next(); KMP(); for(int i=1;i<=len2;++i) printf("%d ",next1[i]); return 0; }
例题
最小生成树
最小生成树为边权和最小的生成树,只存在于联通图中,可以解决联通所有点的最小边权和问题。
思想
对于寻找联通所有点的最小边权和,最朴素的思想就是遍历全部图来寻找最小的边权,但操作难度大,坑还特别多,于是最小生成树应运而生。本文主要介绍 Prim 算法和 Kruskal 算法。
Prim算法
Prim 算法的思想就是选取任意点作为根,逐渐寻找最近的点不断加进树中,再将边权取最小值。
借鉴一下 oi-wiki 的图:
思想十分简单但有效,代码如下:
#include<bits/stdc++.h> using namespace std; const int INF=0x3f3f3f3f; const int maxn=200005; struct data { int dis,x; bool friend operator<(data a,data b) { return a.dis>b.dis; } }; int ne[maxn<<1],x[maxn<<1],w[maxn<<1],head[maxn],idx; int n,m,u1,v1,w1,ans,cnt; bool vis[maxn]; void add(int a,int b,int c) { x[++idx]=b; ne[idx]=head[a]; head[a]=idx; w[idx]=c; } void Prim() { priority_queue<data>q; q.push((data){0,1}); while(!q.empty()) { data u=q.top(); ans+=u.dis; cnt++; vis[u.x]=true; q.pop(); for(int i=head[u.x];i!=-1;i=ne[i]) { if(!vis[x[i]])q.push((data){w[i],x[i]}); } while(!q.empty() && vis[q.top().x]) q.pop(); } } int main() { memset(head,-1,sizeof(head)); cin>>n>>m; for(int i=1;i<=m;i++) { cin>>u1>>v1>>w1; add(u1,v1,w1); add(v1,u1,w1); } Prim(); if(cnt<n) { cout<<"orz"; return 0; } else cout<<ans; return 0; }
可以看到,代码十分的繁琐,实际操作十分困难。
Kruskal算法
kruskal 算法因为其简短的代码和简洁的思想被广泛使用。与 prim 不同的是, kruskal 的思想是加边,有点贪心的味道(就是贪心)。
借鉴一下 oi-wiki 的图 again :
代码基于并查集,因为加边使得点与点之间的关系可以用“祖先”相称。
#include<bits/stdc++.h> using namespace std; const int maxn=200005; int n,m,sum,cnt; int fa[maxn]; int read() { int date=0,w=1; char a=getchar(); while(a<'0' || a>'9') { if(a=='-')w=-1; a=getchar(); } while(a>='0' && a<='9') { date=date*10+(a-'0'); a=getchar(); } return date*w; } void init(int n) { for(int i=1;i<=n;i++)fa[i]=i; } int f(int x) { return fa[x]=(fa[x]==x) ? fa[x] : f(fa[x]); } bool u(int x,int y) { x=f(x),y=f(y); if(x==y)return false; fa[x]=y; return true; } struct edge { int u,v,w; bool operator<(edge a){return w<a.w;} }e[maxn]; bool cmp(edge x,edge y) { return x.w<y.w; } signed main() { n=read(),m=read(); init(n); for(int i=1;i<=m;i++) e[i].u=read(),e[i].v=read(),e[i].w=read(); sort(e+1,e+m+1,cmp); for(int i=1;i<=m;i++) { if(u(e[i].u,e[i].v)) { sum+=e[i].w; cnt++; } } if(cnt!=n-1) { cout<<"orz"; return 0; } cout<<sum; return 0; }
总的来说,最小生成树比较好理解,学过图论的理解起来难度不大。
例题
Trie
字典树,一个好用但有点冷门的东西,值得一学,因为 ACAM 等高级结构都与它有关。
借鉴一下 oi-wiki 的图
思想
如图,可以看到这棵树的边代表了一个字母,例如,1 -> 2 -> 6 -> 7就是 aba
字符串。
trie 的结构十分好懂,做起来也不难,用
放一个模板。
struct trie { int nex[3000005][65],cnt; int exist[3000005]; void insert(string s,int l)//插入字符串 { int p=0; for(int i=0;i<l;i++) { int c=getnum(s[i]); if(!nex[p][c])nex[p][c]=++cnt; p=nex[p][c]; exist[p]++; } } int find(string s,int l)//查找字符串 { int p=0; for(int i=0;i<l;i++) { int c=getnum(s[i]); if(!nex[p][c])return false; p=nex[p][c]; } return exist[p]; } };
字典树可以查找字符串(这不是废话吗)。
例题
十分基础的查找字符串。首先将字符串存入 trie 中,当查找时判断该字符串是否出现过,结束(有点过于简短了)但思想就是这么简单。
进阶练习
跟例题没什么大出入,改一点就能过。
代码如下:
#include<bits/stdc++.h> using namespace std; int m,n,len; struct Trie { int nex[500005][5],cnt; int exist[500005],aaa[500005]; void insert(bool s[]) { int p=0; for(int i=1;i<=len;i++) { int c=s[i]; if(nex[p][c]==-1)nex[p][c]=++cnt; p=nex[p][c]; exist[p]++; } aaa[p]++; } int find(bool s[]) { int p=0,maxx=0; for(int i=1;i<=len;i++) { int c=s[i]; if(nex[p][c]==-1)return maxx; p=nex[p][c]; maxx+=aaa[p]; } return maxx-aaa[p]+exist[p]; } }; Trie a; bool b[10005]; int main() { scanf("%d%d",&m,&n); memset(a.nex,-1,sizeof(a.nex)); for(int i=1;i<=m;i++) { scanf("%d",&len); for(int j=1;j<=len;j++)cin>>b[j]; a.insert(b); } for(int i=1;i<=n;i++) { scanf("%d",&len); for(int j=1;j<=len;j++)cin>>b[j]; cout<<a.find(b)<<"\n"; } return 0; }
字典树还能用来弄 01-trie ,笔者还没学,等我进省队了再说。
LCA(最近公共祖先)
树论大佬必备常识,超级有用!!!(就是因为没学这个 GDKOI 痛失 T1 55555)
接下来开始讲解。
思想
朴素思想
想找到两个树上的点的最近公共祖先,首先应该让深度大的那个点先跳到相同的深度,再同时往上跳,直到找到 LCA 为止。
显然,这样暴力跳的单次查询时间复杂度为
于是不同的方法应运而生。本文主要介绍倍增求 LCA 。
倍增求 LCA
倍增求 LCA 的单次查询复杂度为
我们用游标来让点进行快速移动。
借一下 oi-wiki 的一段话。
在调整游标的第一阶段中,我们要将 u,v 两点跳转到同一深度。我们可以计算出 u,v 两点的深度之差,设其为 y。通过将 y 进行二进制拆分,我们将 y 次游标跳转优化为「y 的二进制表示所含 1 的个数」次游标跳转。 在第二阶段中,我们从最大的 i 开始循环尝试,一直尝试到 0(包括 0),如果
金句啊! oi-wiki 我爱你......
存树的话本人喜欢用链式前向星,十分好用建议一试。
练手题
接下来给 2 道题巩固知识。非常好的题,使我大脑旋转。
例题
LCA 板子题。只要将板子打对就行。
代码如下。
#include<bits/stdc++.h> using namespace std; const int maxn=500005; int dep[maxn<<1],fa[maxn<<1][30]; int n,q,root; int ee,head[maxn<<1],ne[maxn<<1],to[maxn<<1]; int getlca(int x,int y) { if(dep[x]<dep[y]) swap(x,y); while(dep[x]>dep[y]) x=fa[x][(int)log2(dep[x]-dep[y])];//简洁,建议这么写。 if(x==y) return y; for(int j=20;j>=0;j--)//跳到LCA的下面一层 if(fa[x][j]!=fa[y][j]) x=fa[x][j],y=fa[y][j]; return fa[x][0];//返回LCA } void add(int a,int b) { ne[++ee]=head[a]; to[ee]=b; head[a]=ee; } void dfs(int x,int pre)//x为当前节点,pre为父亲节点 { dep[x]=dep[pre]+1; fa[x][0]=pre; for(int i=head[x];i;i=ne[i]) if(to[i]!=pre) dfs(to[i],x); } signed main() { scanf("%d%d%d",&n,&q,&root); for(int i=1,x,y;i<n && cin>>x>>y;i++) add(x,y),add(y,x); dfs(root,0);//预处理 for(int j=1;j<=20;j++) for(int i=1;i<=n;i++) fa[i][j]=fa[fa[i][j-1]][j-1];//预处理 while(q--) { int x,y; scanf("%d%d",&x,&y); printf("%d\n",getlca(x,y)); } return 0; }
给一道进阶题(就是我痛失的 T1......)。
捉迷藏 pro max 版
虽然是比较版的 LCA ,但卡常能将人卡送走,错了无数次才卡过......
代码奉上:
#include<bits/stdc++.h> using namespace std; const int maxn=1000000; int n,q; int head[maxn<<1],ne[maxn<<1],to[maxn<<1],ee; int fa[maxn][30],dep[maxn]; inline int read() { int date=0,w=1; char y=getchar(); while(y<'0' || y>'9') { if(y=='-')w=-1; y=getchar(); } while(y>='0' && y<='9') { date=date*10+(y-'0'); y=getchar(); } return date*w; } inline int getlca(int x,int y) { if(dep[x]<dep[y]) swap(x,y); while(dep[x]>dep[y]) x=fa[x][(int)log2(dep[x]-dep[y])]; if(x==y) return y; for(int j=20;j>=0;j--) if(fa[x][j]!=fa[y][j]) x=fa[x][j],y=fa[y][j]; return fa[x][0]; } inline void add(int a,int b) { ne[++ee]=head[a]; to[ee]=b; head[a]=ee; } inline void dfs(int x,int pre) { dep[x]=dep[pre]+1; fa[x][0]=pre; for(int i=head[x];i;i=ne[i]) if(to[i]!=pre) dfs(to[i],x); } inline void c() { for(int i=0;i<=n;i++) head[i]=to[i]=ne[i]=dep[i]=0; for(int i=n+1;i<=2*n;i++) to[i]=ne[i]=0; ee=0; } signed main() { int A=read(),T=read(); while(T--) { n=read(),q=read(); for(int i=1,x,y;i<n;i++) x=read(),y=read(),add(x,y),add(y,x); dfs(1,0); for(int j=1;j<=20;j++) for(int i=1;i<=n;i++) fa[i][j]=fa[fa[i][j-1]][j-1]; for(int i=1;i<=q;i++) { int x=read(),y=read(),dx=read(),dy=read(); int ans=getlca(x,y),lo; lo=dep[x]-2*dep[ans]+dep[y]; if(lo<=dx)printf("Zayin\n"); else if(dx>dy)printf("Zayin\n"); else if(dx==dy)printf("Draw\n"); else if(dx<dy)printf("Ziyin\n"); } c(); } return 0; }
真是爱死这道题了,卡常把我送走了......
LCA 是很有用的,建议巩固好知识。
dijkstra
众所周知,Dijkstra算法是一个十分有效且常用的算法。既然说了:
- 有效且常用
那我们就有学习的必要了呀!
话不多说,开始讲解。
概念
1.是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。
2.
代码讲解
链式前向星
#define MOD 10000000007 #define INF 0x3f3f3f3f using namespace std; const int maxn=2000005; int n,m,s; int idx=1,e[maxn],w[maxn],head[maxn],ne[maxn]; int dis[maxn],vis[maxn];
MOD和INF不做过多解释,重点看e,w,head和ne数组。
void add(int a,int b,int c) { e[idx]=b; w[idx]=c; ne[idx]=head[a]; head[a]=idx++; }
这是一个简单的链式前向星,虽然有些奇怪的东西。
a是该点的位置,b是与该点相连的一点的位置,c则是边全
权。我们可以看到,e数组存储了一个点到另一个点的的终点,w存储了这两个点中边的边权,ne和head数组则是普通的链式前向星啦!
结构体
struct Node { int dis,x; bool operator<(Node p)const{return dis>p.dis;} Node(int dis,int x):dis(dis),x(x){} };
我们使用重载运算符 主要是装。
Dijkstra 主体
void dijstra() { memset(dis,INF,sizeof(dis)); priority_queue<Node>q; dis[s]=0; Node u(dis[s],s); q.push(u); while(!q.empty()) { Node u=q.top(); q.pop(); if(vis[u.x])continue; vis[u.x]=1; for(int i=head[u.x];i!=-1;i=ne[i]) { if(dis[e[i]]>dis[u.x]+w[i]) { dis[e[i]]=dis[u.x]+w[i]; Node v(dis[e[i]],e[i]); q.push(v); } } } }
首先定义大根堆
跟图有关,那我们就得使出万能且高效的BFS。
如你所见,里面有一个BFS遍历。
while循环的前四行为基操,不做过多讲述,我们来看for循环。
for(int i=head[u.x],i!=-1;i=ne[i])//从头开始,循环到下一个点
从第x个点的链下标出发,向下一个点,也就是
if(dis[e[i]]>dis[u.x]+w[i]) { dis[e[i]]=dis[u.x]+w[i]; Node v(dis[e[i]],e[i]); q.push(v); }
这里我们做出判断,如果新路径的边权总和小于原路径边权总和,就改变最佳路径。
完整代码
这里附上完整代码:
#include<bits/stdc++.h> #define MOD 10000000007 #define INF 0x3f3f3f3f using namespace std; const int maxn=2000005; int n,m,s; int idx=1,e[maxn],w[maxn],head[maxn],ne[maxn]; int dis[maxn],vis[maxn]; struct Node { int dis,x; bool operator<(Node p)const{return dis>p.dis;} Node(int dis,int x):dis(dis),x(x){} }; inline int read() { int date=0,w=1; char c; c=getchar(); while(c<'0' || c>'9') { if(c=='-')w=-1; c=getchar(); } while(c>='0' && c<='9') { date=date*10+(c-'0'); c=getchar(); } return date*w; } void add(int a,int b,int c) { e[idx]=b; w[idx]=c; ne[idx]=head[a]; head[a]=idx++; } void dijstra() { memset(dis,INF,sizeof(dis)); priority_queue<Node>q; dis[s]=0; Node u(dis[s],s); q.push(u); while(!q.empty()) { Node u=q.top(); q.pop(); if(vis[u.x])continue; vis[u.x]=1; for(int i=head[u.x];i!=-1;i=ne[i]) { if(dis[e[i]]>dis[u.x]+w[i]) { dis[e[i]]=dis[u.x]+w[i]; Node v(dis[e[i]],e[i]); q.push(v); } } } } signed main() { memset(head,-1,sizeof(head)); n=read(),m=read(),s=read(); for(int i=1;i<=m;i++) { int a=read(),b=read(),c=read(); add(a,b,c); } dijstra(); for(int i=1;i<=n;i++) cout<<dis[i]<<" "; return 0; }
例题
线段树
作为一个树形数据结构,它的时间复杂度是毋庸置疑的快,达到了
暴力思想
给定一个数组,要求区间中的最大值,输出这个最大值。
我们给一个例子:
5 1 3 5 4 2
暴力思想很简单,开一个 for 循环枚举区间最大值。但如果是多组询问呢?显然
线段树概念
线段树本质上为一棵完全二叉树,每一棵子树的父节点即为当前子树的最大值,这样子就可以实现在树上查找最大值。
以上为线段树的模型。由此我们便可以写出建树的函数。
void Pushup(int k) { t[k]=max(t[k<<1],t[k<<1|1]); }//更新函数,将子节点的信息传递到父节点 //递归建树 build(1,1,n); void build(int k,int l,int r)//k为当前需要建立的节点,l为当前需要建立区间的左端点,r为右端点 { if(l==r)//左端点等于右端点为叶子节点,直接赋值即可 t[k]=a[l]; else { int m=l+((r-l)>>1);//m为中间点,左儿子的节点区间为[l,m],右儿子为[m+1,r] build(k<<1,l,m);//递归构造左儿子节点 build(k<<1|1,m+1,r);//递归构造右儿子节点 Pushup(k);//更新父节点 } }
接下来我们便要查询区间最值,通过将区间中的子树父节点比较即可。
//递归方式区间查询 query(L,R,1,n,1); int query(int L,int R,int l,int r,int k) { if(L==l && R==r)return t[k];//如果当前节点包含于需要查询的区间,则返回节点信息不需要继续递归 else { int res=0;//返回值变量,根据具体查询对象自定义 int m=l+((r-l)>>1);//中间点 if(L<=m)//如果左子树和查询区间交集非空 res=query(L,R,l,m,k<<1); if(R>m)//如果右子树和查询区间交集非空(注意!不可用else if,因为查询区间可能在两边都有交集) res=max(res,query(L,R,m+1,r,k<<1|1)); return res;//返回当前节点得到的信息 } }
这样我们的区间查询就完成了。
如果再加上单点修改或区间修改呢?也十分简单,因为数组数据都在叶子节点,所以我们只需要修改叶子节点,然后再将叶子节点的新数据传递到父亲节点即可。
单点修改:
//递归方式更新 updata(p,v,1,n,1); void updata(int p,int v,int l,int r,int k)//p为下标,v为加数,l,r为区间,k为节点下标 { if(l==r)//左端点=右端点,叶子,直接加即可 a[k]+=v,t[k]+=v;//原数组和线段树数组都得到更新 else { int m=l+((r-l)>>1);//m为中间点,左儿子节点区间为[1,m],右儿子区间为[m+1,r] if(p<=m)updata(p,v,l,m,k<<1);//如果需要更新的节点在左子树区间 else updata(p,v,m+1,r,k<<1|1);//如果需要更新的节点在右子树区间 Pushup(k);//更新父节点的值 } }
区间修改:
void Pushdown(int k,int l,int r) { if(lazy[k]) { lazy[k<<1]+=lazy[k]; lazy[k<<1|1]+=lazy[k]; int m=l+((r-l)>>1); t[k<<1]+=lazy[k]*(m-l+1); t[k<<1|1]+=lazy[k]*(r-m); lazy[k]=0; } } //递归方式区间更新 updata2(L,R,v,1,n,1); void updata2(int L,int R,int v,int l,int r,int k) { if(L<=l && r<=R) { lazy[k]+=v; t[k]+=v*(r-l+1); a[k]+=v; } else { Pushdown(k,l,r); int m=l+((r-l)>>1); if(L<=m) updata2(L,R,v,l,m,k<<1); if(m<R) updata2(L,R,v,m+1,r,k<<1|1); Pushup(k); } }
由于区间中的加数不一定每次都要进行上移做加法,所以我们可以引入一个新概念:懒标记 lazy。我们每次上移时都只做当前层的上移操作,这样就可以免去多余的上移操作从而节省时间。
由于懒标记的加入,查询函数也有所出入,但只不过是在第一行加多一个 Pushdown 函数而已,这是由于当前层的懒标记可能没有全部上移,所以要进行一次上移操作。
例题
典中典
基础题,拿上面的代码就能过。
代码:
#include<bits/stdc++.h> #define INF 0x3f3f3f3f #define int long long using namespace std; const int maxn=400005; int a[maxn],t[maxn<<2];//a为原来区间,t为线段树 int lazy[maxn<<2];//懒标记 int n,m; void Pushup(int k) { t[k]=t[k<<1]+t[k<<1|1]; } //递归建树 build(1,1,n); void build(int k,int l,int r)//k为当前需要建立的节点,l为当前需要建立区间的左端点,r为右端点 { if(l==r)//左端点等于右端点为叶子节点,直接赋值即可 t[k]=a[l]; else { int m=l+((r-l)>>1);//m为中间点,左儿子的节点区间为[l,m],右儿子为[m+1,r] build(k<<1,l,m);//递归构造左儿子节点 build(k<<1|1,m+1,r);//递归构造右儿子节点 Pushup(k);//更新父节点 } } /* //递归方式更新 updata(p,v,1,n,1); void updata(int p,int v,int l,int r,int k)//p为下标,v为加数,l,r为区间,k为节点下标 { if(l==r)//左端点=右端点,叶子,直接加即可 a[k]+=v,t[k]+=v;//原数组和线段树数组都得到更新 else { int m=l+((r-l)>>1);//m为中间点,左儿子节点区间为[1,m],右儿子区间为[m+1,r] if(p<=m)updata(p,v,l,m,k<<1);//如果需要更新的节点在左子树区间 else updata(p,v,m+1,r,k<<1|1);//如果需要更新的节点在右子树区间 Pushup(k);//更新父节点的值 } } //递归方式区间查询 query(L,R,1,n,1); int query(int L,int R,int l,int r,int k) { if(L==l && R==r)return t[k];//如果当前节点包含于需要查询的区间,则返回节点信息不需要继续递归 else { int res=0;//返回值变量,根据具体查询对象自定义 int m=l+((r-l)>>1);//中间点 if(L<=m)//如果左子树和查询区间交集非空 res=res+query(L,R,l,m,k<<1); if(R>m)//如果右子树和查询区间交集非空(注意!不可用else if,因为查询区间可能在两边都有交集) res=res+query(L,R,m+1,r,k<<1|1); return res;//返回当前节点得到的信息 } } */ void Pushdown(int k,int l,int r) { if(lazy[k]) { lazy[k<<1]+=lazy[k]; lazy[k<<1|1]+=lazy[k]; int m=l+((r-l)>>1); t[k<<1]+=lazy[k]*(m-l+1); t[k<<1|1]+=lazy[k]*(r-m); lazy[k]=0; } } //递归方式区间更新 updata2(L,R,v,1,n,1); void updata2(int L,int R,int v,int l,int r,int k) { if(L<=l && r<=R) { lazy[k]+=v; t[k]+=v*(r-l+1); a[k]+=v; } else { Pushdown(k,l,r); int m=l+((r-l)>>1); if(L<=m) updata2(L,R,v,l,m,k<<1); if(m<R) updata2(L,R,v,m+1,r,k<<1|1); Pushup(k); } } //递归方式区间查询 query2(L,R,1,n,1); int query2(int L,int R,int l,int r,int k) { if(L<=l && r<=R)return t[k];//如果当前节点包含 于需要查询的区间,则返回节点信息不需要继续递归 else { Pushdown(k,l,r);//更新懒标记 int res=0;//返回值变量,根据具体查询对象自定义 int m=l+((r-l)>>1);//中间点 if(L<=m)//如果左子树和查询区间交集非空 res=res+query2(L,R,l,m,k<<1); if(R>m)//如果右子树和查询区间交集非空(注意!不可用else if,因为查询区间可能在两边都有交集) res=res+query2(L,R,m+1,r,k<<1|1); return res;//返回当前节点得到的信息 } } signed main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>a[i]; build(1,1,n); while(m--) { int a,b,c,d; cin>>a; if(a==1) { cin>>b>>c>>d; updata2(b,c,d,1,n,1); } if(a==2) { cin>>b>>c; cout<<query2(b,c,1,n,1)<<"\n"; } } return 0; }
典中典2.0
加多了一个乘法操作,并且是区间求和,所以我们要使用两个懒标记。
代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int maxn=4*1e6+5; int a[maxn],t[maxn]; int lazy[maxn],lazzy[maxn]; int n,m,q; void Pushup(int k) { t[k]=(t[k<<1]+t[k<<1|1])%q; } void Pushdown(int k,int l,int r) { int m=l+((r-l)>>1); t[k<<1]=(t[k<<1]*lazzy[k]+lazy[k]*(m-l+1))%q; t[k<<1|1]=(t[k<<1|1]*lazzy[k]+lazy[k]*(r-m))%q; lazzy[k<<1]=(lazzy[k<<1]*lazzy[k])%q; lazzy[k<<1|1]=(lazzy[k<<1|1]*lazzy[k])%q; lazy[k<<1]=(lazy[k<<1]*lazzy[k]+lazy[k])%q; lazy[k<<1|1]=(lazy[k<<1|1]*lazzy[k]+lazy[k])%q; lazzy[k]=1; lazy[k]=0; } void build(int k,int l,int r) { lazzy[k]=1; lazy[k]=0; if(l==r) t[k]=a[l]; else { int m=l+((r-l)>>1); build(k<<1,l,m); build(k<<1|1,m+1,r); Pushup(k); } } void updata(int L,int R,int v,int l,int r,int k) { if(L<=l && r<=R) { lazy[k]=(lazy[k]+v)%q; t[k]=(t[k]+v*(r-l+1))%q; } else { Pushdown(k,l,r); int m=l+((r-l)>>1); if(L<=m) updata(L,R,v,l,m,k<<1); if(m<R) updata(L,R,v,m+1,r,k<<1|1); Pushup(k); } } void updata2(int L,int R,int v,int l,int r,int k) { if(L<=l && r<=R) { t[k]=(t[k]*v)%q; lazzy[k]=(lazzy[k]*v)%q; lazy[k]=(lazy[k]*v)%q; } else { Pushdown(k,l,r); int m=l+((r-l)>>1); if(L<=m) updata2(L,R,v,l,m,k<<1); if(m<R) updata2(L,R,v,m+1,r,k<<1|1); Pushup(k); } } int query(int L,int R,int l,int r,int k) { if(L<=l && r<=R)return t[k]; else { Pushdown(k,l,r); int res=0; int m=l+((r-l)>>1); if(L<=m) res=(query(L,R,l,m,k<<1))%q; if(m<R) res=(res+query(L,R,m+1,r,k<<1|1))%q; return res%q; } } signed main() { cin>>n>>m>>q; for(int i=1;i<=n;i++) cin>>a[i]; build(1,1,n); while(m--) { int a,b,c,d; cin>>a; if(a==1) { cin>>b>>c>>d; if(b>c)swap(b,c); updata2(b,c,d,1,n,1); } if(a==2) { cin>>b>>c>>d; if(b>c)swap(b,c); updata(b,c,d,1,n,1); } if(a==3) { cin>>b>>c; if(b>c)swap(b,c); cout<<query(b,c,1,n,1)%q<<"\n"; } } return 0; }
附加题
较为板子的线段树,删删改改就过了。
代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int maxn=2*1e5+5; long long a[maxn],t[maxn<<2],n; int m,d,b,s; char aa; void Pushup(int k) { t[k] = max(t[k<<1], t[k<<1|1])%d; } void updata(int p,int v,int l,int r,int k) { if(l == r) { t[k] = v; return; } else { int m = (l+r)>>1; if(p <= m) updata(p,v,l,m,k<<1); else updata(p,v,m+1,r,k<<1|1); Pushup(k); } } int query(int L,int R,int l,int r,int k) { if(L <= l && r <= R) return t[k]; else { int res = -INT_MAX,temp = -INT_MAX; int m = (l+r)>>1; if(L <= m) res = max(res, query(L,R,l,m,k<<1)); if(R > m) temp = max(temp, query(L,R,m+1,r,k<<1|1)); return max(res, temp); } } signed main() { cin >> m >> d; for(int i = 0;i < m;i++) { cin >> aa >> b; if(aa == 'Q') { cout << query(n-b+1,n,1,m,1) << "\n"; s = query(n-b+1,n,1,m,1)%d; } else { updata(n+1,(b+s)%d,1,m,1); n++; } } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)