一个枚举算法题目引发的Qt小游戏

前几天在做一道枚举算法的题目,由于其题目的特殊性导致做到最后,竟然演变成一个Qt小游戏的编程。首先来描述一下题目:

有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变原有状态。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。如果某一按钮周围不存在上/下/左/右,则只改变存在的位置。如下图左图点击了三个按钮后结果为右图:
这里写图片描述

那么给定一个初始矩阵,矩阵的5*6=30个灯的状态由键盘依次输入(1为亮,0为灭),求解一个方案,使得按照求解方案按下指定(多个)按钮后,所有的灯都被熄灭,并且输出该方案。比如:
这里写图片描述

算法分析:
首先要满足灯全灭的方案,最多有2^30=2147483648个,而要依次遍历这21亿多个方案,按理来说数据量不是特别大时间不会太长。但是由于题目的特殊性(总时间限制: 1000ms,内存限制: 65536kB),枚举的范围应该更小。

首先我们知道,一个灯按下影响周围灯的状态,一旦某一行的状态确定,要使得本行灯全灭,则下一行的按钮按下的方案就已经确定。比如:第一行需要按下的按钮全部按下后,本行灯的状态也就确定,而其后所有行的按钮按下方案也就确定。

假设灯的状态为:101010,则第二行按下的按钮状态只能与第一行的灯的状态相匹配为101010,因为第二行第1、3、5个按钮按下,则第一行第1、3、5个灯就会被熄灭(按钮影响其上的灯),所以原本101010的第一行灯状态全为000000。但是如果第二行随便一位与第一行不匹配,如101011,则第二行最后一位按下后,把原本第一行已经灭了的灯又重新点亮。并且某一行的灯亮灭与否只会受到本行、上一行、下一行三行按钮的状态影响。比如第一行的灯不会受第三行的影响,因为条件限制(一个按钮只会影响其本身和周围上下左右一个灯,而不会影响上下左右多行灯)。

既然第一行如此,则以后几行皆是如此,因此不需要遍历所有2147483648种情况。而只需要将第一行按钮的所有情况遍历一遍即可(2^6=64种),由于按照下一行按钮状态与本行灯状态相同的方式。所以到最后一行时,前面的行都因为第i+1行的按钮状态与第i行灯状态相同,而使得第i行的灯都已熄灭。则只需要判断最后一行灯是不是已经全部熄灭即可,如果是则结束判定,获取最终方案,如果不是,则第一行的按钮状态加1进行下一轮测试。

关键点一:一个按钮最多被按一次(按偶数次相当于没按,按基数次相当于按一次);
关键点二:某一行的灯状态不会受其间隔一行按钮状态的影响(如第一行灯状态不会受第三行按钮的影响)。
关键点三:第一行按钮方案确定以后,为达到所有灯全灭的要求,以后各行按钮的方案也就都确定。
另外,按钮点击顺序无关紧要,不影响最终结果。

测试代码:

//测试结果就是上面举例所用的截图
#include <iostream>
#include <cstring>
#include <string>
#include <memory>

using namespace std;
//由于灯只有亮灭两种值/状态,按钮也是只有按下和不按两种状态,则所有灯与按钮状态用bit位来存储
//获取bit位
int GetBit(char c, int i)
{
    return ((c >> i) & 0X01);
}

//设置bit位
void SetBit(char &c, int i, int value)
{
    (1 == value) ? (c |= (1 << i)) : (c &= ~(1 << i));
}

//bit位取反
void BackBit(char &c, int i)
{
    //bit位与1异或运算结果为~bit,bit与0异或运算结果仍为bit
    c ^= (1 << i);
}

//遍历输出开关的按下状态
void OutPutResult(char result[])
{
    for(int i = 0; i < 5; i++){
        for(int j = 0; j < 6; j++){
            cout<< GetBit(result[i], j);//第1行的第j列的状态
            if(j < 5){
                cout<<" ";
            }
        }
        cout<<endl;
    }
}

void SetInitStatus(char init[])
{
    cout<<"请输入初始状态(5*6矩阵,值为0/1:)"<<endl;
    int i, j, bit;
    //从键盘读入初始状态:5*6
    for(i = 0; i < 5; i++){
        for(j = 0; j < 6; j++){
            cin >> bit;
            SetBit(init[i], j, bit);//设置出事状态
        }
    }
}

int main(void)
{
    char init[5] = {0};//初始化输入状态
    char lights[5] = {0};//存储中间状态
    char result[5] = {0};//存储开关按下状态
    char line;//某一行
    int n, i, j;

    SetInitStatus(init);

    for(n = 0; n < 64; n++){//遍历第一行的所有状态(6盏灯/6个开关,2^6=64)
        memcpy(lights, init, sizeof(init));
        line = n;//第i行开关状态,第i行状态一旦确定,i+1行为了满足i+1及之前行的全灭状态,开关按下的方法就固定
        for(i = 0; i < 5; i++){
            result[i] = line;//将第i行的开关方案置为line的状态
            for(j = 0; j < 6; j++){
                if(1 == GetBit(line, j)){//开关状态为按下则需要改变周围灯的状态
                    if(j > 0)//左边存在灯
                        BackBit(lights[i], j-1);
                    BackBit(lights[i], j);
                    if(j < 5)//右边灯存在
                        BackBit(lights[i], j+1);
                }
            }
            if(i < 4)//下一行存在,则根据line状态改变下一行灯状态
                lights[i+1] ^= line;
            line = lights[i];//下一行按钮状态方案就是本行灯的状态
        }
        if(lights[4] == 0){//如果该方案可以使第5行全灭(前4行自然全灭)则方案可行
            cout<<"\n满足灯全灭的方案为:"<<endl;
            OutPutResult(result);
            break;//直接结束
        }
    }
    return 0;
}

