【趣题分享】赤壁之战每日演兵(原诸葛亮列传兵法题)求解算法

发布于:2024-12-06 ⋅ 阅读:(167) ⋅ 点赞:(0)


序言

问题如上(每个阵型都可以进行旋转或翻转,最终填满日历拼图),其实这是之前诸葛亮列传的兵法题(可惜狗宝更新后把能这个能白嫖资源的列传给删了,emmm),那个网上有很多现成的答案,现在这个每天都不一样,用脑子想还是得花点时间。

代码如下可自取,感觉作为一道leetcode题不错,但是不用一些特殊的寄巧处理还是挺慢的,穷举情况实在是太多。目前常规7×7拼图求解时间1000秒左右,如果是5×5拼图的话,只需要0.1秒。

求解思路暂时搁置,其实本质是一个整数规划,所以关键在于怎么样能更快,一开始写了个纯穷举+DP,结果跑个5×5的都要将近半个小时,7×7的更是估算要100万个小时。后来根据问题的特殊性做了一些剪枝处理,复杂度大大降低。

贴一个明天(12月6日)的求解结果(这个结果要旋转180度,1表示拼图上原本就不能放的格子,2-9分别表示不同阵型的块):

[[1. 1. 1. 1. 2. 2. 2.]
 [5. 6. 6. 6. 2. 2. 2.]
 [5. 6. 6. 7. 7. 7. 7.]
 [5. 5. 9. 9. 9. 3. 7.]
 [5. 1. 4. 4. 9. 3. 3.]
 [1. 1. 4. 8. 9. 8. 3.]
 [1. 4. 4. 8. 8. 8. 3.]]

1 求解算法代码(python)

代码自取:

# -*- coding: utf-8 -*-
# @author: caoyang
# @email: caoyang@stu.sufe.edu.cn

import time
import numpy
from pprint import pprint
from copy import deepcopy
from functools import lru_cache

FORMATION_ABBR_TO_NAME = {
	"BDQX": "北斗七星阵",
	"FS": "锋矢阵",
	"SLCS": "双龙出水阵",
	"JQHH": "九曲黄河阵",
	"JS": "金锁阵",
	"F": "方阵",
	"XX": "玄襄阵",
	"GX": "钩形阵",
	"YZCS": "一字长蛇阵",
	"SC": "三才阵",
	"TM": "天门阵",
	"HY": "混元阵",
	"YY": "鸳鸯阵",
}
FORMATION_NAME_TO_ABBR = {_abbr: _name for _name, _abbr in FORMATION_ABBR_TO_NAME.items()}

