单次比赛参与人数能超过 1000 人的 ECNU Online Judge 月赛真的值得参加吗?
EOJ (ECNU Online Judge) 是由华东师范大学程序设计竞赛队员自主开发并维护的在线程序评测系统,该系统已拥有 14 年的历史,是国内最早的一批 OJ。目前已被广泛地用于我校的作业提交、考试、竞赛训练和各类校赛的举办。
为了推动我校和各兄弟院校在程序设计竞赛上的训练,我们于 2017 年 12 月推出了月赛。至今,EOJ 已经成功连续举办几十场月赛,即将到达四周年。最初,所有月赛的题目都是我校现役队员利用业余时间精心设计和准备的,其中不乏有交互题、构造题等在国内还不太常见的题型。现在我们联合了华师大二附中一起参与出题,同时开始开设 IOI 赛制的月赛,开始面向高中信息学竞赛学生提供竞赛训练和交流的平台。
EOJ 内置了一套积分系统,月赛的排名会对用户的积分产生影响,并在网站上汇总出一个不断更新的总排名。按照积分分段,在 EOJ 上的用户名的颜色也会不同,从高到低有红名、橙名、紫名、蓝名、绿名等等。
在 EOJ 上做月赛,你不仅能和红名的大神同场竞技,还能看到自己的积分变化曲线,看到自己的进步历程。你也许现在只能做一道题,但要相信,经过持之以恒的训练,你也能和大牛们一样,站到排行榜的最顶端。
在 ECNU Online Judge 注册即可获得每月月赛邮件通知。
同时,因为最近 EOJ Monthly 比赛获得了图森未来的全程赞助,所以比赛中排名优越的同学也将获得精美的奖品
我在19-20的两年内作为月赛的负责人,组织和贡献了不少题目。
为了让大家更好的感受比赛,今天我们来看一套 2019年7月的月赛题目。
比赛链接
https://acm.ecnu.edu.cn/contest/191/
题目贡献人及算法分布
# | Tag | Idea | Developer | Tester |
---|---|---|---|---|
A | 线性基 高斯消元 | cs2017 Xiejiadong | Xiejiadong | Weaver_zhu |
B | 简单数学 贪心 | Xiejiadong | Xiejiadong | Kilo_5723 oxx1108 |
C | 数学 尺取法 | blunt_axe | Weaver_zhu Kilo_5723 | Kilo_5723 oxx1108 |
D | 送分 签到 | Xiejiadong | Xiejiadong | Kilo_5723 |
E | 树形DP | kblack | kblack | Xiejiadong |
Problem A
考虑素因数分解。如果几个数乘起来是一个完全平方数,则其各个素因子幂次都是偶数。
一个区间
任意取数乘起来得到完全平方数则等价于,一些表示素因子幂次奇偶性的二进制串异或起来是否为
。于是可以考虑使用线性基解决问题。
对于本问题,如果答案为
,则需要求出最大的
使得
任意取数异或上
的二进制串为
( 因为
是必须取得 )。
考虑线性基的部分,
以内的素数个数是
数量级的,我们需要
长度的二进制串吗?实际上,
以内的素数不超过
个,而
以上的素因子,最多只有一个。
于是我们的线性基只是需要
左右数量的
位二进制串外加一个整数 ( 表示
以上的素数 )。
加入一个数的时间复杂度是
的,我们从大到小检测每次加入一个数后,是否有新的
的素因子在线性基中可以被取到。
这样枚举每个
是
的。如果用
实现二进制串,空间也是足够的。
#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
const int C = 1005;
int d[1000010];
int pid[1000010];
int m, smallCount;
bool have[100010];
ull g[100010][3],a[3],b[3];
int aBig,bBig;
int bIt;
void init()
{
m=0;smallCount=0;aBig=0;bBig=0;bIt=0;
memset(d,0,sizeof(d));
memset(pid,0,sizeof(pid));
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
memset(g,0,sizeof(g));
memset(have,0,sizeof(have));
}
void precalc()
{
for (int x=2;x<1000003;x++)
{
if (x==C) smallCount=m;
if (d[x]!=0) continue;
pid[x]=m++;
for (int y=x;y<1000003;y+=x)
if (d[y]==0) d[y]=x;
}
return;
}
void Set(ull* A,int p)
{
A[p>>6]^=(ull)1<<(p&63);
}
bool get(ull* A, int p)
{
return (A[p>>6]>>(p&63))&1;
}
void getPrimes(int x)
{
for (int i=0;i<3;i++)
a[i]=0;
aBig=-1;
while(x>1)
{
if (d[x]>=C)
{
aBig=pid[d[x]];
x/=d[x];
continue;
}
int y=d[x];
int t=0;
while(x%y==0) x/=y,t^=1;
if (t) Set(a, pid[y]);
}
return;
}
bool moveIter()
{
if (bBig!=-1)
{
if (!have[bBig]) return false;
for (int i=0;i<3;i++)
b[i]^=g[bBig][i];
bBig=-1;
}
while(bIt>=0)
{
if (!get(b,bIt))
{
bIt--;
continue;
}
if (!have[bIt]) return false;
for (int i=0;i<3;i++)
b[i]^=g[bIt][i];
bIt--;
}
return true;
}
void solve(int x)
{
getPrimes(x);
if (aBig!=-1)
{
if (!have[aBig])
{
have[aBig]=1;
for (int i=0;i<3;i++)
g[aBig][i]=a[i];
return;
}
for (int i=0;i<3;i++)
a[i]^=g[aBig][i];
aBig=-1;
}
for (int it=smallCount-1;it>=0;it--)
{
if (!get(a,it)) continue;
if (!have[it])
{
have[it]=1;
for (int i=0;i<3;i++)
g[it][i]=a[i];
return;
}
for (int i=0;i<3;i++)
a[i]^=g[it][i];
}
return;
}
char sw[100];
int main()
{
precalc();
int y;
scanf("%d",&y);
if (d[y]==y)
{
printf("-1
");
return 0;;
}
getPrimes(y);
for (int i=0;i<3;i++)
b[i]=a[i];
bBig=aBig;
bIt=smallCount-1;
int x=y;
while(true)
{
if (moveIter()) {printf("%d
", x);return 0;}
x--;
solve(x);
}
return 0;
}
Problem B
考虑如果有小朋友在
天来了图书馆。
- 可能他是每间隔
天来一次图书馆,于是所有日期为
且满足
的日期都可以由他产生,故不再考虑这些天;
- 如果他不是每间隔
天来一次图书馆,那么他的间隔只能更小,假设他的间隔为
,一定有
,这样一来,他一定在第
天的时候来了图书馆,那个时候我们已经可以知道不用再对
天考虑了,所以不可能存在这样的情况。
于是,我们只需要从小到大依次考虑没有被标记过(不用再考虑)的天数,并从标记他所能取消的天数就可以了。
这样有两种方式维护,一种是对每个天数判断他的约数是否已经存在,时间复杂度
;
也可以用类似于筛法求质数那样做,时间复杂度
。
可能需要特判
的情况。
#include <bits/stdc++.h>
using namespace std;
char sw[100];
int n,a[200010],v[200010];
bool check(int x)
{
for (int i=1;i<=(int)sqrt((double)x);i++)
{
if (x%i==0&&v[i]) return true;
if (x%i==0&&v[x/i]) return true;
}
return false;
}
int main(){
scanf("%d",&n);
if (n==1) {puts("1");return 0;}
for (int i=1;i<=n;i++)
scanf("%d",&a[i]);
memset(v,0,sizeof(v));
int ans=0;
for (int i=2;i<=n;i++)
if (!check(a[i]-1)) ans++,v[a[i]-1]=1;
cout<<ans<<endl;
}
Problem C
解法1
首先进行初步的分析。不难发现可以将「星星会在时刻
闪烁」的条件替换成「星星会在时刻
闪烁」,这不影响答案。这是因为如果在时刻
选中的星星都会闪烁,那么在时刻
它们也都会闪烁。这样,我们就可以预先把输入进来的
全部对
取模。
然后发现这个题目本质是询问一个区间的线性同余方程组是否有解。也就是询问:
是否有解。
注意到不能直接使用 ExCRT 的方法计算答案,因为答案可能很大。但是这题只需要我们判断方程组是否有解,所以无需算出答案。
可以证明:一个线性同余方程组有解当且仅当其中的任意两个方程形成的方程组都有解。原因是一个线性同余方程组的所有解一定可以写成满足「
(
是素数)」这样一组的条件的任何数。如果一个方程组没有解,那么一定存在「
,并且
,其中
」这两个限制。而对于这两个限制,一定能找到两个方程,它们分别包含了两个限制的信息。也就是说,如果线性同余方程组无解,那么一定存在两个方程,它们组成的方程组无解。换句话说,如果任意两个方程形成的方程组都有解,则整个方程组一定有解。
我们暴力枚举每两个方程,判断它们是否矛盾即可。使用裴蜀定理即可证明它们不矛盾当且仅当
整除
。时间复杂度
。
考虑优化算法。看到了
以后容易想到枚举因数。枚举因数
,考虑所有
是
倍数的星星。如果所有
都相同的话,就没有矛盾。这样一次的复杂度是
的,但是由于我们只需要枚举
是素数或素数的几次方的情况,复杂度可以降为
。于是总共的时间复杂度减少到
。
之前的算法似乎没什么前途,考虑换一种思路。
对于每组限制
,我们将
素因数分解:
。然后,对于每一个
,将其拆成
个限制,模数分别等于
,余数等于
对它们取模的结果。对于一组
,我们最多会将其拆成
组限制。实现素因数分解时可以使用线性筛。
拆分后的好处是什么呢?发现如果两个方程矛盾,那么从它们中一定可以各自选出一个限制,满足限制的模数相同,但余数不同。
这样我们就把问题转化成了:给定
组有序数对
,每次询问一个区间内是否存在
相同且
不同的一组数对。
不难发现对于每一个点
,一定存在一点
使得以
为右端点的区间
中所有
的答案都为 Yes
,而
的区间答案都为 No
。考虑用 2 - pointers 预处理出
。由于具体过程叙述起来较为复杂,请读者自行思考,也可以参考标程。
时间复杂度
。
解法2
尝试维护一个集合,保证其中所有星星都能同时闪烁。考虑何种情况下集合满足这个性质。
我们发现,对每一个质数
,对所有的
使得
,需要让每一个
互不冲突,其中
。而不冲突的定义为:令最大的
,对应的
,则对所有的
都有
。
利用这个性质,滑动窗口维护对每一个
,最大的
使得
中的星星能同时闪烁。
#include<iostream>
#include<cstdio>
#include<map>
using namespace std;
const int maxn=1e6+5,maxm=1e7+5,maxp=7e5+5;
struct num{
int mod,cnt;
num(int _m=0,int _c=0){
mod=_m; cnt=_c;
}
};
int prime[maxp],mindiv[maxm],nextp[maxm],id[maxm],siz;
int divs[maxn][10],mods[maxn][10],sizs[maxn];
int ans[maxn];
map<int,num> stat[maxp];
void init(){
int i,j;
int a,b;
siz=0;
for (i=1;i<maxm;i++) mindiv[i]=i;
for (i=2;i<maxm;i++){
if (i==mindiv[i]){
id[i]=siz;
prime[siz++]=i;
}
for (j=0;j<siz&&prime[j]<=mindiv[i]&&i*prime[j]<maxm;j++)
mindiv[i*prime[j]]=prime[j];
}
for (i=2;i<maxm;i++){
a=mindiv[i]; b=i/a;
nextp[i]=(b%a)?b:nextp[b];
}
}
void make(int div[],int mod[],int &siz,int pos,int val){
siz=0;
while (pos!=1){
div[siz]=pos/nextp[pos];
mod[siz]=val%div[siz];
siz++;
pos=nextp[pos];
}
}
bool violate(int div[],int mod[],int siz){
int i;
int diva,divb,moda,modb;
map<int,num>::iterator it;
for (i=0;i<siz;i++){
while (stat[id[mindiv[div[i]]]].size()){
it=stat[id[mindiv[div[i]]]].end(); it--;
if (it->second.cnt) break;
stat[id[mindiv[div[i]]]].erase(it);
}
if (!stat[id[mindiv[div[i]]]].size()) continue;
it=stat[id[mindiv[div[i]]]].end(); it--;
diva=it->first; divb=div[i];
moda=it->second.mod; modb=mod[i];
if (diva<divb){
swap(diva,divb); swap(moda,modb);
}
if (moda%divb!=modb) return true;
}/*
if (cnt[divs[pos][i]]&&mods[pos][i]!=val[divs[pos][i]])
return true;*/
return false;
}
void add(int div[],int mod[],int siz){
int i;
for (i=0;i<siz;i++){
if (stat[id[mindiv[div[i]]]].find(div[i])!=stat[id[mindiv[div[i]]]].end())
stat[id[mindiv[div[i]]]][div[i]].cnt++;
else
stat[id[mindiv[div[i]]]][div[i]]=num(mod[i],1);
}
}
void del(int div[],int mod[],int siz){
int i;
for (i=0;i<siz;i++)
stat[id[mindiv[div[i]]]][div[i]].cnt--;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out2.txt","w",stdout);
init();
int i,n,q;
int l,r,t;
int x,y;
scanf("%d",&n);
for (i=1;i<=n;i++){
scanf("%d%d",&x,&y);
make(divs[i],mods[i],sizs[i],y,x);
}
l=1; r=1;
while (l<=n){
while (r<=n&&!violate(divs[r],mods[r],sizs[r])){
add(divs[r],mods[r],sizs[r]);
r++;
}
ans[l]=r-1;
del(divs[l],mods[l],sizs[l]);
l++;
}
scanf("%d",&q);
for (i=0;i<q;i++){
scanf("%d%d",&l,&r);
puts(ans[l]>=r?"Yes":"No");
}
return 0;
}
Problem D
我们可以考虑以
格子作为原点建立坐标系。
于是子矩阵就相当于限制了变量
和
的范围,对角线变成了直线
和
。
问题变成了求给定的范围内的整点个数。
需要注意矩阵边长为奇数的时候,矩阵正中央的格子会被重复计算。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL n,q,a,b,c,d;
char sw[100];
int main()
{
ios::sync_with_stdio(false);
cin>>n>>q;
while(q--)
{
LL ans=0;
cin>>a>>b>>c>>d;
if (n%2==1)
{
if (a<=n/2&&c>=n/2&&b<=n/2&&d>=n/2)
ans--;
}
LL l=max(a,b),r=min(c,d);
ans+=max(0LL,r-l+1);
b-=(n-1LL),d-=(n-1LL);
b=-b,d=-d;
l=max(d,a),r=min(b,c);
ans+=max(0LL,r-l+1);
cout<<ans<<endl;
}
return 0;
}
Problem E
状态无非分为两种:理论状态(所有门元器件正常的时候的输出)、实际状态。
于是我们令
表示门元器件
的理论输出 为
,实际输出为
的方案数。
理论和实际状态都为
的是最好处理的,因为理论
的状态只能是所有的输入为
。
理论的状态为
,实际状态为
,那么对于理论状态只需要任意一个输入或者多个输入为
即可。这个处理起来会有点麻烦,但是我们可以通过输入为
或者
的状态减去 输入全
的状态快速得到。
理论的状态为
,实际状态为
的情况和上一种情况类似的处理。
而对于最后一类状态,即理论和实际都为
的状态,我们通过所有的状态减去前面三个状态转移即可。
#include <bits/stdc++.h>
using namespace std;
#define LL long long
using namespace std;
inline char nc(){
/*
static char buf[100000],*p1=buf,*p2=buf;
if (p1==p2) { p2=(p1=buf)+fread(buf,1,100000,stdin); if (p1==p2) return EOF; }
return *p1++;
*/return getchar();
}
inline void read(int &x){
char c=nc();int b=1;
for (;!(c>='0' && c<='9');c=nc()) if (c=='-') b=-1;
for (x=0;c>='0' && c<='9';x=x*10+c-'0',c=nc()); x*=b;
}
inline void read(LL &x){
char c=nc();LL b=1;
for (;!(c>='0' && c<='9');c=nc()) if (c=='-') b=-1;
for (x=0;c>='0' && c<='9';x=x*10+c-'0',c=nc()); x*=b;
}
inline void read(char &x){
for (x=nc();!(x=='('||x==')');x=nc());
}
inline int read(char *s)
{
char c=nc();int len=1;
for(;!(c=='('||c==')');c=nc()) if (c==EOF) return 0;
for(;(c=='('||c==')');s[len++]=c,c=nc());
s[len++]='