2010.10.30.
2014. 2.21.訂正 double → float
石立 喬

Visual C++ 2010 Express の易しい使い方(17)

―――― Lagrange、Newton、Splineなどの色々な補間方式を実現する ――――

概要
 実験結果をグラフに表示する時、測定点をプロットしてから、その間を自在定規で結んだことがあると思う。これを自動的に行うプログラムを紹介する。この作業は「補間」と呼ばれる。補間の方式は数多く知られ、プログラムは比較的簡単なので、ボタンを付けてWindowsプログラムらしくした。

補間
 「狭義の補間(Interpolation)」は、二つの点におけるデータを絶対に正確なものと仮定し、その中間点のデータを推測することを言う。点が実験などの測定点である場合には、測定データに誤差があっても、そのまま補間を行うので、補間はより正しい値を推測するものではない。「広義の補間」は、与えられたすべての点を通るように曲線を描くこと、その曲線の方程式を求めることである。

補間のいろいろ
 一番単純な補間は二点間を直線(一次式)で結ぶもので、「線形補間」と呼ばれる。学校のレポートでグラフを描く時には、「最もいけない」とされるものである。0番目からn番目まで(n+1)個の点を必ず通る曲線(測定点のデータに誤差があっても必ず通るのは良くないが)を表現するには、n次の多項式が必要十分条件であるので、この多項式を求めることを一般に補間と呼ぶ。
 多項式を求める方法には、Lagrange多項式が良く知られているが、再帰プログラムを用いるNewtonの方法もあり、二つの方法による結果は同じになる。ここで紹介するLagrange補間の改良は、筆者の案によるものである(すでに誰かがやっているかも知れないが…)。一方、多項式の次数を限定し、狭い区間毎に異なる多項式を求める方法にSpline法がある。

Lagrange補間
 データ点を(X0,X0)、(X1,Y1)、…、(Xn,Yn)の(n+1)個とすると、これらの点を必ず通るn次の多項式Pn(x)は下式で求まる。LはLagrange、PはPolynomial(多項式)の意。

     

Lagrange補間の改良
 上記のLagrange補間は、両端で大きな振動が生じる場合がある。これを緩和するために、データ点の(X0,Y0)、(X1,Y1)、…、(Xn,Yn)の(n+1)個に対して、両端に2個の点を追加する。入力した(X0,Y0)、(X1,Y1)、…、(Xn,Yn)を、(X1,Y1)、(X2,Y2)、…、(Xn,Yn)、(Xn+1,Xn+1)のように名前を付け替える、新たに(X0,Y0)と(Xn+2,Yn+2)を追加する。(X0,Y0)は、(X1,Y1)と(X2,Y2)を線形的に外挿して求める。
(Xn+2,Yn+2)は、(Xn,Yn)と(Xn+1,Yn+1)を線形的に外挿する。以上の(n+3)個の点に対して通常のLagrange補間を行い、(X0,Y0)と(X1,Y1)の間、(Xn,Yn)と(Xn+1,Yn+1)の間は表示しない。データ点の条件にもよるが、実行結果を見ると、ある程度の効果があることが分る。

Newton補間
 データ点を(X0,Y0)、(X1,Y1)、…、(Xn,Yn)の(n+1)個とすると、これらの点を必ず通るn次の多項式Pn(x)は下式で求まる。〔XiXi-1 … X0〕の計算は、再帰プログラムで容易に実現できる。
       

Spline補間
 Spline補間は、末端部の条件や区間の取り方により何種類かあるが、結果はほとんど同じである。データ点(Xi-1,Yi-1)と(Xi,Yi)の間を結ぶSpline補間の式の一例(最も良く使用される3次Spline補間)を下記に示す。

      

ただし、

      

