UNR #8 题解
目录
uoj887 最酷的排列
uoj888 里外一致
uoj889 二维抄袭检测
uoj890 兵棋
uoj891 难度查找
uoj892 大海的深度
uoj887 最酷的排列
题目描述
组数据,给定长为 的序列 和下标集合 ,你可以对序列进行任意次操作,每次选择一个区间并将其中元素升序排序。
求 的最小可能值。
数据范围
- 。
时间限制 ,空间限制 。
分析
注意到对一个区间排序等价于进行若干次冒泡,因此只需选择长为 的区间。
根据 的提示,从后往前扫描,最终某个位置的数不可能比已经扫描过的位置更小,即 。
小根堆维护已经扫描且尚未使用过的数,每次碰到集合 中的下标,弹出其中的最小值,这是答案的下界。
再来证明上述方法得到的答案可以取到,对于 中的两个相邻下标 ,如果在 处弹出的数(记为 )比 处(记为 )大,每次交换两个数可以让 ,即完成 内部的冒泡。
时间复杂度 。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,t;
long long res;
int a[maxn];
char s[maxn];
int main()
{
for(scanf("%d",&t);t--;)
{
scanf("%d",&n),res=0;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
scanf("%s",s+1);
priority_queue<int,vector<int>,greater<int>> q;
for(int i=n;i>=1;i--)
{
q.push(a[i]);
if(s[i]=='1') res+=q.top(),q.pop();
}
printf("%lld\n",res);
}
return 0;
}
uoj888 里外一致
题目描述
给定长为 的序列 , 次询问,每次给定 ,求有多少 的子集 满足 ,对 取模。
数据范围
- 。
时间限制 ,空间限制 。
分析
先考虑 怎么做,假设不同数字在序列 中的出现次数分别为 。
表示考虑前 个 , 的方案数。
容易写出转移方程:
用生成函数的视角理解,答案为:
本题非常 的一点是,由于答案对 取模,所以如果与有关的项取太多对答案没有任何影响。
将 和 拆开来看,注意到:
表示考虑前 个 ,有 项没选 的系数之和,那么答案为 。
转移方程 ,注意到第二维上限为 ,否则对答案没有贡献。
预处理 即可获得一个 的做法,link。具体的,预处理阶乘中 的幂次,奇数部分的逆元可以用欧拉定理求出。
注意到 的转移本质上是背包,而背包问题可以支持撤回,用莫队维护 和 数组,可以做到 。
由于 很小,并且 时没有贡献,我们希望将所有相同的 放在一起转移,这样可以把 的常数从 上拿下来。
记 出现了 次,将 的定义修改为考虑所有 的情况,转移方程为 。
时间复杂度 ,惊险通过。
这里 是实测三层循环卡满的转移次数,
如果被叉了可以叫我。注意非常重要的一点,下面代码中第 行 一定要倒序枚举,这样可以大幅减少转移次数。
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int B=1000,maxn=6e5+5;
int l,n,q,r;
int a[maxn],c[65],pw[maxn],bel[maxn],cnt[maxn];
ull fac[maxn],inv[maxn];
ull g[64],v[maxn],res[maxn];
struct node
{
int l,r,id;
bool operator<(const node &a)
{
return bel[l]!=bel[a.l]?l<a.l:r<a.r;
}
}f[maxn];
ull qpow(ull a,ull k)
{
ull res=1;
for(;k;a=a*a,k>>=1) if(k&1) res=res*a;
return res;
}
ull get_c(int n,int m)
{
int x=pw[n]-pw[m]-pw[n-m];
return x<64?(fac[n]*inv[m]*inv[n-m])<<x:0;
}
void init(int n)
{
fac[0]=1;
for(int i=1,j=0;i<=n;i++)
{
for(j=i,pw[i]=pw[i-1];!(j&1);j>>=1,pw[i]++) ;
fac[i]=fac[i-1]*j;
}
inv[n]=qpow(fac[n],(1ll<<63)-1);
for(int i=n,j=0;i>=1;i--)
{
for(j=i;!(j&1);j>>=1) ;
inv[i-1]=inv[i]*j;
}
for(int i=0;i<=(n>>1);i++) v[i]=(i&1?-1:1)*get_c(i<<1,i);
}
void add(int x,int v)
{
c[min(cnt[x],64)]--,cnt[x]+=v,c[min(cnt[x],64)]++;
}
int main()
{
scanf("%d%d",&n,&q),init(n<<1);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),bel[i]=(i-1)/B+1;
for(int i=1;i<=q;i++) scanf("%d%d",&f[i].l,&f[i].r),f[i].id=i;
sort(f+1,f+q+1);
for(int i=1,l=1,r=0;i<=q;i++)
{
while(l>f[i].l) add(a[--l],1);
while(r<f[i].r) add(a[++r],1);
while(l<f[i].l) add(a[l++],-1);
while(r>f[i].r) add(a[r--],-1);
int m=c[64];
for(int j=0;j<64;j++) g[j]=!j;
for(int x=63;x>=1;x--)
{
m+=c[x];
for(int j=63;j>=0;j--)
if(g[j]) for(int k=min({c[x],63-j,63/x});k>=1;k--)
g[j+k]+=(get_c(c[x],k)*g[j])<<(k*x);
}
for(int j=0;j<=min(m,63);j++) res[f[i].id]+=g[j]*v[m-j];
}
for(int i=1;i<=q;i++) printf("%llu\n",res[i]);
return 0;
}
uoj889 二维抄袭检测
题目描述
给定长为 的字符串 和 的字符矩阵 。
定义可行路径为从字符矩阵中某个点开始,每次向右或向下走一步,不走出边界的路径。
次询问,给定 ,求最大的 满足,存在一条从 出发、长为 的可行路径,使其对应的字符串为 。
数据范围
- 。
- 。
- 。
- 。
时间限制 ,空间限制 。
分析
时有贪心做法,与正解无关所以此处略去。
为方便后续优化,将矩阵中第 行的字符向右平移 个单位,从而每次需要向右或向右下走一步。
表示能否匹配到第 列第 行,可以转移到 和 (条件略去)。
注意到第一维每次加一,并且第二维长度 ,考虑矩阵乘法优化。
01
矩阵的(|,&)
乘法的转移形式如下:for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) for(int k=1;k<=n;k++) c[i][j]|=a[i][k]&b[k][j];
如果用
bitset
进行压位,转移形式变成:for(int i=1;i<=n;i++) for(int k=1;k<=n;k++) if(a[i][k]) c[i]|=b[k];
当 比较大的时候,可以给时间复杂度除掉一个 。
由于本题 ,所以单次矩阵乘法的时间复杂度为 。特别地,向量乘矩阵的时间复杂度为 。
然而,第 个转移矩阵由第 列的字符和 共同决定。换言之,不同询问的转移矩阵是不同的。
我们找到当前可以匹配的最小行数(即最小的 满足 ),求出它在这一行能够匹配的最长距离,再做一次暴力转移,此后 至少加一。
由于 ,所以上述操作最多重复 次。匹配过程中的转移矩阵仅由匹配的这段字符决定,我们只需要快速求出一行中某段区间的矩阵连乘积。
对每行分别建猫树,询问时用向量乘矩阵,预处理复杂度 ,询问复杂度 。
对满二叉树建猫树,深度相同节点 和 的 是
x>>(lg[x^y]+1)
,其深度为lg[x]-lg[x^y]-1
(默认根节点深度为 )。对于任意区间 (),它对应节点 ,深度最深且相同。
对于区间 ,它的根节点为 和 的 。
从而我们可以 找到根节点,即切割 的节点。
求最长匹配距离最简单的方法是哈希加二分,但是容易被叉。将字符串 和字符矩阵 拼在一起建后缀数组,于是转化为经典问题求两个后缀的 。
时间复杂度 。
下面代码给重点部分加了注释,想看无注释版本的代码可以点这里。
#include<bits/stdc++.h>
using namespace std;
const int maxl=1.5e6+5;
int l,m,n,q,dep;
char s[maxl],t[10][1<<16];
int lg[maxl];
namespace sa
{///后缀数组求lcp
int n;
int c[maxl],x[maxl],y[2*maxl];
int h[maxl],rk[maxl],sa[maxl];
int f[21][maxl];
void get_sa()
{
int m=122;
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>=1;i--) sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1)
{
int num=0;
for(int i=n-k+1;i<=n;i++) y[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) c[x[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i];
for(int i=1;i<=n;i++) y[i]=x[i],x[i]=0;
num=x[sa[1]]=1;
for(int i=2;i<=n;i++)
x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?num:++num;
if(num==n) break;
m=num;
}
}
void get_height()
{
for(int i=1;i<=n;i++) rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++)
{
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
h[rk[i]]=k;
}
}
void init(int _n)
{
n=_n,get_sa(),get_height();
for(int i=2;i<=n;i++) lg[i]=lg[i>>1]+1;
for(int i=1;i<=n;i++) f[0][i]=h[i];
for(int j=1;j<=lg[n];j++)
for(int i=1;i+(1<<j)-1<=n;i++)
f[j][i]=min(f[j-1][i],f[j-1][i+(1<<(j-1))]);
}
int lcp(int i,int j)
{
if(rk[i]>rk[j]) swap(i,j);
int l=rk[i]+1,r=rk[j],k=lg[r-l+1];
return min(f[k][l],f[k][r-(1<<k)+1]);
}
}
struct vec
{
short v;
vec (short _v=0)
{
v=_v;
}
bool get(int x)
{
return v>>x&1;
}
};
struct mat
{
short v[10];
mat ()
{
for(int i=0;i<n;i++) v[i]=0;
}
bool get(int x,int y)
{
return v[x]>>y&1;
}
void set(int x,int y)
{
if(x!=-1) v[x]|=1<<y;
}
}o;
vec operator*(vec &a,mat &b)
{
vec c;
for(int i=0;i<n;i++) if(a.get(i)) c.v|=b.v[i];
return c;
};
mat operator*(mat &a,mat &b)
{
mat c;
for(int i=0;i<n;i++)
for(int k=0;k<n;k++)
if(a.get(i,k)) c.v[i]|=b.v[k];
return c;
}
struct meow
{
mat a[17][1<<17];
void build(int p,int l,int r,int d,int id)
{///(p,l,r)定义同线段树,深度为d,对应字符矩阵中第id行
if(l==r)
{
for(int i=id;i<n;i++) if(t[i][l]==t[id][l]) a[d][l].set(i-1,i),a[d][l].set(i,i);
return ;
}
int mid=(l+r)>>1;
build(p<<1,l,mid,d+1,id),build(p<<1|1,mid+1,r,d+1,id);
mat cur=o;///单位矩阵
for(int i=mid;i>=l;i--) cur=a[dep][i]*cur,a[d][i]=cur;
cur=o;
for(int i=mid+1;i<=r;i++) cur=cur*a[dep][i],a[d][i]=cur;
}
void query(int l,int r,vec &res)
{
if(l==r) res=res*a[dep][l];
else
{
int d=dep-lg[l^r]-1;
///令x=l+(1<<dep),y=l+(1<<dep)
///则d=lg[x]-lg[x^y]-1,注意别忘了减一
res=res*a[d][l],res=res*a[d][r];///向量乘矩阵
}
}
}T[10];
int main()
{
scanf("%d%d%d%d%s",&l,&n,&m,&q,s+1);
for(int i=0;i<n;i++) scanf("%s",t[i]+i);
for(int i=0,k=l;i<n;i++) for(int j=0;j<m;j++) s[++k]=t[i][i+j];
sa::init(l+n*m),dep=lg[m+n-2]+1;
for(int i=0;i<n;i++) o.set(i,i);
for(int i=0;i<n;i++) T[i].build(1,0,(1<<dep)-1,0,i);
s[l+1]=' ';///防止暴力转移时匹配越界
for(int p=0,x=0,y=0;q--;)
{
scanf("%d%d%d",&x,&y,&p),y+=(x--)-2;
if(t[x][y]!=s[p])
{
printf("0\n");
continue;
}
vec res=1<<x;
for(int i=y,j=x;;)
{///f_{i,j}=1满足j最小,res为转移完第i列后的矩阵
///细节拉满,意思就是不能超出s和t的边界
int len=min({sa::lcp(p+i-y,l+j*m+i-j+1),m+j-i,l-p+y-i+1});
assert(len);
if(len>=2) T[j].query(i+1,i+len-1,res);
i+=len;
///暴力转移
vec tmp;
for(int k=j;k<n;k++)
{
if(!res.get(k)) continue;
if(t[k][i]==s[i-y+p]) tmp.v|=1<<k;
if(k+1<n&&t[k+1][i]==s[i-y+p]) tmp.v|=1<<(k+1);
}
res=tmp;
while(j<n&&!res.get(j)) j++;
if(j==n)
{
printf("%d\n",i-y);
break;
}
}
}
return 0;
}
uoj890 兵棋
题目描述
对于长为 的 串 ,定义一次操作为,删除所有满足 的位置,并将剩余位置拼接起来。
给定长为 的由 组成的字符串 ,对于所有将 替换成 或 得到的字符串,求其经过 次操作后序列长度之和,对 取模。
数据范围
- 。
时间限制 ,空间限制 。
分析
对于一个已知的 串 ,我们需要比模拟更快的方法求出其经过 次操作后的长度。
先考虑 的情况,容易发现这是将除第一段以外的每个连续段长度减一,但是可能会引起连续段合并。
这启发我们用 +1/-1
来代替 0/1
,具体地,有以下结论:
记上一个未被删除的字符为 ,将 赋值为 , 赋值为 ,如果权值到达 ,那么下一个未被删除的字符为 ;如果权值到达 ,那么下一个未被删除的字符为 ,最后将权值清零。
接下来可以动态规划。
记 为考虑前 个字符,上一个未被删除的字符为 ,当前权值为 的序列个数与长度之和,用二元组存储。
对于普通转移,我们只需要让序列个数和权值之和分别相加即可;对于涉及到新字符的转移,原先的每个序列都会产生 的贡献,将前者的序列个数加到后者的长度之和中即可。
时间复杂度 。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=1e5+5,mod=998244353;
int k,n,res;
char s[maxn];
pii f[2][2][205];
void add(int &a,int b)
{
if((a+=b)>=mod) a-=mod;
}
void tran(pii &a,pii &b,bool op)
{
add(b.fi,a.fi),add(b.se,a.se);
if(op) add(b.se,a.fi);
}
int main()
{
scanf("%d%d%s",&n,&k,s+1);
if(s[1]!='1') f[1][0][0]=mp(1,1);
if(s[1]!='0') f[1][1][0]=mp(1,1);
for(int i=2;i<=n;i++)
{
memset(f[i&1],0,sizeof(f[i&1]));
for(int x=0;x<=1;x++)
for(int j=0;j<=k;j++)
{
pii &v=f[i-1&1][x][j];
if(!v.fi&&!v.se) continue;
if(s[i]-'0'!=(x^1)) j>0?tran(v,f[i&1][x][j-1],0):tran(v,f[i&1][x][0],1);
if(s[i]-'0'!=x) j<k?tran(v,f[i&1][x][j+1],0):tran(v,f[i&1][x^1][0],1);
}
}
for(int x=0;x<=1;x++) for(int j=0;j<=k;j++) add(res,f[n&1][x][j].se);
printf("%d\n",res);
return 0;
}
uoj891 难度查找
题目描述
本题是交互题。
给定 和 的矩阵 ,保证 。
你可以调用以下函数:
-
long long query(int x,int y);
该函数返回 的值,你可以调用不超过 次。
你需要实现以下函数:
-
long long solve(int n,long long k);
该函数返回矩阵 中第 小的元素。
数据范围
- 。
时间限制 ,空间限制 。
分析
二分加单调栈可以做到询问次数 ,记录已经询问过的位置可以得到 分。
接下来快进到正解。
对于一个 的矩阵,我们尝试维护由前 小元素构成的折线图。
具体的,记 表示第 列中前 个元素排在前 名,则 单调递减。
先求出由偶数列前 小元素构成的折线图(记为 ),令 。
由于 单调递减,所以在上述考虑范围内的元素至少 个,至多 个。
想一想,为什么?
如果第 列只考虑前 个元素,那么我们总共考虑了 个元素。 由单调性可知,在此基础上额外考虑的元素拼起来不会超过一整列,也就是 个元素。用大根堆维护所有 的值,每次弹掉最大的,直到只剩下 个元素,这里需要 次操作。
由于 地位相同,所以采取旋转切割的方式最优,不过需要涉及到将考虑范围从横向到纵向的转换。
不妨 ,则操作次数满足 。至此 ,加上记忆化可以获得 分的好成绩。
还差最后一步优化,注意到如果 ,那么第 列一定在第 列之前弹出。
用大根堆维护折线图的拐点,那么初始只有不超过 个拐点,即只需弹 次,每次弹出时左边和上边都可能变成了拐点。
又因为水平方向至多弹 次,因此 ,解得 。
时间复杂度 。
#include<bits/stdc++.h>
#include"matrix.h"
#define ll long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<ll,int>
using namespace std;
const int maxn=2e5+5;
int p[maxn],r[maxn];
unordered_map<int,ll> h[maxn];
ll get(int x,int y)
{
if(!h[x].count(y)) h[x][y]=query(x,y);
return h[x][y];
}
void work(int n,int m,ll k,int u,int v,bool rev)
{
if(m==1) return p[1]=k,void();
work(m,n/2,(k+1)/2,v,u+1,rev^1);
ll cnt=0;
for(int i=m,j=1;i>=1;i--)
{
while(j<=n/2&&p[j]>=i) j++;
r[i]=min(2*j-1,n),cnt+=r[i];
}
auto val=[&](int x,int y)
{
return !rev?get(x<<u,y<<v):get(y<<v,x<<u);
};
priority_queue<pii> q;
memcpy(p+1,r+1,4*m);
for(int i=1;i<=m;i++) if(p[i]!=p[i+1]) q.push(mp(val(p[i],i),i));
for(;cnt>k;cnt--)
{
int i=q.top().se;
q.pop();
if(p[i]==p[i-1]) q.push(mp(val(p[i],i-1),i-1));
if(--p[i]!=p[i+1]) q.push(mp(val(p[i],i),i));
}
}
ll solve(int n,ll k)
{
work(n,n,k,0,0,0);
ll res=0;
for(int i=1;i<=n;i++) if(p[i]) res=max(res,get(p[i],i));
return res;
}
uoj892 大海的深度
题目描述
给定长为 的数列 , 次操作,每次操作将 修改为 ,然后求 。
数据范围
- 。
- 。
时间限制 ,空间限制 。
分析
我们在首次出现最小值的位置计算区间 的贡献。
令 为 之前第一次 的位置, 为 之后第一次 的位置,则答案为 。
询问分块,将涉及到的不超过 个位置(记为关键点)的值设为 ,跑单调栈,先不统计关键点的贡献。
对于一个询问,我们需要将关键点位置改为确定的值。
先考虑 改变带来的影响。关键点会将序列 切成 段,显然只有段内前缀最小值所在位置的 值可能会改变。
具体的,如果对关键点和前缀 做单调栈,如果跑到前缀 时栈顶为关键点 ,则 ,即给答案减去 。
同理。我们不考虑既为前缀 又为后缀 的情况,因为这等价于整段 ,只有 个,用对待关键点的方法对待整段 即可。
我们不可能对每次询问都暴力跑单调栈,因为前缀 可能远超 个。但是注意到单调栈中元素是递增的,前缀 是递减的。连续加入前缀 的过程,其实就是对单调栈进行 pop 操作的过程。在单调栈每个元素被 pop 之前计算连续段的贡献(如果栈顶为关键点,则连续段的 全部改为栈顶),复杂度就没有问题。
我们需要询问 "第 个连续段中 的前缀 的贡献之和" ,维护成一个关于 的一次函数,博主代码中选择了直接二分。
再考虑 个关键点和整段 位置的贡献。我们不能直接借用上面的单调栈,因为我们要模拟插入一个连续段而不仅仅是前缀 ,这可能要在单调栈中 push 很多元素。
如果考虑 和 限制的来源和去路,有以下 种情况:
- 关键点对关键点/前缀 :总共只有 个点,上面跑单调栈时顺便维护一下。
- 非关键点对前缀 :预处理时已经解决。
- 非关键点对关键点:求 时有用的非关键点就是全体前缀 ,之前的二分刚好就可以帮我们定位!
时间复杂度 。
其实可以做到严格根号,由于 和 都只有 种,对每一段双指针扫前缀 和 ,可以做到 预处理, 回答。
二分做法:link 。
下面是严格根号的代码。
#include<bits/stdc++.h>
#define ll long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<ll,ll>
using namespace std;
const int B=280,maxn=2e5+5,inf=1e6+5;
int m,n,cnt,top;
int a[maxn],c[maxn],st[maxn],pre[maxn],suf[maxn];
int fir[maxn],npre[maxn],nsuf[maxn];///fir为第一次出现最小值的位置
int id[inf],h1[B+5][3*B+5],h2[B+5][3*B+5];
bitset<maxn> b;
vector<int> pos;
struct modi
{
int x,w;
}o[maxn];
struct node
{
int x;
pii w;
};
vector<node> v1[B+5],v2[B+5];
pii operator+(pii a,pii b)
{
return mp(a.fi+b.fi,a.se+b.se);
}
pii operator-(pii a,pii b)
{
return mp(a.fi-b.fi,a.se-b.se);
}
pii operator*(pii a,int b)
{
return mp(a.fi*b,a.se*b);
}
ll calc(const pii &a,int x)
{
return a.fi*x+a.se;
}
void work(int l,int r)
{
b.reset(),cnt=0,pos.clear();
for(int i=l;i<=r;i++) b[o[i].x]=1,c[++cnt]=a[o[i].x],c[++cnt]=o[i].w;
st[top=0]=0;
for(int i=0;i<=n+1;i++)
{
if(!i||i==n+1||b[i]) pos.push_back(i);
else
{
while(top&&a[st[top]]>a[i]) suf[st[top--]]=i;
pre[i]=st[top],st[++top]=i;
}
}
while(top) suf[st[top--]]=n+1;
ll res=0;
for(int i=1;i<pos.size();i++)
{
int l=pos[i-1]+1,r=pos[i]-1;
fir[i]=0;
if(l>r) continue;
int c1=1,c2=1;
for(int j=l;j<=r;j++)
if(pre[j]<l&&suf[j]>r) fir[i]=j,c[++cnt]=a[j];///fir[i]的pre和suf都有可能被修改,最后单独计算它的贡献
else c1+=pre[j]<l,c2+=suf[j]>r,res+=1ll*(j-pre[j])*(suf[j]-j)*a[j];
///v1,v2分别表示前缀后缀min,x为下标,w为贡献(一次函数)
v1[i].resize(c1+1),v2[i].resize(c2+1);
v1[i][0]={0,mp(0,0)},v2[i][0]={n+1,mp(0,0)};
}
sort(c+1,c+cnt+1),cnt=unique(c+1,c+cnt+1)-c-1;
for(int i=1;i<=cnt;i++) id[c[i]]=i;
for(int i=1;i<pos.size();i++)
{
int l=pos[i-1]+1,r=pos[i]-1;
if(l>r) continue;
int c1=1,c2=1,k1=cnt,k2=cnt;
pii w1=mp(0,0),w2=mp(0,0);
///最后加入fir[i]是为了方便后面更新npre,nsuf,但不能计算fir[i]作为前缀后缀min的贡献
for(int j=l;j<=fir[i];j++)
{
if(pre[j]>=l) continue;
///h1[i][k] 最大的l满足a[v1[i][l].x]>=c[k]
if(j!=fir[i]) w1=w1+mp(1,-pre[j])*(suf[j]-j)*a[j];
v1[i][c1]={j,w1};
while(k1&&a[j]<c[k1]) h1[i][k1--]=c1-1;
c1++;
}
while(k1) h1[i][k1--]=c1-1;
for(int j=r;j>=fir[i];j--)
{
if(suf[j]<=r) continue;
///h2[i][k] 最大的l满足a[v2[i][l].x]>c[k]
if(j!=fir[i]) w2=w2+mp(-1,suf[j])*(j-pre[j])*a[j];
v2[i][c2]={j,w2};
while(k2&&a[j]<=c[k2]) h2[i][k2--]=c2-1;
c2++;
}
while(k2) h2[i][k2--]=c2-1;
}
for(int i=l;i<=r;i++)
{
a[o[i].x]=o[i].w,st[top=0]=0;
ll tmp=res;
///计算pre的修改
for(int j=1;j<pos.size();j++)
{
if(fir[j])
{
int x=fir[j],lst=0;
///pop
while(top&&a[st[top]]>a[x])
{
int l=h1[j][id[a[st[top]]]];
if(b[st[top]]) tmp-=calc(v1[j][l].w-v1[j][lst].w,st[top]);
nsuf[st[top--]]=v1[j][l+1].x,lst=l;
///如果栈顶为关键点,我们要将第(lst,l]个前缀min的pre修改为栈顶
///如果栈顶不为关键点,那么前缀min的pre和预处理时相比没有变化,注意并不意味着pre都是栈顶
///因为栈顶代表了一个连续段的最低点,而连续段过最低点后可以反弹变高,这对pre的影响很复杂
}
///push x
if(b[st[top]]) tmp-=calc(v1[j].back().w-v1[j][lst].w,st[top]);
st[++top]=x;
}
///push pos[j]
int x=pos[j];
while(top&&a[st[top]]>a[x]) nsuf[st[top--]]=x;
st[++top]=x;
}
///计算suf的修改
st[top=0]=n+1;
for(int j=pos.size()-1;j>=1;j--)
{
if(fir[j])
{
int x=fir[j],lst=0;
///pop
while(top&&a[st[top]]>=a[x])
{
int l=h2[j][id[a[st[top]]]];
if(b[st[top]]) tmp-=calc(v2[j][l].w-v2[j][lst].w,st[top]);
npre[st[top--]]=v2[j][l+1].x,lst=l;
}
///push x
if(b[st[top]]) tmp-=calc(v2[j].back().w-v2[j][lst].w,st[top]);
st[++top]=x;
}
///push pos[j-1]
int x=pos[j-1];
while(top&&a[st[top]]>=a[x]) npre[st[top--]]=x;
st[++top]=x;
}
///计算关键点和整段min的贡献
for(int j=1;j<pos.size();j++)
{
int x=fir[j],y=pos[j];
if(x) tmp+=1ll*(x-max(pre[x],npre[x]))*(min(suf[x],nsuf[x])-x)*a[x];
tmp+=1ll*(y-npre[y])*(nsuf[y]-y)*a[y];
}
printf("%lld\n",tmp);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=m;i++) scanf("%d%d",&o[i].x,&o[i].w);
for(int l=1,r=B;l<=m;l+=B,r+=B) work(l,min(r,m));
return 0;
}
总结
vector
的push_back
可以看成超大常数 ,不能承受 次操作,可以通过resize
或reserve
操作进行加速。
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/18306875
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?