1、算法描述
有一天acmj在玩一种游戏----用2*1或1*2的骨牌把m*n的棋盘完全覆盖。但他感觉把棋盘完全覆盖有点简单,他想能不能把完全覆盖的种数求出来?由于游戏难度增加他自己已经没法解决了,于是他希望大家能用程序来帮他把问题解决了。
输入
有多组数据。
每组数据占一行,有两个正整数n(0<n<12),m(0<m<12)。
当n,m等于0时输入结束
输出
每组数据输出占一行,输出完全覆盖的种数。
样例输入
2 2
2 3
2 4
2 11
4 11
0 0
样例输出2
3
5
144
51205
2、项目展示
2.1、加载界面
第一界面:主要是运用到了插入图片,显示文字,设置背景,使用计时器,还有一个动态效果的“正在加载”图案,用主要包含row,column的线性布局结构将它们穿插在一起,再设置一些字体的颜色,大小,格式,外边距等让整体界面美观。整体效果如下图所示:
2.2、题目描述
第二界面:主要是显示题目的要求,包括对输入,输出的要求,便于使用者了解题目大概和输入,输出格式,其他基本上就是显示text文本框所包含内容并对其格式进行设置,在该界面下方设置了一个按钮,用于跳转到下一界面;整体效果如下图所示:
2.3、输入页面
第三界面:首先是定义了文本框,显示输入输出样例,让使用者在输入数据时有一个参考,明白输入格式,接下来设置了一个输入文本框TextArea,等待数据传入,在输入合法数据之后点击下方按钮“计算”,会在本页面内的算法代码实现过程传入数据并得出对应的结果,再将必要的输入和输出数据传到下一界面中,同时跳转到下一界面;整体效果如下图所示:
2.4、结果显示
第四界面:当第三界面的数据输入并点击“计算”按钮之后,输入的各组n,m和对应结果都被保存在一个params对象中被传入第四界面,通过对线性结构的运用再将它们分别显示出来。整体效果如下图所示:
3、算法分析
3.1、题意分析
给定n*m的方格,看能分成多少个1 *2的小方块。
3.2、题意解读
摆放方块的时候,先放横着的,再放竖着的。总方案数等于只放横着的小方块的合法方案数。
如何判断,当前方案数是否合法? 所有剩余位置能否填充满竖着的小方块。可以按列来看,每一列内部所有连续的空着的小方块需要是偶数个。
这是一道动态规划的题目,并且是一道 状态压缩的dp:用一个N位的二进制数,每一位表示一个物品,0/1表示不同的状态。因此可以用0 → 2 N − 1 ( N 二 进 制 对 应 的 十 进 制 数 ) 0\rightarrow 2^N-1(N二进制对应的十进制数)0→2 N −1(N二进制对应的十进制数)中的所有数来枚举全部的状态。
算法流程图如下所示:
3.3、状态表示
状态表示:f[i][j]表示已经将前 i -1 列摆好,且从第i−1列,伸出到第 i列的状态是 j的所有方案。其中j是一个二进制数,用来表示哪一行的小方块是横着放的,其位数和棋盘的行数一致。 请看下面的释义。
解释:
首先进行预处理1:对于每种状态,先预处理每列不能有奇数个连续的0。将预处理1的结果都存入st[i]数组中。
然后进行预处理2:看第i-2列伸出来的和第i-1列伸出去的是否冲突。
预处理1和预处理2对于结果的推导如图所示。
3.4、状态转移
若是预处理1和2都可以通过,则可以将该方案转移,所有当前列的方案数都等于之前的第i-1列所有状态k的累加,最终可以计算出所求方案数f[m][0]。
4、项目介绍
项目共分为四个界面:加载界面、题目描述、输入处理、结果显示。这里仅展示我认为较为关键的部分。
4.1、加载页面
图片设置,将项目内的图片加载到区域背景中。(该部分代码仅供参考)
Column(){
}
.margin({ //设置外边距属性
top:50
})
.width('90%') //设置这一块地方的宽,高,背景图片和背景图片属性
.height(200)
.border({ width: 1 })
.backgroundImage($r('app.media.pan1'))
.backgroundImageSize({ width: '1200px', height: '600px' })
设置两秒的定时器,时间到后跳转第二个界面。(该部分代码仅供参考)
aboutToAppear() {
console.info('IntPage 界面打开');
this.startTimer()
}
startTimer() {
// 设置两秒后的定时器
setTimeout(() => {
// 两秒后执行页面跳转
router.pushUrl({url: 'pages/QuestionRequirementsPage'}).then(() => {
console.info('成功跳转到 QuestionRequirementsPage');
}).catch((err) => {
console.error(`跳转界面失败. Code is ${err.code}, message is ${err.message}`);
});
}, 2000); // 2秒
}
设置加载动画,强化动态效果,使用的是官方文档提供的一个动画效果。(该部分代码仅供参考)
LoadingProgress()
.color(Color.Blue)
.width(100)
.height(100)
4.2、题目描述
这个页面就是简单的UI设计,将文本显示在页面上,方便理解题意。
在这里贴出按钮实现页面跳转功能代码。(该部分代码仅供参考)
import router from '@ohos.router' // 在最上面导入api
Button('输入数据', { type: ButtonType.Normal })
.borderRadius(20)
.width(200)
.height(60)
.margin({
top:40
})
.onClick(()=>{
router.pushUrl({url: 'pages/MainPage'}).then(() => {
console.info('成功跳转到 MainPage');
}).catch((err) => {
console.error(`跳转界面失败. Code is ${err.code}, message is ${err.message}`);
});
})
4.3、输入页面
检测到错误格式后,显示弹窗提示。(该部分代码仅供参考)
AlertDialog.show(
{
title: '错误',
message: ' 输入的格式有误,请重新输入',
autoCancel: true,
offset: { dx: 0, dy: -10 },
alignment: DialogAlignment.Center,
gridCount: 3,
confirm: {
value: '确认',
fontColor:Color.White,
backgroundColor:Color.Red,
action: () => {
console.info('Button-clicking callback')
}
}
}
)
将这个页面算出的结果,传输到结果页面,仍然有跳转页面,记得导入api。(该部分代码仅供参考)
router.pushUrl({
url: 'pages/SituationsPage', // 目标页面的URI或路由名
params: {
n: numbers_n,
m:numbers_m,
results: result_arr,
}
}).then(() => {
console.info('成功跳转到 SituationsPage');
}).catch((err) => {
console.error(`跳转界面失败. Code is ${err.code}, message is ${err.message}`);
});
4.4、结果显示
按照上个界面算出的结果,将多个结果显示。(该部分代码仅供参考)
ForEach(this.results, (item,index) => {
Column(){
Row() {
Column() {
Text('n:' + this.n[index])
.textAlign(TextAlign.Center)
.fontSize(35)
.width('50%')
}
Column() {
Text('m:' + this.m[index])
.textAlign(TextAlign.Center)
.fontSize(35)
.width('50%')
}
}
.margin({
top:20
})
Row() {
Text('result:' + item.toString())
.fontSize(35)
.textAlign(TextAlign.Center)
.width('100%')
}
.margin({
top:20
})
}
.width('90%')
.height(150)
.backgroundColor(this.Color)
.borderRadius(15)
.margin({ top: 10 })
}, item => item)
5、核心代码
该项目的核心部分在于输入数据处理以及算法实现部分,我会在下面将代码贴出(代码由Ts语言实现)。
5.1、输入数据处理
function processStringToNumbers(inputString: string): number[]
{
const pairs = inputString.split('\n').join(' ').split(' ');
const numbers: number[] = [];
for (let i = 0; i < pairs.length; i += 2) {
// 检查是否到达'0 0'结束标记
if (pairs[i] === '0' && pairs[i + 1] === '0') {
break; }
// 注意:这里假设每对数字都是有效的,并且为整数
numbers.push(parseInt(pairs[i], 10));
numbers.push(parseInt(pairs[i + 1], 10));
}
return numbers;
}
5.2、完全覆盖算法函数
function completeCoverage(n:number,m:number): number
{
// const M:number = 1<<n
// f的大小应该是 f[m][2^n],共有m列,每一列有2^n状态
const f = new Array(m+1).fill(0).map(m=>new Array(1<<n).fill(0)) //二维数组 f,使用 fill(0) 方法填充初始值为 0,然后通过 map 方法对每一行进行处理,将每一行都填充为长度为 1<<n 的数组,并初始化值为 0
const st = new Array(1<<n).fill(false) //状态数组,长度为 1<<n 的布尔数组 st,元素初始值均为 false
// 枚举n位二进制数(因为状态是按列压缩的,总共二进制位有n个)
for(let i = 0; i< 1<<n; i++){
// 枚举每一位
let isValid = true
let cnt = 0
for(let j = 0; j<n; j++){
// 如果此时第j位是1
if(i >> j & 1){
// 那么需要判断当前的cnt数量是不是奇数
if(cnt & 1){ //即cnt确实为奇数
isValid = false
break;
}
cnt = 0
}else{
cnt++
}
}
if(cnt & 1){
isValid = false
}
st[i] = isValid
}
// 将st数组再次预过滤,成为新的state表
const state = {}
for(let i = 0; i< 1<<n; i++){
state[i] = []
for(let j = 0; j< 1<<n; j++){
if((i&j)===0 && st[i|j]){
state[i].push(j)
}
}
}
// 最后再进行dp
f[0][0] = 1 //先初始化,表示第0列没有被伸到的摆放方式,即空棋盘,有1种方式
// 先枚举所有列
for(let i = 1; i<=m; i++){
// 再枚举每一列里面的所有排列组合
for(let j = 0; j< 1<<n; j++){
// 如果state表里有这个key,说明j是一个合法的状态,则可以转移
for(let k of state[j]){
f[i][j] += f[i-1][k]
}
}
}
return f[m][0]
}
参考文献
华为官方文档