P9237 [蓝桥杯 2023 省 A] 像素放置
暴力
一开始还想先分析哪个点会引出最少的分支数量,然后优化顺序。
但是转念一想,既然都写暴力了,就别考虑这么复杂的问题了()
因为题目给的是九宫格类型的拓展,所以最开始写的暴搜是从左上角开始,每次扩展九宫格的搜索方式,搜索完毕之后再进行check
但是这样只有15分
稍作思考一下,便能发现是这个完全没有必要的九宫格扩展方式导致的,它只会白白地增加大量的分支数目。
然后把九宫格方式改成向上下左右四个方向扩展,果然,变成了四十分。
但就连上下左右四个方向扩展也是没有必要的——只要一个个,一行行地填过去就好了。
果然,修改之后分数来到了50分。
#include <iostream> #include <stdio.h> #include <algorithm> #include <string> #include <cmath> #define R(x) x = read() #define For(i, j, n) for (int i = j; i <= n; ++i) using namespace std; inline int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); } return x * f; } const int N = 15; char a[N][N]; int n, m; int g[N][N]; int lim[N][N], now[N][N]; bool vis[N][N]; void init() { for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) if (a[i][j] != '_') lim[i][j] = a[i][j] - '0'; else lim[i][j] = 100; } bool check() { for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) { if (lim[i][j] == 100) continue; if (now[i][j] != lim[i][j]) return false; } return true; } int dx[4] = {-1, 1, 0, 0}; int dy[4] = {0, 0, -1, 1}; bool dfs(int x, int y, int cnt, int pat) { int pre = g[x][y]; g[x][y] = pat; if (pat) { for (int i = x - 1; i <= x + 1; i++) for (int j = y - 1; j <= y + 1; j++) { if (i < 0 || i >= n || j < 0 || j >= m) continue; now[i][j]++; } } vis[x][y] = 1; if (cnt == 1) { if (check()) return true; else { if (pat) { for (int i = x - 1; i <= x + 1; i++) for (int j = y - 1; j <= y + 1; j++) { if (i < 0 || i >= n || j < 0 || j >= m) continue; now[i][j]--; } } g[x][y] = pre; vis[x][y] = 0; return false; } } int ny = y + 1, nx = x; if (ny == m) { ny = 0; nx = x + 1; } if (dfs(nx, ny, cnt - 1, 0)) return true; if (dfs(nx, ny, cnt - 1, 1)) return true; if (pat) { for (int i = x - 1; i <= x + 1; i++) for (int j = y - 1; j <= y + 1; j++) { if (i < 0 || i >= n || j < 0 || j >= m) continue; now[i][j]--; } } g[x][y] = pre; vis[x][y] = 0; return false; } int main() { scanf("%d%d", &n, &m); for (int i = 0; i < n; i++) scanf("%s", a[i]); init(); for (int i = 0; i < 2; i++) if (dfs(0, 0, n * m, i)) break; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) printf("%d", g[i][j]); puts(""); } return 0; }
一点点优化
做完才检查太慢了,可以尝试每次填入1之后,都检查是否合法
#include <iostream> #include <stdio.h> #include <algorithm> #include <string> #include <cmath> #include <string.h> #define R(x) x = read() #define For(i, j, n) for (int i = j; i <= n; ++i) using namespace std; inline int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); } return x * f; } const int N = 15; char a[N][N]; int n, m; int g[N][N]; int lim[N][N], now[N][N], nowB[N][N]; bool vis[N][N]; void init() { for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) if (a[i][j] != '_') lim[i][j] = a[i][j] - '0'; else lim[i][j] = 100; } bool check() { for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) { if (lim[i][j] == 100) continue; if (now[i][j] != lim[i][j]) return false; } return true; } void reset(int x, int y) { for(int i = x - 1; i <= x + 1; i++) for(int j = y - 1; j <= y + 1; j++) { if (i < 0 || i >= n || j < 0 || j >= m) continue; now[i][j] = nowB[i][j]; } } bool dfs(int x, int y, int cnt, int pat) { int pre = g[x][y]; g[x][y] = pat; if (pat) { memcpy(nowB, now, sizeof(now)); for (int i = x - 1; i <= x + 1; i++) for (int j = y - 1; j <= y + 1; j++) { if (i < 0 || i >= n || j < 0 || j >= m) continue; now[i][j]++; if(now[i][j] > lim[i][j]) { reset(x, y); g[x][y] = 0; return false; } } } vis[x][y] = 1; if (cnt == 1) { if (check()) return true; else { if (pat) { for (int i = x - 1; i <= x + 1; i++) for (int j = y - 1; j <= y + 1; j++) { if (i < 0 || i >= n || j < 0 || j >= m) continue; now[i][j]--; } } g[x][y] = pre; vis[x][y] = 0; return false; } } int ny = y + 1, nx = x; if (ny == m) { ny = 0; nx = x + 1; } if (dfs(nx, ny, cnt - 1, 1)) return true; if (dfs(nx, ny, cnt - 1, 0)) return true; if (pat) { for (int i = x - 1; i <= x + 1; i++) for (int j = y - 1; j <= y + 1; j++) { if (i < 0 || i >= n || j < 0 || j >= m) continue; now[i][j]--; } } g[x][y] = pre; vis[x][y] = 0; return false; } int main() { scanf("%d%d", &n, &m); for (int i = 0; i < n; i++) scanf("%s", a[i]); init(); for(int i = 1; i >= 0; i--) if (dfs(0, 0, n * m, i)) { break; } for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) printf("%d", g[i][j]); puts(""); } return 0; }
但是加入这种即时检查之后,只多过了一个点,而且还是洛谷加的额外测试点,如果在考试当中的话,分数是不会有任何变化的。
那么为什么这个即时检查会形同虚设呢?
分析之后发现,我们每次插入1之后,都会对它所在的九宫格进行检查——但是对于这个九宫格里的很多点,他们的情况甚至还没有确定下来,这个时候检查是没有意义的,那要怎么优化我们的检查方式呢?
正解
一开始在洛谷翻题解的时候,看到了插头DP这样高大上的标签,顿时望而却步,但是想清楚之后,发现这道题只是借用了插头DP的思想来进行记忆化搜索:
前面我们提到,每次插入1之后在相应的九宫格做check是没有意义的,那么我们应该在哪里check呢?
我们应该检查那些状态已经确定的点
即如下函数所示:
bool can(int x,int y,int v){ for(int i=-1;i<=1;++i){ for(int j=-1;j<=1;++j){ int nx=x+i,ny=y+j; if(in(nx,ny) && cur[nx][ny]+v>a[nx][ny])return 0; } } if(in(x-1,y-1) && cur[x-1][y-1]+v<a[x-1][y-1])return 0; if(x==n-1 && in(x,y-1) && cur[x][y-1]+v<a[x][y-1])return 0; if(y==m-1 && in(x-1,y) && cur[x-1][y]+v<a[x-1][y])return 0; return 1; }
对于九宫格内的点还是要进行检查,因为它们如果无法通过检查的话,就连插入操作都无法进行了
除了优化检查之外,我们还要加入记忆化来进一步提高效率
bitset<M>dp[N][N];
dp[i][j][st]表示当前进行到(i,j)位置,且前两行加上(i,j)左边两个点的状态为st的方案是否被搜索过
如果前面不足两行,或者左边不足两格,那么就是0(默认情况下bitset就是0)
这里用bitset的原因是int存不下
#include<bits/stdc++.h> using namespace std; const int S=15,N=10,M=1<<22,INF=0x3f3f3f3f; int n,m,a[S][S],b[S][S],cur[S][S]; bitset<M>dp[N][N]; char s[S][S]; bool ok; bool in(int x,int y){ return 0<=x && x<n && 0<=y && y<m && a[x][y]!=INF; } bool can(int x,int y,int v){ for(int i=-1;i<=1;++i){ for(int j=-1;j<=1;++j){ int nx=x+i,ny=y+j; if(in(nx,ny) && cur[nx][ny]+v>a[nx][ny])return 0; } } if(in(x-1,y-1) && cur[x-1][y-1]+v<a[x-1][y-1])return 0; if(x==n-1 && in(x,y-1) && cur[x][y-1]+v<a[x][y-1])return 0; if(y==m-1 && in(x-1,y) && cur[x-1][y]+v<a[x-1][y])return 0; return 1; } void dfs(int x,int y,int w){ //cnt++; cout << x << " " << y << " " << w << endl; if(x==n && y==0){ ok=1; for(int i=0;i<n;++i){ for(int j=0;j<m;++j){ printf("%d",b[i][j]); } puts(""); } return; } if(ok)return; if(dp[x][y][w])return; dp[x][y][w]=1; for(int v=1;v>=0;--v){ if(ok)return; if(can(x,y,v)){ b[x][y]=v; if(v){ for(int i=-1;i<=1;++i){ for(int j=-1;j<=1;++j){ int nx=x+i,ny=y+j; if(!in(nx,ny))continue; cur[nx][ny]++; } } } int nw=(w<<1)|v; if(y==m-1)dfs(x+1,0,nw&((1<<(2*m))-1)); else dfs(x,y+1,nw&((1<<(2*m+2))-1)); if(v){ for(int i=-1;i<=1;++i){ for(int j=-1;j<=1;++j){ int nx=x+i,ny=y+j; if(!in(nx,ny))continue; cur[nx][ny]--; } } } } } } int main(){ scanf("%d%d",&n,&m); for(int i=0;i<n;++i){ scanf("%s",s[i]); for(int j=0;j<m;++j){ a[i][j]=INF; if(s[i][j]!='_')a[i][j]=s[i][j]-'0'; } } dfs(0,0,0); //printf("cnt:%d\n",cnt); return 0; }
然后就能愉快的AC了
状压为什么要压两行?
这道题每个像素的影响范围和AcWing 1064. 小国王当中一样,都是九宫格,可为什么小国王这道题只需要压一行,但是这里却要压两行?
因为小国王那道题只有“影响范围”一个限制,我只要考虑会不会相互攻击到即可。
但是这道题像素之间在同一个九宫格内不仅会“相互影响”,而且这个影响是具体的:这个九宫格内存在一个像素的数量限制,对于被夹在中间的那一行来说,它既要向前看也要向后看,所以要压两行。
类似地,AcWing 292. 炮兵阵地这道题的影响范围有两行,但是无数量限制,所以这道题也只需要压两行。
一点点细节
if(y==m-1)dfs(x+1,0,nw&((1<<(2*m))-1)); else dfs(x,y+1,nw&((1<<(2*m+2))-1));
这里为什么要和一个全是1的数字进行&运算?不是没有用吗
因为在我们dfs的过程中,w会一直左移,那些高位的1就一直往左移动,如果不和这个数字取余的话,高位的、已经离开两行以内范围的1就一直消不掉,就会导致我们状态转移不断积累,一直变大。
另外,“全是1的数字”这个说法本身就是不严谨的,因为这两个由1<<若干位减一得到的数字,也只有右边一串全是1,左边高位仍然是全0的
这里也有类似的用法:
for (int i = 0; i < n; ++ i) f[1 << i][0] = 0;//开局免费选一个起点(初始状态) for (int cur = 1, cost; cur < 1 << n; ++ cur) for (int pre = cur - 1 & cur; pre; pre = pre - 1 & cur) if (~(cost = get_cost(cur, pre))) for (int k = 1; k < n; ++ k) f[cur][k] = min(f[cur][k], f[pre][k - 1] + cost * k); 作者:一只野生彩色铅笔 链接:https://www.acwing.com/solution/content/59439/ 来源:AcWing 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
pre通过和cut取余,保证pre得到的是cur的子集。
最后,像是这种带有是否成功标准的dfs函数,既可以写成bool函数,也可以维护一个全局变量,来判断是否找到答案。