数织(Nonogram)解谜思路与解谜逻辑2
如果游戏棋盘特别大,那么以生成所有结果再进行筛选的方式也会变得庞大无比导致页面卡死,所以必须有一种轻量的逻辑来对数据进行处理
源码
type RuleList = number[]
type IndexList = number[]
type CellItem = { value: -1 | 0 | 1 }
type CellMap = Record<string, CellItem>
// type BarLimit = { rule: RuleList, cellKeyList: string[], indexList: IndexList }
type BarItem = { rule: RuleList, rule_re: RuleList, cellKeyList: string[], frontLimitIndexList: IndexList, backLimitIndexList: IndexList }
type BarMap = Record<string, BarItem>;
const inputRowRestrict = `3 8 4 2 3 1,1 3 3 3 3 3 4 1 1,1 3 1 4 1 5 1,2 6 1 5 2 5 2,1 6 6 1 1,4 1 1 1 4 3,2 3 2 1 4 2 1 1 4,4 10 1 6 7,3 3 1 9 5 5,3 4 4 1 1 4 3,1 1 3 3 5 6 1 1 1 1 1,10 2 10 2 5 1,12 3 10 3 7,1 7 1 7 2 4 4,1 1 1 1 4 7 4 1 5,1 3 1 1 2 7,9 4 4,1 2 3 8 4 2,5 3 7 1 3 4 1,5 1 6 1 5 1 1,4 1 1 4 2 3 3 3,4 1 1 6 1 2 3 3,4 1 1 1 4 3 1 1 4,5 4 7 3 1 1 2 1,5 4 7 3 2 1 4,5 4 7 2 1 11 2 1 1,7 4 6 1 6 2 6,6 5 8 9 3,5 2 2 1 3 1 4 2 4,6 2 8 1 2 2 3,2 3 1 1 2 4 4 3 3 3,2 1 2 6 4 6 3 2,3 1 7 3 6 1 2,2 5 3 5 2 1 5 2,8 3 5 3 3 2 2,3 5 1 4 3 6 3 1 2,3 3 21 3 3 2,1 1 1 9 1 5 3 2 1,1 2 10 8 3 3,2 1 3 7 1 6 2 2,2 1 4 2 2 7 1 1 2,3 1 1 13 2 1 5 5,12 3 1 1 1 3 8,1 1 1 4 6 6 1 2 5 4 3,3 5 2 1 3 3 3 4 10,14 7 4 1 10,9 5 5 3 8,2 10 4 3 5 4,1 3 3 1 3 3 2 5,4 1 1 1 5 5 1 5`;
const inputColRestrict = `4 3 3 11 3 1 1 5 6,1 7 10 3 1 1 3 4,4 2 12 2 1 4,1 3 15 1 2,1 3 3 8 3 3 4,5 6 1 5 5 4,11 1 1 4 7,13 2 5 8,2 3 3 2 4 4 9,5 1 4 3 7 1 4 4 1 3,5 3 2 2 5 3 1 5 3 1,3 2 1 6 1 5 4,2 2 3 10,2 1 2 9,4 3 1 3 2 5 2,2 7 9 1 7,2 6 3 6 7 3,1 3 1 12 8 3,1 1 1 6 9 9 1,2 2 3 14 16 1,5 1 3 21 9,5 4 6 3 2 3 3 2,3 5 6 7 3 1,3 5 2 5 3 1 1,6 4 3 4 6 1,2 4 4 2 2 1 6,5 5 3 8,1 1 5 3 3 2 5,3 5 1 3 1 1 1 7,2 8 1 2 3 3 1 3,2 4 3 3 7 2,5 1 3 5,10 2 2 1 5 3,4 4 2 4 1 3 6 3,1 3 1 10 7 5,1 1 9 1 4 3 1,1 4 3 1 5 2,2 3 4 5 2,2 1 1 1 3 8 1 3,1 3 1 1 1 5 1 1,3 3 3 5,3 1 4 1 5 3 7,4 5 1 7 5 2 8,4 2 4 1 3 5 4 6,3 1 6 1 1 3 2 4 8,2 2 3 3 1 3 2 6,1 1 4 7 3 1 1 1 11,26 3 8,1 5 2 3 11 1 4 2,5 5 1 6 14 1 2 2`;
/** 开始破解 */
const start = (rowRuleStr: string, colRuleStr: string) => {
let { cellMap, barMap } = createMap(rowRuleStr, colRuleStr)
let archiveList: { barMap: BarMap, cellMap: CellMap }[] = []
let time = 0
let isDone = false
do {
let { isChange, isError } = crackBarMap(barMap, cellMap)
if (isError) {
// 出现错误证明决策失误,取出最后一条存档
let lastArchive = archiveList.pop()
if (!lastArchive) {
throw new Error("无法破解");
}
Object.values(lastArchive.cellMap).some(v => {
if (!v.value) {
v.value = -1
return true
}
return false
})
barMap = lastArchive.barMap
cellMap = lastArchive.cellMap
} else if (!isChange) {
archiveList.push({
barMap: JSON.parse(JSON.stringify(barMap)),
cellMap: JSON.parse(JSON.stringify(cellMap)),
})
isDone = Object.values(cellMap).every(v => {
if (!v.value) {
v.value = 1
return false
}
return true
})
}
time++
} while (time < 99 && !isDone);
console.log('time', time)
// 开始解谜,解谜过程就是不断对barMap进行处理
Object.keys(barMap).forEach(v => {
if (v.includes('r-')) {
let item = barMap[v]
console.log(item.cellKeyList.map(v2 => cellMap[v2].value === -1 ? ' ' : '1'), item.rule)
}
});
}
/** 创建数据集合 */
const createMap = (rowRuleStr: string, colRuleStr: string) => {
let rowRuleArr = getRule(rowRuleStr)
let colRuleArr = getRule(colRuleStr)
/** 单元格集合 */
let cellMap: CellMap = {}
/** 条(行/列)集合 */
let barMap: BarMap = {}
// 填充集合数据
rowRuleArr.forEach((rowRule, rowIndex) => {
barMap[`r-${rowIndex}`] = createBarItem(rowRule)
colRuleArr.forEach((colRule, colIndex) => {
if (!barMap[`c-${colIndex}`]) {
barMap[`c-${colIndex}`] = createBarItem(colRule)
}
cellMap[`${rowIndex}-${colIndex}`] = { value: 0 } // -1不填,0待填,1填入
barMap[`r-${rowIndex}`].cellKeyList.push(`${rowIndex}-${colIndex}`)
barMap[`c-${colIndex}`].cellKeyList.push(`${rowIndex}-${colIndex}`)
})
})
return { cellMap, barMap }
}
/** 获取规则 */
const getRule = (ruleStr: string) => ruleStr.replace(',', ',').split(',').map(v => v.split(' ').map(v2 => Number(v2)).filter(v2 => v2)).filter(v => v)
/** 创建条数据 */
const createBarItem = (rule: RuleList) => ({ rule, rule_re: [...rule].reverse(), cellKeyList: [], frontLimitIndexList: createLimitIndexList(rule), backLimitIndexList: createLimitIndexList([...rule].reverse()) })
/** 创建极限坐标 */
const createLimitIndexList = (rule: RuleList): number[] => rule.reduce<[number[], number]>(([arr, index], v) => [arr.concat(index), index + v + 1], [[], 0])[0];
/** 破解条集合 */
const crackBarMap = (barMap: BarMap, cellMap: CellMap) => {
let isChange = false
let isError = false
Object.values(barMap).forEach(v => {
let cellValueList = v.cellKeyList.map(v => cellMap[v].value)
let cellValueList_re = [...cellValueList].reverse()
let [frontLimitIndexList, frontLimitValueList] = handleLimitIndexList(v.frontLimitIndexList, v.rule, v.rule_re, cellValueList, cellValueList_re)
let [backLimitIndexList, backLimitValueList] = handleLimitIndexList(v.backLimitIndexList, v.rule_re, v.rule, cellValueList_re, cellValueList)
v.frontLimitIndexList = frontLimitIndexList
v.backLimitIndexList = backLimitIndexList
backLimitValueList.reverse()
let absValue = (v.rule.length + 1) * 2
cellValueList.forEach((v2, i2) => {
// 先检查生成的值是否符合规则
if (v2 * frontLimitValueList[i2] < 0 || v2 * backLimitValueList[i2] < 0) {
isError = true
return
}
// 检查是否匹配到了可确定项
if (Math.abs(frontLimitValueList[i2] + backLimitValueList[i2]) !== absValue) return
// 对未填充的单元格进行填充
if (!v2) {
cellMap[v.cellKeyList[i2]].value = (frontLimitValueList[i2]) > 0 ? 1 : -1
isChange = true
}
})
})
return { isChange, isError }
}
/** 处理极限坐标 */
const handleLimitIndexList = (indexList: IndexList, rule: RuleList, rule_re: RuleList, cellValueList: CellItem['value'][], cellValueList_re: CellItem['value'][]) => {
// let cellValueList = limit.cellKeyList.map(v => cellMap[v].value)
let indexArr = [...indexList]
let indexIsChange: Boolean
// 需要将极限坐标调整至符合单元格填充状态
do {
indexIsChange = false
// 第一遍,从前往后扫描,将不可填入的部分位置让开
cellValueList.forEach((v, i) => {
if (v !== -1) return
indexArr.forEach((v2, i2) => {
// 满足条件证明v2与不可填入格冲突了,需要将v2放在不可填入格之后
if (v2 <= i && v2 + rule[i2] > i) {
indexArr[i2] = i + 1
indexIsChange = true
}
/** 第一个不执行下方检查逻辑 */
if (!i2) return
/** 校正index使其符合rule */
if (indexArr[i2] < indexArr[i2 - 1] + rule[i2 - 1] + 1) {
indexArr[i2] = indexArr[i2 - 1] + rule[i2 - 1] + 1
indexIsChange = true
}
})
})
// 反转坐标列表
indexArr.reverse()
let diff = cellValueList.length - 1
// 第二遍,从后往前扫描,将填入的部分对齐
cellValueList_re.forEach((v, i) => {
let originalIndex = diff - i
if (v !== 1) return
indexArr.some((v2, i2) => {
// 填入部分在标记范围内不用处理
if (v2 <= originalIndex && v2 + rule_re[i2] > originalIndex) return true
// 坐标初始值小于填入部分,则需要将该坐标的标记范围末端与填入部分对齐
if (v2 < originalIndex) {
indexArr[i2] = originalIndex + 1 - rule_re[i2]
indexIsChange = true
return true
}
})
})
// 反转坐标列表
indexArr.reverse()
/** 校正index使其符合rule */
indexArr.forEach((v2, i2) => {
/** 第一个不执行下方检查逻辑 */
if (!i2) return
/** 校正index使其符合rule */
if (indexArr[i2] < indexArr[i2 - 1] + rule[i2 - 1] + 1) {
indexArr[i2] = indexArr[i2 - 1] + rule[i2 - 1] + 1
indexIsChange = true
}
})
} while (indexIsChange);
let valueList: number[] = []
let listValue = -1
let listIndex = 0
cellValueList.forEach((v, i) => {
if (i === indexArr[listIndex]) {
listValue = listValue * -1 + 1
}
valueList.push(listValue)
if (i === indexArr[listIndex] + rule[listIndex] - 1) {
listValue = listValue * -1 - 1
listIndex++
}
})
return [indexArr, valueList]
}
start(inputRowRestrict, inputColRestrict)
同样是输入规则字符串,不过这次没有做可视化页面,如果需要的话可以转成js后运行,会输出结果示意图