然而在算法设计并实现完毕以后,我想看看按钮一一按下后,灯是如何从混乱的亮灭状态逐渐变全部熄灭的。所以就有了一个Qt的小游戏,其效果实现如下:
这里写图片描述

Qt界面的鼠标事件处理、绘图事件处理以及按钮的槽函数代码如下(其中class bitOper是上面算法代码的修改和包装,完整测试代码的工程文件可邮箱索取):

#ifndef WIDGET_H
#define WIDGET_H

#include "bitoper.h"
#include <QWidget>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
    void initLamp();
protected:
    void paintEvent(QPaintEvent *);
    void mousePressEvent(QMouseEvent *);
private slots:
    void on_buttonRestart_clicked();

    void on_buttonPrompt_clicked();

private:
    Ui::Widget *ui;

    //用于创建初始矩阵与寻找矩阵求解方案的类
    bitOper lamp;
    //起点坐标、终点坐标
    QPoint startPoint;
    QPoint endPoint;
    //每一格的高度与宽度
    int gridHigh;
    int gridWidth;

    bool pressFlag;
};

#endif // WIDGET_H
/*
 * Time:2017年7月31日11:09:14
 * Author:KangRuoJin
 * Mail:mailbox_krj@163.com
 * Version:v2.1
*/
#include "widget.h"
#include "ui_widget.h"

#include <cstring>
#include <time.h>
#include <stdlib.h>

#include <QDebug>
#include <QPainter>
#include <QMouseEvent>
#include <QPaintEvent>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    startPoint = QPoint(0,0); //起点坐标
    endPoint = QPoint(400,300); //终点坐标

    //宽度与高度
    gridWidth = (endPoint.x() - startPoint.x())/6;
    gridHigh = (endPoint.y() - startPoint.y())/5;

    pressFlag = false;
    memset(lamp.init, 0, 5);
    memset(lamp.lights, 0, 5);
    memset(lamp.result, 0, 5);
    initLamp();
}

void Widget::initLamp()
{
    srand(time(NULL));
    //只用到后六位,则对64取余即可
    lamp.init[0] = rand()%64;
    lamp.init[1] = rand()%64;
    lamp.init[2] = rand()%64;
    lamp.init[3] = rand()%64;
    lamp.init[4] = rand()%64;
}

void Widget::paintEvent(QPaintEvent *ev)
{
    QPainter p(this);
   // p.drawPixmap(this->rect(),QPixmap("://back.PNG"));

    int startX, startY;
    for(int i = 0; i<6; i++){
        for(int j = 0; j<5; j++){
            startX = gridWidth * i +2;
            startY = gridHigh * j +2;
            if(lamp.GetBit(lamp.init[j],i)){//为1画亮(黄)
                if(lamp.GetBit(lamp.result[j],i)  && pressFlag)//需要提示
                    p.drawPixmap(startX, startY, gridWidth-2, gridHigh-2, QPixmap(":/image/press_light.png"));
                else//不需要提示
                    p.drawPixmap(startX, startY, gridWidth-2, gridHigh-2, QPixmap(":/image/light.PNG"));
            }
            else{//为0画灭(黑)
                if(lamp.GetBit(lamp.result[j],i) && pressFlag)
                    p.drawPixmap(startX, startY, gridWidth-2, gridHigh-2, QPixmap(":/image/press_black.png"));
                else
                    p.drawPixmap(startX, startY, gridWidth-2, gridHigh-2, QPixmap(":/image/black.PNG"));
            }
        }
    }
}
void Widget::mousePressEvent(QMouseEvent *ev)
{
    //根据点击点击点位置更新数组
    int i = ev->x()/gridWidth;
    int j = ev->y()/gridHigh;

    lamp.BackBit(lamp.init[j],i);//点击坐标反转
    if(j > 0)
        lamp.BackBit(lamp.init[j-1],i);//点击坐标上一行对应横坐标位置反转
    if(j < 4)
        lamp.BackBit(lamp.init[j+1],i);//点击坐标下一行对应横坐标位置反转
    if(i > 0)
        lamp.BackBit(lamp.init[j],i-1);//点击坐标前一个位置反转
    if(i < 5)
        lamp.BackBit(lamp.init[j],i+1);//点击坐标后一个位置反转

    lamp.SetBit(lamp.result[j], i, 0);//点击一次就将该点击点的result置为0

    update();//更新绘图
}

Widget::~Widget()
{
    delete ui;
}
//重新开始按钮信号槽函数
void Widget::on_buttonRestart_clicked()
{
    pressFlag = false;//不能显示提示
    memset(lamp.init, 0, 5);
    memset(lamp.lights, 0, 5);
    memset(lamp.result, 0, 5);

    initLamp();//重新绘图
    update();
}
//提示按钮信号槽函数
void Widget::on_buttonPrompt_clicked()
{
    pressFlag = true;//可以显示提示
    lamp.ProcessAlgroithm();//计算解决方案,lamp.result存储方案
    update();//重新绘图
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值