Codeforces Round #885 数学专场
妙,我只能说妙。今天补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)\),则操作序列呈:
不难发现规律,若 \(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\),每一次可以进行两个操作:
- 将 \(a\) 加上其个位数字。
- 将 \(a\) 累加到答案中。
求答案的最大值。
贪心策略:所有操作一必然是在最开始进行的,证明显然。
考虑特殊性质:
- \(a\bmod 10=5\),此时最多进行一次操作1。
- \(a\bmod 10\) 为偶数,此时 \(a\) 的个位数字随着不断地操作1最后回归到 \(2,4,8,6\) 的循环中。
- \(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 可能就不会是现在这悲催的结局了。
注意到:
这里长得有点像约数,但不完全是约数。
注意到 \(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\)),有:
观察到什么了吗?\(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";
}