CSP-S复赛模拟1——孙晨佑补题报告

一:赛时分数

T1【小可的动态数字锁(dynamic)】(60/100)

T2【无可奈何花落去(flower)】(20/100)

T3【小可的数字锁(lock)】(10/100)

T4【翻转同余(equiv)】(0/100)

总分(90/400)炸了(

二、赛事思路

T1(dynamic):

第一眼向量有点被唬住了,但浅浅分析一会(不到半小时)之后发现这个矩阵除了主对角线元素上计算的结果,其他结果都被计算了两遍,故非主对角线上的答案只能是2或0,而答案又要求对总答案%2,于是就想到只计算主对角线上1的个数,操作指令为1或2的时候改变该行/列上主对角线上的元素,操作指令为3的时候就输出就好了。

T2(flower):

看到题之后,首先想到brute force算法,否掉之后想到区间dp,后来否掉了。(不会写)考虑到部分分比较好拿,遂考虑的状态不全。(能得20也是情理之中)

T3(lock):

没啥思路,不会骗分,遂10分。(还挺不错)

T4(equiv):

数论一到编程就炸掉,没思路。但数据范围上提到了a奇偶性,尝试往那上面考虑,但是仍然想不出来啥,就放弃了,故0分。

三、题目正解:

T1(dynamic):

题目重现:

小可十分喜欢线性代数这门课。行列式、矩阵、向量...都让小可沉迷不已。

有一天,老师给了小可一个动态变化的矩阵。这个矩阵所有的元素都是0或者1。

老师告诉小可,这个动态矩阵用于一个重要部门的加密。这个矩阵通过特殊的运算能计算出一个分数。在矩阵的变化过程中计算多次分数形成一个01串,就是这个时刻的动态密码。由于矩阵和变化都是动态的,密码也是动态变化的。老师让小可写一个解密程序。你能帮助小可吗?

矩阵分数的计算方法:

给定一个n*n的矩阵,这个矩阵的分数就是第一行与第一列的向量乘积+第二行与第二列的向量乘积+...+第n行与第n列的向量乘积模2的结果。

假设有一个向量A(x_1,y_1,z_1),向量B(x_2,y_2,z_2),这两个向量的乘积结果为x_1*x_2+y_1*y_2+z_1*z_2

如下图,一个3*3的矩阵:

这个矩阵的分数为:

((1*1+1*0+0*1)+(0*1+1*1+1*0)+(1*0+0*1+0*0)) \mod 2=0

正解思路:

观察数据范围,发现暴力时间复杂度太高,就可以找规律。

可以发现,根据题目中运算的定义,相乘的两个位置一定是沿着主对角线对称的。假设是n*n的矩阵,坐标(i,j)的对称坐标是(j,i)。故只有主对角线的元素会影响最终结果,所以我们可以O(1)地维护主对角线上元素的变化。(和赛时思路大差不差)

AC code:

#include<bits/stdc++.h>
using namespace std;
int a[1005][1005];
int main(){
	int n,cnt=0;
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			cin>>a[i][j];
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(i==j&&a[i][j]==1) cnt++;
		}
	}
	int q;
	cin>>q;
	while(q--){
		int opt;
		cin>>opt;
		if(opt!=3){
			int x;
			cin>>x;
			if(a[x][x]==0) cnt++,a[x][x]=1;
			else cnt--,a[x][x]=0;
		}else{
			cout<<cnt%2;
		}
	}
	return 0;
}

T2(flower):

题目重现:

再美丽的花,终究还是会凋谢。

小明养了四盆花,经历了绚烂的盛开,随着秋风涌起,花瓣渐落。

见证了花开的绚烂,看着花瓣日渐减少,小明逐渐意识到,残缺也是一种美。他打算等花掉落了一些花瓣后,将这四朵花做成标本,永久保留下来。

小明想让第一朵花掉落x_1(L_1\le x_1\le R_1)瓣花瓣,让第二朵花掉落x_2(L_2\le x_2\le R_2)瓣花瓣,让第三朵花掉落x_3(L_3\le x_3\le R_3)瓣花瓣,让第四朵花掉落x_4(L_4\le x_4\le R_4)瓣花瓣。如此,这四朵花放一起,就会有一种残缺的不对称的美。

既然要追求不对称的残缺的美,那么x_!\ne x_2, x_2\ne x_3, x_3\ne x_4, x_4\ne x_1。现在小明想知道,(x_1,x_2,x_3,x_4)这个四元组一共有多少?

正解思路:

