单调栈/单调队列训练记录 2025.2

大概套路:

条件:问题可以拆成一些部分之和的最值,且互不干涉,转移范围是一定的,而且要么可直接转移,要么具有可差分性

Cashback

https://www.gxyzoj.com/d/hzoj/p/4376

结论只推到一定是 c 的倍数或是小于等于 c,但是其实在长度超过 c 的时候一定是越短越好

因为长度变长,最小值可能变小,所以可以直接分为两种情况

记当前点去掉的数之和最大为 fi,因为长度小于 c 时一定是无用的,直接从 fi1 转移即可

对于长度恰好为 c 的,可以找 ic+1i 的最小值,单调队列维护即可

Cutlet

https://www.gxyzoj.com/d/hzoj/p/4382

这道题有两维限制,朴素的,设 fi,j 表示当前是第 i 个时刻,未被煎的一面的时长为 j

但是注意到,不属于区间的点是可以统一计算的,所以只用考虑区间内

又注意到,其实翻 0,1,2 次就是所有的情况了,因为可以把不与前后相连的同一面朝下时间连一起

所以将 i 的意义改为第 i 个区间即可

但是背包转移的时间复杂度大,因为是连续的,考虑单调队列

0 直接转移,2 因为朝上面不变,所以只考虑加上了多少

1 比较复杂,假设区间长度为 x,右端点为 r,那么转移范围就是 rjxrj

注意,这里 j 要倒着枚举

点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
int n,k,l[105],r[105],q[200005],f[105][200005],tail,head;
int main()
{
	scanf("%d%d",&n,&k);
	n=n*2;
	for(int i=1;i<=k;i++)
	{
		scanf("%d%d",&l[i],&r[i]);
	}
	for(int i=0;i<=k;i++)
	{
		for(int j=0;j<=n;j++)
		{
			f[i][j]=1e9;
		}
	}
	f[0][0]=0;
	for(int i=1;i<=k;i++)
	{
		for(int j=0;j<=n;j++)
		{
			f[i][j]=f[i-1][j];
		}
		head=1,tail=0;
		int x=r[i]-l[i];
		for(int j=0;j<=r[i];j++)
		{
			if(head<=tail&&q[head]<j-x) head++;
			while(head<=tail&&f[i-1][q[tail]]>f[i-1][j]) tail--;
			q[++tail]=j;
			f[i][j]=min(f[i][j],f[i-1][q[head]]+2);
		}
		head=1,tail=0;
		for(int j=r[i];j>=0;j--)
		{
			if(head<=tail&&q[head]<r[i]-j-x) head++;
			int tmp=r[i]-j;
			while(head<=tail&&f[i-1][q[tail]]>f[i-1][tmp]) tail--;
			q[++tail]=tmp;
			f[i][j]=min(f[i][j],f[i-1][q[head]]+1);
		}
	}
	if(f[k][n/2]!=1e9)
	printf("Full\n%d",f[k][n/2]);
	else printf("Hungry");
	return 0;//
}

Kuzya and Homework

https://www.gxyzoj.com/d/hzoj/p/4378

看到乘除,考虑分解质因数处理,将乘记为正,除记为负

而题目中满足条件的区间,就是每个质数的次数和都是自然数

但是这样很难维护,不具有单调性

我们可以记 li 表示当前右端点为 i,时,最近的左端点是 li

是乘时就是 i,当是除时,判断前面剩下的因子个数够不够即可

所以每次计算乘时,每个因子都往对应的栈里压入相应数量的下标,除时弹出即可

如果一个区间 [x,y]li 大于 x,那么这个区间合法,倒着扫一遍即可

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6;
int n,a[N+5],vis[N+5],p[N+5],f[N+5],cnt,b[N+5];
int l[N+5];
void init()
{
	for(int i=2;i<=N;i++)
	{
		if(!vis[i])
		{
			p[++cnt]=i,f[i]=i;
		}
		for(int j=1;p[j]*i<=N;j++)
		{
			vis[i*p[j]]=1,f[i*p[j]]=p[j];
			if(i%p[j]==0) break;
		}
	}
}
string s;
vector<int> st[N+5];
struct node{
	int id,val;
};
stack<node> s1;
void add(int v,int x)
{
	l[x]=x;
	while(v>1)
	{
		st[f[v]].push_back(x);
		v/=f[v];
	}
}
void del(int v,int x)
{
	l[x]=x;
	while(v>1)
	{
		if(st[f[v]].size()==0)
		{
			l[x]=0;
			break;
		}
		l[x]=min(l[x],st[f[v]].back());
		st[f[v]].pop_back();
		v/=f[v];
	}
}
int main()
{
	init();
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	cin>>s;
	s=" "+s;
	for(int i=1;i<=n;i++)
	{
		if(s[i]=='*') b[i]=1;
	}
	for(int i=1;i<=n;i++)
	{
		if(b[i]) add(a[i],i);
		else del(a[i],i);
//		printf("%d ",b[i]);
	}
	ll ans=0;
	for(int i=n;i>0;i--)
	{
		int now=1;
		while(!s1.empty()&&s1.top().id>=i)
		{
			now+=s1.top().val;
			s1.pop();
		}
		if(l[i]==i) ans+=now;
		s1.push((node){l[i],now});
//		printf("%d ",l[i]);
	}
	printf("%lld",ans);
	return 0;
}

