习题引入

今天上高代的时候遇见这样一道题

老师最后给的解答是这样的

进行因式分解,得到

然后检验他们是不是右侧等式的根

当然这个解法没有问题,但是相信很多同学都有这样的疑问

  • 该怎么因式分解?
  • 一定要带进右边等式算吗?

本篇就是 epi 对这些问题的一点记录

单位根

首先,我们都知道 ,注意到等式左边就出现了类似的结构。、

你一定知道 次多项式至多有 个根

有了这点共识,我们引入单位根

n次单位根是n次幂为1的复数,它们位于复平面的单位圆上,构成正n边形的顶点,其中一个顶点是1

比如,四次单位根有四个,是

有了单位根的概念,我们回头来看这个问题

问题解答

就对应了除 1 以外的五次单位根,也就是说,它们是单位圆的五等分点

我们可以类比的得到

所以等式的右端就对应着单位圆的三十等分点去掉六等分点,加上原点

根据几何直观可得,左边的根包含于右边,得证。

实际上,n次单位根可以记为: (其中 知道欧拉公式吧,还真有关系

于是我们可以做出这样的解答:

左侧的式子的根为 ),注意到 它们都是右侧式子的根,得证

看起来没有区别,但是实际上完全不用做计算就能搞定,感觉这种技巧也是不可不品的证明手段

后记与参考资料

的因式分解和单位根,可以进一步的提出分圆多项式,做练习遇到了再做记录吧。

今天调休,可恶的调休,不过只有一节早八,开心

参考资料:

《高等代数学》第五章多项式

《数学女孩5》第四章与你共轭

抛开情节不谈,《数学女孩》科普真不错吧,给中学阶段的我下放了很多数学知识,现在学的时候统统化做回旋镖了

题目描述

火车从始发站(称为第 站)开出,在始发站上车的人数为 ,然后到达第 站,在第 站有人上、下车,但上、下车的人数相同,因此在第 站开出时(即在到达第 站之前)车上的人数保持为 人。从第 站起(包括第 站)上、下车的人数有一定规律:上车的人数都是前两站上车人数之和,而下车人数等于上一站上车人数,一直到终点站的前一站(第 站),都满足此规律。现给出的条件是:共有 个车站,始发站上车的人数为 ,最后一站下车的人数是 (全部下车)。试问 站开出时车上的人数是多少

这说的也太乱了,读着不是很友好,我们先整理一下,假设up[],dn[],tar[]三个数组,有:

1
2
3
4
5
6
7
up[1] = tar[1] = tar[2] = a
up[2] = dn[2]
dn[n] = tar[n-1] = m
tar[k] = tar[k-1] + up[k] - dn[k]
//if k>=3 && k<=n-1:
up[k] = up[k-1] + up[k-2]
dn[k] = up[k-1]

由于数据量不大,所以完全能爆了,但由于Epi上午刚给一个小盆友复习数列,于是就有了如下过程。

通过观察式子,我们能注意到几件事情:

  1. dn[]完全就是up[]的一部分,没有独立出来的必要

    tar[k] = tar[k-1] + up[k] - dn[k]中的dn[k]加以替换,即可得到:

    tar[k] = tar[k-1] + up[k] - up[k-1]

    进而可知tar[k] - up[k]是一个常数列(其中 2 < k < n - 1)注意是从 k = 2 开始

  2. up[]的递推形式很像斐波那契数列

  3. 结合1,2的信息,可知up[2]是关键数,一旦得知即可推出数列全部信息

不妨先假设up[2] = y,则可以写出up[]数组对应的数列

欸?有斐波那契的影子!观察 的生成规律就会发现这不是一个巧合

于是可以得知:

由于tar[n-1] = mtar[n-1] - up[n-1] = tar[2] - up[2] 可得

