2022年2月上海月赛

2022年2月上海月赛

子串的最大差

  • 给出一个序列,最大差是序列最大值和最小值的差,问给定序列的所有子串的最大差之和
  • 子串指连续序列
  • 1 ≤ n ≤ 1 0 5 1\le n \le10^5 1n105

思路

暴力思路:暴力思路的之一重点,在暴力思路下,1 2 1 这种情况是会算作两次,会发生重复贡献的情况。

解决方案:当前 i 左侧遇到相同的值就结束,右侧不变,这样就能保证之前已经做过贡献的 a[i] 重复计算。为什么遇到相同的结束,而不是不记录数量,继续找到第一个比他大的就行了

因为,遇到相同的还继续往左扫描,此时形成的序列相应的前面相同的数字也会构成此序列,因此直接结束,而不是继续扫描

70 p t s 70pts 70pts

int T;
int a[B];
void work()
{
	int n=read();
	for (int i=1;i<=n;i++) a[i]=read();
	int ans=0;
	for (int i=1;i<=n;i++)
	{
		int l=i-1,r=i+1;
		int totl=0;
		int totr=0;
		while (a[l]<a[i] && l>0)
		{
			if (a[l]!=a[i]) totl++,l--;
			else l--;
		}
		while (a[r]<=a[i] && r<=n)
		{
			totr++,r++;
		}
		ans+=a[i]*(totl+1)*(totr+1);
		
		l=i-1,r=i+1;
		totl=0;
		totr=0;
		while (a[l]>a[i] && l>0)
		{
			if (a[l]!=a[i]) totl++,l--;
			else l--;
		}
		while (a[r]>=a[i] && r<=n)
		{
			totr++,r++;
		}
		ans-=a[i]*(totl+1)*(totr+1);
	}
	cout<<ans;
}
signed main()
{
	T=1;                                                 
	while (T--) work();
	return 0;
}


单调栈解法

我们当前需要解决的问题就是快速得到比 a [ i ] a[i] a[i] 大/小的最近值,然而…

单调栈解决的问题是:往后/从前找到第一个比当前位置大/小的位置下标

所以,直接四个单调栈维护即可。

值得注意的是,依旧是暴力遇到的问题:为了防止算重复,我们会维护左端第一个不大于/不小于的位置,即多了相等的情况。

防止以后忘记维护什么样的栈:如果找最大值,那么矮的都不会起作用,所以需要把栈中小于当前值的都去掉,条件就出来了,最小值也是同理。

单调栈板子

for(int i=n;i>=1;i--)//维护从后第一最大值
{
    while (!s.empty() && a[s.top()]<=a[i]) s.pop();
    f[i]=s.empty()?n+1:s.top();
    s.push(i);
}

100pts

stack<int>s[5];
int n;
int a[B];
int f[5][B];
void work()
{
	cin>>n;
	for (int i=1;i<=n;i++) cin>>a[i];
	for (int i=n;i>=1;i--)
	{
		while (!s[1].empty() && a[s[1].top()]<=a[i]) s[1].pop();
		while (!s[2].empty() && a[s[2].top()]>=a[i]) s[2].pop();
		f[1][i]=s[1].empty()?n+1:s[1].top();
		f[2][i]=s[2].empty()?n+1:s[2].top();
		s[1].push(i); s[2].push(i);
	}
	for (int i=1;i<=n;i++)
	{
		while (!s[3].empty() && a[s[3].top()]<a[i]) s[3].pop(); //去重需要用到 
		while (!s[4].empty() && a[s[4].top()]>a[i]) s[4].pop();
		f[3][i]=s[3].empty()?0:s[3].top();
		f[4][i]=s[4].empty()?0:s[4].top();
		s[3].push(i); s[4].push(i);
	}
	int ans=0;
	for (int i=1;i<=n;i++)
	{
		ans+=a[i]*((f[1][i]-1-i+1)*(i-(f[3][i]+1)+1)-(f[2][i]-1-i+1)*(i-(f[4][i]+1)+1));
	}
	cout<<ans; 
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}


ST+分治解法

对于一个序列,假若最大值的位置是 t t t ,那么所产生的贡献就是 t × ( r − t + 1 ) t\times (r-t+1) t×(rt+1)

