Codeforces Round #885 数学专场

发布时间 2023-07-24 22:06:21作者: weakpyt

妙,我只能说妙。今天补DEF发现除了F诡秘的杨辉三角,我都能独立做出来。

但为什么我感觉DE难度不如C!!!!

A

题意:一个人站在 \((x,y)\) 处,而其他人分别在 \((x_1,y_1)\dots (x_n,y_n)\),每一次这个人先移动一步到上下左右四个格子,然后其他 \(n\) 个人再移动一步,求是否永远这个人与其他人不会走到一个位置。

题解:

对于永远等敏感字眼,我们一般是找题目中的不变量亦或者变量之间的关系

在这个题中,注意到每移动一步之后,每个人的横纵坐标和的奇偶性都会改变,所以若两个人最初坐标奇偶性不同,则永远无法相遇,反过来,两个人最初坐标一致,则无论前者怎么走,后者都有走法使得曼哈顿距离不增大,而前者撞墙之后,后者就会缩小距离,最终相遇。

所以若存在 \(x_i+y_i\bmod 2=x+y\bmod 2\),则输出 “NO”,否则输出“YES”。

int n,x,y,k,m,a[105][105];
signed main(){
	int t;cin>>t;
	while(t--){	
		cin>>n>>m>>k>>x>>y;
		int tag=0,s=(x+y)&1;
		for(int i=1;i<=k;i++){
			int x,y;cin>>x>>y;
			if(((x+y)&1)==s)tag=1;
		}
		if(tag)cout<<"No\n";
		else cout<<"Yes\n";
	}
}

B

题意:有 \(n\) 块木板排成一排,第 \(i\) 块木板的颜色是 \(c_i\),你站在第一块木板前面,需要跳跃到第 \(n\) 块木板后面,每一次只能跳相同颜色的木板。现在你可以更改一块木板的颜色,使得你每一次跳跃的距离(指两块木板中间部分,不计两端点)的最大值最小。

“最大的最小”却不是二分答案,有意思。

在本题中,由于每次只能跳同一个颜色,必然是每一个颜色单独处理。

先将所有点按颜色为第一关键字,下标为第二关键字进行排序,将同色木板分成一组,考虑单独处理这一组。

设这一组木板下标分别为 \(a_1,a_2…a_t(a_1<a_2<\dots <a_t)\),则每两块木板间的距离为 \(a_i-a_{i-1}-1\),特别地,最后一块木板里边界的距离为 \(n-a_t\)

\(d_i=a_i-a_{i-1}\),特别地,\(d_{t+1}=n-a_t\)

