绘制整系数多项式复数根的分布

我在百度贴吧Mathematica吧的这个帖子看到有人在研究如下问题:

求解所有系数为1或-1的n次多项式的根,然后把所有这样的根绘制到复平面上。

我想起以前在Matrix67的博客上也看到过这个问题,里面介绍了Sam Derbyshire用Mathematica跑了四天四夜(也有说法是三天三夜)生成了n=24的所有根,然后用Java程序绘图,得到了下图:

roots1

贴吧中的“三分钟复现”是算到n=17的情况。由于n增加1导致需要求解方程的数量翻倍,每个方程中根的个数也增加1,所以我估计了一下,n=24时大约需要求出24*2^24=402M个解。(24次多项式包括常数项有25个系数,但是由于对称性可以省去一半计算。)这些计算显然不需要以天为数量级的时间。

我决定用C语言解决这个问题,同时改善一下显示的效果,并尝试绘制分辨率更高的图。

解决这个问题有两个比较麻烦的事:

  • 如何求解多项式的根
  • 图上的颜色按照什么绘制

第一个问题我查找了一些方案,最后觉得boj的建议比较好:使用GNU Scientific Library库。这个库里面有很多科学计算的函数,其中的gsl_poly_complex_solve就可以求解多项式的根。

于是我写出求解多项式的程序,这个程序把所有的根都以二进制形式直接保存在文件中。fork出8个进程可以把CPU跑满。

注:请在cygwin或linux下编译,编译出错请先检查是否正确安装GSL库,然后尝试在编译命令中加 -lgsl -lgslcblas -lm 参数

/*程序功能:求解所有指定次数的、系数为-1、1的多项式,并把复数解保存到r[0-7].data中
输出格式:二进制文件,顺序排列每个解(double)的实部和虚部*/

#include <stdio.h>
#include <gsl/gsl_poly.h> //GNU science library 多项式
#include <sys/wait.h>
#include <unistd.h>

#define deg 24 //多项式次数

int main(){
  int i,j,p,total;
  double a[deg+1],z[deg*2]; //a:多项式系数 z:求解结果
  FILE *fp;
  char fn[10];
  a[deg]=1.0; //减少一半运算
  
  for(p=0;p<8;p++){
    if(fork()==0){
      sprintf(fn,"r%d.data",p);
      fp=fopen(fn,"wb");
      for(i=0;i<3;i++) //该进程的三个确定系数
        a[deg-1-i]=(p&(1<<i))?1.0:-1.0;
      total=(1<<(deg-3));
      for(i=0;i<total;i++){
        for(j=0;j<deg-3;j++)
          a[j]=(i&(1<<j))?1.0:-1.0;
        gsl_poly_complex_workspace *w=gsl_poly_complex_workspace_alloc(deg+1);
        gsl_poly_complex_solve(a,deg+1,w,z); //求解
        gsl_poly_complex_workspace_free(w);
        fwrite(z,sizeof(double),deg*2,fp);

        if(i%10000==0)printf("Process%d : %.2f%%\n",p,i*100.0/total);
      }
      printf("Process%d : Completed\n",p);
      exit(0);
    }
  }
  while(wait(NULL)>0); //等待其他进程
  return 0;
}

程序几分钟就跑完了,生成了6G的数据。

然后想办法解决第二个问题。

首先要弄明白图中的颜色表示什么。一个像素的颜色表示那个像素范围内根的密度。所以简单的办法就是按照期望的分辨率(例如2000*2000)建立一个二维数组,然后根据每个根的位置统计每个像素范围内根的个数。

那么又如何根据根的个数算出颜色?颜色值是有范围限制的,例如0~255,所以我想到计算根个数的最大值。但是经过测试发现,画出的图很暗,几乎看不清,只有某些点处是亮的,这说明根聚集在某些点处,这些点附近根的个数和其他平凡点附近根的个数之比可能是发散的,不能通过统计最大值来决定颜色。

我直接手工设定了一个亮度的微调值,调节到比较满意的位置。根的个数从少到多,颜色从黑色渐变到橙色(RGB=255,127,0),超过设定值的少量部分再渐变到白色。测试发现暗部不清楚,于是用平方根函数把亮度往上调了一下(和开根号再乘以10的调分方法一个道理)。

根的密度和颜色的关系如下图所示:

roots2

程序如下:

