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

news2024/12/13 5:52:10

文章目录

  • 序言
  • 1 求解算法代码(python)
  • 2 思路细节
    • 2.1 定义拼图与阵型
    • 2.2 穷举复杂度
    • 2.3 使用缓存进行改进(×)
      • 2.3.1 LRU缓存
      • 2.3.2 将2.2的solve函数改写为可缓存装饰的
    • 2.4 使用剪枝进行改进(×)
    • 2.5 使用更好的状态表示进行动态规划(√)
      • 2.5.1 第1节代码解析
      • 2.5.2 如何找到确切的解?
    • 2.6 局限性与讨论
  • 3 后记


序言

问题如上(每个阵型都可以进行旋转或翻转,最终填满日历拼图),其实这是之前诸葛亮列传的兵法题(可惜狗宝更新后把能这个能白嫖资源的列传给删了,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.]]

用pygame对上面的矩阵做一个可视化的结果:

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

import pygame
from pygame.locals import *

class Puzzle:
	color_map = {
		"white": (255, 255, 255),
		"black": (0, 0, 0),
		"red": (255, 0, 0),
		"orange": (255, 165, 0),
		"yellow": (255, 255, 0),
		"green": (0, 255, 0),
		"blue": (0, 255, 0),
		"turquoise": (64, 224, 208),
		"purple": (128, 0, 128),
		"grey": (128, 128, 128),
	}
	block_index_to_color = list(color_map.keys())
	def __init__(self, 
				 n_puzzle_rows = 7, 
				 n_puzzle_columns = 7,
				 window_height_pixel = 800,
				 window_width_pixel = 800,
				 ):
	
		self.n_puzzle_rows = n_puzzle_rows
		self.n_puzzle_columns = n_puzzle_columns
		self.window_height_pixel = window_height_pixel
		self.window_width_pixel = window_width_pixel
		self.block_height_pixel = window_height_pixel // n_puzzle_rows 	
		self.block_width_pixel = window_width_pixel // n_puzzle_columns 	
		self.window = self.initialize_window()
		self.puzzle = self.initialize_puzzle()
	
	# 初始化窗口
	def initialize_window(self):
		window = pygame.display.set_mode((self.window_height_pixel, self.window_width_pixel))
		pygame.display.set_caption("Calendar Puzzle")
		window.fill(self.color_map["white"])
		return window

	# 初始化拼图及每个块
	def initialize_puzzle(self):
		puzzle = []
		for row in range(self.n_puzzle_rows):
			puzzle.append(list())
			for column in range(self.n_puzzle_columns):
				block = {"row": row,
						 "column": column,
						 "x_location": column * self.block_width_pixel,
						 "y_location": row * self.block_height_pixel,
						 "color": self.color_map["white"],
						 }
				puzzle[row].append(block)
		return puzzle
	
	# 绘制网格线
	def draw_grid(self):
		row_interval_pixel = self.window_height_pixel // self.n_puzzle_rows
		height_interval_pixel = self.window_width_pixel // self.n_puzzle_columns
		for i in range(self.n_puzzle_rows):
			pygame.draw.line(
				self.window, 
				self.color_map["black"], 
				(0, i * row_interval_pixel), 
				(self.window_width_pixel, i * row_interval_pixel),
			)
		for j in range(self.n_puzzle_columns):
			pygame.draw.line(
				self.window, 
				self.color_map["black"], 
				(j * height_interval_pixel, 0), 
				(j * height_interval_pixel, self.window_height_pixel),
			)
	
	# 绘制块
	def draw_block(self, block):
		pygame.draw.rect(
			self.window, 
			block["color"], 
			(block["x_location"], 
			 block["y_location"], 
			 self.block_width_pixel, 
			 self.block_height_pixel,
			 ),
		)
		
	# 绘制拼图
	def draw_puzzle(self):
		for row in range(self.n_puzzle_rows):
			for column in range(self.n_puzzle_columns):
				self.draw_block(self.puzzle[row][column])
		self.draw_grid()
		pygame.display.update()
	
	# 简单展示一个拼图
	def display(self, puzzle_matrix = None):
		run = True		
		while run:
			if puzzle_matrix is not None:		
				for row in range(self.n_puzzle_rows):
					for column in range(self.n_puzzle_columns):
						block_index = int(puzzle_matrix[row][column])
						self.puzzle[row][column]["color"] = self.block_index_to_color[block_index]
						self.draw_block(self.puzzle[row][column])
				self.draw_grid()
				pygame.display.update()
			for event in pygame.event.get():
				if event.type == pygame.QUIT:
				   run = False
				if event.type == KEYDOWN:
					if event.key == K_ESCAPE:
						run = False
						
	# 主程序(交互)
	def interact(self):
		run = True		
		while run:
			self.draw_puzzle()
			for event in pygame.event.get():
				if event.type == pygame.QUIT:
				   run = False
				if event.type == KEYDOWN:
					if event.key == K_ESCAPE:
						run = False
			left, center, right = pygame.mouse.get_pressed()
			# TODO: 事件触发(目前不必要)
		
