[省选前集训2021] 模拟赛7

题目描述

有一排 \(n\) 个灯,每个灯颜色 \(1\)\(m\),一开始所有灯都是关着的。

\(q\) 次操作,每次改变某种颜色灯的状态,每次操作后查询有多少个极长的开着灯的连续段。

\(1\leq n,q\leq 10^5,1\leq m\leq n\)

解法

\(O(nq)\) 暴力期望 \(13\) 分。

如果一个灯从关变开,那么可以合并左边的连续段和右边的连续段。

所以一个点的贡献只和它左边有没有值,右边有没有值有关,答案是所有点贡献之和。

考虑一个颜色被点亮时,影响的颜色是固定的,而且影响的方式也是固定的,现在我们把颜色作为考虑的单位,所以 \(m\leq 100\) 也能做了,期望得分 \(35\) 分。

然后好像是套路的分块调整复杂度,设影响颜色超过 \(\sqrt m\) 的颜色为大颜色,否则称为小颜色:

  • 小颜色对其他颜色的贡献,直接暴力改就行了,时间复杂度 \(O(\sqrt m)\)
  • 大颜色对小颜色的贡献,在小颜色的时候暴力查一下,每次最多查 \(O(\sqrt m)\) 个大颜色,时间复杂度 \(O(\sqrt m)\),大颜色对大颜色的贡献,直接暴力改可以做到 \(O(\sqrt m)\)

哈哈,总时间复杂度 \(O(q\sqrt m)\)(设 \(n,m\) 同阶)

#include <cstdio>
#include <vector>
#include <iostream>
#include <cmath>
#include <map>
using namespace std;
const int M = 100005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || x>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
void write(int x)
{
	if(x<0) {x=-x;putchar('-');}
	if(x<=9) {putchar(x+'0');return ;}
	write(x/10);putchar(x%10+'0');
}
int n,m,q,t,ans,a[M],num[M],fl[M],s[M];
vector<int> g[M],g1[M],g2[M];
void work(int x)
{
	int f=fl[x]==0?1:-1;fl[x]^=1;
	//先修改
	for(int i=0;i<g1[x].size();i++)
		s[g1[x][i]]+=g2[x][i]*f;
	//再查询
	int sum=0;
	if(g[x].size()<t)//小颜色暴力查 
	{
		for(int i=0;i<g[x].size();i++)
			sum+=(fl[g[x][i]]==1?1:0);
	}
	else sum=s[x];
	ans+=f*(num[x]-sum);
}
int main()
{
	freopen("light.in","r",stdin);
	freopen("light.out","w",stdout);
	n=read();m=read();q=read();t=sqrt(n);
	for(int i=1;i<=n;i++)
		a[i]=read();
	for(int i=1,j=1;i<=n;i=j)
	{
		j=i;
		for(;j<=n && a[i]==a[j];j++);
		num[a[i]]++;
		if(i) g[a[i]].push_back(a[i-1]);
		if(j<=n) g[a[i]].push_back(a[j]);
	}
	for(int i=1;i<=m;i++)
	{
		map<int,int> mp;
		for(int j=0;j<g[i].size();j++)
		{
			int v=g[i][j];
			if(g[v].size()>=t) mp[v]++;
		}
		map<int,int>::iterator it=mp.begin();
		for(;it!=mp.end();it++)
		{
			g1[i].push_back((*it).first);
			g2[i].push_back((*it).second);
		}
	}
	for(int i=1;i<=q;i++)
	{
		int c=read();
		work(c);
		printf("%d\n",ans);
	}
}

十字路口

题目描述

\(n\) 个红绿灯,定义周期为红灯持续时间\(+\)绿灯持续时间,已知每一盏灯的周期是相同的,且一开始都是红灯。某个人观察了 \(m\) 次,如果是绿灯则记录下 \(0\),如果是红灯则记录下变为绿灯的时间,但是你不知道每次观测的具体时间,问能否确定周期,如果能确定周期又是多少?

\(1\leq nm\leq 100000,0\leq x_{i,j}\leq10000\)

解法

考试时候做不出来主要是题意有点迷,这时候可以玩一下样例检查一下自己的理解。

\(x_i\) 表示第 \(i\) 次观察的时间,那么根据两次观测的结果可以列出方程,设 \(i\) 行某个灯到绿灯的时间是 \(a\)\(j\) 行这个灯到绿灯的时间是 \(b\),如果 \(a<b\) 那么 \(x_j-x_i=b-a\),这时候建一条 \((i,j)\) 的有向边,那么原图的环长一定是周期的倍数,所以找到原图的最小环就是周期,时间复杂度 \(O(m^3+nm^2)\)

因为本题给的条件是 \(nm\leq 100000\),所以肯定要拿 \(n,m\) 较小的那个来搞以保证复杂度。设 \(y_i\) 表示第 \(i\) 个灯的红灯持续时间,用类似的方法可以求他,时间复杂度 \(O(n^3+mn^2)\)