プログラムの概要
◎準備
1)マウスで入力するデータ点は、多くのメソッドから呼び出されるので、 配列xdata[i]、ydata[i]をそれぞれ、20個だけグローバルに設定する。staticにする必要がある。
2)Form1_Paint()では、グラフ用紙を作成し、目盛を入れ、データ点があれば、それらを赤丸で表示する。
◎マウス操作
1)マウスをグラフ用紙の領域内で移動させると、その時のグラフ用紙上の値を、画面左下に表示する。グラフ用紙上の値範囲外の場合には、「Out of Range」と表示する。これは、Form1_MouseMove()を使用する。マウスによって指定されたパソコン画面上の絶対位置座標(e->X,e->Y)は、
    x=(e->X-X0)/10;
    y=40-(e->Y-Y0)/10;
により、グラフ用紙上の値に変換される。ただし、xおよびyは、float型とする。
2)マウスをグラフ用紙の領域内でクリックすると、その時のグラフ用紙上の値を、配列xdata[i]、ydata[i]に格納し、data_numberをインクリメントし、Invalidate()でForm1_Paintを呼び出して、クリックした場所を赤丸で表示する。最初の値はx_data[0],y_data[0]に、最後の値はx_data[data_number-1],y_data[data_number-1]に入る。
◎各補間プログラム
1)共通事項
 グラフ用紙上で、xをxdata[0]からxdata[data_number-1]まで、0.2f刻みで変化させ、各補間式を実行し、式によって得たxとyの値を、
    xx=X0+x*10
    yy=Y0+(40-y)*10
によってパソコン画面上の絶対位置座標に変換する。
 グラフは、点(xx_old,yy_old)から点(xx,yy)までDrawLineによって描画する。「Lagrange改」を除いて、x=xdata[0]の時は、xx_oldとyy_oldを設定するだけで、線は引かない。「Lagrange改」の場合には、指定範囲外では線を引かないので、flagを用いて制御する。
2)Lagrange補間(buttonLagrange_Click)
 Lagrange補間の式をそのまま忠実に実行する。乗算を繰り返して行なうので最初にlag=1.0f; と初期設定しておく。
3)Newton補間(buttonNewton_Click)
 配列nとcを、それぞれ20個用意する。これは、入力可能なデータ個数の上限に合わせてある。c[i]を求めるために、自作のメソッドdifference(a,b)を用意する。このメソッドは、前述のNewton補間の式に従って作られていて、再帰的に自分を呼び出す。
4)Lagrange補間の改良(buttonLagrange1_Click)
 データ点に対して両端の2点を付加するので、新しくデータ点xdata1とydata1の配列をそれぞれ22個用意する。
 実際に入力したデータ点をxdata1[i+1]=xdata[i]; などにより一つずつずらし、両端、すなわち、xdata1[0]とxtata1[data\number+1]などを線形的に外挿して求める。
 新しく作ったデータ点について通常のLagrange補間の計算をした後、実際に入力したデータ点の間だけに線を描く。初めて線を描く場合にはold_xxやold_yyを設定するのみにとどめるため、flagを用いて制御する。
5)3次Spline補間(buttonSpline_Click)
 Spline補間の式にしたがって、配列h(データ間隔)、dif1(一次微係数)、dif2(二次微係数)をそれぞれ20個用意し、h[0]などを0.0fに設定する。後は、式にしたがって計算する。
◎ボタン関係
1)ボタン[補間消去]をクリックした場合(buttonInterCancel_Click)
 Invalidate()でForm1_Paintを呼び出して、全てのグラフィック描画を一旦消去し、データ点のみを赤丸で表示する。補間結果は描画しないので、補間結果は消去される。
2)ボタン[データ消去]をクリックした場合(buttonDataCancel_Click)
 データの個数data_numberをゼロにクリヤし、配列xdata[i]、ydata[i]もゼロに設定する。次いで、Invalidate()でForm1_Paintを呼び出して、全てのグラフィック描画を消去する。データ点の個数がゼロなので、赤丸は表示されない。

