python
复制代码
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from typing import List, Tuple
import numpy as np
class Box:
"""表示长方体(货车或货物)"""
def __init__(self, length: float, width: float, height: float, position: Tuple[float, float, float] = (0, 0, 0)):
self.length = length # x轴(车厢长度)
self.width = width # y轴(车厢宽度)
self.height = height # z轴(车厢高度)
self.position = position
def get_corners(self) -> List[Tuple[float, float, float]]:
x, y, z = self.position
return [
(x, y, z), (x + self.length, y, z),
(x + self.length, y + self.width, z), (x, y + self.width, z),
(x, y, z + self.height), (x + self.length, y, z + self.height),
(x + self.length, y + self.width, z + self.height), (x, y + self.width, z + self.height)
]
def rotate(self, rotation_type: int) -> None:
dims = [self.length, self.width, self.height]
if rotation_type == 0: # LWH
self.length, self.width, self.height = dims[0], dims[1], dims[2]
elif rotation_type == 1: # LHW
self.length, self.width, self.height = dims[0], dims[2], dims[1]
elif rotation_type == 2: # WLH
self.length, self.width, self.height = dims[1], dims[0], dims[2]
elif rotation_type == 3: # WHW
self.length, self.width, self.height = dims[1], dims[2], dims[0]
elif rotation_type == 4: # HLW
self.length, self.width, self.height = dims[2], dims[0], dims[1]
elif rotation_type == 5: # HWL
self.length, self.width, self.height = dims[2], dims[1], dims[0]
def can_fit_in(self, other: 'Box') -> bool:
return (self.length <= other.length and
self.width <= other.width and
self.height <= other.height)
class BinPacking3D:
"""3D装箱求解器(优化空间分割逻辑)"""
def __init__(self, truck: Box):
self.truck = truck
self.bins = [truck] # 可用空间
self.placed_boxes = [] # 已放置货物
def add_box(self, box: Box) -> bool:
"""尝试放置箱子(按空间效率排序)"""
# 优先选择利用率最高的空间放置
for i in sorted(range(len(self.bins)),
key=lambda x: (self.bins[x].length * self.bins[x].width * self.bins[x].height),
reverse=False):
bin_box = self.bins[i]
if box.can_fit_in(bin_box):
box.position = bin_box.position
self.placed_boxes.append(box)
self._split_bin(i, box) # 优化的空间分割
return True
return False
def _split_bin(self, bin_index: int, placed_box: Box) -> None:
"""
优化的空间分割逻辑:
1. 右侧剩余空间(沿长度方向)
2. 后方剩余空间(沿宽度方向)→ 关键:确保宽度方向能放下第二排
3. 上方剩余空间(沿高度方向)
"""
bin_box = self.bins[bin_index]
x, y, z = bin_box.position # 原始空间起点
px, py, pz = placed_box.position # 放置的箱子起点
pl, pw, ph = placed_box.length, placed_box.width, placed_box.height # 放置的箱子尺寸
new_bins = []
# 1. 右侧空间(长度方向剩余)
if px + pl < x + bin_box.length:
new_bins.append(Box(
length=(x + bin_box.length) - (px + pl),
width=pw, # 保持与放置箱子相同的宽度
height=ph,
position=(px + pl, py, pz)
))
# 2. 后方空间(宽度方向剩余 → 关键修复:确保能放第二排)
if py + pw < y + bin_box.width:
new_bins.append(Box(
length=bin_box.length, # 保持原始空间的全部长度
width=(y + bin_box.width) - (py + pw), # 宽度方向剩余
height=ph,
position=(x, py + pw, pz) # 从原始空间起点开始(而非放置箱子的起点)
))
# 3. 上方空间(高度方向剩余)
if pz + ph < z + bin_box.height:
new_bins.append(Box(
length=bin_box.length, # 保持原始空间的全部长度
width=bin_box.width, # 保持原始空间的全部宽度
height=(z + bin_box.height) - (pz + ph),
position=(x, y, pz + ph)
))
# 替换原始空间为新分割的空间
self.bins.pop(bin_index)
self.bins.extend(new_bins)
def optimize_placement(self, boxes: List[Box]) -> None:
"""优化放置顺序和旋转方式"""
# 按体积排序(大的先放)
boxes_sorted = sorted(boxes, key=lambda b: b.length * b.width * b.height, reverse=True)
for box in boxes_sorted:
best_rotation = 0
best_fit = False
# 尝试所有旋转方式
for rotation in range(6):
box_copy = Box(box.length, box.width, box.height)
box_copy.rotate(rotation)
temp_solver = BinPacking3D(Box(self.truck.length, self.truck.width, self.truck.height))
temp_solver.placed_boxes = [b for b in self.placed_boxes]
temp_solver.bins = [b for b in self.bins]
if temp_solver.add_box(box_copy):
# 计算利用率
used = sum(b.length * b.width * b.height for b in temp_solver.placed_boxes)
total = self.truck.length * self.truck.width * self.truck.height
if (used / total > best_fit) or not best_fit:
best_rotation = rotation
best_fit = True
box.rotate(best_rotation)
if not self.add_box(box):
print(f"无法放入箱子: {box.length}x{box.width}x{box.height}")
def visualize(self, title: str = "货车装载可视化(修复宽度方向)") -> None:
"""可视化保持不变"""
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
# 绘制货车轮廓
self._plot_box_outline(ax, self.truck, color='black', linewidth=1)
# 绘制货物
colors = ['#FF5733', '#33FF57', '#3357FF', '#F3FF33', '#FF33F3', '#33FFF3', '#F333FF']
for i, box in enumerate(self.placed_boxes):
self._plot_box(ax, box, color=colors[i % len(colors)], alpha=0.8)
# 真实比例设置
max_dim = max(self.truck.length, self.truck.width, self.truck.height)
x_scale = self.truck.length / max_dim
y_scale = self.truck.width / max_dim
z_scale = self.truck.height / max_dim
ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([x_scale, y_scale, z_scale, 1]))
# 视角
ax.view_init(elev=20, azim=45) # 降低仰角,更易看到宽度方向的两排
ax.axis('off')
fig.patch.set_facecolor('white')
plt.tight_layout()
plt.show()
def _plot_box(self, ax: Axes3D, box: Box, color: str, alpha: float) -> None:
corners = box.get_corners()
faces = [
[corners[0], corners[1], corners[2], corners[3]],
[corners[4], corners[5], corners[6], corners[7]],
[corners[0], corners[1], corners[5], corners[4]],
[corners[2], corners[3], corners[7], corners[6]],
[corners[0], corners[3], corners[7], corners[4]],
[corners[1], corners[2], corners[6], corners[5]]
]
ax.add_collection3d(Poly3DCollection(faces, facecolors=color, edgecolors='black', alpha=alpha))
def _plot_box_outline(self, ax: Axes3D, box: Box, color: str, linewidth: float) -> None:
corners = box.get_corners()
edges = [
[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]],
[corners[4], corners[5]], [corners[5], corners[6]], [corners[6], corners[7]], [corners[7], corners[4]],
[corners[0], corners[4]], [corners[1], corners[5]], [corners[2], corners[6]], [corners[3], corners[7]]
]
for edge in edges:
x, y, z = zip(*edge)
ax.plot(x, y, z, color=color, linewidth=linewidth)
def main():
# 货车尺寸:12m长 x 3m宽 x 3.5m高(宽度足够放两排1.5m的箱子)
truck = Box(length=12.0, width=3, height=3.5)
# 货物:18个1.5x1.5x1.5的正方体箱子(理论上可放:12/1.5=8排长度,3/1.5=2排宽度 → 8*2=16个,高度3.5/1.5≈2层 → 32个,所以18个应全部放下)
boxes = [Box(1.1, 1.5, 1.0) for _ in range(5)]
boxes.append(Box(1,1,1))
boxes.append(Box(1,1,1))
boxes.append(Box(1,1,1))
boxes.append(Box(1,1,1))
boxes.append(Box(1,1,1))
boxes.append(Box(1,1,1))
boxes.append(Box(1,1,1))
boxes.append(Box(1,1,1))
boxes.append(Box(1,1,1))
boxes.append(Box(2,2,2))
boxes.append(Box(1.2,1,1))
boxes.append(Box(1,1.5,1))
solver = BinPacking3D(truck)
solver.optimize_placement(boxes)
# 输出结果
print(f"货车尺寸: {truck.length}m(长) x {truck.width}m(宽) x {truck.height}m(高)")
print(f"总货物数量: {len(boxes)} 个(1.5x1.5x1.5m)")
print(f"成功装载: {len(solver.placed_boxes)} 个")
print(
f"空间利用率: {sum(b.length * b.width * b.height for b in solver.placed_boxes) / (truck.length * truck.width * truck.height) * 100:.1f}%")
# 可视化(可看到宽度方向的两排货物)
solver.visualize()
if __name__ == "__main__":
main()