【做题记录】csp2025刷题计划-单调栈/单调队列
A. Cashback
设某一个子串的大小为 \(k\)。
- \(k<c\),要删掉 \(0\) 个最小值,等价于 \(k\) 个长为 \(1\) 的区间。
- \(k=c\),就是这个区间之和减掉这个区间最小值。
- \(c<k<2c\),等价于 \(1\) 个长为 \(k\) 的区间和 \(k-c\) 个长为 \(1\) 的区间。
- \(k\ge 2c\),显然不如前几种情况优。可以归纳证明。
于是问题变为将原数组分为若干长为 \(1\) 和长为 \(k\) 的区间的问题。用单调队列预处理以每个位置结尾的长为 \(c\) 的区间的最小值,DP 即可。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,m,duil[maxn];
ll a[maxn],sum[maxn];
ll f[maxn],g[maxn];
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1,hd=1,tl=0;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
while(hd<=tl&&a[duil[tl]]>a[i]){
tl--;
}
duil[++tl]=i;
while(hd<=tl&&i-duil[hd]>=m){
hd++;
}
g[i]=a[duil[hd]];
}
// for(int i=1;i<=n;i++){
// cout<<g[i]<<" ";
// }
// puts("");
for(int i=1;i<=n;i++){
f[i]=f[i-1]+a[i];
if(i>=m){
f[i]=min(f[i],f[i-m]+sum[i]-sum[i-m]-g[i]);
}
}
cout<<f[n];
return 0;
}
}
int main(){return asbt::main();}
B. Cutlet
首先有一个 \(O(n^2)\) 的 DP:设 \(f_{i,j}\) 表示前 \(i\) 分钟,当前朝上的面煎了 \(j\) 分钟的最小翻面次数。于是有方程:
其中第二种转移是翻面的,即仅当 \(\exist k,i\in[l_k,r_k]\) 时可以转移。
那么如果 \(i\) 不在可以翻面的区间里,直接移过来就行了。于是只考虑可翻面的区间。设 \(f_{i,j}\) 表示前 \(r_i\) 分钟,朝上的面煎了 \(j\) 分钟的最小翻面次数。
考虑当在同一区间内如果翻面次数 \(\ge 2\),那一不如只翻 \(1\) 次或 \(2\) 次。于是只需考虑这两个转移。
-
翻 \(1\) 次
枚举当前朝下的面在这个区间内煎的时间 \(k\)。当前朝下的面显然总共煎了 \(r_i-j\) 分钟,那么在这个区间开始前它就煎了 \(r_i-j-k\) 分钟。而这个区间前这一面正好就是朝上的。于是 \(f_{i,j}=\min(f_{i-1,r_i-j-k})\)。
-
翻 \(2\) 次
枚举当前朝上的面在这个区间内煎的时间 \(k\)。在这个区间前这一面也是朝上的,煎了 \(j-k\) 分钟。于是 \(f_{i,j}=min(f_{i-1,j-k})\)。
用单调队列优化这两个转移即可。时间复杂度 \(O(nk)\)。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e5+5;
const int inf=0x3f3f3f3f;
int n,m,duil[maxn];
int f[105][maxn];
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
// cout<<cplx::usdmem();
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
memset(f[0],0x3f,sizeof f[0]);
f[0][0]=0;
for(int i=1,l,r,hd,tl;i<=m;i++){
for(int j=0;j<=n;j++){
f[i][j]=f[i-1][j];
}
cin>>l>>r;
hd=1,tl=0;
for(int j=0;j<=min(r,n);j++){
while(hd<=tl&&f[i-1][duil[tl]]>f[i-1][j]){
tl--;
}
duil[++tl]=j;
while(hd<=tl&&duil[hd]<j-(r-l)){
hd++;
}
f[i][j]=min(f[i][j],f[i-1][duil[hd]]+2);
}
hd=1,tl=0;
for(int j=r;~j;j--){
while(hd<=tl&&f[i-1][duil[tl]]>f[i-1][r-j]){
tl--;
}
duil[++tl]=r-j;
while(hd<=tl&&duil[hd]<l-j){
hd++;
}
f[i][j]=min(f[i][j],f[i-1][duil[hd]]+1);
}
}
// for(int i=0;i<=m;i++){
// for(int j=0;j<=n;j++){
// cout<<f[i][j]<<" ";
// }
// puts("");
// }
printf(f[m][n]>=inf?"Hungry":"Full\n%d",f[m][n]);
return 0;
}
}
int main(){return asbt::main();}
/*
10 1
0 20
*/
C. Kuzya and Homework
要求结果为整数,我们将所有 \(a_i\) 分解质因数,对于每个质数分别考虑。
考虑对于一个左端点 \(l\),能满足要求的右端点一定在从 \(l\) 开始的一段连续区间中。于是我们得对于每个 \(l\) 求出 \(ans_l\) 表示那个最远的右端点。
对于一个质数 \(p\),假设 \(a_i\) 中有 \(k\) 个 \(p\),如果 \(b_i\) 为乘号就在 \(i\) 这里记一个 \(k\),否则记一个 \(-k\)。然后再做一个前缀和 \(pre\)。那么对于 \(l\),我们要求的就是最大的 \(r\),满足 \(\forall k\in[l,r],pre_k-pre_{l-1}\ge 0\)。可以倒着扫,维护一个单调栈,每次在单调栈上二分。但是这样的时间复杂度是无法承受的。我们考虑如果 \(a_i\) 中没有 \(p\),那么 \(i\) 这个位置的答案就是等于 \(i+1\) 的答案的。于是我们只用存储那些包括了 \(p\) 的位置。于是就成了若干次区间的取 \(\min\)。再用并查集扫一遍即可。
先将所有质数都筛出来,时间复杂度可以承受。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e6+5,inf=0x3f3f3f3f;
int n,prn,a[maxn];
int prm[maxn],pre[maxn];
int zh1[maxn],zh2[maxn];
int fa[maxn],ans[maxn];
bool npr[maxn];
string b;
vector<pii> wei[maxn],cun[maxn];
il void init(int x){
for(int i=2;i<=x;i++){
if(!npr[i]){
prm[++prn]=i;
}
for(int j=i*i;j<=x;j+=i){
npr[j]=1;
}
}
}
il int find(int x){
return x!=fa[x]?fa[x]=find(fa[x]):x;
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
cin>>b;
b=" "+b;
init(1e3);
for(int i=1,tmp;i<=n;i++){
tmp=a[i];
for(int j=1,cnt;j<=prn&&prm[j]<=tmp/prm[j];j++){
if(tmp%prm[j]==0){
cnt=0;
while(tmp%prm[j]==0){
tmp/=prm[j],cnt++;
}
wei[prm[j]].pb(mp(i,b[i]=='*'?cnt:-cnt));
}
}
if(tmp>1){
wei[tmp].pb(mp(i,b[i]=='*'?1:-1));
}
}
for(int i=1,top;i<=1e6;i++){
if(wei[i].empty()){
continue;
}
pre[0]=wei[i][0].sec;
for(int j=1;j<wei[i].size();j++){
pre[j]=pre[j-1]+wei[i][j].sec;
}
top=1;
zh1[1]=-inf,zh2[1]=n+1;
for(int j=wei[i].size()-1,k;~j;j--){
while(top&&zh1[top]>pre[j]){
top--;
}
zh1[++top]=pre[j],zh2[top]=wei[i][j].fir;
k=lwrb(zh1+1,zh1+top+1,pre[j]-wei[i][j].sec)-zh1-1;
cun[zh2[k]-1].pb(mp(j?wei[i][j-1].fir+1:1,wei[i][j].fir));
}
}
for(int i=1;i<=n+1;i++){
fa[i]=i,ans[i]=n;
}
for(int i=0;i<=n;i++){
for(pii j:cun[i]){
for(int k=find(j.fir);k<=j.sec;k=find(k+1)){
ans[k]=i;
fa[find(k)]=find(k+1);
}
}
}
// for(int i=1;i<=n;i++){
// cout<<ans[i]<<" ";
// }
// puts("");
ll Ans=0;
for(int i=1;i<=n;i++){
Ans+=ans[i]-i+1;
}
cout<<Ans;
return 0;
}
}
int main(){return asbt::main();}
D. Non-equal Neighbours
设 \(f_{i,j}\) 表示考虑 \(1\) 到 \(i\),\(b_i=j\) 的方案数。于是方程:
其中 \(j\in[1,a_i]\)。
显然可以滚动数组,然后可以用线段树优化。进行区间乘和区间加即可。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e5+5;
const int mod=998244353;
int n,rt,tot,a[maxn],sum[maxn<<6];
int ls[maxn<<6],rs[maxn<<6];
int tgj[maxn<<6],tgc[maxn<<6];
il int New(){
tgc[++tot]=1;
return tot;
}
il void pushup(int id){
sum[id]=(sum[ls[id]]+sum[rs[id]])%mod;
}
il void pushtgj(int id,int l,int r,int v){
sum[id]=(sum[id]+(r-l+1)*1ll*v)%mod;
(tgj[id]+=v)%=mod;
}
il void pushtgc(int id,int v){
sum[id]=sum[id]*1ll*v%mod;
tgj[id]=tgj[id]*1ll*v%mod;
tgc[id]=tgc[id]*1ll*v%mod;
}
il void pushdown(int id,int l,int r){
if(tgc[id]!=1){
if(!ls[id]){
ls[id]=New();
}
if(!rs[id]){
rs[id]=New();
}
pushtgc(ls[id],tgc[id]);
pushtgc(rs[id],tgc[id]);
tgc[id]=1;
}
if(tgj[id]){
if(!ls[id]){
ls[id]=New();
}
if(!rs[id]){
rs[id]=New();
}
int mid=(l+r)>>1;
pushtgj(ls[id],l,mid,tgj[id]);
pushtgj(rs[id],mid+1,r,tgj[id]);
tgj[id]=0;
}
}
il void add(int &id,int L,int R,int l,int r,int v){
if(!id){
id=New();
}
if(L>=l&&R<=r){
pushtgj(id,L,R,v);
return ;
}
pushdown(id,L,R);
int mid=(L+R)>>1;
if(l<=mid){
add(ls[id],L,mid,l,r,v);
}
if(r>mid){
add(rs[id],mid+1,R,l,r,v);
}
pushup(id);
}
il void mul(int &id,int L,int R,int l,int r,int v){
if(l>r){
return ;
}
if(!id){
id=New();
}
if(L>=l&&R<=r){
pushtgc(id,v);
return ;
}
pushdown(id,L,R);
int mid=(L+R)>>1;
if(l<=mid){
mul(ls[id],L,mid,l,r,v);
}
if(r>mid){
mul(rs[id],mid+1,R,l,r,v);
}
pushup(id);
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
add(rt,1,1e9,1,a[1],1);
// cout<<sum[rt]<<"\n";
for(int i=2,tmp;i<=n;i++){
tmp=sum[rt];
mul(rt,1,1e9,1,a[i],mod-1);
add(rt,1,1e9,1,a[i],tmp);
mul(rt,1,1e9,a[i]+1,1e9,0);
// cout<<sum[rt]<<"\n";
}
cout<<sum[rt];
return 0;
}
}
int main(){return asbt::main();}
/*
10
10 10 7 9 8 3 3 10 7 3
*/
E. 「CZOI-R1」消除威胁
首先全都去取绝对值,对答案没有影响。
对于某个相同的 \(a\) 值 \(x\),我们想要快速地求出 \(a_l=a_r=x\) 的具有威胁的区间 \([l,r]\) 的个数。从左向右扫 \(a\) 数组,维护一个单调递减的单调栈,对于每个位置 \(i\) 记录 \(cnt_i\) 表示以 \(i\) 为左端点,有多少个 \(r>i\) 满足 \([i,r]\) 是具有威胁的。这样做会算重,办法是当当前值等于栈顶时不压栈。
对于 \(cnt_i\),如果 \(a_i=0\),那么贡献就是 \(cnt_i+1\choose 2\)。否则我们可以将一部分变成负的。显然变一半是最优的,贡献为 \({\lfloor\frac{cnt_i+1}{2}\rfloor\choose{2}}+{\lceil\frac{cnt_i+1}{2}\rceil\choose{2}}\)。\(O(n)\) 统计即可。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
namespace IO{
const int bufsz=1<<20;
char ibuf[bufsz],*p1=ibuf,*p2=ibuf;
il int read(){
char ch=getchar();
while(ch<'0'||ch>'9'){
ch=getchar();
}
int x=0;
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x;
}
}
using IO::read;
const int maxn=5e5+5;
int n,a[maxn];
int zhan[maxn],cnt[maxn];
il ll calc(int x){
return x*1ll*(x-1)>>1;
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
n=read();
for(int i=1;i<=n;i++){
a[i]=read();
// cout<<a[i]<<" ";
}
// puts("");
int top=0;
for(int i=1;i<=n;i++){
while(top&&a[zhan[top]]<a[i]){
top--;
}
if(top&&a[zhan[top]]==a[i]){
cnt[zhan[top]]++;
}
else{
zhan[++top]=i;
}
}
ll ans=0;
for(int i=1;i<=n;i++){
cnt[i]++;
if(!a[i]){
ans+=calc(cnt[i]);
}
else{
ans+=calc(cnt[i]>>1)+calc((cnt[i]+1)>>1);
}
}
// puts("");
cout<<ans;
return 0;
}
}
int main(){return asbt::main();}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步