実数にはdoubleではなくfloatを使う
 一般的には、整数はint、実数はdoubleを使うのが原則である。コンピュータの高速化、メモリの大容量化により、shortやfloatを使う必要はなくなった。
 ところが、DrawLineメソッドやDrawEllipseメソッドは、座標をint(Int32)またはfloat(Single)で与えることになっていて(他にRectangleやRectangleFも可)、doubleで与えると警告が、doubleとintの混在で与えるとエラーが表示される。
 実数をdoubleで計算し、DrawLineなどのメソッドにはfloatにキャストして使用するのは面倒なので、ここでは最初からfloatで計算した。

プログラム

static int X0=50,Y0=60;
int data_number;
static array<float>^ xdata=gcnew array<float>(20);
static array<float>^ ydata=gcnew array<float>(20);

private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) {

   int i,j;
   float xx,yy;

   Graphics^ g=e->Graphics;

   //薄く線を引く
   for(i=0;i<=60;i++)
      g->DrawLine(Pens::LightGray,X0+10*i,Y0,X0+10*i,Y0+400);
   for(j=0;j<=40;j++)
      g->DrawLine(Pens::LightGray,X0,Y0+10*j,X0+600,Y0+10*j);

   //濃く線を引く
   for(i=0;i<=12;i++)
      g->DrawLine(Pens::Gray,X0+50*i,Y0,X0+50*i,Y0+400);
   for(j=0;j<=8;j++)
      g->DrawLine(Pens::Gray,X0,Y0+50*j,X0+600,Y0+50*j);

   //目盛を入れる
   String^ string1;
   for(i=0;i<=12;i++){
      string1=String::Format(" {0} ",5*i);
      g->DrawString(string1,Font,Brushes::Black,X0-15+50*i,Y0+410);
   }
   for(j=0;j<=8;j++){
      string1=String::Format(" {0} ",5*(8-j));
      g->DrawString(string1,Font,Brushes::Black,X0-30,Y0-6+50*j);
   }

   //データ点を描画する
   for(i=0;i<data_number;i++){
      xx=X0+xdata[i]*10;
      yy=Y0+(40-ydata[i])*10;
      g->DrawEllipse(Pens::Red,xx-2,yy-2,4.0f,4.0f);
   }

}

private: System::Void Form1_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   //データ値に変換する
   float x=(e->X-X0)/10.0f;
   float y=40-(e->Y-Y0)/10.0f;

   String^ string1;

   //前の表示を消して、新しいデータ値を表示する
   g->FillRectangle(Brushes::White,X0-5,Y0+446,90,20);
   if(x>=0.0f && x<=60.0f && y>=0.0f && y<=40.0f)
      string1=String::Format("X={0}, Y={1}",x,y);
   else
      string1="Out of Range";
   g->DrawString(string1,Font,Brushes::Black,X0,Y0+450);

}

private: System::Void Form1_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   //データ値に変換する
   float x=(e->X-X0)/10.0f;
   float y=40-(e->Y-Y0)/10.0f;

   //データ値を格納する
   if(x>=0.0f && x<=60.0f && y>=0.0f && y<=40.0f){
      xdata[data_number]=x;
      ydata[data_number]=y;
      data_number++;
   }

   Invalidate();

}

private: System::Void buttonInterCancel_Click(System::Object^ sender, System::EventArgs^ e) {

   Invalidate();

}

private: System::Void buttonDataCancel_Click(System::Object^ sender, System::EventArgs^ e) {

   //データ個数をリセットする
   data_number=0;

   //データ値をリセットする
   for(int i=0;i<20;i++){
      xdata[i]=0.0f;
      ydata[i]=0.0f;
   }

   Invalidate();

}

