1,问题:
1,问题描述:
Akari 问题
Akari
问题有时又被称为Light up
或者Beleuchtung
,源于日本逻辑解密游戏系列Nikoli
,同属于Nikoli
谜题的除Akari
之外还有Sudoku
(数独)和Kakuro
(数谜)等。
游戏规则很简单。点灯游戏的棋盘是一张方形格网,其中的格子可能是黑色也可能是白色。游戏目标是在格网中放置灯泡,使之能照亮所有的白色方格。如果一个方格所在的同一行或同一列有一个灯泡,并且方格和灯泡之间没有黑色格子阻挡,那么这个方格将被灯泡照亮。同时,放置的每个灯泡不能被另一个灯泡照亮。
某些黑色格子中标有数字。这些数字表示在该格子四周相邻的格子中共有多少个灯泡。
2,题目要求:
格式说明
输入数据由若干文件组成,每个文件描述一个Akari
问题的初始状态,编写程序读入此文件,根据初始状态求解。有的有一个解,有的有多个解,我们保证有解。
文件由若干行组成,第一行为两个整数 n
,m
,代表棋盘的行数和列数。之后的 n
行每行有 m
个整数表示棋盘的每个格子的状态,若它为 -2
,则表示是白格子,若它为 -1
,则表示是没有数字的黑格子,若它为 0-4
,则表示是数字 0-4
的黑格子。若你想把灯泡放在白色格子上面,则需要将 -2
改为 5
,因为 5
表示有灯泡的格子。
2,算法
2.1:algo1:
问题和算法你都可以从https://www.educoder.net/tasks/mthkqlu46583获得;
如下:
对问题进行划分
根据黑色方格中数字的大小,按从大到小的顺序进行排序,并将“在一个有数字的黑色方格周围放置‘车’”定义为一步。
选择枚举
枚举每一步的选择。
若黑色方格中的数字为4,则其相邻的周围格子的“灯泡”的放置方式为1种,即上下左右均放置一个“灯泡”。
若黑色方格中的数字为3,则其相邻的周围格子的“灯泡”的放置方式为4种,即在左右下、下右上、右上左、上左下的格子中放置“灯泡”。
若黑色方格中的数字为2,则其相邻的周围格子的“灯泡”的放置方式有6种,即在上左、上下、上右、左下、左右、下右的格子中放置“灯泡”。
若黑色方格中的数字为1,则其相邻的周围格子的“灯泡”的放置方式有4种,即在上、下、左、右的格子中放置“灯泡”。
若黑色方格中的数字为0,则其相邻的周围格子的“灯泡”的放置方式有1种,即所有周围格子均不放置车。
构造解空间
根据上述讨论构造解空间,初始状态为解空间树的根节点,从编号最大的黑色格子开始尝试填入“灯泡”,填入后判断是否为一个可行解,若为可行解,则解空间向下进行分枝,否则向上进行回溯。解空间树的结构大致如图2所示。
程序设计与实现
根据回溯法的伪代码和上述算法的设计思路,建立合适的数据模型与程序结构,编写程序求解问题,同时记录程序的运行时间。
讨论与改进
分析算法的时间负责度与空间复杂度,根据某一用例和计算机的计算能力,估计程序运行时间,并将该时间与实际运行时间进行比较。
同时讨论与优化程序结构和数据结构,以求达到更快的程序执行速度和更少的内存占用量。
2.2:algo2:
// 根据curm的有效位来获得可以放灯的所有位置
// arg1: 存放可以放灯的所有位置
// arg2: 当前矩阵
void getLeft(vector<POINT>& left, vector<vector<grid> > & curm){
// 根据当前放入的那个灯来更新矩阵,同时检测是否可以成功更新
// 不能成功更新的说名当前点不能放,curm所作的改变需要清除,则返回false然后换下一个点来回溯
bool updateCurmByLight(POINT light, vector<vector<grid> > & curm) {
//回溯函数
void backT(vector<POINT>& left, vector<vector<grid> > & curm,\
vector<vector<int> > & res){
if(如果能cover住所有的位置,则将res更新,然后更新findRes){
return ;
}
//
if(当所有的位置都用了,但是没得到结果,也要回溯){
return ;
}
for(int i=0; i<left.size() &&(!findRes); i++){
auto tmpCurm= curm;
auto tmpLightedUp = lightedUp;
int tmpDarkSum=darkSum;
auto tmpleft = left;
if(left.size() >= 15){
cout<<" left len: "<<left.size()<<endl;
}
if(updateCurmByLight(left[i], curm)){ //可以正确更新,并且内容已经被更新
//更新剩余,放入该灯,接着回溯
getLeft(left, curm);
backT(left, curm, res);
}
//当该点的回溯退出来了,我们需要回滚到进入之前,然后进入到下一个遍历;
curm = tmpCurm;
darkSum = tmpDarkSum;
lightedUp = tmpLightedUp;
left = tmpleft;
}
}
vector<vector<int> > solveAkari(vector<vector<int> > & g){
//存放结果
vector<vector<int> > res(n, vector<int>(m, 0));
//初始化你的可以放灯的位置 调用函数: getLeft
getLeft(left, curm);
//回溯获得结果
backT(left, curm, res);
return res;
}
}
第二种是自己写的,算法是每次放一个灯,而第一种是每次放一个黑格子周围的灯们(比如黑格子数字为3,则有4中放法,是这样去遍历回溯的)
3,解答代码
这里的解答代码很详细,但是采用的是算法中的第2种,可以直接阅读,注释很足;
//
// Created by cxy on 19-7-22.
//
# include <bits/stdc++.h>
# include "akari.h"
using namespace std;
/**
输入数据由若干文件组成,每个文件描述一个Akari问题的初始状态,编写程序读入此文件,
根据初始状态求解。有的有一个解,有的有多个解,我们保证有解。文件由若干行组成,
第一行为两个整数 n,m,代表棋盘的行数和列数。
之后的 n 行每行有 m 个整数表示棋盘的每个格子的状态,
若它为 -2,则表示是白格子,
若它为 -1,则表示是没有数字的黑格子,
若它为 0-4,则表示是数字 0-4 的黑格子。
若你想把灯泡放在白色格子上面,则需要将 -2 改为 5,因为 5 表示有灯泡的格子。
你需要在右侧代码编辑框给出的函数中编写你的代码,函数的参数为我们给出的light up矩阵,
你需要在该函数中返回相同大小的结果矩阵。对于有多个解的light up,你可以返回其中的
任意一组解,我们将对你返回的矩阵进行检查,若结果正确,提示The answer is right!,
否则提示其它。
*/
namespace aka{
//请在命名空间内编写代码,否则后果自负
//用以记录灯的位置
typedef struct{
int x = 0;
int y = 0;
int val = -2;
}POINT;
//为棋盘里的每个格子加一个标志位
typedef struct{
int value = 0;
bool valid = true;
}grid;
//找到结果的标志
bool findRes = false;
//没有被照亮的总数
int darkSum = 0;
//黑色格子的坐标
vector<POINT> bs;
//n行m列
int n = 0, m = 0;
//标记矩阵: 白格子是否在黑格子周围,是白格子并且在黑格子周围,则为true,否则为false
vector<vector<bool> >aroundBlack;
//存放: 在黑点周围的格子围绕的黑点的数值;
vector<vector<int> >aroundBlackValue;
//获得剩余的灯的时候按照围绕的黑格子的数字从高到低排列;
bool cmp(POINT a, POINT b){
return a.val > b.val;
}
// struct {
// bool operator()(int a, int b) const
// {
// return a < b;
// }
// } customLess;
//表记是否被照亮
vector<vector<bool> >lightedUp;
void display(vector<vector<grid> > & ans)
{
printf("your magic matrix--------------------------------> \n");
int n = ans.size(), m = ans[0].size();
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
printf("%4d", ans[i][j].value);
}
printf("\n");
}
printf("your valid matrix \n");
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
printf("%4d", ans[i][j].valid);
}
printf("\n");
}
// printf("your aroundBlack \n");
//
// for (int i = 0; i < n; ++i) {
// for (int j = 0; j < m; ++j) {
// printf("%4d", aroundBlack[i][j]?1:0);
// }
// printf("\n");
// }
}
void pv(vector<POINT> a){
printf("left is \n");
for (int j = 0; j < a.size(); ++j) {
printf("%4d", a[j].x);
}printf("\n");
for (int j = 0; j < a.size(); ++j) {
printf("%4d", a[j].y);
}printf("\n");
for (int j = 0; j < a.size(); ++j) {
printf("%4d", a[j].val);
}printf("\n");
}
void pv1(vector<vector<int> > a){
printf("around black 's value is \n");
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
printf("%4d", a[i][j]);
}
printf("\n");
}
}
void pv2(vector<vector<bool> > a){
printf("lightedUp is \n");
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
printf("%4d", a[i][j]?1:0);
}
printf("\n");
}
}
// 根据curm的有效位来获得可以放灯的所有位置
// arg1: 存放可以放灯的所有位置
// arg2: 当前矩阵
void getLeft(vector<POINT>& left, vector<vector<grid> > & curm){
//扫描矩阵, ,需要先把格子周围的点放入left的前面,
// 不在黑格子周围的放在后面,以保证每次回溯总是从黑格子周围开始;
left.clear();
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
POINT tmp;
if(curm[i][j].valid){ //说明当前格子有效
tmp.x = i;
tmp.y = j;
//接下来要区分它是不是黑格子周围的
if(aroundBlack[i][j]){ //从头放入
//该可以用点周围的黑色灯的值
tmp.val = aroundBlackValue[i][j];
left.emplace(left.begin(), tmp);
}else{ //从尾巴放入
left.push_back(tmp);
}
}
}
}
sort(left.begin(), left.end(), cmp);
}
//设置i,j处的点,要是为0,将其i周围置为false
void setAround0(int i, int j, vector<vector<grid> > & curm){
if(curm[i][j].value == 0){
if(i==0){
if(j==0){
curm[i+1][j].valid = false;curm[i][j+1].valid = false;
}else if(j==m-1){
curm[i][j-1].valid = false;curm[i+1][j].valid = false;
}else{
curm[i][j+1].valid = false;
curm[i][j-1].valid = false;
curm[i+1][j].valid = false;
}
}else if(i==n-1){
if(j==0){
curm[i-1][j].valid = false;curm[i][j+1].valid = false;
}else if(j==m-1){
curm[i-1][j].valid = false;curm[i][j-1].valid = false;
}else{
curm[i-1][j].valid = false;
curm[i][j+1].valid = false;
curm[i][j-1].valid = false;
}
}else{
if(j==0){
curm[i][j+1].valid = false;
curm[i-1][j].valid = false;
curm[i+1][j].valid = false;
}else if(j==m-1){
curm[i][j-1].valid = false;
curm[i-1][j].valid = false;
curm[i+1][j].valid = false;
}else{
curm[i+1][j].valid = false;curm[i][j+1].valid = false;
curm[i-1][j].valid = false;curm[i][j-1].valid = false;
}
}
}
}
// 根据当前放入的那个灯来更新矩阵,同时检测是否可以成功更新
// 不能成功更新的说名当前点不能放,curm所作的改变需要清除,则返回false然后换下一个点来回溯
bool updateCurmByLight(POINT light, vector<vector<grid> > & curm) {
int x0 = light.x, y0= light.y;
//判断当前点是否有效
if(!curm[x0][y0].valid){
return false;
}
//放入该灯,当然本身也需要算被照亮,可以只darkSum-1:
curm[x0][y0].value = 5;
curm[x0][y0].valid = false;
darkSum--;
//放入灯以后,该处周围的黑格子的数字需要减少,并且查看是否有为零的情况,为0的话需要将其周围视作不可用
vector<POINT> blacks;
if(y0 >= 1 && (1<=curm[x0][y0-1].value && 4>=curm[x0][y0-1].value)){
curm[x0][y0-1].value --;
setAround0(x0, y0-1, curm);
}
if(x0 >= 1 && (1<=curm[x0-1][y0].value && 4>=curm[x0-1][y0].value)){
curm[x0-1][y0].value --;
setAround0(x0-1, y0, curm);
}
if(y0 <= n-2 && (1<=curm[x0][y0+1].value && 4>=curm[x0][y0+1].value)){
curm[x0][y0+1].value --;
setAround0(x0, y0+1, curm);
}
if(x0 <= m-2 && (1<=curm[x0+1][y0].value && 4>=curm[x0+1][y0].value)){
curm[x0+1][y0].value --;
setAround0(x0+1, y0, curm);
}
//分别从该灯的+x,-x,+y,-y方向更新被照亮的地方,也就是将其置为fasle
int xp=light.x, xn=xp;
int yp=light.y, yn=yp;
//curm 为黑色格子的方式为-1到4,5是灯,所以只要当前格子值不是-2,就需要停止
//而且当为5的时候需要返回false,当遇到[-1, 4]之间的数据就停止探索;
//x正方向
for(int i=xp+1; i<m ;i++){
if(-1<=curm[i][y0].value && 4>=curm[i][y0].value){
break;
}else if(curm[i][y0].value == 5){
return false;
}else{
//被照亮 若是i没有被照亮,那么就将darkSum减1
if(!lightedUp[i][y0]){
curm[i][y0].valid = false;
darkSum--;
lightedUp[i][y0] = true;
}
}
}
//x负方向
for(int i=xp-1; i>=0 ;i--){
if(-1<=curm[i][y0].value && 4>=curm[i][y0].value){
break;
}else if(curm[i][y0].value == 5){
return false;
}else{
//被照亮 若是i没有被照亮,那么就将darkSum减1
if(!lightedUp[i][y0]){
curm[i][y0].valid = false;
darkSum--;
lightedUp[i][y0] = true;
}
}
}
//y正方向
for(int i=yp+1; i<n ;i++){
if(-1<=curm[x0][i].value && 4>=curm[x0][i].value){
break;
}else if(curm[x0][i].value == 5){
return false;
}else{
//若是i没有被照亮,那么就将darkSum减1, 同时置valid和照亮居正
if(!lightedUp[x0][i]){
curm[x0][i].valid = false;
darkSum--;
lightedUp[x0][i] = true;
}
}
}
//y负方向
for(int i=yn-1; i>=0 ;i--){
if(-1<=curm[x0][i].value && 4>=curm[x0][i].value){
break;
}else if(curm[x0][i].value == 5){
return false;
}else{
//若是i没有被照亮,那么就将darkSum减1, 同时置valid和照亮居正
if(!lightedUp[x0][i]){
curm[x0][i].valid = false;
darkSum--;
lightedUp[x0][i] = true;
}
}
}
return true;
}
//回溯函数
void backT(vector<POINT>& left, vector<vector<grid> > & curm,\
vector<vector<int> > & res){
//返回触发条件:
//1,失败放入,在update的时候就解决了
// 也就是说,update完成后,会决定这个点是放入还是不放入,你不需要关心
//2,找到了解答
//如果发现当前的灯们的顶点(也就是最近放入的一个灯)不能放入,则直接返回
//如果能cover住所有的位置,则将res更新,然后更新findRes
if(darkSum == 0){
bool satisfy = true;
//如果不能满足所有黑色格子上的数字都变为0
for(int i=0; i<bs.size(); i++){
satisfy = satisfy && (curm[bs[i].x][bs[i].y].value == 0);
}
//那就返回
if(!satisfy){
return ;
}
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
res[i][j] = curm[i][j].value;
}
}
findRes = true;
exit(0);
return ;
}
//当所有的位置都用了,但是没得到结果,也要回溯
if(left.empty()){
// cout <<endl<<endl<< "backTrack to last backT's next loop"<<endl<<endl;
return ;
}
//在调用backT后,记得把left,lights和curm退回到押入之前的状态
//这个left是在你选定了一个黑格子作为起点,把它周围的点放到left的最前面,
// 相当于你这个for循环只是在遍历l以该黑格子周围放灯作为起点,就可以得到最终结果
// 所以初始化left的时候,需要先把黑格子周围的点放入left的前面,不在黑格子周围的放在后面,以保证每次回溯总是从前面开始;
#pragma omp parallel for
for(int i=0; i<left.size() &&(!findRes); i++){
auto tmpCurm= curm;
auto tmpLightedUp = lightedUp;
int tmpDarkSum=darkSum;
auto tmpleft = left;
//>>>>>>>>>>>>>打印调试信息>>>>>>>>>>>>>>>>
// display(curm);
// pv(left);
// pv2(lightedUp);
// cout<<"--------dark: "<<darkSum<<" | ready to using x:"<<left[i].x<<", y: "<<left[i].y<<" left len: "<<left.size()<<endl;
if(left.size() >= 15){
cout<<" left len: "<<left.size()<<endl;
}
if(updateCurmByLight(left[i], curm)){ //可以正确更新,并且内容已经被更新
//更新剩余,放入该灯,接着回溯
getLeft(left, curm);
backT(left, curm, res);
}
//当该点的回溯退出来了,我们需要回滚到进入之前,然后进入到下一个遍历;
curm = tmpCurm;
darkSum = tmpDarkSum;
lightedUp = tmpLightedUp;
left = tmpleft;
}
}
vector<vector<int> > solveAkari(vector<vector<int> > & g){
// 请在此函数内返回最后求得的结果
//获得n行m列
n = g.size();
m = g[0].size();
//根据当前矩阵获得的剩下可以使用的灯
vector<POINT> left(0);
//根据回溯法一步步改变的当前矩阵
vector<vector<grid> > curm(0);
//下一步,初始化这个ABlack,然后将其赋值给aroundBlack即可
//标记矩阵: 白格子是否在黑格子周围,是白格子并且在黑格子周围,则为true,否则为false
//点亮矩阵: lightUp标记该格子是否被照亮
vector<vector<bool> > ABlack(n, vector<bool>(m, false));
vector<vector<int> > ABlackValue(n, vector<int>(m, false));
vector<vector<bool> > LUp(n, vector<bool>(m, false));
//初始化当前灯的矩阵和每个位置上的有效位,顺便初始化darkSum,顺便获得aroundBlack的值
for(int i=0; i<n; i++) {
curm.emplace_back(vector<grid>(m));
}
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
curm[i][j].value = g[i][j];
if(g[i][j] != -2){ //说明是黑色格子,标记周围的白格子,他们是在黑色周围,标记数字为0的黑格子的周围无效
//将自己置为fasle
curm[i][j].valid = false;
//需要将数字为0的黑格子周围的点置为false
setAround0(i, j, curm);
//标记自己周围的格子为aroundBlack
if(i==0){
if(j==0){
ABlack[i+1][j] = true;ABlack[i][j+1] = true;
ABlackValue[i+1][j] = curm[i][j].value;
ABlackValue[i][j+1] = curm[i][j].value;
}else if(j==m-1){
ABlack[i][j-1] = true;ABlack[i+1][j] = true;
ABlackValue[i][j-1] = curm[i][j].value;
ABlackValue[i+1][j] = curm[i][j].value;
}else{
ABlack[i][j+1] = true;
ABlack[i][j-1] = true;
ABlack[i+1][j] = true;
ABlackValue[i][j+1] = curm[i][j].value;
ABlackValue[i][j-1] = curm[i][j].value;
ABlackValue[i+1][j] = curm[i][j].value;
}
}else if(i==n-1){
if(j==0){
ABlack[i-1][j] = true;ABlack[i][j+1] = true;
ABlackValue[i][j+1] = curm[i][j].value;
ABlackValue[i-1][j] = curm[i][j].value;
}else if(j==m-1){
ABlack[i-1][j] = true;ABlack[i][j-1] = true;
ABlackValue[i-1][j] = curm[i][j].value;
ABlackValue[i][j-1] = curm[i][j].value;
}else{
ABlack[i-1][j] = true;
ABlack[i][j+1] = true;
ABlack[i][j-1] = true;
ABlackValue[i-1][j] = curm[i][j].value;
ABlackValue[i][j+1] = curm[i][j].value;
ABlackValue[i][j-1] = curm[i][j].value;
}
}else{
if(j==0){
ABlack[i][j+1] = true;
ABlack[i-1][j] = true;
ABlack[i+1][j] = true;
ABlackValue[i][j+1] = curm[i][j].value;
ABlackValue[i-1][j] = curm[i][j].value;
ABlackValue[i+1][j] = curm[i][j].value;
}else if(j==m-1){
ABlack[i][j-1] = true;
ABlack[i-1][j] = true;
ABlack[i+1][j] = true;
ABlackValue[i][j-1] = curm[i][j].value;
ABlackValue[i-1][j] = curm[i][j].value;
ABlackValue[i+1][j] = curm[i][j].value;
}else{
ABlack[i+1][j] = true;ABlack[i][j+1] = true;
ABlack[i-1][j] = true;ABlack[i][j-1] = true;
ABlackValue[i][j-1] = curm[i][j].value;
ABlackValue[i][j+1] = curm[i][j].value;
ABlackValue[i+1][j] = curm[i][j].value;
ABlackValue[i-1][j] = curm[i][j].value;
}
}
}else{ //说明是白格子,一开始当然没有被照亮
darkSum ++;
LUp[i][j] = false;
}
}
}
aroundBlack = ABlack;
aroundBlackValue = ABlackValue;
lightedUp = LUp;
//获得初始的bs
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
if(1<=curm[i][j].value && 4>=curm[i][j].value){
POINT tmp;
tmp.x = i; tmp.y = j;
bs.emplace_back(tmp);
}
}
}
//>>>>>>>>>>>>>打印调试信息>>>>>>>>>>>>>>>>
// display(curm);
// pv(left);
pv2(aroundBlack);
pv1(aroundBlackValue);
cout<<"-----------------------------";
//初始化你的可以放灯的位置 调用函数: getLeft
//存放结果
vector<vector<int> > res(n, vector<int>(m, 0));
//调用backT函数获得结果
getLeft(left, curm);
//通过并行的优化,就是在这里,我们通过每一个可以开始的节点来分别开启线程,这样做时间应该会快一些;
backT(left, curm, res);
//由于这res里面的数字为1,到4的灯在寻找答案的时候都被毁掉了,所以现在做一个恢复:
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
if(1<=g[i][j] && 4>=g[i][j]){
res[i][j] = g[i][j];
}
}
}
return res;
}
}
//问题:
//darksum的更新被重复了,也就是当
//问题:
//需要有一个变量,记录全局的黑色格子边上剩余的个数,要是不为0,那么即使获得答案也不被认可
//现在是在剩余黑格子旁边可以使用的总数非零的情况下获得答案,显然是不行的;
//获得所有的黑点的points,去扫描直到所有都为0,
// 采用abNum 不能判断是否满足了所有的黑点周围都有正确的点