先考虑不重新涂色,答案为 \(\max_{i=1}^{t+1}\lbrace d_i\rbrace\),若重新涂色,则必定是在 \(d_{\max}\) 所代表的两块木板中间,以消去这个最大值,则 \(d'_{\max}=\left\lceil \frac{d_{\max}}{2}\right\rceil\),最后再取最大值即可。

那么对于每个颜色都这样处理,最后取最小就是答案。

#define N 505050
#define int long long
#define pr pair<int,int>
#define mk make_pair
int n,x,y,k,m,pre[N],cnt[N];
pr c[N];
int solve(int l,int r){
	int tot=0;
	for(int i=l;i<=r;i++)cnt[++tot]=c[i].second;cnt[++tot]=n+1;
	int ans=0,mx=0,cmx=0;
	for(int i=tot;i;i--)cnt[i]=cnt[i]-cnt[i-1];
	for(int i=1;i<=tot;i++){
		mx=max(mx,cnt[i]);
	}
	for(int i=1;i<=tot;i++){
		if(mx==cnt[i]){
			cnt[i]=(cnt[i]+1)/2;mx=-1;
		}
		ans=max(ans,cnt[i]);
	}
	return ans-1;
}
signed main(){
	int t;cin>>t;
	while(t--){	
		read(n);int ans=0x3f3f3f3f,s;read(s);
		for(int i=1;i<=n;i++)read(c[i].first),c[i].second=i;
		sort(c+1,c+n+1);int l=0,r=0;c[0]=c[n+1]=mk(0,0);
		for(int i=1;i<=n;i++){
			if(c[i].first!=c[i-1].first)l=i;
			if(c[i].first!=c[i+1].first){
				ans=min(ans,solve(l,i));
			}
		}
		cout<<ans<<"\n";
	}
}

C

题意:

给定数组 \(a,b\),定义一次操作如下:

\(c_i=|a_i-b_i|\),得到 \(a'_i=b_i,b'i=c_i\)

求是否可能通过 \(k\) 次操作,使得所有的 \(a_i\) 都为零。如果可能,输出“YES”,否则输出“NO”。

首先,可以发现每一个 \(i\) 是独立的。(分离独立项原则

则我们可以考虑求出对于每一个 \(i\)\(ans_i\) 所需要满足的条件,最后判断是否存在通解即可。

现在我们来求解 \(ans_i\),将 \(a_i,b_i\) 简记为 \(a,b\)

\(f_0=a,f_1=b,f_i=|f_{i-2}-f_{i-1}|(i\ge 2)\),则在第 \(k\) 次操作后,\(a\) 的值为 \(f_{k}\)

注意到若 \(b\) 远大于 \(a\),不妨设 \(b=ax+m(0\le m<a)\),则操作序列呈:

\[a,ax+m,a(x-1)+m,a,a(x-2)+m,a(x-3)+m,a…… \]

不难发现规律,若 \(x\bmod 2=1\),则操作到 \(m,a\) 状态时需要 \(\lfloor\frac{3}{2}x\rfloor\) 次操作,而 \(x\bmod 2=0\),则操作到状态 \(a,m\) 需要 \(\frac{3}{2}x\) ,此时问题化为了同样性质的子问题,启发我们递归求解答案。

这个过程类似于欧几里得算法,不难写出如下递归函数:

int gcd(int a,int b){
    if(a==0)return 0;
    if(b==0)return 1;
    if(a>=b){
        int r=a%b,k=a/b;
        if(k%2==1)return gcd(b,r)+k+k/2;
		else return gcd(r,b)+k+k/2;
    }
    return 1+gcd(b,abs(a-b));
}

注意到在最后 \(a=0\) 后,整个循环节长度变为 \(3\),这就给了我们求通解的机会。

\(ans_i=x_i+3k(k\in\mathbb{N})\),其中 \(x_i\) 是第一次 \(a=0\) 时候的操作数。

则有通解的充要条件显然是所有的 \(x_i\) 模三同余。

注意 \(a_i=b_i=0\)需要特判。

D

找循环节,不变量是解决数学题的有力手段

题意:

给定数 \(a\) 和 操作数 \(k\),每一次可以进行两个操作:

  1. \(a\) 加上其个位数字。
  2. \(a\) 累加到答案中。

求答案的最大值。

贪心策略:所有操作一必然是在最开始进行的,证明显然。

考虑特殊性质:

  1. \(a\bmod 10=5\),此时最多进行一次操作1。
  2. \(a\bmod 10\) 为偶数,此时 \(a\) 的个位数字随着不断地操作1最后回归到 \(2,4,8,6\) 的循环中。
  3. \(a\bmod 10\) 为奇数且不等于 \(5\),此时进行一次操作1即可化为情况2。

对于第一种情况,答案显然为 \(\max(ak,(a+5)(k-1))\)

对于第二种情况。设最优情况下操作1进行的次数 \(c=4x+r(0\le r<4)\),则可以枚举 \(r\),最多枚举四个,顺带更改 \(a,k\),则化为求 \((a+20x)(k-4x)\) 的最值。这显然是个二次函数,利用顶点坐标公式即可求解。

第三种情况可以转化为第二种情况,在此略去。

#define int long long
signed main(){
	int t,a,k;
	cin>>t;
	while(t--){
		cin>>a>>k;
		if(a%10==0){
			cout<<a*k<<"\n";continue;
		}
		if(a%10ll==5ll){
			cout<<max(a*k,(a+5ll)*(k-1ll))<<"\n";continue; 
		}
		int ans=a*k;
		if(a&1ll){
			a+=a%10ll;k--;ans=max(ans,a*k);
		}
		for(int i=0;i<4;i++){
			if(!k)break;
			int aa=-80ll,bb=20ll*k-4ll*a,cc=a*k,x=-1*bb/(2ll*aa);
			if(x<0)x=0;
			x=min(x,k/4);
			ans=max(ans,x*x*aa+bb*x+cc);
			if((x+1)*4<=k){
				x++;ans=max(ans,x*x*aa+bb*x+cc);
			}
			a+=a%10ll;k--;
		}
		cout<<ans<<"\n";
	} 
}

E

挺有意思的问题,但个人觉得赛场上开题顺序如果是 ABEDCF 可能就不会是现在这悲催的结局了。

注意到:

\[ans_i=\sum_{x=1}^{\infty}\sum_{y=x}^{\infty}[\sum_{i=x}^yi=X_i]=\sum_{x=1}^{\infty}\sum_{y=x}^{\infty}[(x+y)(y-x+1)=2X_i] \]

这里长得有点像约数,但不完全是约数。

注意到 \(x+y,y-x+1\) 的奇偶性不同,而若确定了 \(x+y,y-x+1\) 则必定可以求出合法的 \(x,y\)\(2X_i\) 也是偶数,那么其一对奇偶不同的约数即为一组解。我们可以考虑将 \(2\) 强制乘在其中一个因子上,保证其为偶数。

\(ans_i\) 实际等于 \(X_i\) 的奇因子个数。

考虑如何求出 \(X_i\) 的奇因子个数,这等价于 \(X_i\) 去掉质因子2后的约数个数。记 \(e_i\)\(x_i\) 去掉质因子2后的数,则 \(X_i\) 去掉质因子2后的数为 \(c_i=\prod_{k=0}^ie_i\)\(ans_i=d(c_i)\)

直接求显然不现实,可以考虑维护每个质因子出现次数 \(f_i\),则 \(c_i=\prod (f_k+1)\)

这是一个递推的过程,可以对 \(e_i\) 进行分解质因数来完成 \(f\) 的更新和答案计算,注意 \(x_0\) 的范围是 \(10^9\)。可以写出如下代码:

#include<iostream>
using namespace std;
#define N 1005050
#define int long long
int f[N],n,p,x,ans=1;
int power(int a,int b){
	int ans=1;
	while(b){
		if(b&1)ans=ans*a%p;
		a=a*a%p;
		b>>=1; 
	} 
	return ans;
}
int solve(int d){
	while((d&1)==0)d>>=1;
	for(int i=2;i*i<=d;i++){
		int cnt=0;
		while(d%i==0){
//			cout<<"prime: "<<i<<"\n";
			d/=i;++cnt;
		}
		if(cnt){
			if(!f[i]){
				ans=ans*(cnt+1)%p;f[i]=cnt;
			}
			else ans=ans*power(f[i]+1,p-2)%p*(f[i]+cnt+1)%p,f[i]+=cnt;
		}
	}
	if(d>1){
		if(d<=1e6){
			int i=d,cnt=1;
			if(!f[i]){
				ans=ans*(cnt+1)%p;f[i]=cnt;
			}
			else ans=ans*power(f[i]+1,p-2)%p*(f[i]+cnt+1)%p,f[i]+=cnt;
		} 
		else ans=ans*2ll%p; 
	}
	return ans;
}
signed main(){
	cin>>x>>n>>p;
	solve(x);
	for(int i=1;i<=n;i++){
		int d;cin>>d;solve(d);
		cout<<ans<<"\n";
	}
}

但是交上去挂了,为什么?

模数不固定,虽然是质数但可能很小,极有可能不存在逆元。

怎么办?发现我们只需要维护 \(f\) 的连乘积以及对 \(f\) 的单点修改,这完全可以使用线段树。

注意这里的时间复杂度是不正确的(\(O(n\sqrt n\log n)\)),但实际远远跑不满,再加上将 \(10^6\) 的值域范围先筛一遍质数变成 \(10^5\) 后卡卡可以过。(\(10^6\) 内有 \(78498\) 个质数)。

这里容易被大质数卡得分解过程爆炸,可以特判一下。

#pragma GCC opzitime(3)
#include<iostream>
using namespace std;
#define N 1005050
#define int long long
int f[N],n,p,x,ans=1,pri[N],v[N],tot,id[N];
struct node{
	int l,r,mul;
}t[N<<2];
#define lc x<<1
#define rc x<<1|1
void read(int &x){
	x=0;char ch=getchar();
	while(ch>'9'||ch<'0'){
		ch=getchar();
	}
	while('0'<=ch&&ch<='9'){
		x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
	}
}
void build(int x,int l,int r){
	t[x].l=l,t[x].r=r,t[x].mul=1;
	if(l==r)return ;
	int mid=l+r>>1;
	build(lc,l,mid);
	build(rc,mid+1,r);
}
void updata(int x,int pos,int k){
	if(t[x].l==t[x].r){
		t[x].mul+=k;
		t[x].mul%=p;
		return ;
	}
	if(t[lc].r>=pos)updata(lc,pos,k);
	else updata(rc,pos,k);
	t[x].mul=t[lc].mul*t[rc].mul%p;
}
void solve(int d){
	while((d&1)==0)d>>=1;
	if(d<=1e6&&v[d]==0){
		updata(1,id[d],1);return ;
	}
	for(int i=2;i<=tot;i++){
		int cnt=0;
		while(d%pri[i]==0){
			d/=pri[i];++cnt;
		}
		updata(1,i,cnt);
		if(d<pri[i])break;
		if(pri[i]*pri[i]>d){
			if(d>1e6)ans=2;
			else if(v[d]==0){
				updata(1,id[d],1);
			}
			break;
		}
	}
}
void init(){
	v[1]=1;
	for(int i=2;i<=1e6;i++){
		if(!v[i]){
//			cout<<"pri: "<<i<<"\n";
			pri[++tot]=i;id[i]=tot;
		}
		for(int j=i;j<=1e6;j+=i){
			if(i!=j)v[j]=1;
		}
	}
}
signed main(){
	read(x),read(n),read(p);
	init();
	build(1,1,tot);
	solve(x);
	for(int i=1;i<=n;i++){
		int d;read(d);solve(d);
		printf("%lld\n",ans*t[1].mul%p);
	}
}

F

现在以 \(a_0\) 经过操作的变化为例,依次呈现出如下变化:

\(0: a'_0=a_0\)

\(1: a'_0=a_0\oplus a_1\)

\(2: a'_0=a_0\oplus a_2\)

\(3: a'_0=a_0\oplus a_1\oplus a_2\oplus a_3\)

\(4: a'_0=a_0\oplus a_4\)

\(5: a'_0=a_0\oplus a_1 \oplus a_4 \oplus a_5\)

\(6: a'_0=a_0\oplus a_2 \oplus a_4 \oplus a_6\)

\(7: a'_0=a_0\oplus a_1\oplus a_2\oplus a_3 \oplus a_4 \oplus a_5 \oplus a_6\oplus a_7\)

\(8: a'_0=a_0\oplus a_8\)
……
如果说,我们写一个系数矩阵 \(b\)\(n=8\)),有:

\[\left[ \begin{matrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 1 & 1 & 0 & 0 & 1 & 1 & 0 & 0 \\ 1 & 0 & 1 & 0 & 1 & 0 & 1 & 0 \\ 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ \end{matrix} \right] \]

观察到什么了吗?\(b_{i,j}=(b_{i-1,j-1}+b_{i-1,j})\bmod 2\)。事实上这是一个在模2意义下的杨辉三角。

而在最后一行,变成了自己异或自己,则必然有解。-1就是骗人的。。。

我们考虑求出这个最小的解。显然当全部为0之后,后面无论执行多少次操作都是全0。

那么这个值是可二分的,换句话说是可倍增的(题目更适用倍增一些)。

我们考虑从大到小枚举增量 \(p=2^k\),最初 \(p=n\),后不断除2。

考虑求出上一次操作到 \(p\) 次操作之后的数组 \(b\),显然根据表格有 \(a'_i=a_i\oplus a_{i\oplus p}\)

然后判断是否全0即可解决这个问题。复杂度 \(O(n\log_2 n)\)

有没有什么优化空间呢?

注意到每次“有效”的倍增更改后,\(i,i\oplus p\) 这两个位置的数其实都是 \(a_i\oplus a_{i\oplus p}\),换言之这个数组是前后相等的,我们只需保留一半继续处理即可。

时间复杂度 \(O(n)\)

#include<iostream>
using namespace std;
#define N (1<<21)
int a[N],b[N],n;
int main(){
	ios::sync_with_stdio(false);
	cin>>n;for(int i=0;i<n;i++)cin>>a[i];
	int mx=0;for(int i=0;i<n;i++)mx=max(mx,a[i]);
	if(!mx){
		cout<<"0\n";return 0;
	}
	int ans=0;
	while(n>1){
		int m=n>>1;
		for(int i=0;i<m;i++)b[i]=a[i]^a[i+m];
		mx=0;for(int i=0;i<m;i++)mx=max(mx,b[i]);
		if(mx){
			ans|=m;for(int i=0;i<m;i++)a[i]=b[i];
		}
//		else break;
		n=m;
	} 
	cout<<ans+1<<"\n";
}