设计思路
本次设计采用前后极限对齐取值的方式进行破解
例如10格长度,规则是2 4

初始一看有三个格子颜色一致,但是只以颜色判断是不行的,需要对其进行编号

这里的数字填充规则是不填部分用负数表示,填充部分用正数表示(需要注意-1有可能不存在,有些编码从2开始)
但是由于前后极限数字填充方向不一致,并不是数字相同的代表对齐,而是有一个特殊的校验规则,同一位置的数字之和的绝对值等于规则个数加1再乘2,所以对于本条规则绝对值应该等于6,那么第7位就可确定填充

那么问题就落在了怎么确定前后极限位置上
避让
首先要对不填的位置进行避让,深蓝色表示连续填充的起始位置,此位置作为重要判断依据

从左往右依次判断,到第2位发现已确认不填,那么就要看极限位置是否需要调整,需要调整就要将起始位置放到已确认不填之后,变为

这时候就发现,极限位置重叠了,也就是说需要对极限位置进行梳理

梳理完成后继续判断,第6位发现已确认不填,同样调整位置后再进行梳理

对齐
避让只处理已确认不填的位置,现在需要处理已确认填充的位置
对齐操作要从右往左判断,发现第5位为已确认填充且当前极限位置中没有填充,那么需要将在它前面的、最近的、最后一位填充位置与其对齐

对齐操作不会使极限位置重叠,因此不需要进行梳理
但是,对齐操作导致的位置变动可能会使避让处理失效(同理避让处理也会让对齐操作失效,不过对齐操作紧跟避让操作立马就修复了),所以要重复避让-对齐-避让-对齐操作直至极限位置不再发生变动
处理操作的核心是极限位置尽可能不向右移动,但每次处理都在向右移动,相应的,后极限位置也在向左移动直至两者保持一致,此时代表本条规则被破解了
当所有规则都被破解,那么整个数织也就被破解了
当然,其中还有决策机制,与前作中机制一致这里就不多做解释了
如果有时间我会尽可能将上边罗里吧嗦的逻辑解释转变为页面动画,方便更加直观的观看整个结题过程

浙公网安备 33010602011771号