private: System::Void buttonLaglange_Click(System::Object^ sender, System::EventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   int i,j;
   float x,y,lag;
   float xx,yy,xx_old,yy_old;

   for(x=xdata[0];x<=xdata[data_number-1];x+=0.2f){
      y=0.0f;
      for(i=0;i<data_number;i++){
         lag=1.0f;
         for(j=0;j<data_number;j++)
            if(j!=i) lag*=(x-xdata[j])/(xdata[i]-xdata[j]);
         y+=ydata[i]*lag;
      }
      //画面上の座標に変換する
      xx=X0+x*10;
      yy=Y0+(40-y)*10;
      if(x==xdata[0]){
         xx_old=xx;
         yy_old=yy;
      }
      else{
         g->DrawLine(Pens::Green,xx_old,yy_old,xx,yy);
         xx_old=xx;
         yy_old=yy;
      }
   }

}

private: System::Void buttonNewton_Click(System::Object^ sender, System::EventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   int i,j;
   float x,y,p;
   float xx,yy,xx_old,yy_old;
   array<float>^ n=gcnew array<float>(20);
   array<float>^ c=gcnew array<float>(20);

   for(x=xdata[0];x<=xdata[data_number-1];x+=0.2){
      n[0]=1.0f;
      for(i=1;i<data_number;i++){
         p=1.0f;
         for(j=0;j<i;j++)
            p*=x-xdata[j];
         n[i]=p;
      }
      for(i=0;i<data_number;i++)
         c[i]=difference(i,0);
      y=0.0f;
      for(i=0;i<data_number;i++)
         y+=c[i]*n[i];
      xx=X0+x*10;
      yy=Y0+(40-y)*10;
      if(x==xdata[0]){
         xx_old=xx;
         yy_old=yy;
      }
      else{
         g->DrawLine(Pens::DarkOrange,xx_old,yy_old,xx,yy);
         xx_old=xx;
         yy_old=yy;
      }
   }

}

private: double difference(int a,int b) {

   if(a<=b)   return ydata[a];
   else     return (difference(a,b+1)-difference(a-1,b))/(xdata[a]-xdata[b]);

}

private: System::Void buttonLaglange1_Click(System::Object^ sender, System::EventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   int i,j;
   float x,y,lag;
   float xx,yy,xx_old,yy_old;
   array<float>^ xdata1=gcnew array<float>(22);
   array<float>^ ydata1=gcnew array<float>(22);
   bool flag=true;

   //入力したデータ点を一つずらす
   for(i=0;i<data_number;i++){
      xdata1[i+1]=xdata[i];
      ydata1[i+1]=ydata[i];
   }

   //入力したデータ点を外挿する
   xdata1[0]=2*xdata[0]-xdata[1];
   ydata1[0]=2*ydata[0]-ydata[1];
   xdata1[data_number+1]=2*xdata[data_number-1]-xdata[data_number-2];
   ydata1[data_number+1]=2*ydata[data_number-1]-ydata[data_number-2];

   for(x=xdata1[0];x<=xdata1[data_number+1];x+=0.2f){    //外挿データ点を含む
      y=0.0f;
      for(i=0;i<data_number+2;i++){
         lag=1.0f;
         for(j=0;j<data_number+2;j++)
            if(j!=i) lag*=(x-xdata1[j])/(xdata1[i]-xdata1[j]);
         y+=ydata1[i]*lag;
      }

      if(x>=xdata[0] && x<=xdata[data_number-1]){    //実データ点のみ描画
         xx=X0+x*10;
         yy=Y0+(40-y)*10;
         if(flag){    //描画する最初であったら
            xx_old=xx;
            yy_old=yy;
            flag=false;
         }
         else{
            g->DrawLine(Pens::Magenta,xx_old,yy_old,xx,yy);
            xx_old=xx;
            yy_old=yy;
         }
      }
   }

}

