fill a 3x3x3 box with 2x2x1 slabs. How many slabs ...
Créé le : 17 mars 2025
Répondu en utilisant O3 Mini High par Chat01
Créé le : 17 mars 2025
Répondu en utilisant O3 Mini High par Chat01
fill a 3x3x3 box with 2x2x1 slabs. How many slabs can be put in at max with arbitrary direction and placements? solve with linear programming, build inequalities and find the optimal solution. Don't try enumerations.
We can show that no more than 6 slabs (each of volume 4) can fit into a 3×3×3 cube (of volume 27) by setting up a natural integer‐programming formulation.
Variables.
Let the set 𝒫 be all possible placements of a 2×2×1 slab inside the cube (placements may be in any of the three orientations). For each placement
define a binary variable
with if we choose to place the slab at position , and 0 otherwise.
Constraints.
Each slab covers 4 unit cells. Denote by 𝒞 the set of all 27 unit cells in the cube. For every cell , let
.
Then we require that no cell is covered more than once:
Objective.
Maximize the total number of placed slabs:
Each placed slab covers 4 cells. Summing the constraints over all 27 cells gives:
Because every slab (when placed, ) appears in exactly 4 of these constraints, the left‐side equals:
Thus,
Since the number of slabs must be an integer, we obtain
The linear programming relaxation (and, by integrality, the original integer program) shows that at most 6 slabs can be placed. Moreover, one can verify (by constructing an explicit arrangement, though we do not enumerate all possibilities here) that 6 slabs can indeed be packed into the 3×3×3 cube.
Final Answer: The maximum number of 2×2×1 slabs that can be placed in a 3×3×3 box is 6.
you should define the variables as
Box placed vertically and face to me
Box placed vertically and face to left(or right)
Box placed horizontally
also constraints should just be the length limit in each dimension
We can “count‐by lengths” if we group placements by their orientation. For example, note that a 2×2×1 slab (of volume 4) can be placed in one of three “macroscopic” ways in a 3×3×3 box:
Horizontal placement: The slab lies flat so that its 2×2 face is horizontal (it “sits” on a layer). Its “footprint” is 2 units in the left–right (x) direction and 2 units in the front–back (y) direction, and it “uses” 1 unit in the vertical (z) direction.
Vertical placement “face‐to–me”: The slab stands so that its 2×2 face is vertical and “facing you” (that is, the face is parallel to the front face of the box). In this case the slab is “thin” in the left–right direction (using 1 unit in x) but “tall” in both front–back and vertical directions (using 2 units in y and 2 units in z).
Vertical placement “face–to–side”: The slab is also vertical but rotated 90° from the previous case. Now its 2×2 face is vertical but “facing the side” (parallel to the left face); it uses 2 units in left–right (x), 1 unit in front–back (y), and 2 units in vertical (z).
Introduce variables for the numbers of slabs of each type:
Thus the total number of slabs is
Now we “charge” each slab for the number of unit–length incidences it covers in each coordinate direction. (A “unit incidence” means that a unit cube in the 3×3×3 box contributes 1 unit in, say, the x–direction; since there are 27 unit cubes the total available “x–length” is 27. Similarly for y and z.)
Because the placements are axis–aligned and (by non–overlap) every occupied unit cube is counted once in each coordinate we have:
This gives the inequality
There is also the obvious volume (or “cell–count”) constraint. Since each slab covers 4 unit cubes and the box has 27 cubes,
This immediately gives
Since , , and must be integers, we conclude
Thus the linear–programming formulation is:
The volume (cell–count) constraint is strongest here, forcing
An explicit arrangement showing that 6 slabs can be placed exists (so the bound is sharp).
Final Answer: The maximum number of 2×2×1 slabs that can be packed into a 3×3×3 box is 6.
中文展示解题过程
下面给出一种利用线性规划思想求解的中文展示过程,思路是从各个方向对盒子内单位“长度”的占用来建立约束条件。
考虑将 2×2×1 的板(以下简称“板块”)放入 3×3×3 的盒子中。我们约定三种不同的放置方式,并令:
设变量
分别表示三种放置方式下板块的数量。总板块数为
我们利用盒子内的单位“长度总和”来建立约束。考虑 3×3×3 的盒子,总共有 27 个单位长度(每个坐标方向均有 27 个单位“占用”可能),每块板在各个方向上占用的单位数如下:
因此 x 方向的约束为:
得到 y 方向约束:
因此 z 方向约束为:
每个板块占 2×2×1 = 4 个单位体积,而盒子的总体积为 27,因此有:
从而:
由于板块数为整数,所以有:
综合以上约束,问题可以表述为下面的整数线性规划模型:
注意到体积约束 最直接地给出了
从而板块总数 的最大整数值不超过 6。
由上面的线性规划不等式可知,最多可以放入 6 块 2×2×1 板块。事实上,通过构造一个合适的放置方案可以达到这个上界,所以在 3×3×3 的盒子中,最多能够放入 6 块板块。
这就是利用“长度限制”建立不等式求解的过程。
write a python script to solve it
Below is a Python script that uses the PuLP library to set up and solve the linear programming (integer programming) formulation described above:
pythonimport pulp # Define the problem: maximize the number of slabs (h + vf + vs) problem = pulp.LpProblem("Maximize_Slabs", pulp.LpMaximize) # Define integer variables: # h : number of horizontal slabs # vf : number of vertical slabs with face to me # vs : number of vertical slabs with face to side (left/right) h = pulp.LpVariable("h", lowBound=0, cat="Integer") vf = pulp.LpVariable("vf", lowBound=0, cat="Integer") vs = pulp.LpVariable("vs", lowBound=0, cat="Integer") # Objective: maximize total slabs problem += h + vf + vs, "Total_Slabs" # Add constraints based on the length limits in each dimension: # x-direction: horizontal uses 2, vertical (face-to-me) uses 1, vertical (side) uses 2. problem += 2*h + 1*vf + 2*vs <= 27, "Constraint_x" # y-direction: horizontal uses 2, vertical (face-to-me) uses 2, vertical (side) uses 1. problem += 2*h + 2*vf + 1*vs <= 27, "Constraint_y" # z-direction: horizontal uses 1, both vertical placements use 2. problem += 1*h + 2*vf + 2*vs <= 27, "Constraint_z" # Volume constraint: each slab occupies 4 unit cubes, total available volume is 27. problem += 4*(h + vf + vs) <= 27, "Volume_Constraint" # Solve the problem using the default solver problem.solve() # Print results print("Status:", pulp.LpStatus[problem.status]) print("Optimal number of slabs:", pulp.value(problem.objective)) print("Number of horizontal slabs (h):", h.varValue) print("Number of vertical slabs (face-to-me, vf):", vf.varValue) print("Number of vertical slabs (side, vs):", vs.varValue)
Problem Definition:
We create a maximization problem using PuLP.
Variables:
Three integer variables h, vf, and vs represent the number of slabs in each orientation.
Objective:
The goal is to maximize the total number of slabs: h + vf + vs.
Constraints:
These constraints ensure that the sum of lengths occupied by the slabs in each direction does not exceed the available 27 units, and that the total volume occupied does not exceed 27 unit cubes.
Solve and Display:
The problem is solved using PuLP’s default solver, and the script prints the status, optimal total number of slabs, and the breakdown for each type.
Before running the script, make sure you have PuLP installed (e.g., via pip install pulp).
Result - Optimal solution found
Objective value: 6.00000000
Enumerated nodes: 0
Total iterations: 0
Time (CPU seconds): 0.00
Time (Wallclock seconds): 0.00
Option for printingOptions changed from normal to all
Total time (CPU seconds): 0.00 (Wallclock seconds): 0.00
Status: Optimal
Optimal number of slabs: 6.0
Number of horizontal slabs (h): 0.0
Number of vertical slabs (face-to-me, vf): 0.0
Number of vertical slabs (side, vs): 6.0
this is apparently impractical
下面的模型仅利用了各方向的“长度”限制(以及体积限制),这是一种松弛(relaxation)模型,只给出了一个必要条件,而没有考虑各板块在空间中的具体位置和重叠问题。也就是说,模型允许的解 (即所有 6 块板都采用竖直放置、侧面朝向的方式)虽然满足了各个方向的总长度约束和总体积约束,但在实际装配时,这 6 块板若都采用这种放法,必然会出现重叠或无法排布在 3×3×3 的盒子内。
这种约束反映了板块在 x 方向上所“使用”的单位长度总和不能超过 27,但它并没有保证这些板块能在盒子内不发生重叠地分布。类似地,y 和 z 方向的约束也只是“总体”占用的度量。
要得到一个保证板块之间不重叠的“实际可行”解,我们必须:
这样的模型在规模上会远比当前的“聚合模型”复杂,需要大量的变量和约束,因此通常需要用枚举或启发式方法进行求解,而这正是题目中希望避免枚举的原因。
因此,要得到一个“实际”的、能装入 3×3×3 盒子的板块摆放方案,需要构造一个更细致的模型,将板块具体位置及其覆盖的单元格都纳入变量和约束考虑,这会显著增加模型的复杂性。
then refine the model and give new script
下面给出一个精细化模型的 Python 脚本,该脚本构造了一个完整的整数规划模型,其中枚举了所有可能在 3×3×3 立方体内放置 2×2×1 板块(共 36 个可能位置),并对每个单元格添加“不重叠”约束,确保每个立方体内的单元格最多被一个板块覆盖。最终目标是最大化放置板块的总数。
pythonimport pulp # --------------------------- # 1. 枚举所有可能的板块放置方式 # --------------------------- placements = [] # (1) 水平放置:板块的 2×2 面平放,占用 2 单位 x,2 单位 y,1 单位 z。 # 可放置位置:x ∈ {0,1},y ∈ {0,1},z ∈ {0,1,2} for z in range(3): for x in range(2): # x=0,1 for y in range(2): # y=0,1 cells = [(x, y, z), (x+1, y, z), (x, y+1, z), (x+1, y+1, z)] placements.append({'cells': cells, 'orientation': 'horizontal', 'start': (x, y, z)}) # (2) 竖直放置——正面朝向(face-to-me):板块占用 1 单位 x,2 单位 y,2 单位 z。 # 可放置位置:x ∈ {0,1,2},y ∈ {0,1},z ∈ {0,1} for x in range(3): for y in range(2): for z in range(2): cells = [(x, y, z), (x, y+1, z), (x, y, z+1), (x, y+1, z+1)] placements.append({'cells': cells, 'orientation': 'vertical_face', 'start': (x, y, z)}) # (3) 竖直放置——侧面朝向(face-to-side):板块占用 2 单位 x,1 单位 y,2 单位 z。 # 可放置位置:x ∈ {0,1},y ∈ {0,1,2},z ∈ {0,1} for x in range(2): for y in range(3): for z in range(2): cells = [(x, y, z), (x+1, y, z), (x, y, z+1), (x+1, y, z+1)] placements.append({'cells': cells, 'orientation': 'vertical_side', 'start': (x, y, z)}) print("Total placements enumerated:", len(placements)) # 应为 36 # --------------------------- # 2. 建立整数规划模型 # --------------------------- # 创建问题:最大化板块数 problem = pulp.LpProblem("Maximize_Slabs", pulp.LpMaximize) # 为每个放置方式定义二元变量:变量 p_i = 1 表示选择第 i 个放置方式,0 表示不放置 placement_vars = {} for i, placement in enumerate(placements): placement_vars[i] = pulp.LpVariable(f"p_{i}", cat="Binary") # 目标函数:最大化所有选择的板块数 problem += pulp.lpSum(placement_vars[i] for i in placement_vars), "Total_Slabs" # 对于 3×3×3 立方体内每个单元格,确保该单元格最多被一个板块覆盖 cube_cells = [(x, y, z) for x in range(3) for y in range(3) for z in range(3)] for cell in cube_cells: # 找出所有覆盖该 cell 的板块放置 placements_covering = [placement_vars[i] for i, placement in enumerate(placements) if cell in placement['cells']] if placements_covering: problem += pulp.lpSum(placements_covering) <= 1, f"Cell_{cell}_constraint" # --------------------------- # 3. 求解模型并输出结果 # --------------------------- problem.solve() print("\n求解状态:", pulp.LpStatus[problem.status]) print("最优板块数:", pulp.value(problem.objective)) print("\n选择的放置方案:") for i, placement in enumerate(placements): if pulp.value(placement_vars[i]) > 0.5: # 取 1 的情况 print(f"方案 {i}: 方向 = {placement['orientation']}, 起始点 = {placement['start']}, 覆盖单元 = {placement['cells']}")
枚举放置方式:
根据 3 个不同的放置方向(水平、竖直正面朝向、竖直侧面朝向)枚举所有合法的放置位置,每个放置方式都会列出它覆盖的 4 个单元格。
构造整数规划模型:
求解与输出:
调用 PuLP 的求解器求解该整数规划问题,并输出状态、最优板块数及所选择的放置方案。
这个精细化模型考虑了每个板块在盒子内的具体位置,从而保证所得的解在实际物理摆放中是可行的。
Option for printingOptions changed from normal to all
Total time (CPU seconds): 0.00 (Wallclock seconds): 0.01
求解状态: Optimal
最优板块数: 6.0
选择的放置方案:
方案 3: 方向 = horizontal, 起始点 = (1, 1, 0), 覆盖单元 = [(1, 1, 0), (2, 1, 0), (1, 2, 0), (2, 2, 0)]
方案 8: 方向 = horizontal, 起始点 = (0, 0, 2), 覆盖单元 = [(0, 0, 2), (1, 0, 2), (0, 1, 2), (1, 1, 2)]
方案 12: 方向 = vertical_face, 起始点 = (0, 0, 0), 覆盖单元 = [(0, 0, 0), (0, 1, 0), (0, 0, 1), (0, 1, 1)]
方案 23: 方向 = vertical_face, 起始点 = (2, 1, 1), 覆盖单元 = [(2, 1, 1), (2, 2, 1), (2, 1, 2), (2, 2, 2)]
方案 29: 方向 = vertical_side, 起始点 = (0, 2, 1), 覆盖单元 = [(0, 2, 1), (1, 2, 1), (0, 2, 2), (1, 2, 2)]
方案 30: 方向 = vertical_side, 起始点 = (1, 0, 0), 覆盖单元 = [(1, 0, 0), (2, 0, 0), (1, 0, 1), (2, 0, 1)]
verify it
我们来验证所给解中6块板块的放置方案是否满足在3×3×3盒子内完全不重叠且都在盒子范围内。
方案 3(水平放置,起始点 (1, 1, 0))
覆盖单元格:
方案 8(水平放置,起始点 (0, 0, 2))
覆盖单元格:
方案 12(竖直放置——正面朝向,起始点 (0, 0, 0))
覆盖单元格:
方案 23(竖直放置——正面朝向,起始点 (2, 1, 1))
覆盖单元格:
方案 29(竖直放置——侧面朝向,起始点 (0, 2, 1))
覆盖单元格:
方案 30(竖直放置——侧面朝向,起始点 (1, 0, 0))
覆盖单元格:
每个方案覆盖4个单元格,总共覆盖 个单元格,而3×3×3的盒子共有27个单元格,说明有3个单元格未被覆盖,这是允许的。
盒子的坐标范围是 。
我们将各方案覆盖的单元格集合列出并检查是否有重复单元格:
逐一比较可以发现:
经过检查,所有6个方案覆盖的单元格两两互不相交。
因此,该整数规划模型给出的解是实际可行的,6块板块的放置方案经过验证是正确的。
refine the script make the box and slab dimension variables to generalize the solving method
下面的脚本对盒子的尺寸和板块(slab)的尺寸都进行了参数化,能够生成所有可能的放置方式(考虑所有唯一旋转),并建立每个单元格不被重复覆盖的约束,从而用整数规划求解最大可放入板块的个数。你只需要修改下面的参数即可适用于不同尺寸的盒子和板块。
pythonimport pulp from itertools import permutations # --------------------------- # 1. 参数定义 # --------------------------- # 盒子尺寸(单位:单元格数):Bx, By, Bz box_dims = (3, 3, 3) # 可修改为任意 (Bx, By, Bz) Bx, By, Bz = box_dims # 板块(slab)尺寸(单位:单元格数):例如 2×2×1 slab_dims = (2, 2, 1) # 可修改为任意 (a, b, c) # --------------------------- # 2. 枚举板块所有唯一的旋转(放置方向) # --------------------------- # 由于板块可以旋转,可能会得到重复的方向(如2×2×1中前两个数相同), # 因此使用set来剔除重复的旋转。 orientations = set(permutations(slab_dims)) orientations = list(orientations) print("Unique orientations:", orientations) # --------------------------- # 3. 枚举所有在盒子内的可能放置方案 # --------------------------- placements = [] # 对于每一种旋转(方向),确定所有合法的放置位置 for orientation in orientations: dx, dy, dz = orientation # 起始位置必须满足板块完全放入盒子内 for sx in range(Bx - dx + 1): for sy in range(By - dy + 1): for sz in range(Bz - dz + 1): # 枚举该放置方式覆盖的所有单元格 cells = [] for i in range(dx): for j in range(dy): for k in range(dz): cells.append((sx + i, sy + j, sz + k)) placements.append({ 'cells': cells, 'orientation': orientation, 'start': (sx, sy, sz) }) print("Total placements enumerated:", len(placements)) # --------------------------- # 4. 建立整数规划模型 # --------------------------- problem = pulp.LpProblem("Maximize_Slabs", pulp.LpMaximize) # 为每个放置方案建立一个二元决策变量:1 表示选择该方案放置板块,0 表示不放置 placement_vars = {} for i, placement in enumerate(placements): placement_vars[i] = pulp.LpVariable(f"p_{i}", cat="Binary") # 目标:最大化放置的板块总数 problem += pulp.lpSum(placement_vars[i] for i in placement_vars), "Total_Slabs" # 盒子内所有单元格列表(坐标范围 0 ~ Bx-1, 0 ~ By-1, 0 ~ Bz-1) box_cells = [(x, y, z) for x in range(Bx) for y in range(By) for z in range(Bz)] # 对每个单元格添加约束:最多只能被一个板块覆盖 for cell in box_cells: covering_vars = [placement_vars[i] for i, placement in enumerate(placements) if cell in placement['cells']] if covering_vars: problem += pulp.lpSum(covering_vars) <= 1, f"Cell_{cell}_constraint" # --------------------------- # 5. 求解模型并输出结果 # --------------------------- problem.solve() print("\nSolution status:", pulp.LpStatus[problem.status]) print("Optimal number of slabs:", pulp.value(problem.objective)) print("\nSelected placements:") for i, placement in enumerate(placements): if pulp.value(placement_vars[i]) > 0.5: print(f"Placement {i}: orientation {placement['orientation']}, start {placement['start']}, covers {placement['cells']}")
参数定义
box_dims 定义了盒子的尺寸,例如 (3,3,3)。slab_dims 定义了板块的尺寸,例如 (2,2,1)。枚举唯一旋转
利用 itertools.permutations 枚举板块所有可能的旋转,并用 set 去重,保证后续枚举放置位置时只考虑唯一的方向。
枚举放置方案
对于每种旋转,遍历所有可能的起始位置(满足板块完全放入盒子内),记录该方案覆盖的所有单元格。
构建整数规划模型
为每个放置方案定义一个二元变量,并对盒子内每个单元格建立“不重叠”约束:同一单元格只能被至多一个板块覆盖。
求解与输出
使用 PuLP 求解该模型,并输出最优放置方案和总板块数。
通过修改 box_dims 和 slab_dims,你可以将该脚本用于其他盒子和板块尺寸的组合,验证最大可放置板块数量以及具体的放置方案。
animate the process
下面给出一个完整的 Python 脚本,该脚本不仅将盒子和板块尺寸参数化、求解最大装板问题,而且利用 matplotlib 的 3D 动画功能展示了板块逐步放入盒子内的过程。你只需要修改参数部分即可适用于不同的盒子尺寸和板块尺寸。
pythonimport pulp from itertools import permutations import matplotlib.pyplot as plt from mpl_toolkits.mplot3d.art3d import Poly3DCollection import matplotlib.animation as animation # --------------------------- # 1. 参数定义 # --------------------------- # 盒子尺寸(单位:单元格数):Bx, By, Bz box_dims = (3, 3, 3) # 如 (3,3,3) Bx, By, Bz = box_dims # 板块(slab)的尺寸(单位:单元格数) slab_dims = (2, 2, 1) # 如 (2,2,1) # --------------------------- # 2. 枚举板块所有唯一旋转(方向) # --------------------------- # 利用排列组合并去重,得到所有唯一的放置方向 orientations = set(permutations(slab_dims)) orientations = list(orientations) print("Unique orientations:", orientations) # --------------------------- # 3. 枚举所有在盒子内的可能放置方案 # --------------------------- placements = [] # 对于每种旋转,遍历所有起始位置,使板块完全放入盒子内 for orientation in orientations: dx, dy, dz = orientation for sx in range(Bx - dx + 1): for sy in range(By - dy + 1): for sz in range(Bz - dz + 1): cells = [] for i in range(dx): for j in range(dy): for k in range(dz): cells.append((sx + i, sy + j, sz + k)) placements.append({ 'cells': cells, 'orientation': orientation, 'start': (sx, sy, sz) }) print("Total placements enumerated:", len(placements)) # --------------------------- # 4. 建立整数规划模型(MILP) # --------------------------- problem = pulp.LpProblem("Maximize_Slabs", pulp.LpMaximize) # 为每个放置方案建立二元决策变量 placement_vars = {} for i, placement in enumerate(placements): placement_vars[i] = pulp.LpVariable(f"p_{i}", cat="Binary") # 目标函数:最大化板块总数 problem += pulp.lpSum(placement_vars[i] for i in placement_vars), "Total_Slabs" # 对于盒子内的每个单元格,添加不重叠约束:该单元格最多被一个板块覆盖 box_cells = [(x, y, z) for x in range(Bx) for y in range(By) for z in range(Bz)] for cell in box_cells: covering_vars = [placement_vars[i] for i, placement in enumerate(placements) if cell in placement['cells']] if covering_vars: problem += pulp.lpSum(covering_vars) <= 1, f"Cell_{cell}_constraint" # 求解模型 problem.solve() print("\nSolution status:", pulp.LpStatus[problem.status]) print("Optimal number of slabs:", pulp.value(problem.objective)) # 选择放置方案(即决策变量取1的方案) selected_placements = [placement for i, placement in enumerate(placements) if pulp.value(placement_vars[i]) > 0.5] # --------------------------- # 5. 定义辅助函数:生成长方体的顶点及面数据 # --------------------------- def cuboid_data(origin, size): """ 给定原点 (X, Y, Z) 和尺寸 (dx, dy, dz),返回长方体顶点及各面顶点列表。 """ X, Y, Z = origin dx, dy, dz = size vertices = [ [X, Y, Z], [X+dx, Y, Z], [X+dx, Y+dy, Z], [X, Y+dy, Z], [X, Y, Z+dz], [X+dx, Y, Z+dz], [X+dx, Y+dy, Z+dz], [X, Y+dy, Z+dz] ] faces = [ [vertices[0], vertices[1], vertices[2], vertices[3]], # 底面 [vertices[4], vertices[5], vertices[6], vertices[7]], # 顶面 [vertices[0], vertices[1], vertices[5], vertices[4]], # 前侧面 [vertices[2], vertices[3], vertices[7], vertices[6]], # 后侧面 [vertices[1], vertices[2], vertices[6], vertices[5]], # 右侧面 [vertices[4], vertices[7], vertices[3], vertices[0]] # 左侧面 ] return vertices, faces # --------------------------- # 6. 动画绘制:板块逐步放入盒子内 # --------------------------- fig = plt.figure() ax = fig.add_subplot(111, projection='3d') ax.set_xlim([0, Bx]) ax.set_ylim([0, By]) ax.set_zlim([0, Bz]) ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') ax.set_title("板块逐步放入盒子内的动画演示") # 绘制盒子边框(wireframe) def draw_box(ax, Bx, By, Bz): # 盒子8个角点 corners = [(x, y, z) for x in [0, Bx] for y in [0, By] for z in [0, Bz]] # 盒子12条边,每条边连接两个角点 edges = [ (0,1), (0,2), (0,4), (1,3), (1,5), (2,3), (2,6), (3,7), (4,5), (4,6), (5,7), (6,7) ] for edge in edges: p1 = corners[edge[0]] p2 = corners[edge[1]] xs = [p1[0], p2[0]] ys = [p1[1], p2[1]] zs = [p1[2], p2[2]] ax.plot(xs, ys, zs, color='black', linewidth=1) draw_box(ax, Bx, By, Bz) def update(frame): # 每个帧显示前 frame 个板块 # 为避免重复绘制,先删除先前绘制的板块(Poly3DCollection对象) ax.collections = [col for col in ax.collections if not isinstance(col, Poly3DCollection)] # 绘制盒子边框(防止被覆盖) draw_box(ax, Bx, By, Bz) # 绘制已放入的板块 for i in range(frame): if i < len(selected_placements): placement = selected_placements[i] dx, dy, dz = placement['orientation'] sx, sy, sz = placement['start'] _, faces = cuboid_data((sx, sy, sz), (dx, dy, dz)) poly = Poly3DCollection(faces, alpha=0.5) poly.set_facecolor("cyan") ax.add_collection3d(poly) return ax.collections # 创建动画:每隔 1 秒增加一块板块 ani = animation.FuncAnimation(fig, update, frames=len(selected_placements)+1, interval=1000, blit=False, repeat=False) plt.show()
参数化
box_dims 与 slab_dims 即可适用于不同尺寸盒子和板块。枚举放置方案与建模
动画演示
运行该脚本后,将弹出一个窗口展示动画演示,直观显示板块的逐步放入过程。
Objective value: 6.00000000
Enumerated nodes: 0
Total iterations: 0
Time (CPU seconds): 0.00
Time (Wallclock seconds): 0.00
Option for printingOptions changed from normal to all
Total time (CPU seconds): 0.00 (Wallclock seconds): 0.00
Solution status: Optimal
Optimal number of slabs: 6.0
/home/dap/miniconda3/envs/physefem/lib/python3.10/site-packages/matplotlib/animation.py:872: UserWarning: Animation was deleted without rendering anything. This is most likely not intended. To prevent deletion, assign the Animation to a variable, e.g. anim, that exists until you output the Animation using plt.show() or anim.save().
下面给出的脚本对盒子和板块的尺寸都做了参数化,生成所有可能的放置方式,构建不重叠的 MILP 模型求解最大可放入板块数量,并利用 matplotlib 的 3D 动画展示板块逐步放入盒子内的过程。关键是将动画对象赋值给变量(例如 anim),以防止其被垃圾回收,从而避免“Animation was deleted without rendering anything”警告。
pythonimport pulp from itertools import permutations import matplotlib.pyplot as plt from mpl_toolkits.mplot3d.art3d import Poly3DCollection import matplotlib.animation as animation # --------------------------- # 1. 参数定义 # --------------------------- # 盒子尺寸(单位:单元格数),例如 3×3×3 box_dims = (3, 3, 3) Bx, By, Bz = box_dims # 板块(slab)的尺寸(单位:单元格数),例如 2×2×1 slab_dims = (2, 2, 1) # --------------------------- # 2. 枚举板块所有唯一旋转(方向) # --------------------------- # 利用排列组合并去重,得到所有唯一的放置方向 orientations = set(permutations(slab_dims)) orientations = list(orientations) print("Unique orientations:", orientations) # --------------------------- # 3. 枚举所有在盒子内的可能放置方案 # --------------------------- placements = [] # 对于每种旋转,遍历所有起始位置,使板块完全放入盒子内 for orientation in orientations: dx, dy, dz = orientation for sx in range(Bx - dx + 1): for sy in range(By - dy + 1): for sz in range(Bz - dz + 1): cells = [] for i in range(dx): for j in range(dy): for k in range(dz): cells.append((sx + i, sy + j, sz + k)) placements.append({ 'cells': cells, 'orientation': orientation, 'start': (sx, sy, sz) }) print("Total placements enumerated:", len(placements)) # --------------------------- # 4. 建立 MILP 模型(不重叠约束) # --------------------------- problem = pulp.LpProblem("Maximize_Slabs", pulp.LpMaximize) # 为每个放置方案建立二元决策变量 placement_vars = {} for i, placement in enumerate(placements): placement_vars[i] = pulp.LpVariable(f"p_{i}", cat="Binary") # 目标函数:最大化板块总数 problem += pulp.lpSum(placement_vars[i] for i in placement_vars), "Total_Slabs" # 对于盒子内每个单元格,确保至多被一个板块覆盖 box_cells = [(x, y, z) for x in range(Bx) for y in range(By) for z in range(Bz)] for cell in box_cells: covering_vars = [placement_vars[i] for i, placement in enumerate(placements) if cell in placement['cells']] if covering_vars: problem += pulp.lpSum(covering_vars) <= 1, f"Cell_{cell}_constraint" # 求解模型 problem.solve() print("\nSolution status:", pulp.LpStatus[problem.status]) print("Optimal number of slabs:", pulp.value(problem.objective)) # 选择放置方案:即决策变量取 1 的那些方案 selected_placements = [placement for i, placement in enumerate(placements) if pulp.value(placement_vars[i]) > 0.5] # --------------------------- # 5. 辅助函数:生成长方体顶点与面数据 # --------------------------- def cuboid_data(origin, size): """ 给定原点 (X, Y, Z) 和尺寸 (dx, dy, dz),返回长方体的顶点列表及其面的顶点列表 """ X, Y, Z = origin dx, dy, dz = size vertices = [ [X, Y, Z], [X+dx, Y, Z], [X+dx, Y+dy, Z], [X, Y+dy, Z], [X, Y, Z+dz], [X+dx, Y, Z+dz], [X+dx, Y+dy, Z+dz], [X, Y+dy, Z+dz] ] faces = [ [vertices[0], vertices[1], vertices[2], vertices[3]], # 底面 [vertices[4], vertices[5], vertices[6], vertices[7]], # 顶面 [vertices[0], vertices[1], vertices[5], vertices[4]], # 侧面1 [vertices[2], vertices[3], vertices[7], vertices[6]], # 侧面2 [vertices[1], vertices[2], vertices[6], vertices[5]], # 侧面3 [vertices[4], vertices[7], vertices[3], vertices[0]] # 侧面4 ] return vertices, faces # --------------------------- # 6. 设置动画 # --------------------------- fig = plt.figure() ax = fig.add_subplot(111, projection='3d') ax.set_xlim([0, Bx]) ax.set_ylim([0, By]) ax.set_zlim([0, Bz]) ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') ax.set_title("板块逐步放入盒子的动画演示") # 绘制盒子边框(wireframe) def draw_box(ax, Bx, By, Bz): corners = [(x, y, z) for x in [0, Bx] for y in [0, By] for z in [0, Bz]] edges = [ (0,1), (0,2), (0,4), (1,3), (1,5), (2,3), (2,6), (3,7), (4,5), (4,6), (5,7), (6,7) ] for edge in edges: p1 = corners[edge[0]] p2 = corners[edge[1]] xs = [p1[0], p2[0]] ys = [p1[1], p2[1]] zs = [p1[2], p2[2]] ax.plot(xs, ys, zs, color='black', linewidth=1) draw_box(ax, Bx, By, Bz) def update(frame): # 清除之前添加的板块(保留盒子边框) ax.collections = [col for col in ax.collections if not isinstance(col, Poly3DCollection)] draw_box(ax, Bx, By, Bz) # 显示前 frame 个放置的板块 for i in range(frame): if i < len(selected_placements): placement = selected_placements[i] dx, dy, dz = placement['orientation'] sx, sy, sz = placement['start'] _, faces = cuboid_data((sx, sy, sz), (dx, dy, dz)) poly = Poly3DCollection(faces, alpha=0.5) poly.set_facecolor("cyan") ax.add_collection3d(poly) return ax.collections # 注意:将动画对象赋值给全局变量 anim 以防止被垃圾回收 anim = animation.FuncAnimation(fig, update, frames=len(selected_placements)+1, interval=1000, blit=False, repeat=False) plt.show()
参数化定义:
修改 box_dims 与 slab_dims 可以适应不同尺寸的盒子和板块。
枚举放置方案:
对所有唯一旋转(利用 permutations 去重后)遍历所有可能的起始位置,确保板块完全放入盒子内。
MILP 模型:
对每个单元格添加“不重叠”约束,确保同一单元格最多被一个板块覆盖,从而求得最大板块数及对应放置方案。
动画部分:
使用 matplotlib 的 3D 动画功能逐步显示放置的板块,动画对象被赋值给变量 anim,这样就不会因垃圾回收而被删除。调用 plt.show() 后,即可看到动画效果。
运行该脚本后,窗口会显示 3D 盒子的图形,并以动画形式逐步展示板块如何放入盒子内。