/*程序功能:根据r[0-7].data中的复数数据,生成密度图*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define DIM 1024 //图片分辨率 DIM*DIM
#define RANGE 2.0 //绘制范围 x,y∈[-RANGE,RANGE]
#define B 12.0 //亮度微调参数,越大越暗
#define BUFFSIZE ((1<<24)*24/8*2) //读文件缓冲区大小,24次多项式约800M

double *data; //读文件缓冲区
int (*den)[DIM]; //像素点范围内根的个数
unsigned char (*bitmap)[DIM][3]; //用于生成位图

int total=0,max;

void countroot(int cnt){ //按照像素区域对方程的根计数
  double x,y;
  int i,px,py;
  for(i=0;i<cnt;i++){
    x=data[i*2];
    y=data[i*2+1];
    px=DIM/2*x/RANGE+DIM/2; //计算对应的像素坐标
    py=DIM/2*y/RANGE+DIM/2;
    if(px>0&&px<DIM&&py>0&&py<DIM)
      den[px][py]++;
  }
  total+=cnt; //总点数计数
}

void colorfunc(int d,unsigned char *color){ //根密度与颜色的映射函数
  int t;
  t=(long long)255*sqrt((double)d/max);
  if(t<256){ //黑至橙渐变
    color[0]=t;
    color[1]=t/2;
    color[2]=0;
  }else if(t<512){ //橙至白
    color[0]=255;
    color[1]=128+(t-256)/2;
    color[2]=t-256;
  }else{ //白
    color[0]=color[1]=color[2]=255;
  }
}

void genpic(){ //根据密度数据生成位图
  int i,j;
  printf("Generating pic...\n");
  max=B*total/DIM/DIM; //颜色255对应的根密度
  for(j=0;j<DIM;j++)
    for(i=0;i<DIM;i++)
      colorfunc(den[i][j],bitmap[j][i]);
}

void savefile(){ //保存文件
  char fn[20];
  sprintf(fn,"roots_%d.ppm",DIM);
  FILE *fp=fopen(fn,"wb");
  printf("Saving to %s...\n",fn);
  fprintf(fp,"P6\n%d %d\n255\n",DIM,DIM);
  fwrite(bitmap,1,DIM*DIM*3,fp);
  fclose(fp);
  printf("Completed!\n");
}

int main(){
  int p,cnt;
  char fn[10];
  FILE *fp;
  data=malloc(BUFFSIZE*sizeof(double));
  den=malloc(DIM*DIM*sizeof(int));
  bitmap=malloc(DIM*DIM*3);
  if(!data||!den||!bitmap){
    printf("Out of memory!\n");
    exit(-1);
  }
  for(p=0;p<8;p++){
    sprintf(fn,"r%d.data",p);
    printf("Loading : %s\n",fn);
    fp=fopen(fn,"rb");
    fseek(fp,0,SEEK_END); //定位到文件尾
    cnt=ftell(fp)/sizeof(double)/2; //根据文件大小计算数据量
    fseek(fp,0,SEEK_SET); //定位到文件头
    fread(data,sizeof(double),2*cnt,fp);
    fclose(fp);
    printf("Loaded, count=%d\n",cnt);
    printf("Processing : %s\n",fn);
    countroot(cnt);
  }
  genpic();
  savefile();
  return 0;
}

运行结果:

roots3

注:分辨率太高之后有些图片查看器可能不识别,我测试命令行版的ffmpeg可以打开,商业软件Photoshop更方便一些。

如果不考虑磁盘读写(中间文件我保存在了ramdisk),生成几万乘几万分辨率的图也不过几分钟。

后来我又跑了n=26的情况,发现除了细节放大后比较平滑之外没有显著区别,故不单独贴图。但是运行到98%时遇到了一个错误:

gsl: zsolve.c:78: ERROR: root solving qr method failed to converge
Default GSL error handler invoked.

并不是内存不足或溢出导致,应该是GSL解某个方程时出了问题。

有人问我为什么不是计算出根分布后直接统计并生成图片,而是保存中间文件。我觉得程序在运行时经常需要用同一组数据生成不同分辨率的图片,所以前者反而不方便。

最后贴几张细节图,感受一下数学之美:

roots5

roots6

roots4

《绘制整系数多项式复数根的分布》有一个想法

  1. Getting patterns in a structured problem is ordinary.. It should not be described as “the Beauty of Math”….

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注