除此之外,所有包含该最大值的所有子序列都已经产生贡献,只需要知道不包含这个位置的子序列,他们就是在 t 的左侧,和右侧,我们发现可以通过这种方式进行分治,分治的时间复杂度是 O ( n ) O(n) O(n) 的,最小值也是同理。比较巧妙

需要的就是RMQ维护最大最小值,同时维护位置,由于这里大小相等的数,位置取谁都无所谓,所以可以用。

RMQ板子

logg[0]=-1;
for (int i=1;i<=n;i++) f[i][0]=a[i],logg[i]=logg[i>>1]+1;
for (int j=1;j<=20;j++)
    	for (int i=1;i+(1<<j)-1<=n;j++)
            	f[i][j]=max(f[i][j-1],f[i+(1<<(j-1)+1)][j-1]);
int s=logg[r-l+1];
cout<<max(f[l][s],f[r-(1<<s)+1][s]);

100 pts 代码

int f[B][23];
int F[B][23];
int G[B][23];//维护下表 
int g[B][23];
int logg[B];
int a[B];
int n;
void st()
{
	logg[0]=-1;
	for (int i=1;i<=n;i++) f[i][0]=a[i],F[i][0]=i,g[i][0]=a[i],G[i][0]=i,logg[i]=logg[i>>1]+1;
	for (int j=1;j<=20;j++)
		for (int i=1;i+(1<<j)-1<=n;i++)
		{
			if (f[i][j-1]>f[i+(1<<(j-1))][j-1])
			{
				f[i][j]=f[i][j-1];
				F[i][j]=F[i][j-1];
			}
			else 
			{
				f[i][j]=f[i+(1<<(j-1))][j-1];
				F[i][j]=F[i+(1<<(j-1))][j-1];
			}
			if (g[i][j-1]<g[i+(1<<(j-1))][j-1])
			{
				g[i][j]=g[i][j-1];
				G[i][j]=G[i][j-1];
			}
			else 
			{
				g[i][j]=g[i+(1<<(j-1))][j-1];
				G[i][j]=G[i+(1<<(j-1))][j-1];
			}
		}
}
int ask(int l,int r,int x)
{
	int s=logg[r-l+1];
	if (x==1) 
	{
		if (f[l][s]>f[r-(1<<s)+1][s]) return F[l][s];
		else return F[r-(1<<s)+1][s];
	}
	else 
	{
		if(g[l][s]<g[r-(1<<s)+1][s]) return G[l][s];
		else return G[r-(1<<s)+1][s];
	}
}
int find_max(int l,int r)
{
	if (l>r) return 0;
	int t=ask(l,r,1);
	return a[t]*(t-l+1)*(r-t+1)+find_max(l,t-1)+find_max(t+1,r); 
}

int find_min(int l,int r)
{
	if (l>r) return 0;
	int t=ask(l,r,0);
	return a[t]*(t-l+1)*(r-t+1)+find_min(l,t-1)+find_min(t+1,r); 
}
void work()
{
	n=read();
	for (int i=1;i<=n;i++) a[i]=read();
	st();
	int ans=0;
	ans+=find_max(1,n);
	ans-=find_min(1,n);
	cout<<ans;
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}

CDQ解法

学CDQ之前我先去看了一下扫描线,复习了一下板子。

扫描线的主要实现方式:对x轴进行离散化,用线段树维护x轴,维护的内容是区间最小值,已经最小值的个数,简单的将因该是维护 0 的个数,这样通过区间总数就可以直接得到区间中不为0的数量,通过枚举y轴的变化,来直接算出不为零的位置对面积所做出的贡献,这样看来就像是一条扫描线。

主要解决的问题是二维面积问题,周长问题。

这里简单的说一下为什么线段树的叶子结点维护的是区间而不是单点,这导致很多写法发生更改,出现 l e n − 1 len-1 len1 情况。

因为受到离散化的影响,位置与位置之间相差可能很大,仅仅表示大小关系,如果叶子结点维护的是点信息的话,合并的时候,区间内部除端点外的其他点无法进行表示,反而,如果用区间来替代,合并就很轻松、

可以认为,这是用离散化之后又想用线段树的必然操作

总的来说,这也不算是板子,最多就是联系了一下数据结构,复习了离散化,线段树

痛苦的是我单单看扫描线就看了一天多才看明白,看了很多文章都看不懂…真菜