Non-equal Neighbours

https://www.gxyzoj.com/d/hzoj/p/CF1585F

反正是完全跑在了另一条路上

看到这种计数,大概率是容斥,观察发现,有 x 对相等就会把序列分成 nx 段,段内相等,相邻段可能相等

因为是容斥,符号是按奇偶性变化的,而且段数一样,所以考虑至少被分成的段数是奇数/偶数的情况

fi,0/1 表示前 i 个数至少被分成的段数偶数/奇数的情况

考虑转移,朴素的方程显然是 fi,0/1=j=1ifj,1/0(mink=jiak)

考虑优化,可以发现,这里的取 min 具有单调性,而且因为是求和,可差分

所以可以将当前 ai 的部分单独拎出来计算,前面的直接用即可

这里记单调栈中 ai 的前面一个元素的下标为 x ,必然存在 j=1xfj,1/0(mink=jxak)=fx,0/1

所以式子是 fi,0/1=fx,0/1+aij=1+xifj,1/0

后面部分前缀和即可

点击查看代码
#include<cstdio>
#define ll long long
using namespace std;
const int mod=998244353;
int n,a[200005],top,st[200005];
ll f[200005][2],s[200005][2];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	f[0][0]=s[0][0]=1;
	for(int i=1;i<=n;i++)
	{
		while(top&&a[st[top]]>=a[i]) top--;
		st[++top]=i;
		f[i][0]=((top==1?0:f[st[top-1]][0])+(s[i-1][1]-(top==1?0:s[st[top-1]-1][1])+mod)*a[i]%mod)%mod;
		f[i][1]=((top==1?0:f[st[top-1]][1])+(s[i-1][0]-(top==1?0:s[st[top-1]-1][0])+mod)*a[i]%mod)%mod;
		s[i][0]=(s[i-1][0]+f[i][0])%mod;
		s[i][1]=(s[i-1][1]+f[i][1])%mod;
	}
	printf("%lld",((f[n][0]-f[n][1])*(n%2?mod-1:1)%mod+mod)%mod);
	return 0;
}

「CZOI-R1」消除威胁

https://www.gxyzoj.com/d/hzoj/p/4381

反正是读错题了

因为是正负性上操作,所以可以一开始全弄成正的,先考虑如果一个区间内值为 x 的数有 y 个,且其余都小于 x,则显然尽量等分更优

关键在于这个区间,单调栈处理后排序即可

蓬莱「凯风快晴 −富士火山−」

https://www.gxyzoj.com/d/hzoj/p/4383

有两个性质,最深的选择层一定是全选,最优情况下一定选了根

如果将每一层的点数弄成一个数组,假设当前层数为 x,那么在 x 的前面且数值大于当前层并为经过更少层的点,所取的值必然是 cntx

所以,单调栈找第一个大于等于当前点的数,直接转移就行了

点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
int n,edgenum,head[500005],f[500005],s[500005],top;
struct edge{
	int to,nxt;
}e[1000005];
void add_edge(int u,int v)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	head[u]=edgenum;
}
int dep[500005],cnt[500005];
void dfs(int u,int fa)
{
	dep[u]=dep[fa]+1;
	cnt[dep[u]]++;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
	}
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add_edge(u,v);
		add_edge(v,u);
	}
	dfs(1,0);
	f[1]=1,s[1]=1,top=1;
	int ans=0;
	for(int i=2;i<=n;i++)
	{
//		printf("%d ",cnt[i]);
		if(!cnt[i]) break;
		while(top&&cnt[s[top]]>cnt[i]) top--;
		f[i]=f[s[top]]+cnt[i]*(i-s[top]);
		s[++top]=i;
		ans=max(ans,f[i]);
	}
	printf("%d",ans);
	return 0;
}

[HNOI2016] 序列

https://www.gxyzoj.com/d/hzoj/p/4384

