數織(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號