【算法竞赛】dfs+csp综合应用(蓝桥2023A9像素放置)
目录
一、 题目
二、思路
(1)算法框架选择
(2)剪枝策略
具体来说就是:
三、代码
(1) 数据读取与初始化
(2) 检查当前填充是否符合要求
(3) 递归 DFS 进行填充
(4) 读取输入 & 调用 DFS
(5) 完整代码
一、 题目
9.像素放置 - 蓝桥云课
【将题目翻译过来就是】:
该游戏基于 n × m
的网格棋盘进行,每个网格可以是白色或黑色,并且某些网格内标注了数字 x
(0 ≤ x ≤ 9)。
核心规则:
-
每个带数字
x
的网格周围 8 个相邻格子(上下左右 + 左上、左下、右上、右下)中,必须恰好有x
个格子被填充为黑色。 -
其余未标注数字的网格可以自由填充。
-
最终输出符合条件的填充方案,白色格子用
0
表示,黑色格子用1
表示。
二、思路
这道题本质上是一个约束满足问题(CSP),每个数字格子对其九宫格区域内的黑色格子数施加了严格约束,我们需要在满足所有约束的条件下找到唯一的合法解,个人认为这个问题的挑战在于:
-
所有约束相互关联,修改一个格子可能影响多个数字区域
-
需要同时处理正向推导(根据已知约束确定颜色)和反向验证(尝试填充后检查约束)
-
必须确保解的唯一性
(1)算法框架选择
我们采用DFS框架,但与我们一般用传统DFS解题的区别在于:
-
非路径式搜索:不是寻找路径,而是构建完整的二维状态
-
顺序填充:按行优先顺序逐个填充格子(0,0)→(0,1)→...→(n-1,m-1)
-
随时验证:在填充过程中随时验证已确定的约束
(2)剪枝策略
当处理到格子(x,y)时,其左上方的格子(x-1,y-1)的后续填充操作不会再改变该格子的状态,这其实也就是说:
-
在填充(x,y)后,必须立即验证(x-1,y-1)的约束
-
当处理到行末(y=m-1)时,还需验证正上方格子(x-1,y)的约束
这种剪枝策略基于"最早失败"原则——一旦发现局部约束不满足,立即停止当前搜索路径,可以比较有效的减少时间复杂度
具体来说就是:
当前处理位置 | 必须验证的约束位置 |
---|---|
非行末(y<m-1) | (x-1,y-1) |
行末(y=m-1) | (x-1,y-1)和(x-1,y) |
弄明白了这两个问题,其实这个问题就比较轻松了,核心就是如何实现约束
三、代码
(1) 数据读取与初始化
#include <iostream>
#include <algorithm>
using namespace std;
const int MAX_N = 15;
int n, m;
char map[MAX_N][MAX_N];
int f[MAX_N][MAX_N]; // 存储填色方案
-
由于
n, m ≤ 15
,可以使用15 × 15
的二维数组存储棋盘数据。 -
map[][]
用于存储输入的棋盘布局。 -
f[][]
用于存储填充方案(0
代表白色,1
代表黑色)。
(2) 检查当前填充是否符合要求
bool check(int x, int y) {
if (map[x][y] == '_') return true; // 没有限制的网格,直接返回 true
int num = map[x][y] - '0';
int cnt = 0; // 统计周围黑色格子的数量
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
int newX = x + i, newY = y + j;
if (newX < 0 || newX >= n || newY < 0 || newY >= m) continue; // 越界检查
if (f[newX][newY] == 1) cnt++; // 统计黑色格子
}
}
return cnt == num; // 若黑色格子数量符合 `x`,则返回 true
}
-
遍历
3×3
的范围,统计黑色格子数量。 -
若符合
x
约束,返回true
,否则返回false
。
(3) 递归 DFS 进行填充
void dfs(int x, int y) {
if (x == n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) cout << f[i][j];
cout << endl;
}
return;
}
// 计算下一步的坐标
int nextX = (y == m - 1) ? x + 1 : x;
int nextY = (y == m - 1) ? 0 : y + 1;
// 先尝试填黑色
f[x][y] = 1;
if ((x == 0 || y == 0 || check(x-1, y-1)) && (y == m - 1 || check(x-1, y)))
dfs(nextX, nextY);
// 尝试填白色
f[x][y] = 0;
if ((x == 0 || y == 0 || check(x-1, y-1)) && (y == m - 1 || check(x-1, y)))
dfs(nextX, nextY);
}
-
递归填充每个网格,并在
x == n
时输出方案。 -
由于每个格子可以填
0
或1
,因此分别尝试后递归调用dfs
。 -
在填充后,检查左上角及已填充区域是否仍满足
x
约束。
(4) 读取输入 & 调用 DFS
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
scanf(" %c", &map[i][j]); // 读取地图数据
}
}
dfs(0, 0); // 递归填充
return 0;
}
-
读取棋盘信息,并调用
dfs(0,0)
开始填充。 -
由于
scanf(" %c")
前有空格,可自动跳过空白符(空格、换行)。
(5) 完整代码
//每个格子都要填到,但是可能重复访问,比如先填黑色,再填白色
//如何解决重复访问且保证不会死循环:按顺序遍历格子
#include <iostream>
#include <algorithm>
using namespace std;
int n,m;
char map[15][15];
int f[15][15];//填色方案
bool check(int x,int y)
{
if(map[x][y]=='_') return true;
else
{
int num=map[x][y]-'0';
int cnt=0;//涂黑的个数
for(int i=-1;i<=1;i++)
{
for(int j=-1;j<=1;j++)
{
int newx=x+i,newy=y+j;
if(newx<0||newx>=n||newy<0||newy>=m) continue;
if(f[newx][newy]==1) cnt++;
}
}
if(cnt==num) return true;
else return false;
}
}
void dfs(int x,int y)//剩余的可以填黑色的方格数,已经填过的方格数
{
//按逐行逐列的顺序填充
//全部填充完
if(x==n)
{
//检查最后一行是否全满足条件
//(还剩最后一行没有检查)
for(int y=0;y<m;y++)
{
if(!check(n-1,y)) return;//该方案不符合条件
}
//输出符合条件的方案
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++) cout<<f[i][j];
cout<<endl;
}
return;
}
//每次填充完当前格子检查左上方格子是否合格
//因为它的状态不会再被影响了,已经固定了
//到达最后一列的情况要特殊判断,不仅要判断左上方还要判断上方,还涉及到换行
if(y==m-1)
{
//填黑色
f[x][y]=1;
//第一行不用检查,第一列只用检查上方
if(x==0||check(x-1,y-1)&&check(x-1,y)||y==0&&check(x-1,y)) dfs(x+1,0);//换行
//填白色
f[x][y]=0;
if(x==0||check(x-1,y-1)&&check(x-1,y)||y==0&&check(x-1,y)) dfs(x+1,0);//换行
}
//最后一列之前只用检查左上方
else
{
//填黑色
f[x][y]=1;
if(x==0||y==0||check(x-1,y-1)) dfs(x,y+1);
//填白色
f[x][y]=0;
if(x==0||y==0||check(x-1,y-1)) dfs(x,y+1);
}
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
//scanf前面加空格读换行
for(int j=0;j<m;j++) cin>>map[i][j];
}
dfs(0,0);
return 0;
}