实在是抽象,看到区间求值,不好线段树,考虑莫队

它的难点在于如何加点和删点,也就是如何计算类似于右端点 r 在左端点处于区间 [l,r1] 的贡献

显然的,在区间最小值前的点的贡献就是区间最小值,可以ST表

剩下部分可以想到差分,在预处理的时候,可以单调栈,每次记录的是以当前的为终点的贡献,因为是从第一个小于等于当前点的位置转移,直接相减即可

点击查看代码
#include<cstdio>
#include<cmath>
#include<algorithm>
#define ll long long
using namespace std;
int n,m,a[100005],st[100005][20],blen,l1[100005],pos[100005];
int top,s[100005],f[100005][20];
ll gr[100005],gl[100005],ans[100005];
struct node{
	int l,r,id;
}q[100005];
void init()
{
	blen=sqrt(n);
	l1[0]=-1;
	for(int i=1;i<=n;i++)
	{
		st[i][0]=a[i],pos[i]=i/blen,l1[i]=l1[i>>1]+1,f[i][0]=i;
	}
	for(int i=1;i<=l1[n];i++)
	{
		for(int j=1;j+(1<<i)-1<=n;j++)
		{
			int x=j+(1<<(i-1));
			if(st[j][i-1]<st[x][i-1])
			st[j][i]=st[j][i-1],f[j][i]=f[j][i-1];
			else st[j][i]=st[x][i-1],f[j][i]=f[x][i-1];
		}
	}
}
int query(int l,int r)
{
	int x=l1[r-l+1];
	if(st[l][x]<st[r-(1<<x)+1][x]) return f[l][x];
	else return f[r-(1<<x)+1][x];
}
bool cmp(node x,node y)
{
	if(pos[x.l]==pos[y.l]) return x.r<y.r;
	return x.l<y.l;
}
ll getl(int l,int r)
{
	int p=query(l-1,r);
	return 1ll*a[p]*(r-p+1)+gl[l-1]-gl[p];
}
ll getr(int l,int r)
{
	int p=query(l,r+1);
//	printf("%d %d %d\n",l,r,p);
	return 1ll*a[p]*(p-l+1)+gr[r+1]-gr[p];
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	init();
	for(int i=1;i<=m;i++)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		q[i]=(node){l,r,i};
	}
	sort(q+1,q+m+1,cmp);
	for(int i=1;i<=n;i++)
	{
//		printf("%d ",pos[i]);
		while(top&&a[s[top]]>=a[i]) top--;
		gr[i]=gr[s[top]]+1ll*(i-s[top])*a[i];
		s[++top]=i;
//		printf("g%d ",gr[i]);
	}
	top=0,s[0]=n+1;
	for(int i=n;i>0;i--)
	{
		while(top&&a[s[top]]>=a[i]) top--;
		gl[i]=gl[s[top]]+1ll*(s[top]-i)*a[i];
		s[++top]=i;
	}
	int l=q[1].l,r=l-1;
	ll sum=0;
	for(int i=1;i<=m;i++)
	{
//		printf("%d %d\n",q[i].l,q[i].r);
		while(l>q[i].l) sum+=getl(l--,r);
		while(r<q[i].r) sum+=getr(l,r++);
		while(l<q[i].l) sum-=getl(++l,r);
//		printf("%d\n",sum);
		while(r>q[i].r) sum-=getr(l,--r);
		ans[q[i].id]=sum;
	}
	for(int i=1;i<=m;i++)
	{
		printf("%lld\n",ans[i]);
	}
	return 0;
}

[USACO18FEB] Snow Boots G

https://www.gxyzoj.com/d/hzoj/p/4401

先离散化,排序后找每个上线的最大间距,暴力线段树即可

区区区间间间

https://www.gxyzoj.com/d/hzoj/p/3693

弱智题,暴力单调栈即可

[POI 2014] BAR-Salad Bar

https://www.gxyzoj.com/d/hzoj/p/4536

一个方向显然,看什么情况下 p 个数小于 j 个数,把它清空即可

接下来考虑倒着,如果当前的 p 个数大于等于 j 个数,就清空并记录,这样倒着经过每一段都必然满足条件

最后统计答案,跳到终止点即可

时间复杂度 O(n),因为每一次移动的距离至少是 k2,在一般情况下期望是 2 次,最劣就是 log

[ARC177D] Earthquakes

https://www.gxyzoj.com/d/hzoj/p/3886

可以将原序列按位置排序后分成若干段,每段之间互不影响,此时,只需要分别看每段的概率即可

设当前点的位置是从左往右第 x 个,倒下时间为 t,记他的概率为 px

