我在百度贴吧Mathematica吧的这个帖子看到有人在研究如下问题:
求解所有系数为1或-1的n次多项式的根,然后把所有这样的根绘制到复平面上。
我想起以前在Matrix67的博客上也看到过这个问题,里面介绍了Sam Derbyshire用Mathematica跑了四天四夜(也有说法是三天三夜)生成了n=24的所有根,然后用Java程序绘图,得到了下图:
贴吧中的“三分钟复现”是算到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的调分方法一个道理)。
根的密度和颜色的关系如下图所示:
程序如下:
/*程序功能:根据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;
}
运行结果:
注:分辨率太高之后有些图片查看器可能不识别,我测试命令行版的ffmpeg可以打开,商业软件Photoshop更方便一些。
如果不考虑磁盘读写(中间文件我保存在了ramdisk),生成几万乘几万分辨率的图也不过几分钟。
后来我又跑了n=26的情况,发现除了细节放大后比较平滑之外没有显著区别,故不单独贴图。但是运行到98%时遇到了一个错误:
gsl: zsolve.c:78: ERROR: root solving qr method failed to converge
Default GSL error handler invoked.
并不是内存不足或溢出导致,应该是GSL解某个方程时出了问题。
有人问我为什么不是计算出根分布后直接统计并生成图片,而是保存中间文件。我觉得程序在运行时经常需要用同一组数据生成不同分辨率的图片,所以前者反而不方便。
最后贴几张细节图,感受一下数学之美:






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