P5490 【模板】扫描线 & 矩形面积并,100pts 代码

#define int long long
#define root 1,len-1,1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1 
int T;
int b[B]; 
int len;
namespace Seg
{
	struct node
	{
		int l,r,col,minxx,tot;
		node(){l=r=col=minxx=tot=0;minxx=0x3f3f3f3f;}
		void init(int l_,int r_){l=l_,r=r_,col=0,minxx=0,tot=b[r+1]-b[l];}
	}z[B<<2];
	node operator +(const node &l,const node &r)
	{
		node p;
		p.l=l.l,p.r=r.r;
		p.minxx=min(l.minxx,r.minxx);
		if (p.minxx==l.minxx) p.tot+=l.tot;
		if (p.minxx==r.minxx) p.tot+=r.tot;
		p.col=0;
		return p;
	}
	void color(int rt,int v){z[rt].col+=v;z[rt].minxx+=v;}
	void updata(int rt) {z[rt]=z[rt<<1]+z[rt<<1|1];}
	void push(int rt)
	{
		if (z[rt].col)
		{
			color(rt<<1,z[rt].col);
			color(rt<<1|1,z[rt].col);
			z[rt].col=0;
		}
	}
	void build(int l,int r,int rt)
	{
		if (l==r) {z[rt].init(l,r);return;}
		int m=l+r>>1;
		build(lson);
		build(rson);
		updata(rt);
	}
	void modify(int l,int r,int rt,int nowl,int nowr,int v)
	{
		if (nowl<=l && r<=nowr) {color(rt,v);return;}
		int m=l+r>>1;
		push(rt);
		if (nowl<=m) modify(lson,nowl,nowr,v);
		if (m<nowr) modify(rson,nowl,nowr,v);
		updata(rt);
	}
	node query(int l,int r,int rt,int nowl,int nowr)
	{
		if (nowl<=l && r<=nowr) return z[rt];
		push(rt);
		int m=l+r>>1;
		if (nowl<=m)
		{
			if (m<nowr) return query(lson,nowl,nowr)+query(rson,nowl,nowr);
			return query(lson,nowl,nowr);
		}
		else return query(rson,nowl,nowr);
	}	
} 
int n,m;
struct node
{
	int y;
	int xl,xr;
	int val;
}a[B<<2];
int cmp(node a,node b)
{
	return a.y<b.y;
} 
int tota;
int totb;
void work()
{
	cin>>n;
	for (int i=1;i<=n;i++)
	{
		int xl=read(),yl=read(),xr=read(),yr=read();
		b[++totb]=xl;
		b[++totb]=xr;
		a[++tota]={yl,xl,xr,1};
		a[++tota]={yr,xl,xr,-1};
	}
	sort(b+1,b+1+totb);
	len=unique(b+1,b+1+totb)-(b+1);
	Seg::build(root);
	for (int i=1;i<=tota;i++)
	{
		a[i].xl=lower_bound(b+1,b+1+len,a[i].xl)-b;
		a[i].xr=lower_bound(b+1,b+1+len,a[i].xr)-b;
	}
	sort(a+1,a+1+tota,cmp);
	int ans=0;
	for (int i=1;i<=tota;i++)
	{
		if (a[i].xl!=a[i].xr) Seg::modify(root,a[i].xl,a[i].xr-1,a[i].val);
		Seg::node Ans=Seg::query(root,1,len-1);
		ans+=((b[len]-b[1])-(Ans.minxx==0)*Ans.tot)*(a[i+1].y-a[i].y);
	}
	cout<<ans;
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}

CDQ 分治解法

…怎么说,题解说板子题,我很蒙蔽。

维护区间最大值之和或者最小值之和,是CDQ的板子????

我只能说和CDQ很像,但是很巧妙。

首先,分治计算前半部分作为左端点,右半部分作为右端点对答案做出的贡献,

解释一下题解,我的看法是,计算了后半部分最为最大值所产生的贡献,这要满足其中一半所有形成的贡献,那么就可以全部解决

