1 条题解
-
0oistream (oistream) LV 8 MOD @ 2020-08-06 18:41:58
搜索题。
其实怎么感觉都不像蓝题,加强版才有蓝题难度...
看到这题 \(N\leq 10\) 第一感觉是爆搜。
然后就开始写了。
先看定义:
int square[11]={0,1,4,9,16,25,36,49,64,81,100}; int prime[205]; bool notPrime[205]; int board[11][11]; int ans[11][11]; bool visNum[105]; int sum=2147483647;
int *square
:预处理 \(N^2\)int *prime
:素数,用线性筛预处理。 \(10^2+10^2=200\),所以最大可能遇到的素数不超过 \(200\)。bool *notPrime
:同样线性筛素数时使用,notPrime[i]
标记 \(i\) 是否为素数。其实最后发现反而是这个数组接下来要用。int **board
:即棋盘,DFS 时要用。int **ans
:记录当前最优答案,初始值全为 \(0\) 即可。int sum
:记录当前最优答案的 \(\sum_{(i=1)\or (j=1)}a_{i,j}\)(人话:第一行第一列的数的总和),由于要取最小值,这里要将sum
赋成极大值,0x7f7f7f7f
~~OJ~~ 也可以。bool *visNum
:就是 DFS 中通常使用的那个标记是否已访问的数组。visNum[i]
为真则代表在当前的情况下数 \(i\) 已经被使用了。
然后是线性筛素数,此处略去,要用到的两个数组上面提到了。
贯彻自顶向下的方法,咱们先看 DFS 函数(主函数过于简单这里也不放了,记得初始时把
board[1][1]
赋成 \(1\),visNum[1]
赋成true
就行了)。bool dfs(int n,int x,int y,int k,int s) { if(x==n+1) { if(s<sum) { memcpy(ans,board,sizeof(ans)); sum=s; return true; } return false; } bool ok=false; for(int i=1;i<=square[n];i++) { //处理 } return ok; }
注意此处
bool dfs(int,int,int,int,int)
是有返回值的。其实直接开全局变量也可,但在一个函数能干的事就不要牵扯到其它的东西了。所以此处采用返回值的方式,每找到一个便返回true
,回溯时统计一下,只要有一个true
就也返回true
,这样只要找到一种答案,最终的返回值就为true
。递归填完整个表后判断一下当前情况与最优情况哪个更优,如果当前更优就把当前情况拷贝过去。注意
memcpy()
的参数位置以及sizeof()
应取ans
的而不是board
的,不要写反了。下面看
for
循环内部。int newX=x; int newY=y+1; if(y==n){newX=x+1;newY=1;} if(visNum[i]) continue; if(!isPrime(n,x,y,i)) continue; board[x][y]=i; if((x==1)||(y==1)) s+=i; visNum[i]=true; bool nowOk=dfs(n,newX,newY,k+1,s); ok|=nowOk; visNum[i]=false; if((x==1)||(y==1)) s-=i; board[x][y]=0;
用空行分了三段,其中
- 第一段计算出下一步要填的坐标,此处是一行一行的填表,也可以一列一列的填表。
- 第二段判断当前选的数是否被使用,以及当前数与已填好的数之间是否符合和为素数的要求。
- 接下来的第三段我用缩进体现了以下递归时与回溯时各条语句的一一对应关系。
- 首先当前格子填上选择的数;
- 然后如果当前填的格子属于第一行第一列,那么还需要给参数
s
(当前情况第一行第一列的和)加上当前填的数; - 然后还要把选定的数“占”上;
- 最后用记录下返回值(是否刷新了最优情况),
ok|=nowOk
是或运算(在bool
类型下按位或和逻辑或从运算结果来看没有差别),只要参与运算的两数有一个为true
,运算结果即为true
。
注意到上面的 DFS 代码中用到了一些额外的函数,这些函数均不复杂,下面是这些函数:
bool out(int n,int x,int y) { return (x>n)||(y>n)||(x<1)||(y<1); } bool isPrime(int n,int x,int y,int i) { bool result=true; if(!out(n,x-1,y)) { result&=(!(notPrime[i+board[x-1][y]])); } if(!out(n,x,y-1)) { result&=(!(notPrime[i+board[x][y-1]])); } return result; }
其中
bool out(int,int,int)
接受 \(n\) 和当前格子坐标,判断当前坐标是否在棋盘内。bool isPrime(int,int,int,int)
接受 \(n\) ,当前格子坐标和当前要填入的数,判断它与已填入的数是否满足“和为素数”的要求。由于我们是一行一行填,每一行内从左向右填,所以只需要判断它与其左边的数和上面的数和是否为素数即可。与 DFS 内部的ok
变量类似,这里使用了result
变量记录返回的结果。同样使用了位运算(与)来更新,与运算中只要两个操作数有一个为false
则返回的为false
。
至此为止基于搜索算法的程序已写完,由于本题数据很小所以可以直接输入数据来测试一下性能。
输入 \(0,1,2,3,4\) 时,程序都能很快地给出答案,但当我输入 \(5\) 时,程序大概过了 \(5~\text{s}\) 才输出答案。
于是,我挂上了火车头优化,加速到了 \(2~\text{s}\) 左右,但依然不尽人意(毕竟才 \(5\) 啊...)
于是,我并不期望 \(\text{AC}\) 地提交了一次,成绩却出乎意料:我的代码居然通过了 \(5\) 个点中的 \(4\) 个!
事实上,这道题的数据完全是用脚造的!说是 \(N\leq 10\),但其实只有 \(N\leq 5\)!
这可成全了俺蒟蒻(
于是乎,愉快的
if(n==5) printTable();
,\(\text{AC}\)。为防无耻 \(\text{Ctrl+CV}\),此处将所有的数均用其它字符代替了。
void printTable() { cout<<"b f w a k"<<endl; cout<<"* i o i *"<<endl; cout<<"n o i w c"<<endl; cout<<"* a * k *"<<endl; cout<<"* o i s *"<<endl; }
这里还需要说一下,虽然打表看起来很像作弊,但其实它无论在大部分 OJ 中还是在 CCF 的竞赛中都是允许的(毕竟有的时候能打出来表也不容易...)。但是,考虑到现在的大部分题目都打不了表,要么打出来表过长要么表的生成器耗时/空无法承受,还是不要老是想“歪”招了 \(\text{qwq}\)
- 1