解出,很快就可以求出数列的所有信息了(好耶!

过关代码如下:Epi没有做特殊判断好孩子千万不要学他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
int a,n,m,x;
//int people[20],up[20];
int magicnum,ans;
int fib[25]={0,1};
int main()
{
cin>>a>>n>>m>>x;
for(int i=2;i<25;i++)
fib[i]=fib[i-1]+fib[i-2];
//up[k]=up[k-1]+up[k-2] fibonacci up[1]=a,up[2]= y
//people[k] = a-y+up[k] is a const num; k>=2
//people[n-1] = a-y+up[k-1] = m;
magicnum = (m-(fib[n-3]+1)*a)/(fib[n-2]-1);
ans = (fib[x-2]+1)*a+(fib[x-1]-1)*magicnum;
cout<< ans;
}

好的,你说的对,但我选择爆破。

此处应有:我以后天天笑.jpg

epi在经过大一的洗礼后发现自己的技术严重不足,于是他决定去Github,油管,b站之类的地方找找项目来抄

目标:实现伪3D效果

好了,什么是 伪3D效果呢?大概如下

根据已知的右侧的二维地图(可以用二维数组存储),在控制台中显示伪3D图像,人物可wasd移动,但是没有跳跃和下蹲(即没有z轴的概念)

呃,看起来很复杂,姑且先做做看!

如果想要了解更多与伪3D有关的故事,请移步差评君的这期视频

代码参考:https://github.com/OneLoneCoder/CommandLineFPS

核心:光线投射算法

视角的确定

伪3D的实现离不开透视,而透视离不开视角

image-20240622151030490

我们可以看到视角的范围内的每一条线都可以对应到二维图上的一个边界点(或者无边界)

为视野范围角, 为屏幕宽度 , 为当前屏幕像素的 x 坐标

1
double RayAngle = (PlayerA - FOV/2) + (double(x)/ScreenWidth) * FOV;

image-20240622152433355

高度的确定

每一条视线都对应一列像素,它们在二维平面上的顶部的高度遵循透视原理, 近小远大

1
2
int nCeiling = (float)(nScreenHeight / 2.0) - nScreenHeight / ((float)fDistanceToWall);
int nFloor = nScreenHeight - nCeiling;

通过距离来调整高度的公式是一种简化的透视投影模型。它让物体的高度与距离成反比,这种关系在真实世界的透视投影中是合理的,如下图所示

image-20240622155912649

距离的测定

我们使用如下策略测得距离

1
2
3
4
5
6
7
8
9
while (未撞墙&&未越界)
{
移动一小步
if(越界)
else if (撞墙)
{
记录当前距离
}
}

简单来说,如果没有检测到边界,就向该方向移动一小步,知道检测到边界或者超过最大检测范围

碰撞的判断

因为墙面的坐标都是整数,而我们的测试坐标都是小数,所以距离的测定我们采用一种较为特殊的办法

学过高等数学的你一定知道,点积的大小可以刻画向量的共线性

(当 , 均为单位向量时)

于是我们可以计算 以进一步判断射线是否接近墙的边缘,这有助于在渲染时处理边界效应

代码:片段小记

安全声明:下列代码因方便个人理解有些许改动(如省略了部分强制类型转换),若需要能实现的代码,请移步本文开头的代码参考。

控制台屏幕缓冲区

1
2
3
4
5
6
7
wchar_t *screen = new wchar_t[W * H];
HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
SetConsoleActiveScreenBuffer(hConsole);
DWORD dwBytesWritten = 0;

screen[W * H - 1] = '\0';
WriteConsoleOutputCharacterW(hConsole, screen, W * H, {0, 0}, &dwBytesWritten);

比较流畅的移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
auto tp1 = chrono::system_clock::now();
auto tp2 = chrono::system_clock::now();

while (1)
{
tp2 = chrono::system_clock::now();
chrono::duration<float> elapsedTime = tp2 - tp1;
tp1 = tp2;
float Time = elapsedTime.count();

if (GetAsyncKeyState((unsigned short)'A') & 0x8000)
A -= (Speed * 0.75) * Time;

if (GetAsyncKeyState((unsigned short)'D') & 0x8000)
A += (Speed * 0.75) * Time;

if (GetAsyncKeyState((unsigned short)'W') & 0x8000)
{
X += sin(A) * Speed * Time;
Y += cos(A) * Speed * Time;
if (map[X * W + Y] == '#')
{
X -= sin(A) * Speed * Time;
Y -= cos(A) * Speed * Time;
}
}

if (GetAsyncKeyState((unsigned short)'S') & 0x8000)
{
X -= sin(A) * Speed * Time;
Y -= cos(A) * Speed * Time;
if (map[X * W + Y] == '#')
{
X += sin(A) * Speed * Time;
Y += cos(A) * Speed * Time;
}
}
}

光线投射算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
for (int x = 0; x < nScreenWidth; x++)
{
float RA = (A - FOV / 2) + (x / W) * FOV;
float Step = 0.05;
float Distance = 0;
bool HitWall = false;
bool Boundary = false;
float EyeX = sin(RA);
float EyeY = cos(RA);

while (!HitWall && Distance < Depth)
{
Distance += Step;
int TestX = (int)(X + EyeX * Distance);
int TestY = (int)(Y + EyeY * Distance);
if (TestX < 0 || TestX >= W || TestY < 0 || TestY >= H)
{
HitWall = true;
Distance = fDepth;
}
else
{
if (map[TestX * W + TestY] == '#')
{
HitWall = true;
vector<pair<float, float>> p;
for (int tx = 0; tx < 2; tx++)
for (int ty = 0; ty < 2; ty++)
{
float vy = (float)TestY + ty - Y;
float vx = (float)TestX + tx - X;
float d = sqrt(vx * vx + vy * vy);
float dot = (EyeX * vx / d) + (EyeY * vy / d);
// <EyeX,EyeY>·<vx/d,vy/d>
p.push_back(make_pair(d, dot));
}
sort(p.begin(), p.end(), [](const pair<float, float> &left, const pair<float, float> &right) { return left.first < right.first; });
float fBound = 0.01;
if (acos(p.at(0).second) < fBound)
Boundary = true;
if (acos(p.at(1).second) < fBound)
Boundary = true;
if (acos(p.at(2).second) < fBound)
Boundary = true;
}
}
}
}
int Ceiling = (float)(H / 2.0) - H / W);
int Floor = H - Ceiling;
}

渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
short nShade = ' ';
if (Distance <= Depth / 4.0)
nShade = 0x2588;
else if (Distance < Depth / 3.0)
nShade = 0x2593;
else if (Distance < Depth / 2.0)
nShade = 0x2592;
else if (Distance < Depth)
nShade = 0x2591;
else
nShade = ' ';

if (Boundary)
nShade = ' ';

for (int y = 0; y < H; y++)
{
if (y <= Ceiling)
screen[y * W + x] = ' ';
else if (y > Ceiling && y <= Floor)
screen[y * W + x] = nShade;
else
{
float b = 1.0f - (((float)y - H / 2.0f) / ((float)H / 2.0f));
if (b < 0.25)
nShade = '#';
else if (b < 0.5)
nShade = 'x';
else if (b < 0.75)
nShade = '.';
else if (b < 0.9)
nShade = '-';
else
nShade = ' ';
screen[y * W + x] = nShade;
}

显示坐标和小地图

1
2
3
4
5
6
7
swprintf_s(screen, 40, L"X=%3.2f, Y=%3.2f, A=%3.2f FPS=%3.2f ", X, Y, A);
for (int nx = 0; nx < W; nx++)
for (int ny = 0; ny < H; ny++)
{
screen[(ny + 1) * W + nx] = map[ny * W + nx];
}
screen[((int)X + 1) * W + (int)Y] = 'P';

Bug & Fun-stuff

  1. 把地图的长和宽设置成相同值会省去很多麻烦
  2. 你跑本文开头提到的代码报错了,请记得把WriteConsoleOutputCharacter改为WriteConsoleOutputCharacterW
  3. 参考视频(油管:xW8skO7MFYw)而幽默的是该视频把光线投射算法叫做Potter Algorithm,导致我一头雾水
  4. 1992年5月5日发行的 《德军总部3D》 运用的也是该算法
  5. 致敬传奇人物 约翰·卡马克

什么是质数?

质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。

质数(Prime number),又称素数,指在大于1的自然数中,除了1和该数自身外,无法被其他自然数整除的数(也可定义为只有1与该数本身两个正因数的数)。

可见 整除 对于质数的判断有很重要的作用


进而我们有质数判断的最基本的写法如下↓

质数判断 1.0

1
2
3
4
5
6
7
8
9
bool Isprime(int n)
{
if (n == 1) return false;
for(int i = 2; i < n; i++)
{
if(n % i == 0) return false;
}
return true;
}

即对于 不是 的因数


但是我们还能继续优化这个算法

Read more »
0%