private: System::Void buttonSpline_Click(System::Object^ sender, System::EventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   int i;
   float x,y,yy0,yy1,yy2,yy3;
   float xx,yy,xx_old,yy_old;

   array<float>^ h=gcnew array<float>(20);
   array<float>^ dif1=gcnew array<float>(20);
   array<float>^ dif2=gcnew array<float>(20);

   h[0]=0.0f;
   dif2[0]=0.0f;
   dif2[data_number-1]=0.0f;

   for(i=1;i<data_number;i++){
      h[i]=xdata[i]-xdata[i-1];
      dif1[i]=(ydata[i]-ydata[i-1])/(xdata[i]-xdata[i-1]);
   }
   for(i=1;i<data_number-1;i++)
      dif2[i]=(dif1[i+1]-dif1[i])/(xdata[i+1]-xdata[i-1]);

   i=1;
   for(x=xdata[0];x<xdata[data_number-1];x+=0.2){
      if(x<xdata[i]){
         yy0=dif2[i-1]/(6*h[i])*(xdata[i]-x)*(xdata[i]-x)*(xdata[i]-x);
         yy1=dif2[i]/(6*h[i])*(x-xdata[i-1])*(x-xdata[i-1])*(x-xdata[i-1]);
         yy2=(ydata[i-1]/h[i]-h[i]*dif2[i-1]/6)*(xdata[i]-x);
         yy3=(ydata[i]/h[i]-h[i]*dif2[i]/6)*(x-xdata[i-1]);
         y=yy0+yy1+yy2+yy3;

         xx=X0+x*10;
         yy=Y0+(40-y)*10;
         if(x==xdata[0]){
            xx_old=xx;
            yy_old=yy;
         }
         else{
            g->DrawLine(Pens::DarkCyan,xx_old,yy_old,xx,yy);
            xx_old=xx;
            yy_old=yy;
         }
      }
      else i++;
   }

}


補間プログラムの実行結果の一例
 下図において、赤丸は適当にマウスで入力したデータ点、緑の線はLagrange補間、マゼンタはLagrange補間の改良型、シアンは3次Spline補間によるものである。Newton補間の結果はLagrange補間の結果と全く同じなので、ここでは表示していない。Lagrangeに比べ、Lagrange改良型は両端における暴れ方が減っていることが分る。3次Splineは、暴れはないが、線がスムーズでない部分がある。「方眼紙」の下は、マウスが指している座標である(この例では、一番右のデータ点を指している)。 このプログラムは、プログラムを作成する楽しみもあるが、実際にデータを入れてみて、どのような結果になるかを試してみる面白さもある。電気に詳しい諸君であれば、補間方程式で得られた結果の「暴れ」の原因がお分かりと思う。データ群は高い周波数成分を有するのに、強制的にn次式でしか表されず、高調波成分がカットされるからである。




付録:ボタンの作り方
 「易しい使い方(3)」で、すでにボタンの作り方について簡単に述べてあるが、あらためて解説する。
 1)「Form1.h[デザイン]」を画面に表示する。
 2)「表示」→「ツールボックス」をクリックする。
 3)ツールボックスで、「Button」コントロールをクリックする。
 4)四角い枠に「ab」と書かれたカーソルが、マウスに付いてくるので、Form1上の希望する位置でクリックする。大体の位置やサイズを決めておいて、詳細は、「Form1.h」の上部にある
    this->buttonLaglange->Location = System::Drawing::Point(40, 18);
    this->buttonLaglange->Size = System::Drawing::Size(75, 23);
などの括弧内を直接編集して揃えるとよい(「コード エディターで変更しないでください。」とあるが、大丈夫)。
 5)設定したいボタンを右クリックし、「プロパティ」ウインドウで、
     デザイン ---- (Name)    「button1」などを、「buttonLanguage」など分かりやすい名前に変更
     表示 ------- BackColor  右にある「▼」をクリックし、「カスタム」、「Web」、「システム」タブを切り替えて選択
                       「LightGreen」などと直接入力も可
              Text      「button1」などを、「Laglange」などに変更
 6)各ボタンをダブルクリックすると、イベントハンドラメソッドのスケルトンが現れるので、そこに内容を記述する。

 「buttonClear」などの名称を間違えて設定してしまった場合には、
 1)ボタンを右クリックして、「削除」をクリックする。
 2)「Form1.h」で、関連するイベントハンドラ関数を削除する。
の手続きが必要である。

「Visual C++ の勉強部屋」(目次)へ