各种算法及体会
单调栈/队列
单调栈用于求解该点左/右侧首个大/小于该点的位置(注意!不需要用什么线段树啊二分啊
注意有时在用单调栈解决区间问题时可能会重复计算,故把左右单调栈一个小于,一个小于等于即可!
单调队列即滑动窗口,可以求解定区间最大/小值
最长上升子序列(LIS)
O(nlogn)维护方法,考虑二分,每次用更小值替换之前的位置,大于结尾的加入vector,最后的size即所求。
最长公共子序列(LCS)可以转化为这个问题。
for(int i=1;i<=n;i++){//O(nlogn) LIS
int x=lower_bound(v.begin(),v.end(),a[i])-v.begin();
if(x==v.size()) v.push_back(a[i]);
else v[x]=a[i];
}
ST表
用于维护RMQ问题,即区间最大/小值。不能修改,但O(1)查询。(欧拉序+st表的lca经常用来卡常)
void ST(){
lg[0]=-1;
for(int i=1;i<=n;i++){
f[i][0]=h[i],lg[i]=lg[i>>1]+1;
}
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}
}
}
int query(int l,int r){
int k=lg[r-l+1];
return min(f[l][k],f[r-(1<<k)+1][k]);
}
分块
O(n根n)
//分块
#include<bits/stdc++.h>
using namespace std;
int opt,x,y,c,st[100005],ed[100005],a[100005],pos[100005],len,n,m,cnt;
long long sum[100005],add[100005];
struct block{
void build(){
len=(int)sqrt(n);
cnt=n/len;
if(n%len) cnt++;
for(int i=1;i<=cnt;i++){
st[i]=(i-1)*len+1;
ed[i]=i*len;
}
ed[cnt]=n;
for(int i=1;i<=cnt;i++){
for(int j=st[i];j<=ed[i];j++){
sum[i]+=a[j];
pos[j]=i;
}
}
}
void update(int l,int r,long long k){
if(pos[l]==pos[r]){
for(int i=l;i<=r;i++){
a[i]+=k;
}
sum[pos[l]]+=(r-l+1)*k;
}
else {
for(int i=pos[l]+1;i<pos[r];i++){
add[i]+=k;
}
for(int i=l;i<=ed[pos[l]];i++){
a[i]+=k;
}
sum[pos[l]]+=(ed[pos[l]]-l+1)*k;
for(int i=st[pos[r]];i<=r;i++){
a[i]+=k;
}
sum[pos[r]]+=(r-st[pos[r]]+1)*k;
}
}
long long query(int l,int r){
long long ans=0;
if(pos[l]==pos[r]){
for(int i=l;i<=r;i++){
ans+=a[i];
}
ans+=(r-l+1)*add[pos[l]];
}
else {
for(int i=pos[l]+1;i<pos[r];i++){
ans+=sum[i]+add[i]*(ed[i]-st[i]+1);
}
for(int i=l;i<=ed[pos[l]];i++){
ans+=a[i];
}
ans+=add[pos[l]]*(ed[pos[l]]-l+1);
for(int i=st[pos[r]];i<=r;i++){
ans+=a[i];
}
ans+=add[pos[r]]*(r-st[pos[r]]+1);
}
return ans;
}
};
block B;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
B.build();
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&opt,&x,&y);
if(opt==1){
long long k;
scanf("%lld",&k);
B.update(x,y,k);
}
else {
printf("%lld\n",B.query(x,y));
}
}
return 0;
}
莫队
O(n根n)左右
//带修莫队
#include<bits/stdc++.h>
using namespace std;
int n,m,ans,out[140005],c[140005],pos[140005],l=1,r,now,s[1000005],an,bn;
struct node{
int v,l,r,id,t;
}a[140005],b[140005];
bool cmp(const node &p,const node &q){//重在排序
if(pos[p.l]!=pos[q.l]) return p.l<q.l;
if(pos[p.r]!=pos[q.r]) return p.r<q.r;
return p.t<q.t;
}
void add(int x){
s[c[x]]++;
if(s[c[x]]==1) ans++;
}
void del(int x){
s[c[x]]--;
if(s[c[x]]==0) ans--;
}
void change(int now,int x){
if(b[now].l>=a[x].l&&b[now].l<=a[x].r){
s[c[b[now].l]]--;
s[b[now].r]++;
if(s[c[b[now].l]]==0) ans--;
if(s[b[now].r]==1) ans++;
}
swap(b[now].r,c[b[now].l]);
}
int main(){
scanf("%d%d",&n,&m);
int len=(int)pow(n,0.666);//带修莫队这个块长最快
for(int i=1;i<=n;i++){
scanf("%d",&c[i]);
pos[i]=(i-1)/len+1;
}
for(int i=1;i<=m;i++){
char opt;
scanf(" %c",&opt);
if(opt=='Q') {
an++;
a[an].id=an;
a[an].t=bn;
scanf("%d%d",&a[an].l,&a[an].r);
}
else if(opt=='R'){
bn++;
scanf("%d%d",&b[bn].l,&b[bn].r);//l支笔,r颜色
}
}
sort(a+1,a+an+1,cmp);
for(int i=1;i<=an;i++){//
while(l<a[i].l) del(l++);
while(l>a[i].l) add(--l);
while(r<a[i].r) add(++r);
while(r>a[i].r) del(r--);
while(now<a[i].t) change(++now,i);
while(now>a[i].t) change(now--,i);
out[a[i].id]=ans;
}
for(int i=1;i<=an;i++){///
printf("%d\n",out[i]);
}
return 0;
}
基环树
最常见条件:n个点,n条边(注意题目给的条件是否联通,可能是森林
基环内向树:每个点只有一条出边
基环外向树:每个点只有一条入边
处理手法:找环,断边,跑树形dp
void circle(int x,int p){
vis[x]=1;
for(int i=h[x];i;i=e[i].nxt){
int y=e[i].to;
if((i^1)==p) continue;
if(vis[y]){
xx=x,yy=y,k=i;
continue;//不能return,要把联通块找完
}
circle(y,i);
}
}
扫描线
求矩形面积并,周长,二维数点等等
线段树作用即把静态O(n^2)变为动态O(nlogn).
想象过程,就是在一张图上,一条线从上到下扫描。所以线段树本质维护的是矩形的长,而宽又是从上到下排序过的,所以每次乘上差值即可。
常用手法比如把点的问题转化为矩形,可以求最大面积交/并
注意这个线段树写法和别的不太一样,有时需要离散化或动态开点。而且记录询问的数组开2倍。线段树必须开16倍空间,读入add就是二倍,线段树4倍,在pushup不可避免访问叶子节点的儿子,再有2倍。
#include<bits/stdc++.h>
#define ll long long
#define lid (id<<1)
#define rid (id<<1|1)
using namespace std;
const int maxn=100015;
struct node{
ll x,yl,yr;
int fl;
}p[maxn<<1];
struct tree{
ll l,r,sum;
}t[maxn<<4];
int n,tg[maxn<<4];
ll s[maxn<<1],ans;
bool cmp(node a,node b){
return a.x<b.x;
}
void pushup(int id){
if(tg[id]>0) t[id].sum=t[id].r-t[id].l;//注意,这里代表是否矩形完全覆盖该位置
else t[id].sum=t[lid].sum+t[rid].sum;
}
void build(int id,int l,int r){
t[id].l=s[l];//离散化,t[id]的l/r不是普遍意义
t[id].r=s[r];
if(r-l==1){
t[id].sum=0;
return ;
}
int mid=(l+r)>>1;
build(lid,l,mid);//维护的是面积,所以是按照矩形的边计算,mid相同
build(rid,mid,r);
pushup(id);
}
void add(int id,ll yl,ll yr,int fl){
if(t[id].l==yl&&t[id].r==yr){
tg[id]+=fl;
pushup(id);
return;
}//这种写法实际上mid=t[lid].r=t[rid].l
if(yl<t[lid].r) add(lid,yl,min(t[lid].r,yr),fl);
if(yr>t[rid].l) add(rid,max(t[rid].l,yl),yr,fl);
pushup(id);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
ll xl,yl,xr,yr;
scanf("%lld%lld%lld%lld",&xl,&yl,&xr,&yr);
p[i]=(node){xl,yl,yr,1};
p[i+n]=(node){xr,yl,yr,-1};
s[i]=yl,s[i+n]=yr;
}
sort(s+1,s+n*2+1);
sort(p+1,p+n*2+1,cmp);
build(1,1,2*n);
add(1,p[1].yl,p[1].yr,p[1].fl);
for(int i=2;i<=2*n;i++){
ans+=(p[i].x-p[i-1].x)*t[1].sum;
//printf("%d ",i);
add(1,p[i].yl,p[i].yr,p[i].fl);
}
printf("%lld",ans);
return 0;
}
网络流
最大流=最小割
O(n^2m)但玄学上界,一般都能跑。。
推这个,怎么说,不一定能看出来是网络流,甚至可能是把一个数据类的抽象到图论上。总之很巧妙,像dp。就先看限制约束条件,可以先思考连边,然后考虑是最大流、最小割还是什么总体-局部的转换,最后建图吧?
有一个最大权闭合子图,套路性的正边-源点,负边-汇点,答案为正边权和-最小割。
求解一条边能否在最小割和是否一定在最小割中的问题时,考虑在最终的残量网络上用 tarjan 求解。
void add(int u,int v,int val){
nxt[++cnt]=h[u],to[cnt]=v,h[u]=cnt,w[cnt]=val;
nxt[++cnt]=h[v],to[cnt]=u,h[v]=cnt,w[cnt]=0; //有向图!
}
int bfs(){
memset(dep,0,sizeof(dep));
queue<int>q;
q.push(s);
dep[s]=1;
now[s]=h[s];
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=h[x];i;i=nxt[i]){
int y=to[i];
if(w[i]>0&&dep[y]==0){//直接考虑能走的边
q.push(y);
now[y]=h[y];//当前弧优化
dep[y]=dep[x]+1;//分层图
if(y==t) return 1;
}
}
}
return 0;
}
int dfs(int x,int sum){//这里,sum对整条路的最大贡献流量
if(x==t) return sum;
int k,res=0;//k当前最小剩余流量
for(int i=now[x];i&∑i=nxt[i]){
now[x]=i;//当前弧优化,走过的
int y=to[i];
if(w[i]>0&&(dep[y]==dep[x]+1)){//保证是最短路
k=dfs(y,min(sum,w[i]));
if(k==0) dep[y]=0;//剪枝,增广完毕
w[i]-=k,w[i^1]+=k;//正反向边
res+=k;//经过该点的流量和
sum-=k;//经过该点当前剩余流量
}
}
return res;
}
void dinic(){
while(bfs()){
ans+=dfs(s,inf);
}
}
费用流
保证最大流的同时费用最大/小
O(n2m2)
void add(int u,int v,ll flow,ll val){
nxt[++cnt]=h[u],to[cnt]=v,h[u]=cnt,w[cnt]=val,f[cnt]=flow;
nxt[++cnt]=h[v],to[cnt]=u,h[v]=cnt,w[cnt]=-val,f[cnt]=0;
}
queue<int>q;
bool SPFA(){
memset(dis,-0x3f,sizeof(dis));
memset(fl,0x3f,sizeof(fl));
memset(vis,0,sizeof(vis));
q.push(s); vis[s]=1,dis[s]=0,pre[t]=-1;
while(!q.empty()){
int x=q.front();
q.pop();
vis[x]=0;
for(int i=h[x];i;i=nxt[i]){
int y=to[i];
if(f[i]>0&&dis[x]+w[i]>dis[y]){
dis[y]=dis[x]+w[i];
pre[y]=x;
lst[y]=i;
fl[y]=min(fl[x],f[i]);
if(!vis[y]){ vis[y]=1;q.push(y); }
}
}
}
return pre[t]!=-1;
}
void EK(){
while(SPFA()){
int x=t;
if(res+dis[t]*fl[t]<0){
ans+=(ll)res/-dis[t];
return ;
}
ans+=fl[t];
res+=dis[t]*fl[t];
while(x!=s){
f[lst[x]]-=fl[t];
f[lst[x]^1]+=fl[t];
x=pre[x];
}
}
}
上下界网络流(可行、最大、最小、费用流)
主要思想就是先让它直接流下界的流量,然后连汇点t到源点s(因为流量应该相等),多余或不足的流量从新的源点汇点S、T补,然后跑最大流,如果把新源点汇点的边跑满,则可行。
最大流就是在残量网络上s-t跑最大流+可行流。
最小流就是删去和S、T连边以及t-s后跑最大流,w(t-s)-最大流。
费用流基本是可行费用流,套板子即可。
这个算法真的,不太用动脑(?只要能用的,按题意模拟即可。(算是网络流中最好想的吧。。)
一些tips:
1,如果求类似最大值最小,考虑和二分答案相结合,用最大流、可行流check等等
2,与源汇点、二分图有关三大套路:格子染色(用于仅与相邻有关的)、数学分奇偶性、矩阵分横纵以点连线
3,拆点,每个状态为一个点或入边->出边,中间是限制条件。点数太大了考虑动态加点。
4,任意两边的交点都在顶点上的图称为平面图,如果我们把面作为点,相邻的边连线,可以得到对偶图,对于数据点过大的,平面图最小割=对偶图最短路。
5,分数规划,其实就是把分母乘过去,然后二分验证。
6,对于平方的处理:拆边,假设全一种情况,考虑增量;或根据2k+1来连。
·如果不刻意卡,6e3-1e5的点数甚至都能过去!网络流就是要敢写!!!
后缀数组
用于解决字符串相关问题。倍增法+基数排序O(nlogn),考虑每次把2^(k-1)的两个字符串合并,遇到结束时用空串补足。
sa[i]后缀数组,记录排名为i的后缀在原串中的位置,sa[排名]=位置,rk[位置]=排名。
height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公共前缀(排名相邻的两个后缀的最长公共前缀),h[i]=height[rk[i]]也就是suffix(i)和它前一名后缀的最长公共前缀。(即LCP,最长公共前缀)
注意性质h[i]>=h[i-1]-1,考虑设suffix(k)是suffix(i-1)前一名的后缀,则最长公共前缀为h[i-1]。那么suffix(k+1)排在suffix(i)前,理解上同减一位即可,又考虑到还可能有前几位相同但比suffix(k+1)长的串夹在二者中间(感性理解一下),故得证。(注:不是严谨证明,只是便于理解)
这个性质可以O(n)求h[i],而对于询问任意两个后缀的最长公共前缀,答案几位两者排名之间的height最小值,RMQ问题用ST表维护即可。height数组的特性非常好用,不重不漏,还一定包含公共子串的最值!!!
(真服了,一个板子看了一个下午。。。)
//后缀排序,后缀数组基础,用于字符串问题
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+10;//特别注意,数组开二倍!因为有+k
int n,m,x[maxn],y[maxn],c[maxn],sa[maxn],rk[maxn],height[maxn];
char s[maxn];
void getsa(){
//按第一个字母排序
for(int i=1;i<=n;i++) c[x[i]=s[i]]++;//桶
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>0;i--) sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1){
//按第二关键字排序
for(int i=0;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) y[i]=sa[i];//记录当前某位对应的原位置
for(int i=1;i<=n;i++) c[x[y[i]+k]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];//记录某数有若干个,便于排序(注意0的存在,所以从1开始
for(int i=n;i>0;i--) sa[c[x[y[i]+k]]--]=y[i];//按第二位排,注意对应从大到小的顺序
//按第一关键字排序
for(int i=0;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) y[i]=sa[i];
for(int i=1;i<=n;i++) c[x[y[i]]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>0;i--) sa[c[x[y[i]]]--]=y[i];//首位排
//把后缀放入桶
for(int i=1;i<=n;i++) y[i]=x[i];//原字符串
m=0;
for(int i=1;i<=n;i++){//相对大小即可
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=m;
else x[sa[i]]=++m;
}
if(m==n) break;//均不相同即为完成
}
}
//height[i]为suffix(sa[i-1]和sa[i])最长公共前缀,h[i]为suffix(i)和它前一名后缀的最长公共前缀
void geth(){//性质:h[i]>=h[i-1]-1
for(int i=1;i<=n;i++) rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++){
if(rk[i]==1) continue;//第一名height为0
if(k) k--;//上一个height-1
int j=sa[rk[i]-1];//前邻后缀j
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;//性质,有点双指针的意思
height[rk[i]]=k;
}
}
int main(){
scanf("%s",s+1);
n=strlen(s+1);
m=122;//'z'
getsa();
geth();
for(int i=1;i<=n;i++) printf("%d ",sa[i]);
printf("\n");
for(int i=1;i<=n;i++) printf("%d ",height[i]);
return 0;
}
字符串处理技巧
1,断环为链。对于循环串,直接复制到后面。对于回文串,反向复制到后面,在中间加一个大于所有串内ascii的。
2,考虑每个子串都是每个后缀的前缀,而height数组的特殊性质能够满足sum(height)排除全部重复子串,max(height)为最长重复子串。
3,对于非重复最长公共子串、k次重复子串等许多问题都考虑后缀数组,二分+按height划分层次判断,或者用单调队列维护区间最小值。
4,查询公共串时用ST表维护check,O(1)处理两个部分的公共串。
manacher
快速求解回文串,重点在于维护maxr,然后直接根据对称性给个初值,剩下暴力扩展,是O(n)的
#include<bits/stdc++.h>
using namespace std;
int cnt,a[30000005],p[30000005],ans;
string s;
int main(){
cin>>s;
a[0]=-1;
for(int i=0;i<s.size();i++){
cnt+=2;
a[cnt]=s[i]-'a'+1;
}
for(int x=1,r=0,mid=0;x<=cnt;x++){
if(x<=r) p[x]=min(p[mid*2-x],r-x+1);//重点在这里……
while(a[x-p[x]]==a[x+p[x]]) ++p[x];//暴力扩展
if(p[x]+x>r) r=p[x]+x-1,mid=x;//更新
if(p[x]>ans) ans=p[x];
}
printf("%d",ans-1);
return 0;
}
光速幂
O(1)快速幂
实际上就是$ a^b =a^{siz*(b/siz)+ (b \mod siz)} =a^{siz}的b/siz次方 * a^{b\mod siz}$
qa[0]=pa[0]=1;
for(int i=1;i<=siz;i++) qa[i]=1ll*qa[i-1]*a%mod;
for(int i=1;i<=siz;i++) pa[i]=1ll*pa[i-1]*qa[siz]%mod;
int qpow(int x,int y){
return 1ll*pa[y/siz]*qa[y%siz]%mod;
}
轮廓线dp
就是状压的一种思路,把两行状态的枚举转化为逐格dp,感觉转移好像有一条轮廓线一样,把转移变为O(1)
高维前缀和/sosdp(子集dp)
用于对于集合的每个子集求解一些问题,枚举子集的复杂度就到了O(2n),没法逐个计算,而高维前缀和可以做到O(n*2n)
理解方面,和容斥原理有关,考虑我们按照一定顺序枚举,形象理解为每次枚举从头到尾某个元素不选,而该元素后面都选,又按照由小到大转移,每个全部包含的区间都经历了同样的枚举,从而保证是不重复的
//要求最多O(log)求出对于每个子集中满足条件的个数
//如果不要求时间复杂度,转化成求一段区间内lst在l之前的,莫队差不多可以写
//对于这一类求集合中子集的问题,理解方面,和容斥原理有关,考虑我们按照一定顺序枚举,并计算补集每次删掉的元素,从而保证转移是不重复的
#include<bits/stdc++.h>
using namespace std;
int n=24,m,dp[1<<24],ans;
int main(){
scanf("%d",&m);
for(int i=1;i<=m;i++){
int s=0;
for(int j=1;j<=3;j++){
char c;
scanf(" %c",&c);
if(c>='y') continue;
s|=(1<<(c-'a'));
}
dp[s]++;
}
for(int i=0;i<n;i++){
for(int j=0;j<(1<<n);j++){
if(j&(1<<i)) dp[j]+=dp[j^(1<<i)];
}
}
for(int i=0;i<(1<<n);i++) ans^=(m-dp[i])*(m-dp[i]);
printf("%d",ans);
return 0;
}
原根
考试见到的,反正以后还要学
阶
由欧拉定理知,若 \((a,m)=1\) ,则 \(a^{\varphi (m)} \equiv 1 \pmod {m}\)
因此满足 \(a^n \equiv 1 \pmod{m}\)的最小正整数 \(n\) 存在,这个 \(n\) 称作 \(a\) 模 \(m\) 的阶,记作 \(\delta_m(a)\) 或 \(ord_m(a)\)
性质: $a,a^2 ,\cdots,a^{\delta_m(a)} $模 \(m\) 两两不同余。
若 \(a^n \equiv 1 \pmod m\),则 \(\delta_m(a)\mid n\).
原根
设 \(m \in \mathbf{N}^{*},g\in \mathbf{Z}\). 若 \((g,m)=1\),且 \(\delta_m(g)=\varphi(m)\),则称 \(g\) 为模 \(m\) 的原根。
即 \(g\) 满足 \(\delta_m(g) = \left| \mathbf{Z}_m^* \right| = \varphi(m).\) 当 \(m\) 是质数时,我们有 \(g^i \bmod m,\,0 \lt i \lt m\) 的结果互不相同。
设 \(m \geqslant 3, (g,m)=1\),则 \(g\) 是模 \(m\) 的原根的充要条件是,对于 \(\varphi(m)\) 的每个素因数 \(p\),都有\(g^{\frac{\varphi(m)}{p}}\not\equiv 1\pmod m.\)
若一个数 \(m\) 有原根,则它原根的个数为 \(\varphi(\varphi(m)).\)
一个数 \(m\) 存在原根当且仅当 \(m=2,4,p^{\alpha},2p^{\alpha}\),其中 \(p\) 为奇素数,\(\alpha\in \mathbf{N}^{*}.\)
素数 \(p\) 的最小原根 \(g_p=O\left(p^{0.25+\epsilon}\right)\),其中 \(\epsilon>0\).因此暴力寻找最小原根复杂度是可以接受的。
珂朵莉树
珂朵莉树,一种暴力数据结构,做一些区间修改区间查询的问题,对随机数据比较有效
对一个序列,进行一个区间推平操作(如把一个区间内变为同一个数),然后我们把每段插入到set中自动排序
对于其他操作,还是比较暴力的(对于每个段进行操作)……复杂度在随机数据下大概是O(nlog^2n)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=1e9+7,maxn=1e5+5;
int n,m,seed,vmax,a[maxn];
struct node{
int l,r;
mutable int v;//这一段相同元素的值,即使是常量也可以被修改
bool operator<(const node &a) const{
return l<a.l;
}
};
set<node>s;
set<node>::iterator split(int pos){//分裂成[l,pos-1] [pos,r]
set<node>::iterator it=s.lower_bound((node){pos,0,0});
if(it!=s.end()&&it->l==pos) return it;
it--;
if(it->r<pos) return s.end();
int l=it->l,r=it->r,v=it->v;
s.erase(it);
s.insert((node){l,pos-1,v});
return s.insert((node){pos,r,v}).first;
}
void assign(int l,int r,int x){
set<node>::iterator itr=split(r+1),itl=split(l);//注意顺序,否则会因操作r时删去了l的指针而re
s.erase(itl,itr);//前闭后开
s.insert((node){l,r,x});
}
void add(int l,int r,int x){
set<node>::iterator itr=split(r+1),itl=split(l);
for(auto it=itl;it!=itr;it++){
it->v+=x;//it是常量迭代器,本来不能下哦i高,但是加了mutable就好了
}
}
struct rankk{
int num,cnt;
bool operator<(const rankk &a)const{
return num<a.num;
}
};
int rnk(int l,int r,int x){
set<node>::iterator itr=split(r+1),itl=split(l);
vector<rankk>v;
for(auto i=itl;i!=itr;i++){
v.push_back((rankk){i->v,i->r-i->l+1});
}
sort(v.begin(),v.end());
int i;
for(i=0;i<v.size();i++){
if(v[i].cnt<x) x-=v[i].cnt;
else break;
}
return v[i].num;
}
int qpow(int x,int y,int mod){
int res=1; x%=mod;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
int query(int l,int r,int x,int y){
set<node>::iterator itr=split(r+1),itl=split(l);
int ans=0;
for(auto i=itl;i!=itr;i++){
ans=(ans+qpow(i->v,x,y)*(i->r-i->l+1)%y)%y;
}
return ans;
}
int rnd(){
int ret=seed;
seed=(seed*7+13)%mod;
return ret;
}
signed main(){
scanf("%lld%lld%lld%lld",&n,&m,&seed,&vmax);
for(int i=1;i<=n;i++){
a[i]=rnd()%vmax+1;
s.insert((node){i,i,a[i]});
}
for(int i=1;i<=m;i++){
int opt,l,r,x,y;
opt=rnd()%4+1;
l=rnd()%n+1;
r=rnd()%n+1;
if(l>r) swap(l,r);
if(opt==3) x=rnd()%(r-l+1)+1;
else x=rnd()%vmax+1;
if(opt==4) y=rnd()%vmax+1,printf("%lld\n",query(l,r,x,y));
else if(opt==3) printf("%lld\n",rnk(l,r,x));
else if(opt==2) assign(l,r,x);
else add(l,r,x);
}
return 0;
}
线段树分治
(等到线段树类算法攒够了要新开一个专题)
我们对时间(询问)建立一颗线段树,树的节点存的信息是“覆盖了这个区间的操作”。
实现上每遍历到一个节点就去add,出了某个节点需要del,用一个可撤销并查集维护。
注意,可撤销并查集只能按秩合并,不能路径压缩!
使用情景:每个操作影响范围是一个区间,多组操作后询问。