这题主打一个容斥原理,考虑以下五种情况:

1、总情况

总情况就随便选,所以答案是  \prod _{i=1} ^{4} (R_i-L_i+1)

2、两两相交的情况

可以发现,这种情况在总情况中被重复计算了两次,所以需要减去(min(R_i,R_j)-max(L_i,L_j)+1)

也就是(i,j)的值分别为(1,2)(2,3)(3,4)(4,1)这四种情况。注意:原题并没有说(1,3)不同等其他情况,但不等关系不存在传递性。

3、三三相交的情况

虽然不等关系不存在传递性,但是显然三三相交的情况一定不合法,因为总共就四个区间。

假设(1,2,3)这三个相同,那么在总情况被算了一次,然后两两相交的时候,(1,2)(2,3)总共又减掉了两次,所以需要再加上一次(因为这是非法情况,应该被算0次)。

假设i,j,k三个区间相同,那么方案为min(R_i,R_j,R_k)-max(L_i,L_j,L_k)+1

4、两对相等,但是对之间不相等

例如1=23=4这种。这种情况在总情况被算一次,在两两相交的时候被减掉两次,所以要加回来一次。

5、四个全部相等

这种情况下在⼀开始被算了⼀次,然后两两相交的时候减掉四次,三三相等加回 来四次,两对相等加了两次,总共被加了三次,所以要减掉三次。 分类讨论整理即可。不过要注意的是,有些区间不存在,这种区间不要进⾏计算。

AC code:

 #include<bits/stdc++.h>
 #define ll long long
 using namespace std;
 const ll mod=998244353;
 int t;
 ll l1,l2,l3,l4,r1,r2,r3,r4;
 ll two(ll a,ll b,ll c,ll d){
     return max(0ll,(min(b,d)-max(a,c)+1)%mod);
 }
 ll thr(ll a,ll b,ll c,ll d,ll e,ll f){
     return max(0ll,(min(b,min(d,f))-max(a,max(c,e))+1)%mod);
 }
 int main(){
     scanf("%d",&t);
     while(t--){
     scanf("%lld%lld%lld%lld%lld%lld%lld%lld",&l1,&r1,&l2,&r2,&l3,&r3,&l4,&r4);
     ll a1=(r1-l1+1)%mod,a2=(r2-l2+1)%mod,a3=(r3-l3+1)%mod,a4=(r4-l4+1)%mod;
     ll all=a1%mod*a2%mod*a3%mod*a4%mod;
     ll f1=(((two(l1,r1,l2,r2)*a3%mod*a4%mod//1==2
     +two(l2,r2,l3,r3)*a4%mod*a1%mod)%mod//2==3
     +two(l3,r3,l4,r4)*a2%mod*a1%mod)%mod//3==4
     +two(l4,r4,l1,r1)*a2%mod*a3%mod)%mod;//4==1
     ll f2=(((thr(l1,r1,l2,r2,l3,r3)*a4%mod//1==2==3√ 
    +thr(l2,r2,l3,r3,l4,r4)*a1%mod)%mod//2==3==4√ 
    +thr(l3,r3,l4,r4,l1,r1)*a2%mod)%mod//1==3==4√ 
    +thr(l4,r4,l1,r1,l2,r2)*a3%mod)%mod;//1==2==4√ 
    ll kf2=(two(l1,r1,l2,r2)*two(l3,r3,l4,r4)%mod//1==2&&3=4√ 
    +two(l1,r1,l4,r4)*two(l2,r2,l3,r3)%mod)%mod;//1==4&&2==3√ 
    ll f4=max(0ll,(min(r1,min(r2,min(r3,r4)))-max(l1,max(l2,max(l3,l4)))+1)%mod);//1
     printf("%lld\n",((all-f1+f2+kf2-f4*3)%mod+mod)%mod);
}
 return 0;
 }

T3(lock):

题目重现:

小可设计了一个数字锁,谁要是成功解开了这个数字锁,就能获得小可准备好的礼物。

小可的数字锁的形状类似井字。如图所示。一共24个格子。每个格子都填了一个数字(只有可能是1、2、3中的一个)。如果图中黄色部分的所有格子的数字都相同,这个锁就被解开了。

我们能对这个锁做的操作有8种。我们看到,图中一共有4段由格子构成的线段。在他们的两个端点都分别有一个带圈的数字。我们按一个带圈的数字,那么这个线段上所有格子的数字都会向这个方向前进一位。原本在最首位的数字会到尾部。

比如:图中的状态,我们按带圈的数字1,相对应的线段:

原本的状态(从上到下看):

2 1 2 3 3 1 1

按下之后(从上到下看):

1 2 3 3 1 1 2

再比如,还是图中的状态,我们按带圈的数字6:

原本的状态(从上到下看):

2 1 2 3 3 1 1

按下之后(从上到下看):

1 2 1 2 3 3 1

请你输出一下如何最快解锁这个数字锁。

正解思路:

考虑使⽤ IDA* 。

我们知道迭代加深搜索,就是给 dfs 加上⼀个深度极限。如果在这个限定了的深度内搜索不到答案,那么 我们就把这个深度极限扩展⼀下,继续搜索。这样的话,第⼀次搜到结果就必然是最短(这个原理等同 于 bfs 第⼀次搜到就是最短)。这样想,迭代加深搜索就是⽤ dfs 去写 bfs 。

但是迭代加深搜索也有⼀个严重的问题,那就是重复性。我们会反反复复地搜索⽐较浅的那些层。这样 我们就需要加⼀些剪枝来加快搜索速度。在迭代加深搜索的基础上,我们设定⼀个估价函数 h() ⽤来描述 当前状态到⽬标状态的估计值,然后⽤ now+h()>depth 来剪枝。这就是 IDA* 。

那么这个题的估价函数怎么设定?我们想⼀下,每⼀步的最优状态,也就是在原有的状态上加⼀个新的 ⽬标数字。

⽐如: 假设原本⻩⾊区域有 5 个 1 , 1 个 2 , 2 个 3 。不管其他地⽅如何,我们最快到达解锁状态的⽅法就是把其他 的数字都转化成 1 。每⾛⼀步,最好的情况也就是把⼀个其他数字换成 1 。也就是说想要达到解锁状态最 少也还需要三步。那么如果这时候,距离搜索深度极限还有 2 步,那么这个搜索就没有必要继续下去 了。因为在限定深度内,即使是最理想的状态也⽆法⾛到解锁的状态。

那么我们的估价函数,就很容易写出来了。我们把⻩⾊部分所有数字的个数统计出来,挑选出⼀个出现 最多的次数,然后⽤ 8 去减,也就是最理想状态还需要⼏步。然后如果⽬前深度加上估价函数⼤于限定 深度,就直接退出。

当然还有⼀个需要注意的地⽅。⽐如我们第⼀步按了带圈数字 1 ,第⼆部按了带圈数字 6 。这两个操作是 相反的,相邻放在⼀起刚好抵消掉了,那么这就是做了⽆⽤功。所以我们还需要加⼀个 pre 来保存⼀下 上⼀步的状态是什么。那么我们之前按了带圈数字 1 ,那么之后都不能按带圈数字 6 了吗?当然不是。这 个问题意想便知,只是相反的操作不能相邻。

AC code :

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int pos[8][8]={{1,3,7,12,16,21,23},{2,4,9,13,18,22,24},{11,10,9,8,7,6,5},{20,19,18,17,16,15,14},{24,22,18,13,9,4,2},{23,21,16,12,7,3,1},{14,15,16,17,18,19,20},{5,6,7,8,9,10,11}};
int center[8]={7,8,9,12,13,16,17,18};
int preStatus[9]={5,4,7,6,1,0,3,2,-1};
int a[30],k,vis[4],flag;
char path[1000];
int evaluate(){
	vis[1]=vis[2]=vis[3]=0;
	for(int i=0;i<8;i++)	vis[a[center[i]]]++;
	return 8-max(vis[1],max(vis[2],vis[3]));
}
void move(int x){
	int tmp=a[pos[x][0]];
	for(int i=0;i<6;i++)	a[pos[x][i]]=a[pos[x][i+1]];
	a[pos[x][6]]=tmp;
}
void idastar(int depth,int pre){
	if(flag)	return ;
	int eva=evaluate();
	if(depth>k||depth+eva>k)	return ;
	if(!eva){
		flag=1;
		path[depth]='\0';
		printf("%s\n%d\n",path,a[center[0]]);
		return ;
	}
	for(int i=0;i<8;i++){
		if(i==preStatus[pre])	continue;
		move(i);
		path[depth]=i+'1';
		idastar(depth+1,i);
		move(preStatus[i]);
	}
}
int main(){
    while(scanf("%d",&a[1]),a[1]){
    	flag=0;
    	for(int i=2;i<=24;i++)	scanf("%d",&a[i]);
    	if(!evaluate()){
    		printf("It's unlocked!\n%d\n",a[center[0]]);
    		continue;
    	}
    	for(k=1;!flag;k++){
    		idastar(0,8);
    	}
    }
    return 0;
}