平衡一下两种方法就可以做到 \(O(nm\sqrt {nm})\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1005;
const int inf = 0x3f3f3f3f;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || x>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
void write(int x)
{
	if(x<0) {x=-x;putchar('-');}
	if(x<=9) {putchar(x+'0');return ;}
	write(x/10);putchar(x%10+'0');
}
int n,m,ans=inf,a[M*M],f[M][M];
int id(int x,int y)
{
	return (x-1)*n+y;
}
int main()
{
	freopen("crossing.in","r",stdin);
	freopen("crossing.out","w",stdout);
	n=read();m=read();//m行n列 
	if(n<m)
	{
		for(int i=1;i<=n*m;i++)
			a[i]=read();
	}
	else
	{
		for(int i=1;i<=m;i++)
			for(int j=1;j<=n;j++)
				a[(j-1)*m+i]=read();
		swap(n,m);
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			f[i][j]=inf;
	for(int i=1;i<=m;i++)
		for(int j=1;j<=n;j++)
			for(int k=1;k<=n;k++)
				if(a[id(i,j)] && a[id(i,k)]>a[id(i,j)])
					f[j][k]=min(f[j][k],a[id(i,k)]-a[id(i,j)]);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			for(int k=1;k<=n;k++)
				f[j][k]=min(f[j][k],f[j][i]+f[i][k]);
	for(int i=1;i<=n;i++)
		ans=min(ans,f[i][i]);
	if(ans==inf) puts("-1");
	else printf("%d\n",ans);
}

密室逃脱

题目描述

\(n\) 个房间编号为 \(1,n\),有 \(n-1\) 的通道,第 \(i\) 个通道连接房间 \(i\)\(i+1\),通道正常情况下是关闭着的,要打开第 \(i\) 个通道需要有 \(a_i\) 个人在房间 \(i\) 按住开关或者 \(b_i\) 个人在房间 \(i+1\) 按住开关,按开关的人不能进行其他操作(比如移动和按另一个开关),一旦松开开关通道会立即关上。

在房间 \(1\) 有一个通道通往出口,需要 \(m\) 个人按住开关,你想知道在保证这个通道无论如何都不会被打开的情况下,最多可以有多少个人(你可以任意指定他们所在的初始房间)

\(1\leq n\leq 1000,1\leq m,a_i,b_i\leq 10000\)

解法

这道题就真的是状态定义的艺术了。

一开始我是从后往前 \(dp\) 的,设 \(dp[i][j]\) 为考虑后 \(i\) 个房间,有 \(j\) 个走到了 \(i\) 房间的最大放置人数,原理是我考虑人从后往前走,但是可能会遇到前面的人去给后面开门的情况,所以就凉了。

正解是从前往后 \(dp\),但是状态定义十分神奇,设 \(f[i][j]\) 为考虑前 \(i\) 个房间,在全局的所有情况下第 \(i\) 个房间最多会有 \(j\) 个人的最大放置人数,这样就巧妙地解决了跑来开门的问题,妙处就是:我们考虑的是部分,但是把全局的情况定义到状态中了,接下来只需要严格按这个东西转移就可以了:

  • \(j<a_i\),可以是后面的人开门到这里来:\(f[i+1][j+b_i]\),或者是两个门之间割裂:\(f[i+1][0\sim b_i-1]\)
  • \(a_i\leq j<a_i+b_i\),前面的人开门到后面去:\(f[i+1][j-a_i]\)
  • \(a_i+b_i\leq j\),可以直接开门到后面去 \(f[i+1][j]\)

时间复杂度 \(O(n\times\max(n,a_i+b_i))\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || x>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,a[M],b[M],f[M][20*M];
int main()
{
	freopen("escape.in","r",stdin);
	freopen("escape.out","w",stdout);
	n=read();m=read();
	for(int i=1;i<n;i++)
		a[i]=read(),b[i]=read();
	for(int i=1;i<m;i++)
		f[1][i]=i;
	for(int i=1;i<n;i++)
		m=max(m,a[i]+b[i]);
	for(int i=1;i<n;i++)
	{
		int s=0,mx=0;
		for(int j=0;j<=m;j++)
		{
			if(j<a[i])
			{
				f[i+1][j+b[i]]=max(f[i+1][j+b[i]],f[i][j]+b[i]);
				mx=max(mx,f[i][j]);
			}
			else if(j<a[i]+b[i])
				f[i+1][j-a[i]]=max(f[i+1][j-a[i]],f[i][j]);
			else
				f[i+1][j]=max(f[i+1][j],f[i][j]);
		}
		for(int j=0;j<b[i];j++)
			f[i+1][j]=max(f[i+1][j],mx+j);
	}
	for(int i=0;i<=m;i++)
		ans=max(ans,f[n][i]);
	printf("%d\n",ans);
}
posted @ 2021-04-04 17:36  C202044zxy  阅读(372)  评论(0编辑  收藏  举报