最短距离造桥问题演示动画
上传时间:2026-04-09 10:19 · 作者:ytry1983
最短距离:造桥选址问题动画演示
利用“平移”思想解决线段和最小问题
💡 几何原理
设桥宽为 $d$。路径全长 = $AM + MN + NB$。
因为桥 $MN$ 垂直于河岸且长度固定,所以 $MN = d$。
通过平移,构造平行四边形 $AMNA'$,使得 $AM = A'N$。
则路径长度 = $A'N + d + NB$。要使和最小,只需 $A'N + NB$ 最小。
根据两点之间线段最短,$A', N, B$ 三点共线时,路径最短。
📝 解题口诀
- 岸边两点隔河望
- 垂直平移一桥长
- 连接平移点与点
- 交点之处定桥梁
查看解析页源码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>最短距离:造桥选址问题动画演示</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.step-transition {
transition: all 0.5s ease-in-out;
}
@keyframes pulse-orange {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.2); }
}
.animate-pulse-custom {
animation: pulse-orange 2s infinite;
}
</style>
</head>
<body class="bg-slate-50 min-h-screen font-sans p-4 md:p-8 flex flex-col items-center">
<div class="max-w-4xl w-full bg-white rounded-2xl shadow-xl overflow-hidden">
<!-- Header -->
<div class="bg-blue-600 p-6 text-white text-center">
<h1 class="text-2xl font-bold">最短距离:造桥选址问题动画演示</h1>
<p class="mt-2 opacity-90">利用“平移”思想解决线段和最小问题</p>
</div>
<!-- Animation Area -->
<div class="relative h-[400px] md:h-[500px] w-full bg-blue-50 border-y border-slate-200 overflow-hidden">
<svg id="animation-svg" class="w-full h-full" viewBox="0 0 600 500" preserveAspectRatio="xMidYMid meet">
<!-- 河流背景 -->
<rect x="0" y="150" width="600" height="100" fill="#e0f2fe" />
<line x1="0" y1="150" x2="600" y2="150" stroke="#0ea5e9" stroke-width="2" stroke-dasharray="5,5" />
<line x1="0" y1="250" x2="600" y2="250" stroke="#0ea5e9" stroke-width="2" stroke-dasharray="5,5" />
<text x="10" y="210" fill="#0ea5e9" class="text-sm italic font-bold">河 流 (宽度为 d)</text>
<!-- 点 A 和 B -->
<g id="points-static">
<circle cx="100" cy="50" r="6" fill="#ef4444" />
<text x="90" y="35" class="font-bold">村庄 A</text>
<circle cx="500" cy="350" r="6" fill="#ef4444" />
<text x="510" y="370" class="font-bold">村庄 B</text>
</g>
<!-- 动态图层 -->
<g id="dynamic-layers"></g>
</svg>
<!-- Overlay Info -->
<div id="info-box" class="absolute bottom-6 left-6 right-6 bg-white/95 backdrop-blur-sm p-4 rounded-xl shadow-md border border-slate-100 transition-opacity duration-300">
<h3 id="step-title" class="text-lg font-bold text-slate-800"></h3>
<p id="step-desc" class="text-slate-600 text-sm mt-1"></p>
</div>
</div>
<!-- Controls -->
<div class="p-6 flex justify-between items-center bg-slate-50">
<button id="prev-btn" class="px-6 py-2 rounded-full border border-slate-300 text-slate-600 hover:bg-white transition-all active:scale-95 disabled:opacity-50">
上一步
</button>
<div id="dot-indicators" class="flex gap-2"></div>
<button id="next-btn" class="px-6 py-2 rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-all shadow-lg shadow-blue-200 active:scale-95">
下一步
</button>
</div>
</div>
<!-- Info Grid -->
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl w-full">
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
<h4 class="font-bold text-blue-800 flex items-center gap-2">
<span>💡</span> 几何原理
</h4>
<p class="text-sm text-slate-600 mt-2 leading-relaxed">
设桥宽为 $d$。路径全长 = $AM + MN + NB$。<br/>
因为桥 $MN$ 垂直于河岸且长度固定,所以 $MN = d$。<br/>
通过平移,构造平行四边形 $AMNA'$,使得 $AM = A'N$。<br/>
则路径长度 = $A'N + d + NB$。要使和最小,只需 $A'N + NB$ 最小。<br/>
根据两点之间线段最短,$A', N, B$ 三点共线时,路径最短。
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
<h4 class="font-bold text-blue-800 flex items-center gap-2">
<span>📝</span> 解题口诀
</h4>
<ul class="text-sm text-slate-600 mt-2 space-y-2 list-disc list-inside">
<li>岸边两点隔河望</li>
<li>垂直平移一桥长</li>
<li>连接平移点与点</li>
<li>交点之处定桥梁</li>
</ul>
</div>
</div>
<script>
const steps = [
{ title: "1. 初始场景", desc: "村庄 A 和 B 隔河相望,河宽为定值,我们需要建一座垂直于河岸的桥 MN。" },
{ title: "2. 平移点 A", desc: "关键:将 A 沿垂直河岸方向向下平移一个河宽的距离,得到点 A'。此时 AA' = MN。" },
{ title: "3. 连接 A'B", desc: "连接 A' 和 B 得到一条直线。根据'两点之间线段最短',A'B 就是除去桥宽后的最短路径。" },
{ title: "4. 确定桥的位置", desc: "线段 A'B 与下岸的交点即为 N,向上作垂线交上岸于 M。MN 即为桥的位置。" },
{ title: "5. 完成最短路径", desc: "最终路径为 A-M-N-B。因为 AM 始终等于 A'N,所以 AM+NB = A'N+NB = A'B(定值)。" }
];
let currentStep = 0;
const dynamicLayer = document.getElementById('dynamic-layers');
const stepTitle = document.getElementById('step-title');
const stepDesc = document.getElementById('step-desc');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const dotIndicators = document.getElementById('dot-indicators');
// 初始化指示器
steps.forEach((_, i) => {
const dot = document.createElement('div');
dot.className = `h-2 w-8 rounded-full transition-all ${i === 0 ? 'bg-blue-600 w-12' : 'bg-slate-300'}`;
dotIndicators.appendChild(dot);
});
function updateUI() {
// 更新文字
stepTitle.textContent = steps[currentStep].title;
stepDesc.textContent = steps[currentStep].desc;
// 更新按钮
prevBtn.disabled = currentStep === 0;
nextBtn.textContent = currentStep === steps.length - 1 ? "重新开始" : "下一步";
// 更新指示器
Array.from(dotIndicators.children).forEach((dot, i) => {
dot.className = `h-2 transition-all rounded-full ${i === currentStep ? 'bg-blue-600 w-12' : 'bg-slate-300 w-8'}`;
});
// 清空并重新绘制动态图层
dynamicLayer.innerHTML = '';
// 常量定义
const pointA = { x: 100, y: 50 };
const pointB = { x: 500, y: 350 };
const riverY1 = 150;
const riverY2 = 250;
const riverWidth = 100;
const pointAPrime = { x: 100, y: 150 };
const pointN = { x: 300, y: 250 };
const pointM = { x: 300, y: 150 };
if (currentStep >= 1) {
// A' 点和 AA' 辅助线
createSVGElement('line', { x1: pointA.x, y1: pointA.y, x2: pointAPrime.x, y2: pointAPrime.y, stroke: '#f97316', 'stroke-width': 2, 'stroke-dasharray': '4' });
createSVGElement('circle', { cx: pointAPrime.x, cy: pointAPrime.y, r: 6, fill: '#f97316', class: 'animate-pulse-custom' });
createSVGElement('text', { x: pointAPrime.x - 15, y: pointAPrime.y + 25, class: 'font-bold fill-orange-500', textContent: "A' (平移点)" });
// 箭头
createSVGElement('path', { d: `M${pointA.x-5} ${pointAPrime.y-10} L${pointA.x} ${pointAPrime.y} L${pointA.x+5} ${pointAPrime.y-10}`, fill: 'none', stroke: '#f97316', 'stroke-width': 2 });
}
if (currentStep >= 2) {
// A'B 辅助线
createSVGElement('line', { x1: pointAPrime.x, y1: pointAPrime.y, x2: pointB.x, y2: pointB.y, stroke: '#94a3b8', 'stroke-width': 2, 'stroke-dasharray': '8' });
}
if (currentStep >= 3) {
// 桥 MN
createSVGElement('line', { x1: pointM.x, y1: pointM.y, x2: pointN.x, y2: pointN.y, stroke: '#10b981', 'stroke-width': 4 });
createSVGElement('circle', { cx: pointM.x, cy: pointM.y, r: 4, fill: '#10b981' });
createSVGElement('circle', { cx: pointN.x, cy: pointN.y, r: 4, fill: '#10b981' });
createSVGElement('text', { x: pointM.x - 20, y: pointM.y - 10, class: 'text-xs font-bold fill-emerald-600', textContent: 'M' });
createSVGElement('text', { x: pointN.x - 20, y: pointN.y + 20, class: 'text-xs font-bold fill-emerald-600', textContent: 'N' });
}
if (currentStep >= 4) {
// 最终红线路径
createSVGElement('line', { x1: pointA.x, y1: pointA.y, x2: pointM.x, y2: pointM.y, stroke: '#ef4444', 'stroke-width': 3 });
createSVGElement('line', { x1: pointN.x, y1: pointN.y, x2: pointB.x, y2: pointB.y, stroke: '#ef4444', 'stroke-width': 3 });
// 强调等量关系
createSVGElement('line', { x1: pointA.x, y1: pointA.y, x2: pointAPrime.x, y2: pointAPrime.y, stroke: '#f97316', 'stroke-width': 2, opacity: 0.4 });
createSVGElement('line', { x1: pointM.x, y1: pointM.y, x2: pointN.x, y2: pointN.y, stroke: '#f97316', 'stroke-width': 2, opacity: 0.4 });
}
}
function createSVGElement(type, props) {
const el = document.createElementNS('http://www.w3.org/2000/svg', type);
for (let key in props) {
if (key === 'textContent') {
el.textContent = props[key];
} else {
el.setAttribute(key, props[key]);
}
}
dynamicLayer.appendChild(el);
return el;
}
nextBtn.addEventListener('click', () => {
currentStep = (currentStep + 1) % steps.length;
updateUI();
});
prevBtn.addEventListener('click', () => {
if (currentStep > 0) {
currentStep--;
updateUI();
}
});
// 初始加载
updateUI();
</script>
</body>
</html>