if __name__ == "__main__":
	puzzle = Puzzle(
		n_puzzle_rows = 7, 
		n_puzzle_columns = 7,
		window_height_pixel = 800,
		window_width_pixel = 800,
	)
	puzzle.display(
		[[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 思路细节

2.1 定义拼图与阵型

拼图类问题大多可以转化为动态规划解决,关键在于如何状态的设计以及递归形式。

但不管使用什么样的方法,首先还是要把所有的阵型以及拼图表示为矩阵(二维数组)形式,我们先用列表来定义

# -*- 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, "sparse": generate_sparse_formation(_formation)}, formations))
				break
			else:
				# 可能出现的重复阵型,添加不同的序号后缀作为命名区分
				index += 1
				new_abbr = f"{abbr}_{index}"
	return formation_variants

# 阵型稀疏表示:即只存储formation矩阵中数值为1的坐标
def generate_sparse_formation(formation):
	sparse_formation = list()
	formation_height, formation_width = len(formation), len(formation[0])
	for i in range(formation_height):
		for j in range(formation_width):
			if formation[i][j] == 1:
				sparse_formation.append((i, j))
	return sparse_formation

这里用1来表示拼图(或阵型)上已有的块,0则表示该位置是空,如:

127日日历拼图:
[[0, 0, 0, 0, 0, 0, 1],
 [0, 0, 0, 0, 0, 1, 1],
 [0, 0, 0, 0, 0, 0, 1],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 1, 1, 1, 1]]

由于阵型是可以自由旋转和翻转的,因此同一个阵型最多可以对应8种不同的变换形式(4次旋转,2次翻转,4×2=8),比如在上面define_formation_variants函数的定义,北斗七星阵有8种变体(有4×22×4两种形状的矩阵),锋矢阵只有4种(因为它是轴对称图形,都是3×3),一字长蛇阵就只有2种变体(1×55×1)。


2.2 穷举复杂度

为了便于描述,我们做一些必要的数学标记:

  • H H H:拼盘的高度
  • W W W:拼盘的宽度
  • h h h:阵型的高度
  • w w w:阵型的宽度
  • n n n:可用的阵型数

在当前问题背景下: H = W = 7 , n = 8 H=W=7, n=8 H=W=7,n=8 h h h w w w不固定,存在多种不同的组合,例如, ( h , w ) = ( 2 , 3 ) , ( 2 , 4 ) , ( 3 , 3 ) , ( 1 , 5 ) (h,w)=(2,3),(2,4),(3,3),(1,5) (h,w)=(2,3),(2,4),(3,3),(1,5)

不过,我们还是可以简单估计一下穷举的复杂度,比如,可以假定所有阵型都相同,且不区分它们的高度和宽度,那么每个阵型的每种变体至多有 ( H − h + 1 ) ( W − w + 1 ) (H-h+1)(W-w+1) (Hh+1)(Ww+1)种可能的放法,而每个阵型又至多有 8 8 8种变体,那么穷举的复杂度就是:

N = ( 8 ( H − h + 1 ) ( W − w + 1 ) ) n N=(8(H-h+1)(W-w+1))^n N=(8(Hh+1)(Ww+1))n

而且在这 N N N种情况中,每种情况由 n n n个阵型及其对应的放置位置 L = { ( x 1 , y 1 ) , . . . , ( x n , y n ) } L=\{(x_1,y_1),...,(x_n,y_n)\} L={(x1,y1),...,(xn,yn)}构成,因此,算法还需判断第 i i i个阵型是否可以在 ( x i , y i ) (x_i,y_i) (xi,yi)处被安置(可能根本就放不下),这需要还遍历每个阵型的所有块,这将需要 h w hw hw次检查

  • 不过,在2.1的代码的155行(formation_variants[new_abbr] = list(map(lambda _formation: {"matrix": _formation, "sparse": generate_sparse_formation(_formation)}, formations))),我们为每个阵型的变体存储了矩阵形式(matrix),以及稀疏表示形式(sparse),通过稀疏表示(只存储阵型上块的位置,而非整个 h × w h\times w h×w矩阵),这样可以一定程度将 h w hw hw次的检查减少一定数量,但数量级相差很小。

因此,穷举的总复杂度是:

Θ ( H , W , h , w , n ) = Θ ( N ⋅ h w ) = Θ ( 8 n h w ( H − h + 1 ) n ( W − w + 1 ) n ) \Theta(H,W,h,w,n)=\Theta(N\cdot hw)=\Theta(8^nhw(H-h+1)^n(W-w+1)^n) Θ(H,W,h,w,n)=Θ(Nhw)=Θ(8nhw(Hh+1)n(Ww+1)n)

按照 H = W = 7 , n = 8 , h = w = 3 H=W=7, n=8, h=w=3 H=W=7,n=8,h=w=3来估计,约有 4.26 × 1 0 21 4.26×10^{21} 4.26×1021种情况需要穷举。

显然,这样的复杂度是不可接受的。

不过我们还是提供一版穷举搜索的递归函数solve

def solve(puzzle, # List[List[Int]]
		  formation_matrix_and_sparse_list,	# 可用阵型,形如:({"matrix": List[List[Int]], "sparse": List[Tuple(x, y)]}, ...)
		  solution_id = list(), # 用于迭代的求解列表List[Int],存储阵型的序号
		  solution_xy = list(), # 用于迭代的求解列表List[(x, y)],存储阵型放入位置的坐标
		  puzzle_height = 7,
		  puzzle_width = 7,
		  ):
	# 检查拼图上的空余块数是否和剩余阵型的总块数匹配,如果不匹配,问题显然无解
	total_formation_blocks = 0
	for formation_id, formation_matrix_and_sparse in enumerate(formation_matrix_and_sparse_list):
		if not formation_id in solution_id:
			total_formation_blocks += len(formation_matrix_and_sparse["sparse"])
	total_empty_blocks = sum([row.count(0) for row in puzzle])
	if total_formation_blocks != total_empty_blocks:
		print(f"问题无解:当前所有阵型的块数总和{total_formation_blocks},拼图剩余块数{total_empty_blocks}")
		return False
	elif total_formation_blocks == 0:
		# 拼图填满,成功!
		print(f"成功找到一组解:{solution_id}, {solution_xy}")
		with open("solution.txt", 'a', encoding="utf8") as f:
			f.write(f"{solution_id}\t{solution_xy}\n")
		return True
		
	# 准备一个拼图的副本
	puzzle_copy = deepcopy(puzzle)
	for formation_id, formation_matrix_and_sparse in enumerate(formation_matrix_and_sparse_list):
		# 取一个阵型,试着把它放入拼图
		# 首先检查该阵型是否已经被使用过了
		if formation_id in solution_id:
			continue
		# 放入策略是该阵型的左上角的块(即formation[0][0])依次放入拼图的每一个位置
		# 检查合法性,如果不合法则依次移动窗口
		formation_matrix, formation_sparse = formation_matrix_and_sparse["matrix"], formation_matrix_and_sparse["sparse"]
		formation_height, formation_width = len(formation_matrix), len(formation_matrix[0])
		is_accommodate = False
		for x in range(puzzle_height - formation_height + 1):
			for y in range(puzzle_width - formation_width + 1):
				# 遍历拼图每一个可以容纳阵型的位置:
				# x: 0 => puzzle_height - formation_height
				# y: 0 => puzzle_width - formation_width
				# 判断当前阵型是否可以放入拼图的(x, y)位置
				can_put_in = True
				puzzle_updated = deepcopy(puzzle_copy)
				for i, j in formation_sparse:
					# 检查阵型的每一个块是否可以被容纳(基于阵型矩阵的稀疏表示来搜索,这样循环的次数会少一些)
					# i, j就是阵型上块所在的相对坐标
					# x + i, y + j是阵型上的块放入拼图的绝对坐标
					if puzzle_copy[x + i][y + j] == 1:
						# 当前位置已经被其他阵型的块占据,也可能本来就不能放入
						can_put_in = False
						break
					else:
						# 当前块空闲,直接放入
						puzzle_updated[x + i][y + j] = 1
				if can_put_in:
					# 阵型可以放入拼图的(x, y)位置,则放入
					# 更新拼图,求解列表
					is_accommodate = True
					solution_id_updated = solution_id + [formation_id]
					solution_xy_updated = solution_xy + [(x, y)]
					result_flag = solve(
						deepcopy(puzzle_updated),
						formation_matrix_and_sparse_list,
						solution_id = solution_id_updated,
						solution_xy = solution_xy_updated,
						puzzle_height = puzzle_height,
						puzzle_width = puzzle_width,
					)
					if result_flag:
						# 说明接下来的存在某分支找到了正确的解答,则终止(需求只要找到一个正解即可)
						return True
				else:
					# 不可放入,直接删除puzzle_update,释放内存
					del puzzle_updated
		if not is_accommodate:
			# 说明当前阵型无法在拼图上的任何位置被放入
			# 此时到达叶子节点
			# print(f"分支无解:第{formation_id}块无法被放入拼图")
			# pprint(puzzle)
			return False
	# 应该是到不了这里的
	return False

def test_1():
	with open("solution.txt", 'w', encoding="utf8") as f:
		pass
	calendar_puzzle = [[0] * 5 for i in range(5)]
	pprint(calendar_puzzle)
	formation_variants = define_formation_variants(names = ["双龙出水阵", "玄襄阵", "九曲黄河阵", "三才阵", "金锁阵"])
	pprint(formation_variants)
	n_products = 1
	for formations in formation_variants.values():
		n_products *= len(formations)
	print(f"共计{n_products}种不同的阵型组合")
	# 遍历所有阵型并求解
	for i, formation_matrix_and_sparse_list in enumerate(itertools.product(*formation_variants.values())):
		if i % 1 == 0:
			print(i, time.strftime("%Y-%m-%d %H:%M:%S"))
		# pprint(formation_matrix_and_sparse_list)
		solve(puzzle = calendar_puzzle, 
			  formation_matrix_and_sparse_list = formation_matrix_and_sparse_list, 
			  solution_id = [],
			  solution_xy = [],
			  puzzle_height = len(calendar_puzzle),
			  puzzle_width = len(calendar_puzzle[0]),
			  )
test_1()

在上面这个例子中,我们测试的是一个5×5的拼图👇

在这里插入图片描述

而运行情况是这样的:

在这里插入图片描述

5个运行结果存储在同目录下的solution.txt中(记录每个阵型左上角放置的位置👇,解不唯一):

[0, 1, 2, 3, 4]	[(2, 0), (0, 3), (1, 1), (2, 2), (0, 0)]
[0, 1, 2, 3, 4]	[(0, 0), (1, 3), (1, 1), (0, 2), (3, 0)]

从上面的运行截图可以看到,5个阵型的变体一共有4096种组合(4×4×4×8×8=4096),而每秒钟只能处理2种情况,一共需要半个多小时。如果是7×7的拼图,一共会有50万以上的阵型组合数,而每种组合的处理时间只会更长(事实上这需要2个小时,全部穷举完估计需要100万小时,那确实不如开动脑筋相出解法)


2.3 使用缓存进行改进(×)

2.3.1 LRU缓存

一种提升思路是使用缓存,LRU缓存(最近最少使用)较为常用,此外还有LFU和FIFO是常见的缓存算法。

在Python中,直接使用lru_cache即可,刷DP题常用方法:

from functools import lru_cache

@lru_cache(None)
def f(a, b):
	print("Run f ...")
	return a + b + c

c = 100	
print(f(1, 2))
c = 102
print(f(1, 2))

运行结果:

Run f ...
103
103
  • 这个例子其实可以看出,缓存有时是危险的,因为它只检查两次调用f时的参数是否一致(其实如果第二次是f(1., 2.)也是会直接调用缓存,但是如果f只有一个参数,那么两次数据类型不同就不会调用缓存了,版本特色了属于是),但是外部的c改变了之后,事实上运行结果应该改变,但是第二次因为没有真正调用f(没有显示Run f ...),所以结果依然是103,但应该是105

但是能带lru_cache装饰器的函数,要求参数列表必须是hashable的,比如Int, Str, Boolean这些简单数据类型都是可以的,而List, Dict则不行,不过Tuple是可行的,所以可以用Tuple替代掉List,来实现hashable👇

from functools import lru_cache

@lru_cache(None)
def f(a, b):
	print("Run f ...")
	return a[0] + b[0] + c

c = 100	
print(f((1, 2), (2, 3)))
c = 102
print(f([1, 2], [2, 3]))

运行结果:

Run f ...
103
Traceback (most recent call last):
  File "game.py", line 11, in <module>
    print(f([1, 2], [2, 3]))
TypeError: unhashable type: 'list'

2.3.2 将2.2的solve函数改写为可缓存装饰的

def solve(puzzle, # List[List[Int]]
		  formation_matrix_and_sparse_list,	# 可用阵型,形如:({"matrix": List[List[Int]], "sparse": List[Tuple(x, y)]}, ...)
		  solution_id = list(), # 用于迭代的求解列表List[Int],存储阵型的序号
		  solution_xy = list(), # 用于迭代的求解列表List[(x, y)],存储阵型放入位置的坐标
		  puzzle_height = 7,
		  puzzle_width = 7,
		  ):
	...

2.2的solve函数列表中有三个参数是List,我们要把它们处理掉:

  • puzzle可以直接扔了,因为我们可以直接根据原始日历拼图,以及solution_idsolution_xy 直接复原这个puzzle,虽然需要费点时间
  • solution_id solution_xy 直接改写为tuple即可
# 使用缓存加速(失败,没有加速效果)
def solve_cached(puzzle, # 原始日历拼图
				 formation_matrix_and_sparse_list,	# 可用阵型,形如:({"matrix": List[List[Int]], "sparse": List[Tuple(x, y)]}, ...)
				 puzzle_height = 7,
				 puzzle_width = 7,
				 total_formation = 8,
				 is_prune = False,
				 ):
	@lru_cache(None)
	def _solve_cached(_solution_id,	# 以元组表示(列表不支持哈希缓存),比如给定5个阵型,已经放入第0个和第2个阵型,则可以记录为(0, 2)
					  _solution_xy, # 以元组表示,比如上面例举的阵型(列表不支持哈希缓存),第0个放在(0, 0),第2个放在(3, 4),则记录为((0, 0), (3, 4))
					  ):
		# 算法终止
		if len(_solution_id) == total_formation:
			# 阵型全部放入,意味着拼图填满,成功!
			print(f"成功找到一组解:{_solution_id}, {_solution_xy}")
			with open("solution.txt", 'a', encoding="utf8") as f:
				f.write(f"{_solution_id}\t{_solution_xy}\n")
			return True

		# 根据_solution_id和_solution_xy复原当前的日历拼图情况
		_puzzle = deepcopy(puzzle)
		for _formation_id, (_x, _y) in zip(_solution_id, _solution_xy):
			# 遍历每一个阵型及其放置的位置
			_formation_sparse = formation_matrix_and_sparse_list[_formation_id]["sparse"]
			for _i, _j in _formation_sparse:
				assert not _puzzle[_x + _i][_y + _j], f"{(_x, _i, _y, _j)}, {_current_puzzle}"
				_puzzle[_x + _i][_y + _j] = 1

		# # 剪枝
		# if is_prune:
			# if _solution_id and can_prune(_puzzle, puzzle_height, puzzle_width):
				# print("剪枝")
				# return False

		# 准备一个拼图的副本
		_puzzle_copy = deepcopy(_puzzle)
		for _formation_id, _formation_matrix_and_sparse in enumerate(formation_matrix_and_sparse_list):
			# 取一个阵型,试着把它放入拼图
			# 首先检查该阵型是否已经被使用过了
			if _formation_id in _solution_id:
				continue
			# 放入策略是该阵型的左上角的块(即formation[0][0])依次放入拼图的每一个位置
			# 检查合法性,如果不合法则依次移动窗口
			_formation_matrix, _formation_sparse = _formation_matrix_and_sparse["matrix"], _formation_matrix_and_sparse["sparse"]
			_formation_height, _formation_width = len(_formation_matrix), len(_formation_matrix[0])
			_is_accommodate = False
			for _x in range(puzzle_height - _formation_height + 1):
				for _y in range(puzzle_width - _formation_width + 1):
					# 遍历拼图每一个可以容纳阵型的位置:
					# _x: 0 => puzzle_height - _formation_height
					# _y: 0 => puzzle_width - _formation_width
					# 判断当前阵型是否可以放入拼图的(_x, _y)位置
					_can_put_in = True
					_puzzle_updated = deepcopy(_puzzle_copy)
					for _i, _j in _formation_sparse:
						# 检查阵型的每一个块是否可以被容纳(基于阵型矩阵的稀疏表示来搜索,这样循环的次数会少一些)
						# _i, _j就是阵型上块所在的相对坐标
						# _x + _i, _y + _j是阵型上的块放入拼图的绝对坐标
						if _puzzle_copy[_x + _i][_y + _j] == 1:
							# 当前位置已经被其他阵型的块占据,也可能本来就不能放入
							_can_put_in = False
							break
						else:
							# 当前块空闲,直接放入
							_puzzle_updated[_x + _i][_y + _j] = 1
					if _can_put_in:
						# 阵型可以放入拼图的(x, y)位置,则放入
						# 更新拼图,求解列表
						_is_accommodate = True
						_solution_id_updated = list(_solution_id) + [_formation_id]
						_solution_xy_updated = list(_solution_xy) + [(_x, _y)]
						# # 我感觉不需要排序,因为_solution_id_updated应该本来就是升序的
						# _xy_to_id = {_xy: _id for _id, _xy in zip(_solution_id_updated, _solution_xy_updated)}
						# _solution_id_updated = sorted(_solution_id_updated)
						# _solution_xy_updated = sorted(_solution_xy_updated, key = lambda _xy: _xy_to_id[_xy])
						_result_flag = _solve_cached(
							_solution_id = tuple(_solution_id_updated),
							_solution_xy = tuple(_solution_xy_updated),
						)
						if _result_flag:
							# 说明接下来的存在某分支找到了正确的解答,则终止(需求只要找到一个正解即可)
							return True
					else:
						# 不可放入,直接删除puzzle_update,释放内存
						del _puzzle_updated
			if not _is_accommodate:
				# 说明当前阵型无法在拼图上的任何位置被放入
				# 此时到达叶子节点
				return False
		# 应该是到不了这里的
		return False
	_solve_cached(_solution_id = tuple(), _solution_xy = tuple())
	_solve_cached.cache_clear() # 清除缓存

很不幸,这样的改进,几乎不能给到任何提升,原因是这里本质上,选取了(solution_id, solution_xy)作为状态,但这个状态事实上是很少发生碰撞的。


2.4 使用剪枝进行改进(×)

那就想到是否可以提前做一些剪枝来减少复杂度,正如2.3代码solve_cached被注释掉的30-34行:

		# # 剪枝
		# if is_prune:
			# if _solution_id and can_prune(_puzzle, puzzle_height, puzzle_width):
				# print("剪枝")
				# return False

也就是根据当前拼图的情况_puzzle,来判断是否还需要继续递归搜索?

关键在于如何设计can_prune函数,这里笔者给到一种思路:

# 快速检查当前拼图是否还可能有解
def can_prune(puzzle, puzzle_height, puzzle_width):
	# 注意到阵型至少由5个块组成,且除了方阵之外,其他所有阵型出现的块数都是5
	# 因此可以在代码中,把方阵放在第一个位置,优先放入,接下来只需要判断剩余连通块中的空余块的数量,是否是5的倍数即可
	
	# 使用BFS递归算法判断给定坐标(_x, _y)所在连通块的连通块数
	checked_block = list()	# 存储已经检查过的拼图上的空闲块的坐标
	def _bfs(_x, _y):
		checked_block.append((_x, _y))
		_neighbors = list()
		if _x > 0:
			_neighbors.append((_x - 1, _y))	# 上
		if _y > 0:
			_neighbors.append((_x, _y - 1))	# 左
		if _x < puzzle_height - 1:
			_neighbors.append((_x + 1, _y))	# 下
		if _y < puzzle_width - 1:
			_neighbors.append((_x, _y + 1))	# 右
		for _i, _j in _neighbors:
			if (_i, _j) in checked_block:
				# 已经遍历过该邻居了
				continue
			else:
				_bfs(_i, _j)
	
	for x in range(puzzle_height):
		for y in range(puzzle_width):
			if puzzle[x][y] == 1 or (x, y) in checked_block:
				# 我们只找空闲的连通块
				continue
			_bfs(x, y)
			if len(checked_block) % 5 == 0:
				continue
			else:
				return True
	return False
  • 注意到阵型至少由5个块组成,且除了方阵之外,其他所有阵型出现的块数都是5
  • 因此可以在代码中,把方阵放在组合的第一个位置,优先递归搜索,接下来只需要判断剩余连通块中的空余块的数量,是否是5的倍数即可
  • 使用BFS递归算法判断给定坐标(_x, _y)所在连通块的连通块数
  • PS:关于这个拼图中连通块的寻找算法,在以前的扫雷还有围棋判断棋子死活中是常用的

这里已经用到很多每日演兵拼图问题特有的特点进行剪枝了,很不幸,这样剪枝,提升速度还是不大,以2.2中5×5的拼图穷举搜索为例,也就是从1秒2个循环加快到1秒3个循环,杯水车薪。

当然,我认为肯定是有更好的剪枝方法,这里也是BFS本身也有时间成本。


2.5 使用更好的状态表示进行动态规划(√)

2.5.1 第1节代码解析

因此,最后我们回到第1节中的最终代码:

# 使用动态规划:这里我们用编码,所以无需用到稀疏表示,matrix即可
def solve_dp(puzzle, # 原始日历拼图
			 formation_variants,	# 可用阵型及其变形,形如:{"XX": List[Dict{matrix: List[List[Int]], sparse: List[Tuple(x, y)]}], "XXX": 类似前面}
			 for_solution = False,	# 是否找到解法
			 ):
	...

这里我们将状态表示为 (当前拼图,剩余可用阵型),与之前穷举不同的地方在于:

  • 穷举法会事先给定好每个阵型的变体,然后把8个给定的阵型变体组合好,输入到solve函数中求解,这样在solve函数外面,还需要套一层循环来遍历所有的阵型变体组合,比如5×5的例子中,有4096种组合,7×7的每日演兵下,则有50万以上的组合:
    def test_1():
      ...
      # 遍历所有阵型并求解
      for i, formation_matrix_and_sparse_list in enumerate(itertools.product(*formation_variants.values())):
      	if i % 1 == 0:
      		print(i, time.strftime("%Y-%m-%d %H:%M:%S"))
      	# pprint(formation_matrix_and_sparse_list)
      	solve(puzzle = calendar_puzzle, 
      		  formation_matrix_and_sparse_list = formation_matrix_and_sparse_list, 
      		  solution_id = [],
      		  solution_xy = [],
      		  puzzle_height = len(calendar_puzzle),
      		  puzzle_width = len(calendar_puzzle[0]),
      		  )
    
    • PS:阵型组合数,也就是对应复杂度中的 8 n 8^n 8n这一项(其中 n = 8 n=8 n=8表示可用阵型数),当然如前所述,不是所有的阵型都有8个变体,只是至多8个变体。
  • 而在目前的状态 (当前拼图,剩余可用阵型) 中,剩余可用阵型 并不是阵型的变体,而就是所有可用的阵型编号,我们会在solve_dp中考虑它们的旋转/翻转得到的至多8种变体,因此solve_dp函数外面不需要再套一层循环了:
    # 运行每日练兵
    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)
    

那么怎么样才能减少穷举的数量呢?

  • 我们想到可以把7×7的拼图拉成一个“直线”,这样就是一个长度49的零一字符串puzzle_code
  • 那么每个阵型同样可以拉成一个长度为 h w hw hw的零一字符串formation_code,任务就变成把formation_code插入到puzzle_code中去

这两步非常容易实现:

def solve_dp(...):
	...
	# 将给定的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

	# 生成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')

这里就出现一个很关键的点阵型可以旋转翻折,拼图同样可以

所以一个当前拼图的编码puzzle_code,事实上等价于另外7种旋转翻折的拼图,我们只需要考虑其中一种即可,那就是取puzzle_code最大的那个(也就是1更靠前的puzzle_code,视觉上就是块集中在左上角):

def solve_dp(...):
	...
	# 注意到一个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_height, formation_width)的阵型,只可能在以下的位置插入拼图:
    • _pointer = i × puzzle_width + j
    • 其中:i取值范围是(0, 1, ..., puzzle_height - formation_height)j取值范围是(0, 1, ...., puzzle_width - formation_width)
def solve_dp(...):
	...
	# 注意到,一个形状为(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
	
	# 将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

最后我们写一段带缓存的递归算法:

def solve_dp(...):
	...
	# 递归算法
	@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

2.5.2 如何找到确切的解?

有人可能发现了,如果只是用零一字符串表示puzzle,动态规划的状态 (当前拼图,剩余可用阵型)(即_dp函数的_puzzle_unique_code_remained_formations_ids) 中也没有包含任何和解相关的信息,最终的结果中根本显示不出解的形式,算法只是找到了一个解,但并不知道那个解长啥样,怎么办呢?

一种方法就是在_puzzle_unique_code上做文章,既然零一字符串不行,那就给每个阵型插进去的块赋予不同的标记,比如8个阵型,可以用2,3,4,5,6,7,8,9这8个数字来表示它们的块,这样填进去_puzzle_unique_code就是一个由0~9十个字符构成的字符串了(因为拼图上原始会有一些块本来就不能放,那些位置需要表示成1,所以阵型用的是2~9

这样虽然解决了找到确切解的问题,但是却使得状态 (当前拼图,剩余可用阵型) 的总数大大增加,可能会影响性能。

好消息是,这次并没有使得性能下降太多,较于只用零一字符串的_puzzle_unique_code,几乎是没有任何变化的。

def solve_dp(...):
	...
	# 将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)

这里用_for_solution 参数来区分是否需要找到确切的解(即使用0-9十个字符),还是只要确定拼图是否有解(即使用零一字符串)👆


2.6 局限性与讨论

2.5动态规划算法的一个局限性是只能处理方阵,因为旋转不改变形状,这样就不需要重新考虑阵型插入位置可能发生的变化了(即_generate_possible_pointer函数取巧)。

不过解决手头的任务也足够了吧。

此外2.5的算法中没有考虑其他的剪枝,实际上从人的思考角度来看,一般会把拼图中凹凸不平的地方优先处理掉,使得剩下的区域较为平整(最好是矩形),可能这种思路可以进一步改进算法速度,但缺少理论保证。


3 后记

以前总是习惯留一些后记,如今似乎也没什么好说的。

明天高百总决赛,参加的学校加油吧,有生之年希望真的能站上一次高百总决赛的赛道。

分享一些KR同人作品(原作者:B站@戈谭噩梦),细节拉满,有些东西AI还是做不到的,生活还是需要一些激情的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2256727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java项目实战II基于微信小程序的私家车位共享系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 随着城市化进程的加速&…

STM32 实现 TCP 服务器与多个设备通信

目录 一、引言 二、硬件准备 三、软件准备 四、LWIP 协议栈的配置与初始化 五、创建 TCP 服务器 1.创建任务以及全局变量 2.创建 TCP 控制块 3.绑定端口 4. 进入监听状态 5.设置接收回调函数 六、处理多个客户端连接 七、总结 一、引言 在嵌入式系统开发中&…

LobeChat-46.6k星!顶级AI工具集,一键部署,界面美观易用,ApiSmart 是你肉身体验学习LLM 最好IDEA 工具

LobeChat LobeChat的开源&#xff0c;把AI功能集合到一起&#xff0c;真的太爽了。 我第一次发现LobeChat的时候&#xff0c;就是看到那炫酷的页面&#xff0c;这么强的前端真的是在秀肌肉啊&#xff01; 看下它的官网&#xff0c;整个网站的动效简直闪瞎我&#xff01; GitH…

计算机键盘的演变 | 键盘键名称及其功能 | 键盘指法

注&#xff1a;本篇为 “键盘的演变及其功能” 相关几篇文章合辑。 英文部分机翻未校。 The Evolution of Keyboards: From Typewriters to Tech Marvels 键盘的演变&#xff1a;从打字机到技术奇迹 Introduction 介绍 The keyboard has journeyed from a humble mechanical…

第三部分:进阶概念 7.数组与对象 --[JavaScript 新手村:开启编程之旅的第一步]

第三部分&#xff1a;进阶概念 7.数组与对象 --[JavaScript 新手村&#xff1a;开启编程之旅的第一步] 在 JavaScript 中&#xff0c;数组和对象是两种非常重要的数据结构&#xff0c;它们用于存储和组织数据。尽管它们都属于引用类型&#xff08;即它们存储的是对数据的引用而…

面试中遇到的一些有关进程的问题(有争议版)

一个进程最多可以创建多少个线程&#xff1f; 这个面经很有问题&#xff0c;没有说明是什么操作系统&#xff0c;以及是多少位操作系统。 因为不同的操作系统和不同位数的操作系统&#xff0c;虚拟内存可能是不一样多。 Windows 系统我不了解&#xff0c;我就说说 Linux 系统…

Excel技巧:如何批量调整excel表格中的图片?

插入到excel表格中的图片大小不一&#xff0c;如何做到每张图片都完美的与单元格大小相同&#xff1f;并且能够根据单元格来改变大小&#xff1f;今天分享&#xff0c;excel表格里的图片如何批量调整大小。 方法如下&#xff1a; 点击表格中的一个图片&#xff0c;然后按住Ct…

Stable Audio Open模型部署教程:用AI打造独家节拍,让声音焕发新活力!

Stable Audio Open 是一个开源的文本到音频模型&#xff0c;允许用户从简单的文本提示中生成长达 47 秒的高质量音频数据。该模型非常适合创建鼓点、乐器即兴演奏、环境声音、拟音录音和其他用于音乐制作和声音设计的音频样本。用户还可以根据他们的自定义音频数据微调模型&…

Linux上传代码的步骤与注意事项

最近因为工作需要&#xff0c;要上传代码到 DPDK 上&#xff0c;代码已经上传成功&#xff0c;记录一下过程&#xff0c;给大家提供一个参考。我这次需要上传的是pmd&#xff0c;即poll mode driver。 1 Coding Style 要上传代码&#xff0c;第一件事就是需要知道Coding Styl…

运费微服务和redis存热点数据

目录 运费模板微服务 接收前端发送的模板实体类 插入数据时使用的entity类对象 BaseEntity类 查询运费模板服务 新增和修改运费模块 整体流程 代码实现 运费计算 整体流程 总的代码 查找运费模板方法 计算重量方法 Redis存入热点数据 1.从nacos导入共享redis配置…

如何在windows10上部署WebDAV服务并通过内网穿透实现公网分享内部公共文件

WebDAV&#xff08;Web-based Distributed Authoring and Versioning&#xff09;是一种基于HTTP协议的应用层网络协议&#xff0c;它允许用户通过互联网进行文件的编辑和管理。这意味着&#xff0c;无论员工身处何地&#xff0c;只要连接到互联网&#xff0c;就能访问、编辑和…

gRPC 快速入门 — SpringBoot 实现(1)

目录 一、什么是 RPC 框架 &#xff1f; 二、什么是 gRPC 框架 &#xff1f; 三、传统 RPC 与 gRPC 对比 四、gRPC 的优势和适用场景 五、gRPC 在分布式系统中应用场景 六、什么是 Protocol Buffers&#xff08;ProtoBuf&#xff09;&#xff1f; 特点 使用场景 简单的…

深入浅出:SOME/IP-SD的工作原理与应用

目录 往期推荐 相关缩略语 SOME/IP 协议概述 协议介绍 SOME/IP TP 模块概述和 BSW 模块依赖性 原始 SOME/IP 消息的Header格式 SOME/IP-SD 模块概述 模块介绍 BSW modules依赖 客户端-服务器通信示例 Message 结构 用于SD服务的BSWM状态处理 往期推荐 ETAS工具…

字节高频算法面试题:小于 n 的最大数

问题描述&#xff08;感觉n的位数需要大于等于2&#xff0c;因为n的位数1的话会有点问题&#xff0c;“且无重复”是指nums中存在重复&#xff0c;但是最后返回的小于n最大数是可以重复使用nums中的元素的&#xff09;&#xff1a; 思路&#xff1a; 先对nums倒序排序 暴力回…

相机动态/在线标定

图1 图2 基本原理 【原理1】平行线在射影变换后会交于一点。如图所示,A为相机光心,蓝色矩形框为归一化平面,O为平面中心。地面四条黄色直线为平行且等距的车道线。HI交其中两条车道线于H、I, 过G作HI的平行线GM交车道线于M。HI、GM在归一化平面上的投影分别为JK、PN,二者会…

在 Windows 11 WSL (Ubuntu 24.04.1 LTS) | Python 3.12.x 下部署密码学库 charm

1. 在 Windows 11 上部署 Ubuntu (WSL) 由于作者没有高性能的 Ubuntu 服务器或个人电脑&#xff0c;且公司或学校提供的 Ubuntu 服务器虽然提供高性能 GPU 等硬件配置但通常不会提供 root 权限&#xff0c;因而作者通过在搭载了 Windows 11 的个人电脑上启动 Ubuntu (WSL) 来进…

【中间件开发】Redis基础命令详解及概念介绍

文章目录 前言一、Redis相关命令详解及原理1.1 string、set、zset、list、hash1.1.1 string1.1.2 list1.1.3 hash1.1.4 set1.1.5 zset 1.2 分布式锁的实现1.3 lua脚本解决ACID原子性1.4 Redis事务的ACID性质分析 二、Redis协议与异步方式2.1 Redis协议解析2.1.1 redis pipeline…

设计模式的艺术读书笔记

设计模式的艺术 面向对象设计原则概述单一职责原则开闭原则里氏代换原则依赖倒转原则接口隔离原则合成复用原则迪米特法则 创建的艺术创建型模式单例模式饿汉式单例与懒汉式单例的讨论通过静态内部类实现的更好办法 简单工厂模式工厂方法模式重载的工厂方法工厂方法的隐藏工厂方…

计算机毕设-基于springboot的甜品店管理系统的设计与实现(附源码+lw+ppt+开题报告)

博主介绍&#xff1a;✌多个项目实战经验、多个大型网购商城开发经验、在某机构指导学员上千名、专注于本行业领域✌ 技术范围&#xff1a;Java实战项目、Python实战项目、微信小程序/安卓实战项目、爬虫大数据实战项目、Nodejs实战项目、PHP实战项目、.NET实战项目、Golang实战…

Mac 录制电脑系统内的声音的具体方法?

1.第一步&#xff1a;下载BlackHole 软件 方式1&#xff1a;BlackHole官方下载地址 方式2&#xff1a; 百度云下载 提取码: n5dp 2.第二步&#xff1a;安装BlackHole 双击下载好的BlackHole安装包&#xff0c;安装默认提示安装。 3.第三步&#xff1a;在应用程序中找到音频…