問題文
$\mathrm{O}$ を原点とする $xyz$ 空間において、点 $\mathrm{P}$ と点 $\mathrm{Q}$ は次の3つの条件(a),(b),(c)を満たしている。
(a) 点 $\mathrm{P}$ は $x$ 軸上にある。
(b) 点 $\mathrm{Q}$ は $yz$ 平面上にある。
(c) 線分 $\mathrm{OP}$ と線分 $\mathrm{OQ}$ の長さの和は $1$ である。
点 $\mathrm{P}$ と点 $\mathrm{Q}$ が条件(a),(b),(c)を満たしながらくまなく動くとき、線分 $\mathrm{PQ}$ が通過してできる立体の体積を求めよ。
(2023 京都大学 理系第5問)
$$\frac{2}{15}\,\pi$$
※ この答えは学校側が公表したものではありません。個人が作成した非公式の答えです。
コード
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as ani
from matplotlib.colors import ListedColormap
from scipy.spatial.transform import Rotation
from mpl_toolkits.mplot3d.art3d import Line3D,Poly3DCollection
#紙の準備
fig = plt.figure()
fig.suptitle("2023京大数学 理系第5問",color="0.7",ha="right",x=0.96,y=0.96)
ax1 = fig.add_subplot(111,projection='3d',computed_zorder=False)
ax1.set_xlim(-0.65,0.85)
ax1.set_ylim(-0.75,0.75)
ax1.set_zlim(-1.10,0.40)
ax1.set_box_aspect((1,1,1))
ax1.axis("off")
ax2 = fig.add_axes((0.35,0,0.6,0.3))
ax2.set_xlim(0,8)
ax2.set_ylim(-4,0)
ax2.axis("off")
#眺める角度の調整
view = [0,-90]
ax1.view_init(view[0],view[1])
#字幕スペースの準備
jimaku = plt.Rectangle((0,0),1,0.13,fc="darkslategrey",
zorder=20,transform=fig.transFigure)
fig.patches.append(jimaku)
kotobadic = dict(color="w",size=16,ha="center",va="center",
alpha=0,zorder=30,fontfamily="HGSoeiKakupoptai")
kotoba = fig.text(0.5,0.065,"",**kotobadic)
#らてふ
lines = [
"=\\pi\\int_0^1 y^2\\;dx",
"=\\pi\\int_0^1 \\left(\\,1-\\sqrt{x}\\;\\right)^4\\,dx",
"=\\pi\\int_0^1 \\left(\\,1-4\\,x^{\\frac{1}{2}}+6\\,x-4\\,x^{\\frac{3}{2}}+x^2\\;\\right)\\,dx",
"=\\pi\\,\\Bigg[\\,x-\\frac{8}{3}\\,x^{\\frac{3}{2}}+3\\,x^2-\\frac{8}{5}\\,x^{\\frac{5}{2}}+\\frac{1}{3}\\,x^3\\;\\Bigg]_0^1",
"=\\pi\\,\\Bigg(\\,1-\\frac{8}{3}+3-\\frac{8}{5}+\\frac{1}{3}\\;\\Bigg)",
"=\\,\\frac{1}{15}\\,\\pi"
]
#数式スペースの準備
mathdic = dict(size=16,usetex=True,clip_on=True)
colors = [(1,1,1,alpha) for alpha in np.arange(0,11)/10]
cmap = ListedColormap(colors)
txts,gyos,secs = [],[],[0]
for i,line in enumerate(lines):
latex = "$\\displaystyle"+line+"$"
text = ax2.text(0,-2*(i+1)+0.8,latex,fontdict=mathdic)
txts.append(text)
bbox = text.get_window_extent().transformed(ax2.transData.inverted())
cx = np.arange(-2,bbox.x1+2,0.1)
cy = np.arange(-2*(i+1),-2*i+0.1,2)
X,Y = np.meshgrid(cx,cy)
C = np.append(0,np.ones(len(cx)-2)).reshape((1,len(cx)-1))
gyos.append(ax2.pcolormesh(X,Y,C,cmap=cmap,zorder=10))
secs.append(secs[-1]+(len(cx)-20)//2)
if i==0:
secs.append(secs[-1])
else:
secs.append(secs[-1]+20)
sahen = fig.text(0.315,0.205,"$V$",**mathdic,zorder=30,alpha=0)
txts += [ax2.text(1.6,-11.2,"$\\displaystyle \\times\\;2$",**mathdic,alpha=0,zorder=20)]
txts += [ax2.text(2.5,-11.2,"$\\displaystyle =\\,\\frac{2}{15}\\,\\pi$",**mathdic,alpha=0,zorder=20)]
#平面図形の準備
nuri = Line3D([],[],[],c="darkkhaki")
sankaku = Poly3DCollection([],fc="khaki")
shahen = Line3D([],[],[],c="orange",lw=3,zorder=5)
himo = Line3D([0,0],[0,0],[1,0],c="darkorange",lw=5,solid_capstyle="round",clip_on=False,zorder=10)
heimen = [nuri,shahen,sankaku,himo]
#回すものの準備
m = 50
x = np.linspace(0,1,m+1)
y = np.zeros_like(x)
z = (1-np.sqrt(x))**2
sokumen = [np.stack([x,y,z]).T]
plate = Poly3DCollection([np.vstack([sokumen[0],[0,0,0]])],fc="darkkhaki",alpha=0)
#回転座標の準備
n = 60
for i in range(1,n+1):
rotvec = i*np.array([0,0,-2*np.pi])/n
R = Rotation.from_rotvec(rotvec)
s = R.apply(sokumen[0])
sokumen.append(s)
#回転面を作成
bands,bands2 = [],[]
for i in range(n):
sikakus,sikakus2 = [],[]
for j in range(m):
sikaku = np.array([sokumen[i][j],sokumen[i][j+1],
sokumen[i+1][j+1],sokumen[i+1][j]])
sikakus.append(sikaku)
sikaku2 = np.array([1,1,-1])*sikaku
sikakus2.append(sikaku2)
band = Poly3DCollection(sikakus,fc="g",alpha=0.2,clip_on=False)
bands.append(band)
band2 = Poly3DCollection(sikakus2,fc="g",alpha=0,clip_on=False)
bands2.append(band2)
#切断面の準備
cuts = []
for j in range(m+1):
en = np.array([[],[],[]])
for i in range(n+1):
en = np.append(en,sokumen[i][m-j].reshape(3,1),axis=1)
cuts.append(en)
danmen = Line3D([],[],[],c="darkorange",lw=3)
#お気に入りのイージング関数
def easing(x):
if x<0.0:
return 0
elif x<0.5:
return 2*x**2
elif x<1.0:
return 1-(-2*x+2)**2/2
else:
return 1
#司会役
class Sikai:
def __init__(self,clips):
self.text = text
self.texts = []
self.verbs = []
self.objects = []
self.periods = []
for clip in clips:
self.texts.append(clip[0])
self.verbs.append(clip[1:-1:2])
self.objects.append(clip[2:-1:2])
self.periods.append(clip[-1])
endings = np.cumsum(self.periods)
self.endings = np.append(0,endings)
self.T = self.endings[-1]
def work(self,i):
p = np.argmin(~(i<self.endings[1:]))
k = i-self.endings[p]
l = self.periods[p]-1-k
a = easing(k/(self.periods[p]-1))
if k==0:
kotoba.set_text(self.texts[p])
elif k<6:
kotoba.set_alpha(easing(k/5))
elif l<6:
kotoba.set_alpha(easing(l/5))
waza = Waza(k,l,a)
V = self.verbs[p]
X = self.objects[p]
for v,x in zip(V,X):
getattr(waza,v)(x)
#使える技の数々
class Waza:
def __init__(self,k,l,a):
self.k = k
self.l = l
self.a = a
#眺める角度を変える
def shiten(self,x):
ax1.view_init((1-self.a)*view[0]+self.a*x[0],
(1-self.a)*view[1]+self.a*x[1])
if self.l==0:
view[0]=x[0]
view[1]=x[1]
#通過領域を塗りつぶす
def nuru(self,x):
t = self.k/(self.k+self.l)
oldnuri = nuri.get_data_3d()
newnuri = np.hstack([oldnuri,[[t**2,0],[0,0],[(1-t)**2,1-t]]])
nuri.set_data_3d(newnuri)
shahen.set_data_3d([[0,t],[0,0],[1-t,0]])
sankaku.set_verts([[(0,0,1-t),(0,0,0),(t,0,0)]])
himo.set_data_3d([[0,0,t],[0,0,0],[1-t,0,0]])
#回転させる
def gururi(self,x):
ax1.add_collection3d(bands[self.k])
plate.set_verts([np.vstack([sokumen[self.k+1],[0,0,0]])])
#断面で切る
def wagiri(self,x):
if self.k==0:
ax1.add_line(danmen)
elif self.l==0:
danmen.set_visible(False)
danmen.set_data_3d(cuts[self.k])
for band in bands:
band.set_facecolors(["g"]*self.l+["darkorange"]*self.k)
#数式を書いていく
def kakikaki(self,x):
p = np.argmin(~(self.k<secs[1:]))
q = self.k-secs[p]
if p%2==0:
C = gyos[p//2].get_array()
C[2*q+1] = 0
C[2*q+1:2*q+20] = np.arange(0.05,1.00,0.05)
gyos[p//2].set_array(C)
else:
a = easing(q/19)
top = -2*(p//2-1+a)
ax2.set_ylim(top-4,top)
txts[p//2-1].set_alpha(1-a)
#何かを見せる
def arawaru(self,x):
if self.k==0:
for obj in x:
if type(obj)==Line3D:
ax1.add_line(obj)
elif type(obj)==Poly3DCollection:
ax1.add_collection3d(obj)
for obj in x:
obj.set_alpha(self.a)
#下側の回転面を見せる
def arawaru2(self,x):
if self.k==0:
for obj in x:
ax1.add_collection3d(obj)
for obj in x:
obj.set_alpha(0.2*self.a)
#何かを消す
def kieru(self,x):
for obj in x:
obj.set_alpha(1-self.a)
#台本
clips = [
("長さ$\\;\\mathbf{1}\\;$のひもがあります","arawaru",heimen,30),
("スルっと引っこぬきます","nuru",[],70),
("斜辺が通過した部分は","kieru",heimen,"arawaru",[plate],30),
("$\\mathbf{y\\;\\leqq\\;\\left(\\,1-\\sqrt{x}\\;\\right)\\,^2}$",30),
("これを","shiten",(30,-60),30),
("回します","gururi",[],n),
("円錐モドキが出来ました","kieru",[plate],30),
("輪ゴムを通すとこんな感じ","wagiri",[],m+1),
("積分したくねえ","kieru",[jimaku],"arawaru",[sahen],30),
(None,"kakikaki",[],secs[-1]),
("おや!?","shiten",(0,-60),"arawaru",[jimaku],30),
("回転体のようすが・・・","arawaru2",bands2,"arawaru",[txts[-2]],30),
("そっちもアリなんかい!",15),
("というわけで",15),
("答えは","arawaru",[txts[-1]],30),
("おしまい!",40)
]
#司会役の手配
sk = Sikai(clips)
#上演
def update(i):
sk.work(i)
mov = ani.FuncAnimation(fig,update,sk.T-10,interval=100)
plt.show()