那么,枚举有右半部分,维护最大值,可以发现最大值只增不降,然后左边部分从中间开始找到第一个比后边最大值大的位置,此时这一块区间就可以计算了,那么对于更往前的部分,都是由前半部分提供最大值,我们可以在枚举前半部分的时候维护后缀最大值的和,然后算区间最大值的和
a n s + = s u m [ l ] − s u m p [ p − 1 ] + m a x x × ( m i d − p ) ans+=sum[l]-sump[p-1]+maxx\times (mid-p) ans+=sum[l]sump[p1]+maxx×(midp)
其中 p 就是第一个最大值。

可以解决的原因是,解决了必须带有后半部分的所有子序列答案,

为了满足该条件,我们就会发现计算有两种,一种是最大值在右侧,一种是最大值在左侧,我们可以维护右侧最大值,然后找最后一个满足要求的左端点,剩下的部分就是最大值在左侧的情况,此时我们发现需要做的就是维护区该区间的最大值之和。并且经发现,随着后半部分长度增加,最大值只增不减,导致左端点呈现单调性,所以可以用指针在 O ( n ) O(n) O(n) 时间内完成。

最后两段才是重点,也是分治的本质:完成属于他的一部分,不重不漏。

#include<bits/stdc++.h>
//#define int long long
using namespace std;
int read(){int x;scanf("%d",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int n;
int a[B]; 
int ans;
int sum[B];
void solve1(int l,int r)
{
	if (l==r) return;
	int mid=l+r>>1;
	sum[mid+1]=0;
	int maxx=-0X3f3f3f3f;
	for (int i=mid;i>=l;i--)
	{
		maxx=max(maxx,a[i]);
		sum[i]=sum[i+1]+maxx;
	}
	int p=mid;
	maxx=-0x3f3f3f3f;
	for (int i=mid+1;i<=r;i++)
	{
		maxx=max(a[i],maxx);
		while (l<=p && a[p]<=maxx) p--;
		ans+=(sum[l]-sum[p+1])+(mid-p)*maxx;	 
	}
	solve1(l,mid);
	solve1(mid+1,r);
}
void solve2(int l,int r)
{
	if (l==r) return;
	int mid=l+r>>1;
	sum[mid+1]=0;
	int maxx=0x3f3f3f3f;
	for (int i=mid;i>=l;i--)
	{
		maxx=min(maxx,a[i]);
		sum[i]=sum[i+1]+maxx;
	}
	int p=mid;
	maxx=0x3f3f3f3f;
	for (int i=mid+1;i<=r;i++)
	{
		maxx=min(a[i],maxx);
		while (l<=p && a[p]>=maxx) p--;
		ans-=((sum[l]-sum[p+1])+(mid-p)*maxx);	 
	}
	solve2(l,mid);
	solve2(mid+1,r);
}
void work()
{
	cin>>n;
	for (int i=1;i<=n;i++) a[i]=read();
	solve1(1,n);
	solve2(1,n);
	cout<<ans;
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}

历时两天,我终于写完了这几种写法

数字填充

  • 给出一个字符串,其中有数字和下划线,现在填写下划线数字,要求是13的倍数,求方案数

这一看就是数位DP,当时就没有思路,所以直接去看了答案,不过答案真的是很low

我没有想到第二位维护的是余数,有了余数就可以判断是否是倍数,并且还可以转移,转移之间余数的变化和数字的变化一样,都 $\times 10 $ 然后在对 13 13 13 取模,好妙啊~

新知识:判断一个数是否是x的倍数,只需要看余数是否为0,而当数字发生增加或者减少时,其余数变化同数字变化相同,换句话或,有用的部分只有余数部分,无论主要部分怎么变化

更简练的说:当数字发生乘法变化时,其余数变化同数字变化相同,主要部分依旧满足整除条件。

30pts

#include<bits/stdc++.h>
#define int long long
using namespace std;
int read(){int x;scanf("%lld",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
string s;
int tot;
int n;
int ans;
void dfs(int now)
{
	if (now==n+1)
	{
		if (tot%13==0) ans++;
		return; 
	}
	if (s[now-1]=='_')
	{
		for (int i=0;i<=9;i++)
		{
			tot=tot*10+i;
			dfs(now+1);
			tot/=10;
		}
	}
	else 
	{
		tot=tot*10+s[now-1]-'0';
		dfs(now+1);
		tot/=10;
	}
}
void work()
{	
	cin>>s;
	n=s.size();
	dfs(1);
	cout<<ans;
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}

100pts

#define int long long
using namespace std;
int read(){int x;scanf("%lld",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
int T;
int f[B][50];
int n;
string s;
void work()
{
	cin>>s;
	n=s.size();
	if (s[0]=='_')
	{
		for (int i=0;i<=9;i++) f[1][i]=1; 
	}
	else f[1][s[0]-'0']=1;
	for (int i=1;i<n;i++)
	{
		for (int j=0;j<=12;j++)
		{
			if (s[i]!='_')
			{
				int k=s[i]-'0';
				int w=(j*10+k)%13;
				f[i+1][w]+=f[i][j];
				f[i+1][w]%=mod;
			}
			else
			{
				for (int k=0;k<=9;k++)
				{
					int w=(j*10+k)%13;
					f[i+1][w]+=f[i][j];
					f[i+1][w]%=mod;
				}
			}
		}
	}
	cout<<f[n][0]%mod;
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}

子集和(三)

题目大意

  • 给出一个序列,求子序列之和是 k 的倍数的数量
  • $1\le n\le 10^3 $
  • 1 ≤ k ≤ 1 0 5 1\le k\le 10^5 1k105

思路

受到上面题目的原因,可以直接将倍数转化成余数问题,然后看到数据不是很大, 所以可以考虑DP,

f [ i ] [ j ] f[i][j] f[i][j] 表示前 i i i 个数最后余数为 j j j 的方案数。

转移有两个方向,就是当前选和不选
f [ i ] [ j ] → f [ i + 1 ] [ ( j + a [ i + 1 ] ) m o d    k ] f [ i ] [ j ] → f [ i + 1 ] [ j ] f[i][j]\to f[i+1][(j+a[i+1])\mod k] \\ f[i][j]\to f[i+1][j] f[i][j]f[i+1][(j+a[i+1])modk]f[i][j]f[i+1][j]

空间感觉刚刚好

/*
60分好拿 其实拿了100
*/
const int B=1e6+10;
const int mod=1e9+7;
const int inf=0x3f3f3f3f;
int T;
int n,k;
int f[1009][100009];
int a[B];
void work()
{
	cin>>n>>k;
	for (int i=1;i<=n;i++) a[i]=read();
	f[0][0]=1;
	for (int i=0;i<n;i++)
	{
		for (int j=0;j<k;j++)
		{
			int x=(j+a[i+1])%k;
			f[i+1][x]+=f[i][j]%mod;
			f[i+1][j]+=f[i][j]%mod;
			f[i+1][x]%=mod; 
			f[i+1][j]%=mod;
		}
	}
	cout<<(f[n][0]-1+mod)%mod;
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}

社交软件

题目大意

  • 给出三种操作
    • + x y 表示 x , y x,y x,y 之间是朋友关系
    • - x y 表示 x , y x,y x,y 之间解除朋友关系
    • ! x 表示 x x x 向朋友们群发图片
  • 朋友的朋友不是朋友,即仅表示二元关系
  • 求每个人收到图片的数量

思路

对于一个建立操作和解除操作,在这区间内, x , y x,y x,y 发布的照片都会记录答案,所以就是求出 x , y x,y x,y 在区间内部发布图片的数量,我们可以用前缀和进行维护,不难发现,维护后缀和更加的容易,因为会存在没有解除的情况,此时就变成了后缀和。

前缀和思想,但是我依旧没做出来,我真的耗材~~~

int T;
int f[B];
int g[B];
struct node
{
	char c;
	int x,y;
}a[B];
int n,m; 
void work()
{
	cin>>n>>m;
	for (int i=1;i<=m;i++)
	{
		cin>>a[i].c;
		if (a[i].c=='!') cin>>a[i].x;
		else cin>>a[i].x>>a[i].y;
	}
	for (int i=m;i>=1;i--)
	{
		if (a[i].c=='!')
		{
			f[a[i].x]++;
		}
		else
		{
			if (a[i].c=='+')
			{
				g[a[i].y]+=f[a[i].x];
				g[a[i].x]+=f[a[i].y];
			}
			else
			{
				g[a[i].y]-=f[a[i].x];
				g[a[i].x]-=f[a[i].y];
			}
		}
	}
	for (int i=1;i<=n;i++)
	{
		cout<<g[i]<<" "; 
	}
}
int main()
{
	T=1;
	while (T--) work();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值