T4(equiv):

题目重现:

小可在学习同余方面的知识。突发奇想,小可想到了一个奇特的同余形式:

a^b \equiv b^a(\mod 2n)

给定an,小可想知道,有多少个在[1,2n]中的b满足这个同余方程。

正解思路:

这个题可以通过打表找规律。

我们在看这个题的时候,应该会发现,数据范围中对的奇偶性进⾏了分类。其实这就是题⽬提⽰的从 奇偶性⻆度⼊⼿。

a^b \equiv b^a (\mod 2n)(n\geq 1)

我们可以通过观察得到:

模数一定是偶数

ab的奇偶性一定相同 (同余的定义可证)

 1、对奇数情况进行讨论

我们可以把一个奇数表示为x=2k+1。容易得到x\mod 2=1

所以能得到a^b\equiv a^{2k+1}\equiv a^{2k}a(\mod 2)

显然,因为a是偶数,所以a的幂模2就是1,所以a^b\equiv a^{2k}a\equiv a(\mod 2)

同理b^a\equiv b(\mod 2)

因为a^b\equiv b^a(\mod 2)

所以a\equiv b(\mod 2)

所以只有一种情况——a=b

继续对2的幂进行讨论。奇数的平方可以表示成x^2=4k(k+1)+1,所以显然有x^2\equiv 1(\mod 4)

所以a^b\equiv a^{2k+1}\equiv (a^2)^ka\equiv a(\mod 4)

同理,又有a\equiv b(\mod 4)

对于8来说,我们观察x^2=4k(k+1)+1=8\frac{k(k+1)}{2}+1,这里k(k+1)显然一定能整除以2。所以有x^2\equiv 1(\mod 8)

所以a^b\equiv a^{2k+1}\equiv (a^2)^ka\equiv a(\mod 8)

同理,又有a\equiv b(\mod8)

可以发现之前都能证明a\equiv b(\mod 2^n),对于更大的2^n,尝试证明。

——引入一个定理:

\forall d\ge 1,d\mid m,若有a\equiv b(\mod m),则有a\equiv b(\mod d)

既然a^b\equiv b^a(\mod 2^n),所以也有a^b\equiv b^a(\mod 2^{n-1})

同理,有:a^b\equiv b^a(\mod 2^{n-2})以及a^b\equiv b^a(\mod 2)

所以a\equiv b(\mod 2^n)

可以发现,对于奇数的情况,只有一种情况a\equiv b(\mod 2^n)。所以输入技术,直接输出1即可。这个规律我们也可以通过暴力打表发现。

2、对偶数情况进行讨论

我们可以把a中的2拆出来,就可以得到a^b\equiv 2^b(\frac{a}{2})^b(\mod 2^n)

当b大于n时,显然2^n\mid 2^ b,所以也能得到结论2^n\mid b^a

我们把b表示成b=k2^x

所以b^a=k^a2^{ax}

显然有2^n\mid 2^{ax}

也就是说x\ge \lceil \frac{n}{a}\rceil

所以所有的k2^{x}形式的b都可以。可以知道,x最小是\lceil \frac{n}{a}\rceil

这样的b有多少个呢?注意,我们当前的前提是b>n,所以b\le n的情况不能算上。也就是说,在这个范围内,2^{x_{min}}的倍数都符合条件。也就是\frac{2^n}{2^x}-\frac{n}{2^x}

那么b\le n的情况怎么办呢?数据范围非常小,暴力验证即可。

 AC code:

 #include<bits/stdc++.h>
 #define ll long long
 using namespace std;
 ll qpow(ll a,ll b,ll mod){
     ll ans=1ll;
     while(b){
         if(b&1)ans=ans*a%mod;
         b>>=1;
         a=a*a%mod;
     }
     return ans%mod;
 }
 int main(){
    ll a,n;
    while(~scanf("%lld%lld",&n,&a)){
        if(a&1){
            printf("1\n");
            continue;
        }
        ll m=1<<n;
        ll ans=0ll;
        for(ll b=2;b<=n;b+=2)
        if(qpow(a,b,m)==qpow(b,a,m)) ans++;
        ll x=(n+a-1)/a;
        ans+=(m>>x)-(n>>x);
        printf("%lld\n",ans);
    }
    return 0;
 }

四、赛后总结

以后可以适当做一点有思维含量的题,就不会出现数论题爆0的情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值