显然两侧先倒的不能干涉到当前位置,因为方向单一,直接数出个数相除即可

因为必须是有效的倒下,单调栈处理即可

对于剩下部分,如果要求的点是端点,才有情况

统计的时候枚举时间,可以把概率扔进线段树求乘积

点击查看代码
#include<cstdio>
#include<algorithm>
#define lid id<<1
#define rid id<<1|1
#define ll long long
using namespace std;
const int mod=998244353;
int n,h,pos[200005],idx,s[200005],top;
int f[200005],g[200005],fl[200005],mp[200005];
ll p[200005];
struct node{
	int t,x;
}a[200005],b[200005];
bool cmp(node x,node y)
{
	return x.x<y.x;
}
ll qpow(ll x,int y)
{
	ll res=1;
	while(y)
	{
		if(y&1) res=res*x%mod;
		x=x*x%mod;
		y>>=1;
	}
	return res;
}
void get(int st)
{
	pos[st]=++idx;
	int ed=n;
	for(int i=st+1;i<=n;i++)
	{
		if(a[i].x-a[i-1].x>h)
		{
			ed=i-1;
			break;
		}
		pos[i]=idx;
	}
	top=0;
	for(int i=st;i<=ed;i++)
	{
		while(top&&a[s[top]].t>=a[i].t) top--;
		f[i]=f[s[top]]+1;
		if(s[top]==i-1||i==st) fl[i]++;
		s[++top]=i;
	}
	top=0;
	for(int i=ed;i>=st;i--)
	{
		while(top&&a[s[top]].t>=a[i].t) top--;
		g[i]=g[s[top]]+1;
		if(s[top]==i+1||i==ed) fl[i]++;
		s[++top]=i;
	}
	for(int i=st;i<=ed;i++)
	{
		p[i]=qpow(2,mod-2)*fl[i]%mod;
		p[i]=p[i]*qpow(2,ed-st+1-f[i]-g[i]+2)%mod;
	}
}
struct seg_tr{
	int l,r;
	ll sum;
}tr[800008];
void build(int id,int l,int r)
{
	tr[id].l=l,tr[id].r=r;
//	printf("%d %d %d\n",id,l,tr[id].l);
	if(l==r) return;
	int mid=(l+r)>>1;
	build(lid,l,mid);
	build(rid,mid+1,r);
}
void update(int id,int x,ll val)
{
	if(tr[id].l==tr[id].r)
	{
		tr[id].sum=(tr[id].sum+val)%mod;
		return;
	}
	int mid=(tr[id].l+tr[id].r)>>1;
	if(x<=mid) update(lid,x,val);
	else update(rid,x,val);
	tr[id].sum=tr[lid].sum*tr[rid].sum%mod;
}
ll query(int id,int l,int r)
{
	if(l>r) return 1;
//	printf("%d %d %d %d %d\n",id,l,r,tr[id].l,tr[id].r);
	if(l==tr[id].l&&tr[id].r==r)
	{
		return tr[id].sum;
	}
	int mid=(tr[id].l+tr[id].r)>>1;
	if(r<=mid) return query(lid,l,r);
	else if(l>mid) return query(rid,l,r);
	else return query(lid,l,mid)*query(rid,mid+1,r)%mod;
}
int main()
{
	scanf("%d%d",&n,&h);
	for(int i=1;i<=n;i++)
	{
		int x;
		scanf("%d",&x);
		a[i]=(node){i,x};
	}
	sort(a+1,a+n+1,cmp);
	for(int i=1;i<=n;i++)
	{
		mp[a[i].t]=i;
		if(!pos[i]) get(i);
	}
	build(1,1,idx);
//	for(int i=1;i<=n;i++)
//	{
//		printf("%d %d %d\n",f[i],g[i],p[i]);
//	}
//	printf("%d ",idx);
//printf("%d ",tr[1].l);
	for(int i=1;i<=n;i++)
	{
		int x=mp[i];
//		printf("%d %lld %d\n",pos[x],p[x],idx);
		ll ans=query(1,1,pos[x]-1)*p[x]%mod*query(1,pos[x]+1,idx)%mod;
		printf("%lld ",ans);
		update(1,pos[x],p[x]);
	}
	return 0;
}
posted @   wangsiqi2010916  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
历史上的今天:
2024-02-23 20240219比赛总结
2024-02-23 20240221比赛总结
2024-02-23 20240222比赛总结
2024-02-23 BSGS学习笔记
2024-02-23 概率学习笔记
2024-02-23 期望学习笔记
2024-02-23 基环树学习笔记
点击右上角即可分享
微信分享提示