[省选前集训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);
}
标签:颜色,int,复杂度,sqrt,省选前,leq,2021,include,集训 来源: https://www.cnblogs.com/C202044zxy/p/14616589.html