# 生成给定日期的日历拼图
def generate_calendar_puzzle(month = 12, day = 3):
	month_days = [31, 28, 31, 30, 31, 60, 31, 31, 30, 31, 30, 31]
	calendar_puzzle = [[0] * 7 for i in range(7)]
	
	for i in range(35 - month_days[month - 1]):
		calendar_puzzle[6][7 - i - 1] = 1
	calendar_puzzle[(day - 1) // 7 + 2][(day - 1) % 7] = 1
	calendar_puzzle[0][6] = 1
	calendar_puzzle[1][6] = 1
	calendar_puzzle[(month - 1) // 6][(month - 1) % 6] = 1
	return calendar_puzzle

# 定义阵型及其变体
def define_formation_variants(names):
	abbrs = map(lambda _name: FORMATION_NAME_TO_ABBR[_name], names)
	# 北斗七星阵
	BDQX = [
		[[1, 0], [1, 1], [0, 1], [0, 1]],
		[[0, 1], [1, 1], [1, 0], [1, 0]],
		[[1, 0], [1, 0], [1, 1], [0, 1]],
		[[0, 1], [0, 1], [1, 1], [1, 0]],
		[[0, 0, 1, 1], [1, 1, 1, 0]],
		[[1, 1, 0, 0], [0, 1, 1, 1]],
		[[0, 1, 1, 1], [1, 1, 0, 0]],
		[[1, 1, 1, 0], [0, 0, 1, 1]],
	]
	# 锋矢阵
	FS = [
		[[1, 1, 1], [1, 0, 0], [1, 0, 0]],
		[[1, 1, 1], [0, 0, 1], [0, 0, 1]],
		[[0, 0, 1], [0, 0, 1], [1, 1, 1]],
		[[1, 0, 0], [1, 0, 0], [1, 1, 1]],
	]
	# 双龙出水阵
	SLCS = [
		[[1, 1], [1, 0], [1, 1]],
		[[1, 1], [0, 1], [1, 1]],
		[[1, 1, 1], [1, 0, 1]],
		[[1, 0, 1], [1, 1, 1]],
	]
	# 九曲黄河阵
	JQHH = [
		[[0, 1, 1], [0, 1, 0], [1, 1, 0]],
		[[1, 1, 0], [0, 1, 0], [0, 1, 1]],
		[[1, 0, 0], [1, 1, 1], [0, 0, 1]],
		[[0, 0, 1], [1, 1, 1], [1, 0, 0]],
	]
	# 金锁阵
	JS = [
		[[1, 0], [1, 1], [1, 1]],
		[[0, 1], [1, 1], [1, 1]],
		[[1, 1, 1], [1, 1, 0]],
		[[1, 1, 1], [0, 1, 1]],
		[[1, 1], [1, 1], [1, 0]],
		[[1, 1], [1, 1], [0, 1]],
		[[1, 1, 0], [1, 1, 1]],
		[[0, 1, 1], [1, 1, 1]],
	]
	# 方阵
	F = [
		[[1, 1, 1], [1, 1, 1]],
		[[1, 1], [1, 1], [1, 1]],
	]
	# 玄襄阵
	XX = [
		[[1, 1, 1, 1], [1, 0, 0, 0]],
		[[1, 1, 1, 1], [0, 0, 0, 1]],
		[[1, 1], [0, 1], [0, 1], [0, 1]],
		[[1, 1], [1, 0], [1, 0], [1, 0]],
		[[1, 0, 0, 0], [1, 1, 1, 1]],
		[[0, 0, 0, 1], [1, 1, 1, 1]],
		[[0, 1], [0, 1], [0, 1], [1, 1]],
		[[1, 0], [1, 0], [1, 0], [1, 1]],		
	]
	# 钩形阵
	GX = [
		[[1, 0], [1, 0], [1, 1], [1, 0]],
		[[0, 1], [0, 1], [1, 1], [0, 1]],
		[[1, 1, 1, 1], [0, 1, 0, 0]],
		[[1, 1, 1, 1], [0, 0, 1, 0]],
		[[0, 1], [1, 1], [0, 1], [0, 1]],
		[[1, 0], [1, 1], [1, 0], [1, 0]],
		[[0, 1, 0, 0], [1, 1, 1, 1]],
		[[0, 0, 1, 0], [1, 1, 1, 1]],
	]
	# 一字长蛇阵
	YZCS = [
		[[1, 1, 1, 1, 1]],
		[[1], [1], [1], [1], [1]],
	]
	# 三才阵
	SC = [
		[[1, 1, 1], [0, 1, 0], [0, 1, 0]],
		[[0, 1, 0], [0, 1, 0], [1, 1, 1]],
		[[1, 0, 0], [1, 1, 1], [1, 0, 0]],
		[[0, 0, 1], [1, 1, 1], [0, 0, 1]],
	]
	# 天门阵
	TM = [
		[[0, 1, 0], [1, 1, 1], [0, 1, 0]],
	]
	# 混元阵
	HY = [
		[[0, 1, 0], [0, 1, 1], [1, 1, 0]],
		[[0, 1, 0], [1, 1, 0], [0, 1, 1]],
		[[1, 0, 0], [1, 1, 1], [0, 1, 0]],
		[[0, 0, 1], [1, 1, 1], [0, 1, 0]],
		[[0, 1, 1], [1, 1, 0], [0, 1, 0]],
		[[1, 1, 0], [0, 1, 1], [0, 1, 0]],
		[[0, 1, 0], [1, 1, 1], [0, 0, 1]],
		[[0, 1, 0], [1, 1, 1], [1, 0, 0]],
	]
	# 鸳鸯阵
	YY = [
		[[0, 1, 1], [1, 1, 0], [1, 0, 0]],
		[[1, 1, 0], [0, 1, 1], [0, 0, 1]],
		[[0, 0, 1], [0, 1, 1], [1, 1, 0]],
		[[1, 0, 0], [1, 1, 0], [0, 1, 1]],
		
	]
	formation_variants = dict()
	for abbr in abbrs:
		formations = eval(abbr)
		index = -1
		new_abbr = abbr
		while True:
			if not new_abbr in formation_variants:
				formation_variants[new_abbr] = list(map(lambda _formation: {"matrix": _formation}, formations))
				break
			else:
				# 可能出现的重复阵型,添加不同的序号后缀作为命名区分
				index += 1
				new_abbr = f"{abbr}_{index}"
	return formation_variants
	
# 使用动态规划:这里我们用编码,所以无需用到稀疏表示,matrix即可
def solve_dp(puzzle, # 原始日历拼图
			 formation_variants,	# 可用阵型及其变形,形如:{"XX": List[Dict{matrix: List[List[Int]], sparse: List[Tuple(x, y)]}], "XXX": 类似前面}
			 for_solution = False,	# 是否找到解法
			 ):
	# 将给定的puzzle矩阵转为puzzle_height×puzzle_width长度的编码向量(以零一字符串表示)
	def _puzzle_to_code(_puzzle):
		return ''.join([''.join(map(str, _puzzle_row)) for _puzzle_row in _puzzle])
	
	# _puzzle_to_code的逆运算
	def _code_to_puzzle(_puzzle_code):
		_puzzle = numpy.zeros((puzzle_height, puzzle_width))
		_pointer = -1
		for _i in range(puzzle_height):
			for _j in range(puzzle_width):
				_pointer += 1
				_puzzle[_i, _j] = _puzzle_code[_pointer]
		return _puzzle
	
	# 注意到一个puzzle通过旋转/翻转操作,一共会有8种等价的变体(自身与翻转)
	# 取最大的编码向量(以零一字符串的数值作为表示)作为唯一编码值,以减少很多重复
	# 特别地,每日演兵是一个正方阵,所以连行列都不需要区分,这在下面的formation编码中有很大的助益
	# 最大编码值对应的拼图,即块集中在左上角
	def _puzzle_to_unique_code(_puzzle):
		_rotate_puzzle_copy = _puzzle.copy()
		_rotate_puzzle_copy_flip = numpy.fliplr(_rotate_puzzle_copy)
		_codes = [_puzzle_to_code(_rotate_puzzle_copy), _puzzle_to_code(_rotate_puzzle_copy_flip)]
		# 旋转3次
		for _ in range(3):
			_rotate_puzzle_copy = numpy.rot90(_rotate_puzzle_copy)
			_rotate_puzzle_copy_flip = numpy.rot90(_rotate_puzzle_copy_flip)
			_codes.append(_puzzle_to_code(_rotate_puzzle_copy))
			_codes.append(_puzzle_to_code(_rotate_puzzle_copy_flip))
		return max(_codes)

	# 生成formation编码()的方法:
	# - 将formation_matrix每行扩展到跟puzzle_width(零填充)
	# - 然后类似_puzzle_to_code的方法,但是要把右侧的零给排除
	# - _block_number表示用哪个数字表示块,一般用1,但是为了能看到最终求解的结果,还需要存一份区分不同阵型编码的(即colorful,五彩斑斓的)的阵型编码
	def _formation_to_code(_formation_matrix, _block_number = 1):
		_formation_matrix = numpy.array(_formation_matrix, dtype=int) * _block_number
		_formation_matrix_expand = numpy.concatenate([_formation_matrix, numpy.zeros((_formation_matrix.shape[0], puzzle_width - _formation_matrix.shape[1]))], axis=1)
		_formation_matrix_expand = numpy.asarray(_formation_matrix_expand, dtype=int)
		_formation_code = ''.join([''.join(map(str, _formation_row)) for _formation_row in _formation_matrix_expand])
		return _formation_code.rstrip('0')

	# 将formation在指定的位置(pointer)插入puzzle
	# 如果指定的位置无法插入,则返回False与空字符串
	# 否则返回True和插入后的puzzle_code
	def add_formation_code_to_puzzle_code(_puzzle_code, _formation_code, _pointer):
		# print(_puzzle_code, len(_puzzle_code))
		# print(_formation_code, len(_formation_code))
		_result_code = str()
		# 插入部分:_pointer, _pointer + 1, ..., _pointer + len(_formation_code) - 1
		_start_pointer, _end_pointer = _pointer, _pointer + len(_formation_code)
		for _i in range(_start_pointer, _end_pointer):
			_formation_char_int = int(_formation_code[_i - _pointer])
			_puzzle_char_int = int(_puzzle_code[_i])
			if _formation_char_int == 0 or _puzzle_char_int == 0:
				# 阵型和拼图至少有一个在当前位置是空的
				_result_code += str(_formation_char_int + _puzzle_char_int)
			else:
				# 否则无法插入阵型
				return False, None
		# 补上头尾
		_result_code = _puzzle_code[: _start_pointer] + _result_code + _puzzle_code[_end_pointer: ]
		return True, _result_code

	# 注意到,一个形状为(formation_height, formation_width)的阵型,只可能在以下的_pointer插入拼图:
	# - _pointer = i × puzzle_width + j
	# - 其中:i取值范围是(0, 1, ..., puzzle_height - formation_height),j取值范围是(0, 1, ...., puzzle_width - formation_width)
	def _generate_possible_pointer(_formation_height, _formation_width):
		for _i in range(puzzle_height - _formation_height + 1):
			for _j in range(puzzle_width - _formation_width + 1):
				yield _i * puzzle_width + _j

	# 递归算法
	@lru_cache(None)
	def _dp(_puzzle_unique_code, # Str 当前拼图的编码值(唯一编码值)
			_remained_formation_ids, # Tuple 剩余可用的阵型编码
			_for_solution = False, # 是否需要找到确切的解法
			):
		# 终止条件:_puzzle_unique_code全是1,或者_remained_formation_ids为空
		if not _remained_formation_ids:
			# 找到一组解
			print("成功找到一组解!")
			for _char in _puzzle_unique_code:
				assert int(_char) > 0
			if _for_solution:
				pprint(_code_to_puzzle(_puzzle_unique_code))
				with open("solution.txt", 'w', encoding="utf8") as f:
					f.write(str(_code_to_puzzle(_puzzle_unique_code)))
			return True
		# 遍历每个可用的阵型
		for _remained_formation_id in _remained_formation_ids:
			# 遍历每个可用阵型的变体
			_can_put_in = False
			if for_solution:
				formation_variant_codes = formation_codes_num[_remained_formation_id]
			else:
				formation_variant_codes = formation_codes[_remained_formation_id]
			
			for _formation_variant_code, (_formation_height, _formation_width) in zip(formation_variant_codes, formation_sizes[_remained_formation_id]):
				# 遍历每个可能可以插入的位置
				for _possible_pointer in _generate_possible_pointer(_formation_height, _formation_width):
					# 试着插入
					_add_flag, _updated_puzzle_code = add_formation_code_to_puzzle_code(
						_puzzle_code = _puzzle_unique_code, 
						_formation_code = _formation_variant_code,
						_pointer = _possible_pointer,
					)
					# 成功:可以插入阵型
					if _add_flag:
						# 则迭代
						_remained_formation_ids_list_copy = list(_remained_formation_ids)
						_remained_formation_ids_list_copy.remove(_remained_formation_id)
						_updated_remained_formation_ids = tuple(_remained_formation_ids_list_copy)
						_result_flag = _dp(
							_puzzle_unique_code = _updated_puzzle_code,
							_remained_formation_ids = _updated_remained_formation_ids,
							_for_solution = _for_solution,
						)
						_can_put_in = True
						if _result_flag:
							# 找到一个即可
							return True
					# 失败:此处不可以插入阵型
					else:
						pass
			if not _can_put_in:
				# 当前阵型以及它的所有变体无法在任何位置插入
				return False
	# 将puzzle转为矩阵
	if isinstance(puzzle, list):
		puzzle = numpy.array(puzzle, dtype=int)
	puzzle_height, puzzle_width = puzzle.shape
	assert puzzle_height == puzzle_width, "动态规划算法目前仅支持正方阵的求解"
	total_formation = len(formation_variants)
	
	print(f"拼图形状:{(puzzle_height, puzzle_width)}")
	print(f"阵型总数:{total_formation}")
	
	# 将阵型转为矩阵:List[List[Str(formation_code)]]
	formation_codes = list() # 零一字符串(块统一用1表示)
	formation_codes_num = list() # 每个阵型的块用不同的数字表示
	formation_sizes = list() # 记录阵型的高和宽
	
	for i, formation_name in enumerate(formation_variants):
		formation_variant_codes = list()
		formation_variant_codes_num = list()
		formation_variant_sizes = list()
		for formation_variant in formation_variants[formation_name]:
			formation_matrix = formation_variant["matrix"]
			formation_variant_codes.append(_formation_to_code(formation_matrix, _block_number = 1))
			formation_variant_codes_num.append(_formation_to_code(formation_matrix, _block_number = i + 2))
			formation_variant_sizes.append((len(formation_matrix), len(formation_matrix[0])))
		formation_codes.append(formation_variant_codes)
		formation_codes_num.append(formation_variant_codes_num)
		formation_sizes.append(formation_variant_sizes)

	_puzzle_unique_code = _puzzle_to_unique_code(puzzle)
	# _dp(_puzzle_unique_code, _remained_formation_ids=tuple(range(total_formation)), _for_solution = False)
	_dp(_puzzle_unique_code, _remained_formation_ids=tuple(range(total_formation)), _for_solution = for_solution)

# 运行每日练兵
def run():
	# 生成今日拼图
	month, day = int(time.strftime("%m")), int(time.strftime("%d"))
	calendar_puzzle = generate_calendar_puzzle(month, day)
	print(f"{month}{day}日拼图:")
	pprint(calendar_puzzle)
	# 生成今日阵型
	names = ["方阵", "北斗七星阵", "九曲黄河阵", "钩形阵", "金锁阵", "玄襄阵", "双龙出水阵", "锋矢阵"]
	formation_variants = define_formation_variants(names)
	solve_dp(calendar_puzzle, formation_variants, for_solution=True)

if __name__ == "__main__":
	run()


2 思路细节

未完待续