一,前言
哈喽哈喽又来写补题报告了,今天还挺开心啊,简单写写补题吧
今天的第三,四题依旧官方验证的难,打成这样还挺不错的了嘿嘿
二,成绩
1,四维矩形【100/100】幽默第一题
2,合体【100/100】前年最水第二题,今年最幽默第二题
3,同步机器人【70/100】心态决定成败,显然这次成了
4,矩阵【25/100】最逆天第四题太不体面了(其实是第三题吧宝子)
总分:【295/400】有300的可能吗,今日班级rank?
依旧两个重复题目哦,感兴趣的宝子建议回到24年第4次模拟赛补题
补充
本次第一第二题都是大水题在这里不说啦,本次重点放在第三,第四题,第一题语法题,对第二题感兴趣的宝子可以移步24年,一定注意大输入输出,切换scanf/printf!!!
三,题三
赛前:新第三题?是什么把矩阵赶到第四题了啊,抓紧瞅一眼,我去字符数组输入!我去不存图!我去久违的网格走路题!我去搜索模板!等着宝贝,我秒完俩水题就来找你
赛中:
p1:这啥东西,完蛋,深搜和宽搜到底用哪个啊,我靠应该是宽搜吧
p2:宽搜根本就写不下去啊,越写越崩,毁灭吧(嘻嘻其实心态还行),反正还有时间,直接换深搜,这把肯定行
p3:这里是地狱吗。。。
赛后:
p1:这一坨屎能拿七十分?爽了今天撞大运
p2:呜呜呜呜呜我的宽搜啊啊啊啊,改两行就100了为什么啊啊啊啊
cpp
#include<bits/stdc++.h>
using namespace std;
int n,m,sx1,sy1,sx2,sy2,zx1,zy1,zx2,zy2,tx1,ty1,tx2,ty2,vis1[35][35],vis2[35][35],ans=1e9+7;
char a[35][35];
int dx[]={0,1,0,-1};
int dy[]={1,0,-1,0};
/*queue<pair<int,int> >q1,q2;*/
int check(int x,int y){
if(a[x][y]=='#'||x>n||x<1||y>m||y<1){
return 0;
}
return 1;
}
void dfs(int x1,int y1,int x2,int y2,int step){
if(x1==zx1&&y1==zy1&&x2==zx2&&y2==zy2){
ans=min(step,ans);
return ;
}
for(int i=0;i<4;i++){
int xx1=x1+dx[i],yy1=y1+dy[i],xx2=x2+dx[i],yy2=y2+dy[i];
if(vis[xx1][yy1][xx2][yy2]) continue;
if(!check(xx1,yy1)&&!check(xx2,yy2)){
continue;
}
if(!check(xx1,yy1)){
xx1=x1;
yy1=y1;
}
else if(!check(xx2,yy2)){
xx2=x2;
yy2=y2;
}
vis[xx1][yy1][xx2][yy2]=1;
dfs(xx1,yy1,xx2,yy2,step+1);
vis[xx1][yy1][xx2][yy2]=1;
}
}
/*void bfs(){写炸了的宽搜
int ans=0,flag=0;
q1.push({sx1,sy1});//第一个机器人位置
q2.push({sx2,sy2});
while(!q1.empty()){
tx1=q1.front().first,ty1=q2.front().second;
tx2=q2.front().first,tx2=q2.front().second;
q1.pop();
q2.pop();
for(int i=0;i<4;i++){
int xx1=tx1+dx[i],yy1=ty1+dy[i],xx2=tx2+dx[i],yy2=ty2+dy[i];
if(vis[xx1][yy1]&&vis[xx2][yy2]) continue;//任意一方重复走一定不是答案
if(!check(xx1,yy1)){
xx1=tx1;//越界不变
yy1=ty1;
}
if(!check(xx2,yy2)){
xx2=tx2;
yy2=ty2;
}
q1.push({xx1,yy1});
q2.push({xx2,yy2});
vis[xx1][yy1]=1;
step[xx1][yy1]=step[tx1][ty1]+1;
if(xx1==zx1&&yy1==zy1&&xx2==zx2&&yy2==zy2){
flag=1;
ans=min(ans,step[xx1][yy1]);
}
}
}
//队列为空,没有可行路线
if(flag==0){//未到达终点
cout<<-1;
}
else{
cout<<ans;
}
}*/
int main(){
//freopen("sync.in","r",stdin);
//freopen("sync.out","w",stdout);
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
cin>>sx1>>sy1>>sx2>>sy2>>zx1>>zy1>>zx2>>zy2;
dfs(sx1,sy1,sx2,sy2,0);
if(ans==1e9+7){
cout<<-1;
}
else{
cout<<ans;
}
}
(好吧这dfs完全不对)
好了这边是心路历程,回到学术方面
这个题我写了基本完整地两套代码
这个题的应用肯定是bfs(虽然最后写炸了)题面如下:
小 C 正在测试两个机器人。测试场地是一张 n 行 m 列的网格地图,其中字符
.表示可以通行的空地,字符#表示不能通行的障碍物。两个机器人分别称为机器人 A 和机器人 B。小 C 每次可以选择上、下、左、右中的一个方向,并向两个机器人同时发送这条移动指令。
收到指令后,两个机器人分别按照以下规则行动:
如果机器人沿指令方向移动一格后仍在地图内,并且到达的格子不是障碍物,那么它会移动到该格子。
否则,它会停留在原来的格子。
两个机器人的行动互不影响。它们可以同时位于同一个格子,也可以同时经过同一个格子。
机器人 A 和机器人 B 各有一个目标位置。小 C 希望在某次指令执行完毕后,两个机器人同时位于各自的目标位置。请你求出最少需要发送多少条指令。
机器人到达目标位置后不会自动停机。如果之后收到的指令能让它移动,它仍然会离开目标位置。
这么看来我的基础真的很差了,这样的最短路显然要使用bfs,若使用dfs去遍历所有路线将会TLE45分(不知道考场怎么拿到70)
写炸的根源是什么呢,把这两个机器人的状态分开了,这里的状态,不是移动时变化的状态,而是整体全局的状态,不管是写的宽搜还是广搜,都没有认清一个点:当前状态变化是由两个机器人共同决定的,单独 A 的坐标、单独 B 的坐标都不能代表完整局面,合在一起,才能代表一个独立状态,vis数组标记的状态不是一个位置,而是一个"局面"
我们重新看一下bfs队列的意义:里面存储着还没处理完的局面,和处理完一个局面后拓展出的局面,而存储的一定是局面,我的两个机器人状态分开存,两个独立队列无法保证,目前拿出的一组A、B 位置属于同一个局面,队列只能保证入队顺序对齐,分支一多,取出时A和B会跨局面胡乱配对,拿到的是不属于同一时刻的A、B组合,而结合成一个队列才能避免这个问题
这里补充两个我遇到的大坑
第一,BFS 第一次碰到终点状态,一定是最短步数
因为它分层遍历,距离(直接的最近距离)初始点相同的点将会在同一层遍历,不遍历完这一层的点,下一层甚至更远的点一定不会被遍历到,自然不会出现绕远路的情况,这一点与dfs求出所有路线步数求最小值不同
第二,vis\[\]\[\]数组看似标记的是格子和位置,其实完全不是,而是局面,是我这个主体是否曾经处在过这个局面,这点在单人迷宫还好,放到这个题混淆了是相当致命的,vis的作用是为了防止重复处理同一个局面的!这里所说的局面概念,是所有能影响后续走向的全部信息
而我在写代码的时候把【局面】拆成了两个独立个体,用两个独立队列分别保存 A、B, 潜意识认为:A 是一套独立搜索、B 是另一套独立搜索,之后再配对(出队时候的状态无从匹配)。但搜索的基本单元根本不是 A,也不是 B,而是【A+B 共同构成的局面】
按照这样的思路,我的考场代码底子还可以,开上四维vis,内容不再是简单标记0/1改为存储步数,队列合并成一个,注意越界判断在前,其实别的离正解真的很近了,卡的点就是没有理解局面的含义,注意要判断起点是否与终点重合(谁出的破样例让我掉了15分)
AC代码
cpp
#include<bits/stdc++.h>
using namespace std;
int n,m,sx1,sy1,sx2,sy2,zx1,zy1,zx2,zy2,tx1,ty1,tx2,ty2,vis[35][35][35][35],ans=1e9+7;
char a[35][35];
int dx[]={0,1,0,-1};
int dy[]={1,0,-1,0};
struct node{
int x1,y1,x2,y2;
};
queue<node>q;
int check(int x,int y){
if(x>n||x<1||y>m||y<1){
return 0;
}
if(a[x][y]=='#'){
return 0;
}
return 1;
}
/*void dfs(int x1,int y1,int x2,int y2,int step){
if(x1==zx1&&y1==zy1&&x2==zx2&&y2==zy2){
ans=min(step,ans);
return ;
}
for(int i=0;i<4;i++){
int xx1=x1+dx[i],yy1=y1+dy[i],xx2=x2+dx[i],yy2=y2+dy[i];
if(vis[xx1][yy1][xx2][yy2]) continue;
if(!check(xx1,yy1)&&!check(xx2,yy2)){
continue;
}
if(!check(xx1,yy1)){
xx1=x1;
yy1=y1;
}
else if(!check(xx2,yy2)){
xx2=x2;
yy2=y2;
}
vis[xx1][yy1][xx2][yy2]=1;
dfs(xx1,yy1,xx2,yy2,step+1);
vis[xx1][yy1][xx2][yy2]=0;
}
}*/
void bfs(){
int ans=0,flag=0;
node w;
w.x1=sx1,w.x2=sx2,w.y1=sy1,w.y2=sy2;
vis[sx1][sy1][sx2][sy2]=0;
q.push(w);
while(!q.empty()){
tx1=q.front().x1,ty1=q.front().y1;
tx2=q.front().x2,ty2=q.front().y2;
q.pop();
for(int i=0;i<4;i++){
int xx1=tx1+dx[i],yy1=ty1+dy[i],xx2=tx2+dx[i],yy2=ty2+dy[i];
if(!check(xx1,yy1)){
xx1=tx1;//越界不变
yy1=ty1;
}
if(!check(xx2,yy2)){
xx2=tx2;
yy2=ty2;
}
if(vis[xx1][yy1][xx2][yy2]!=-1) continue;//这个局面经历过了
node ww;
ww.x1=xx1;ww.x2=xx2;ww.y1=yy1;ww.y2=yy2;
q.push(ww);//差点忘记了
vis[xx1][yy1][xx2][yy2]=vis[tx1][ty1][tx2][ty2]+1;
if(xx1==zx1&&yy1==zy1&&xx2==zx2&&yy2==zy2){
cout<<vis[xx1][yy1][xx2][yy2];
return ;
}
}
}
cout<<-1;
}
int main(){
//freopen("sync.in","r",stdin);
//freopen("sync.out","w",stdout);
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
cin>>sx1>>sy1>>sx2>>sy2>>zx1>>zy1>>zx2>>zy2;
memset(vis,-1,sizeof vis);
bfs();
}
四,题四
(呃熟人见面挺尴尬啊,这样我赶时间打个暴力给您,记得多给我点分)
神tm6层循环25分(n<=10),也是非常合理
本次判题机非常强大,剩下的75分n<=300的情况下竟然二维异或前缀和O()能直接A掉(早知道我也这么写了欲哭无泪)
不管了提升最重要A了也没啥用(嘴硬中),先来看题:
现在给你一个 n 行 m 列的矩阵,矩阵上每个格子有一个整数,其中第 i 行第 j 列对应的格子上的整数为 gi,j。
现在定义该矩阵的一个子矩阵 的快乐值为该子矩阵上的所有数字的异或和。
一组数字 a1,a2,...,an 的异或和为 a1 xor a2 xor ... xor ana1 xor a2 xor ... xor an。(其中 xor 表示按位异或运算)
现在问你,该矩阵的所有子矩阵的快乐值之和为多少?
只记得24年做的时候做了很久,但是忘了正解是啥了(裂)
本题也是没有赛上,我们来把这个题搞明白
我们看到有一些测试点矩阵所有的元素都是1或者0,我们单独分析这种情况,这样子矩阵的异或和的规律出来了
子矩阵内1的个数为奇数的话子矩阵的异或和为1
为偶数则子矩阵异或和为0
总答案:多少个子矩阵内1的个数为奇数
我们的求解方式如下:
p1:枚举一个上界和一个下界,这个上界和下界会从整个矩阵中去处一个列数相同,行数为下界减上界加1的小矩阵,我们对于这个小矩阵,给它分成好多列,并设置c\[\]数组来统计每列的1的个数是奇数还是偶数,是1则为奇数,是0则为偶数,如cj=1表示在当前上下界限制中的小矩阵的第j列中1的个数为奇数,得到c\[\]数组
p2:对c数组求解一维前缀异或和
cpp
循环1到j枚举列数
s[j]=c[j]^s[j-1]
得到s数组,在当前上界和下界限制中,对s\[\]求区间L,R的异或前缀和就是求解 行数为上界+下界-1行和列数为r-l+1列的子矩阵异或和,即
cpp
s[x]=s[r]^s[l-1]
注意:24年的题解中也有声明,异或的逆运算是它本身,即连续运算两次相同值直接抵消,值不变
p3:那为了使我们的异或和为1,在选取L,R区间时,所求的异或和sR^sL-1中其中一者是1,则另一者必须为0,所以我们遍历s数组并固定sR,这样出现了两种情况
sR=1;那么前面出现sL-1=0的情况下,L,R对应子矩阵异或和为1,以R作为右界的子矩阵数量即为前面出现的0的数量
sR=0;那么前面出现sL-1=1的情况下,L,R对应子矩阵异或和为1,以R作为右界的子矩阵数量即为前面出现的1的数量
所以我们遍历 s,维护每一右界R前面出现过的 0 的数量cnt0、1 的数量cnt1,并根据R的情况计算它对答案的贡献,sR为1则其贡献为cnt0,sR为0则其贡献为cnt1,对贡献进行累加即为答案
(这边合法对子的数量可以直接乘法原理来做,一次性统计全部 pre 数组里 0 和 1 的总数相乘)
注意s0等于0,其贡献为1,所以cnt0初值为1,cnt1初值为0
那这是这个,我们部分样例的一个解题思路,我们现在拓展到完整问题
由于我们每个数最大为2的10次方,所以它的二进制表示至多10位
并且异或运算有一个关键原则,每个二进制位完全独立
所以我们这么解这个问题,k循环遍历0到10位,把矩阵里的数二进制中的第k位单独拿出来,我们可以获得一个0,1构成的与原来行列数矩阵,直接按照上文的方法进行求解,解出第k位的贡献
贡献怎么算?回顾一下,我们按照p1,p2,p3的方法解出了当前第k位取出的0,1矩阵中所有子矩阵的异或和,我们回顾单个子矩阵的异或和的概念显然只能这个和只能是0,1,那这第k位这一层的这一个子矩阵贡献就是求出的该子矩阵异或和(0,1)乘以位权,根据乘法分配律,位权都相同取出来,这一层贡献就是第k位这一层所有子矩阵的异或和之和乘以当前位权2^k,把所有层的贡献都累加起来获得答案
AC代码如下
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=303;
typedef long long ll;
int a[N][N],now[N],pre[N];
ll ans=0;
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
for(int u=1;u<=n;u++){
memset(now,0,sizeof now);
for(int d=u;d<=n;d++){
for(int i=1;i<=m;i++){
now[i]^=a[d][i];
}
for(int i=1;i<=m;i++){
pre[i]=pre[i-1]^now[i];
}
for(int b=0;b<10;b++){
int cnt[2]={1,0};
for(int i=1;i<=m;i++) cnt[pre[i]>>b&1]++;
ans+=(cnt[1]*cnt[0]*(1ll<<b));
}
}
}
cout<<ans<<"\n";
}
五,总结
励志攻克第三四题,不要放弃希望,只要写了,总会中的
六,祝福
天天开心同学们