普通视图

发现新文章,点击刷新页面。
昨天以前VictriD's blog

斯普拉遁3打工模式联机网络过程分析

作者 Victrid
2022年11月9日 19:45

连接已经中断的截屏

引入

今天打工,又被掉线的队友气到了。正好之前根据Nintendo Clients写了一个Wireshark的P2P协议解析插件,又抓过一些打工的网络数据,大概将打工模式的网络交互做了记录,可以做一些简单的分析。

整个打工的交互过程都是通过任天堂的SSL证书,和任天堂服务器分发的密钥进行加密的。想要拿到、或者修改具体在联机中传输了什么样的内容,必须破解Switch以绕过证书的验证,而这就违反了使用协议,会被封禁。我们在本文中使用的工具和方法,仅仅从公开内容出发,不涉及对Switch本身的破解。

这篇文章也是基于本人就网络流量观察的猜测,与任天堂没有任何关联,也不对其中的正确性做任何保证。读完这篇文章,你只能大概知道为什么你的队友/你会掉线,既不能解决队友/你掉线的问题,也不能让你上传说。

我们就游戏的几个阶段进行描述。

匹配

任天堂在本代采用了和怪物猎人相同的新一代NPLN服务器架构。据说是引入了微服务的概念,在各种效果上都有所提升。但很遗憾,除了一些少见的情形外,这对打工几乎没有任何帮助。

打洞与NAT类型检测

目前的网络基础架构使得设备往往都隐藏在路由器的NAT背后。在进行P2P联机之前,需要进行NAT打洞。

一个关于NAT打洞的比喻是,你告诉路由器,我现在去一个住东川路800号(IP地址)202室(端口号)的客人家里串门,他一会儿也来,帮我留个门。路由器里面的NAT表就是看门人,有的NAT啥也不管,有的NAT只放住东川路800号的人进来,有的NAT不仅要检查是不是住东川路800号,还要看房间号是不是202室才让进来。

A,B,C类型的网络,就对应了这三种看门人。两个人要联机,实际上是一个双向的交互过程。比如说,A要和C互相串门,作为中间人的任天堂只需要告诉C A住在哪一号哪一室,C去A那里串门之后,C家里的看门人就会给A留门。读者可以自己想一想A,B,C网络之间哪些可以互联,哪些不能互联。如果感兴趣的读者,可以进一步阅读NAT 类型

网络活动

  • 游戏向nncs[1,2]-lp1.n.n.srv.nintendo.net所指向的几个服务器发三次三个内容分别为e,fg的UDP包。服务器会返回9个包,但收得到几个取决于你的网络。包中记录了:

    • 这是在测试哪种类型的连接
    • 你的IP地址和端口号是什么

    例如我是B类网络,只收回了6个包。

    网络测试

  • 向游戏服务器发送你收到的IP地址和端口号,通知你准备参加打工了。

  • 在匹配过程中,服务器会推送你的队友的IP地址和端口号信息。此时取决于你的网络,会使用Pia v11协议,发送目标变量ID、源变量ID、包ID均为0的一个握手包。这一过程也进行NAT打洞。

  • 当收到这样的握手包之后,会回复一个只写入源变量ID,其他均为0的一个握手确认包。源变量ID和目标变量ID为随机变量,可能是匹配系统分配的编号。这个编号在一次匹配(就是排到的一组队友,包含“以相同队伍继续”的游戏)中是不变的,但在不同的匹配中不同。

  • 当收到握手确认包之后,意味着握手方到被握手方建立了一个单向的连接。此后就会开始交换加密的游戏信息,表现为目标变量ID和源变量ID对应握手方和被握手方的变量ID,包ID从一个随机数递增。

  • 如果没有收到握手确认,游戏会通知服务器无法连接到对方,此时可能匹配系统会重新匹配其他玩家,并告知对方无法匹配,不再发出对应的包。循环这一步骤,直到都能够互相连接。

  • 这是一个传输过程的例子:

    握手的例子

    注意图中标为橙黄色,我与波兰IP的玩家联机的三个包。这就是一个握手,握手确认,开始传输加密信息的流程。黑底的三个ICMP包,则表明我的上级路由告诉我,无法连接到俄罗斯玩家,因此通知服务器,也不再向俄罗斯玩家发包了。

  • 有一种情况,双方有一方无法连接到另一方,但运气好(我猜是你俩排位的契合程度,减去可能的延迟,再减去不能连接的扣分项,还是比重新找一个队友的代价高),任天堂会提供TURN协议的中转服务器。TURN协议原封不动地包装了待发送的Pia协议包,由任天堂的服务器提供中转,转发给对方。

    TURN协议对于接收的一端是透明的。具体通讯模式可以参考下图。游戏仅会对一对用户某一个方向建立信道,数据在信道中是单向的。

    注意下图中TURN包的的数据部分,以幻数32 ab 98 64开始,表明这是一个Pia协议包,也就是任天堂P2P协议包。

  • 当所有队友均准备好之后,便会显示“打工的时间到了”,这时会与游戏服务器进行通信,开始本局打工。

常见问题

这一步常出现的问题,大多是报错。有两种报错形式:游戏界面内的黑色错误框,和游戏模糊化作为背景,Switch系统的错误框。以下记为游戏框和系统框。

  • 游戏框说发生连接错误。 这一步通常不是我方出现的P2P的网络错误。这种情况我猜测是任天堂服务器推送给你的,比如一个队友和另一个队友之间连不上了,但他们都能连上你,你得重新匹配。
  • 系统框说无法连接到服务器/连接错误。 这种情况通常是与你和任天堂的服务器间的连接相关,比如你的连接不稳定,或者任天堂的服务器欠修了。
  • 游戏框说“连接已中断”,一共四行(即封面图)。 这种情况有可能是上面两种的集合。与上述游戏框的区别是系统框的无法连接错误可能通常发生在收到TCP(即你与服务器的连接)错误包、重复包上,而游戏框通常发生在连接断开上。
  • 系统框说无法与对方的游戏机进行连接。 这种情况通常是偶发的,发生于你与队友的UDP连接故障。比如网络不稳定导致的UDP传输发生了多次错误。如果频繁出现,可能需要注意路由器或者加速器的质量,或许存在错误判断NAT类型的情况。
  • 游戏框说未在规定时间内凑齐人数。 这种情况如果频繁出现,需要先检查你的NAT类型。上文介绍了NAT类型,类型越差排到的人就越少。如果NAT类型很好,但仍然很难匹配到人的话,可能需要注意加速器质量,或者你的网络服务提供商是否存在连接阻断的情形。

如果有任意一个队友与任天堂服务器间的网络连接较差,就会出现卡在“打工的时间到了”的界面。如果该队友此时与任天堂服务器断开连接,可能出现“发生连接错误”的游戏框,也有可能在直升机上蒸发。

游戏过程

网络活动

与服务器通信

在整个打工的过程中,只有少数的动作会与游戏服务器通信。因为我无法得到具体的通信内容,这一步可能包括:

  • 投蛋进框
  • 击杀、生成巨大鲑鱼
  • 自己阵亡、复活队友

玩家间通信

其他的所有内容都是仅通过P2P通信完成的。每对玩家间的通信通过一来一回2个Pia协议的单向“通道”(表现为递增的包编号)进行,丢包不会重传。每秒钟向每个队友发送约15个包(和游戏的Tick Rate差不多),每个包的大小在约100B到约700B不等,结算时包的大小可能达到1KB。

P2P通信中分为普通事件和关键事件。移动、涂地等行为是普通事件,只要收到玩家发来的信息(比如移动),就会在游戏中发生。而成为游泳圈、复活、捡蛋是关键信息,只要整个游戏有一个人在关键事件上没有同步,就都认为关键事件未发生。

我根据游戏的掉线行为和向不同人发送的包的大小差别猜测,整局游戏中会按一定规则选取玩家作为同步基准(接下来称为房主),网络较好的玩家可能更容易被选为房主。

游戏过程中

图中标为橙黄色的为一个通道,可以注意到包编号是递增的。另外也可以观察到,与一个人交换的数据普遍比其他玩家大,可能是负责同步的房主。图中粉色的条目则为与游戏服务器的通信。

掉线情况的处理有多种。

  • 上述的通道总共有12条。如果某一个通道无法工作,游戏会试图挽救,向NPLN服务器请求建立一个中继(当然也可能直接少人)。需要注意的是,中继是单通道的。比如我收不到一个队友的包,我只会请求服务器中转队友到我方向上的包,同时NPLN服务器会通知这个队友,向我发送的包通过任天堂进行中转。
  • 如果非房主玩家与任天堂服务器断开了连接,该玩家会显示“发生连接错误”的游戏内错误框,游戏服务器会通知所有玩家该玩家掉线了。
  • 如果房主无法连接到某玩家,可能出现即使与任天堂的连接没有断开,也会导致该玩家被游戏服务器踢掉。
  • 如果房主掉线,个人认为可能任天堂没有写重新选房主的代码,因此会结束所有人的游戏,对所有人弹出“发生连接错误”的游戏内错误框。

常见问题

  • 蛋丢不进框:任天堂的服务器欠修了。这一点最近很少见。
  • 队友傻站着,死了不复活:可能是你与该队友的连接出现了问题,但系统仍然在尝试通过TURN转发机制重连。通常运气不好的话,不超过10秒这个队友(或者你)就会掉线。
  • 蛋捡不起来、蛋被队友抢走、队友见死不救:除了第三个可能跟队友的水平有关,产生的原因最有可能是你与房主玩家的延迟比较高。如果某局游戏一开始捡蛋顺畅,但突然开始捡蛋变卡,可能是房主的连接(或者你的连接)出现了重大变动,比如中间链路发生换路,你得准备好掉线了。
  • 结算的时候发生连接错误:结算是由房主上传到任天堂服务器,再由任天堂服务器下载到你的设备上的。当房主上传完毕后,结算就已经记录了。因此如果查看历史,没有正确结束,说明房主掉线;如果有正常的结算,说明你与任天堂的服务器连接不稳定。

DHCP无类型静态路由──一个摆设

作者 Victrid
2022年1月31日 20:01

引子

过年回到老家,连着2.4GHz Wi-Fi,在电视里过年的歌曲和相声,与长辈们的麻将声中,和争分夺秒在14天里打完一个寒假的游戏的小孩们抢总共不到100M的网速;不止如此,爱看的网站个个都是CONNECTION_RESETDNS_PROBE_FINISHED_NXDOMAINERR_NAME_NOT_RESOLVED,着实感觉生活不易。我翻出我好久之前买的树莓派,打算搭一个单臂路由,来解决众多设备的上网难题,作为软路由的替代。

但是单臂路由就有一个问题。只要把网关指向单臂路由而不是主路由,数据就要在单臂路由的臂上往返。之前买的4B版网络还算行,但找不到了,3B版的有线网速度就比较让人着急了。相当于总的带宽被树莓派的收发速率所限制,而且单臂的做法,也使得原本的全双工互不干扰的传输变成实质上的半双工传输,上传和下载的带宽共同占据同一个带宽。

但是办法总比问题多,配一个路由表,有些网站直接从主路由出去,有些网站走树莓派,分流了效果不就好了吗?主路由想想也是NetGe*r, Mercu*y, T*-link这种主流厂家的产品,现在都是买路由器当摆件的年代了,支持路由表想必不太现实。而用户端的路由表,看着我手上这些设备,能手动配置路由表的都寥寥无几。

不让用户配,总得让网络有一个配置方法吧。毕竟路由表是三层网络的核心部件,总不能靠设备自己猜网关在哪儿啊。DHCP提供了Router选项,用来配置该网络的网关。而同样由DHCP提供的121 Classless-Static-Route,RFC3442 [1]写明了是用于配置无类型网络(CIDR)静态路由的。打开Wireshark抓个包,看到本机发出的DHCP request包里面带了这个选项,内心一片窃喜。如果能够把路由表通过DHCP配置到每个设备中,就可以显著降低树莓派的流量了。

选项121:无类型静态路由

DHCP协议想必读者们都不陌生,但通常也就止步于通过DHCP用于获取本机的IP地址、子网掩码和网关信息,用于配置IPv4协议栈,也就是第一个广播Discover包和返回的Offer包。实际上,客户端会接收到相关配置信息的Offer时,会再次广播一个DHCP Request包用于向整个网络确认。这一IP如果被确认,就会返回一个ACK包。

实际上,除了获取常见的网络参数,DHCP上还承载了更多的功能。DHCP通过选项,对这些功能进行分类。除了IP地址、子网掩码等之外,还有一些不常见的功能,例如指定DNS服务器、LDAP服务器、POP3和日志服务器,还提供了TFTP选项和用于PXE网络启动的各种参数等。

参数的数量非常多,DHCP采用请求机制。即客户端将需要获取的选项字段记录在request包中,DHCP服务器对应这一请求返回有效的字段的内容。

Linux上常用的DHCP服务端在发出DHCP request时,会请求编号为121和249的选项,这些选项的目的是配置无类型静态路由。编号121是RFC3442指定的,而249则被早期微软的DHCP服务器所使用。因为我没有Windows的设备,我们侧重在121选项上,249选项的配置方法应该是类似的。

RFC3442指定,如果121选项有效,则以其中的内容覆盖网关配置和已被废弃的选项33:有状态静态路由配置。选项是以String,或者说uint8数组存储的。每一个条目按照一下方式排布:

1
子网前缀长度,网络地址的显著位,网关地址

即,例如12.34.56.0/23的网关在192.168.1.1192.0.0.0/7的网关在192.168.1.2,而0.0.0.0/0(默认网关)在192.168.1.1,则需要写为:

1
2
3
23, 12,34,56, 192,168,1,1,
7, 192, 192,168,1,2,
0, 192,168,1,1

逗号、空格和换行是用于分隔8位无符号整数的,不会加入数组。上面的部分总共有19个数,在包中会以

1
2
3
-----------------------------------------
| 121 | 19 | 23 | 12 | 34 | 56 | 192 | ....
-----------------------------------------

的形式存储。前两位的121是选项编号,19是数组长度。

问题:溢出

如您所见,数组长度也是用一个8位无符号整数来存储的,这意味着最多只能够表示255个字符。我们看到,一个前缀长度为24的条目就需要占用8个字符,255个字符能够表述的条目数量可能大概在30-40条左右。相比于分配给中国的网络数量(APNIC大概在8000条左右,做了进一步的聚合之后大概在5000条左右),简直是杯水车薪。

同样考虑到条目大小的问题,在RFC3396[2]中,DHCP协议引入了编码长可变长度数组选项的连接(Concatenation-requiring)。简单来说,DHCP的条目线性连接,以255选项结束。DHCP每一个条目的长度不能超过255个字节,当出现多个同一选项的条目时,支持连接选项的客户端需要将这些条目的内容按照出现的先后顺序连接。例如客户端收到如下的选项:

1
2
3
Option MagicNum [length 2] 192,168;
...
Option MagicNum [length 3] 1,0,24;

将会按照顺序连接起来,并记录为MagicNum [length 5] 192,168,1,0,24

为了兼容性,该RFC还建议DHCP服务器确定客户端是否支持分段。例如,一旦DHCP客户端请求了需要分段的选项(如121选项),那么服务器就应该假定该客户端支持分段,并严格按照分段来发送,否则就不发送需要分段的选项。

我们观察到,Linux上的常见DHCP客户端与网络配置工具,均支持这一分段选项的接收(否则就不能请求需要分段的选项了)。但目前常见的Linux DHCP服务器,均不支持分段发送。尤其令人大跌眼镜的是ISC的dhcp,其配套的客户端支持这一分段选项,而服务器并非如此。例如一个长300字节的路由表,服务器会将长度标记为255(最大值),但将整个长为300字节的路由表接在后面,发生了选项越界溢出。多出来的这45个字节会被DHCP客户端解释为其他的选项。如果字节无意义还好,在解包的时候会认为发生错误并丢弃;如果恰好能够解释为特定的内容,就会导致非常奇怪的结果。

DHCP包的大小问题

就算通过一番奇技淫巧,把分段整出来了,还会面临另一个难关:DHCP包太小,路由表塞不下。

可是DHCP包设计之初就考虑到了大小问题,其长度字段是16位整数,即65536个字节。装这么个路由表绰绰有余。这其中的缘由,就又牵扯到实现问题上了。

DHCP包虽然是以UDP协议包裹的,但和UDP、TCP等常见协议不一样。因为其工作在非常特殊的阶段,此时客户端可能还没有建立一个完整的IPv4协议栈,最早发出的DHCP Discover/Offer包为了稳定不会分段,以防止一些客户端不能正确处理。在这里MTU就限制了DHCP包的最大大小。而一些程序为了省事,将这个大小进一步缩小为DHCP协议的最小长度576字节。

如果你仔细观察最初的DHCP Discover包的内容,就会发现,其请求的选项都是基础而必要的选项。而更长、可能超过MTU限制的选项则会放在第二次的Request中。此时的Request,按照规范,可以附有客户端能够接受的最大包长度,以供服务器按需排布恰当的内容。

但是有哪些客户端这样做了呢?几乎没有。为了对付这些无信息的客户端,DHCP服务器会设置一个可以接受的大小,以此为发送的上限。有些DHCP服务器会设置为与MTU相关的大小,而有些DHCP服务器则会按照576字节(最小值)来配置。ISC的DHCP服务器实现甚至限制最大值为1500,就算MTU设置得再大,客户端可以接受的包再大,也不会发出超过1500字节的包(甚至IP包都配置为Don’t Fragment)。需要注意的是,IP协议是有分段(fragmentation)的,除了Offer包的兼容性问题之外,其他包的大小并不会受到单个发包MTU的限制。

无用之物

DHCP能出这么多问题,也是因为DHCP客户端的实现过于奔放,跨越二十年,支持的字段各有不同,为了兼容性只能如此。

Android和iOS的客户端都不支持这一选项,这些设备的DHCP实现压根就不会请求121选项,即使收到这些选项也会忽略。这就使得这个功能比较鸡肋了。

虽然DHCP协议有足足255个选项,大部分的选项都分配了固定的语义,但因为DHCP协议客户端和服务端的设计者各自为政,真正使用的选项,也就只有几个而已。同时也因为网络本身的发展,诸如SDN等概念的提出和“越俎代庖”的各种网络设备,DHCP协议在未来网络中扮演的角色也会越来越边缘化。


  1. Ted L., Bernie V., et al. "The Classless Static Route Option for Dynamic Host Configuration Protocol (DHCP) version 4", RFC 3442, December 2002. ↩︎

  2. Ted L., Stuart C. "Encoding Long Options in the Dynamic Host Configuration Protocol (DHCPv4)", RFC 3396, November 2002. ↩︎

LaTeX插入其他PDF中的矢量图

作者 Victrid
2021年10月21日 08:46

引子:矢量图

$\LaTeX$的使用者,常常会遇到插入图表的情形。TikZ是非常常用的图表工具,但是用TikZ绘图的难度可能比较高,有些时候,诸如Matlab,pyplot之类的工具可能用起来会更加方便。

一个解决方法是导出成png。png算是最通用的,也是支持特性最多的图像格式。相比于jpg等格式,其支持透明背景的特性能够更好地在高级排版中运用。

但是png格式有一个问题。其储存的图像是以标量图的形式展现的。这就导致其经不起放大。远看PNG图像没有什么问题,但放大之后就会出现锯齿。

标量图与矢量图

至于什么是标量图,什么是矢量图,这要从图像的存储结构说起。具体的实现会比较复杂,我们就从他们是怎样显示的,简要对比其中的区别。

你的显示屏,其实是由很多个像素点构成的长方形点阵。如果你凑近看,你能看见色彩斑斓的像素点。每个像素点上显示什么,是由你的显示卡来处理生成的。很容易就能够想到,这种点阵上,不同斜率上的点的距离是不同的。比如45度的斜线,他的点之间的距离就是横向和纵向排列的1.4倍。而如果是更普遍的角度,可能整个显示屏上都不存在能排列在同一条直线上的点。这样的绘图方式对于人眼是难以接受的。

因此,我们采用了一些近似的算法,来让线条看上去是那样的。如果你玩过旧主机,或是像素风的游戏,就能够体会到这一点。当代的算法会高级许多,通过插值模拟的方法,这些算法能让你的显示看起来更平滑。

标量图的原理和显示屏相同。他们储存的是点阵。计算机在显示标量图的时候,因为不清楚图像的实际内容是怎么产生的,因此只能以上图左边的形式,把像素放大到你需要的大小。

而矢量图则不同。矢量图存储的是一系列描述如何显示你需要的图像的指令。计算机直接向图形卡发送绘图的指令,因此图形卡能够直接按照你的显示器尺寸绘制响应的图形。比如你计算机上安装的字体,就是一种矢量图。上面的图片右边的abc看起来很好看,但如果你把这张图放大,最后也能看见像左图一样的插值色块。但文字‘abc’,你把网页缩进到再大,换再高清的屏幕,显示效果都是同样地好。因为你无论如何缩进,文字都是由图形卡来绘制的。而图形卡的高级特性,能够使你看见其设计上最清晰的内容。

我把标量图分辨率调高,不就行了吗?

这又是另一个问题。调高图片的分辨率,看上去显示效果很好,但这样的图形缩小后的显示效果不太好。同样,因为不清楚图像的实际内容是怎么产生的,对于更小的像素,也只能够通过平均的方法来展示。这样的展示方式通常情况下问题不大,但仍然会出现模糊、虚化的问题。尤其在字体上。上图的两个文字,你如果走远一点,或把眼睛眯起来看,就会发现他们的形状是有差异的。

就算你经过精心选择,找到了完美的图片分辨率(比如说直接截屏),打印的效果也与显示不同。

就算是很好的显示屏,比如15英寸4K屏,每一个英寸中能排列的像素点也不到300个。而以笔者20年前的激光打印机为例,每个英寸都能打印1200个像素点。这种素质称为DPI/PPI,是由设备的特性决定的。打印机同样是一种显示设备,不过他将内容显示到纸张上。因为显示原理上的不同,打印机能够很高效地提高显示的素质,而显示屏则受制于屏幕与半导体技术。你会发现,打印机打出来的字几乎像是从模子里印出来的一样,就是这个原理。


因此,能够使用矢量图的时候,就尽量不使用标量图,是每一个使用$\LaTeX$,对排版有着高标准的人都应该具备的策略。

很多图表工具,比如Matlab,pyplot,都能够将结果以矢量图的形式输出。目前笔者常用的是eps格式,不需要额外调包,而且调节大小都支持。eps,即封装PostScript。PostScript是一种打印机图形描述文件,但支持这种文件的打印机很少,使其真正广泛应用的则是PDF,PostScript的后继者。

这就出现了一个问题。其他的PDF里面也有我需要的矢量图,如何将其截取出来呢?截屏显然是一个错误答案。

从PDF中导出图形eps

这里需要使用一个工具:Inkscape。Inkscape是绘制矢量图的开源工具,支持多种平台。

  • 安装后新建文档,选择导入
  • 打开需要截取的PDF,找到有需要的图形的那一页,进行导入。如果你的电脑字体安装不全,建议使用从Poppler/Cairo导入。
  • 选取需要的图形后,使用编辑-反选,并删去其他页面上的内容。
  • 选择文件-另存为,文件格式选择eps格式。文字输出选项可以自行调节。
  • 这样需要的图形就被导出为eps格式了。如果你选择了“创建LaTeX文件”选项,还会有一个eps_tex后缀的文件生成。这种方式可以使用LaTeX来处理图形中的字体。

导入LaTeX

前者可以直接用\includegraphics导入。例如:

1
2
3
4
5
6
7
8
9
10
% \usepackage{graphicx}
% \usepackage{transparent}
% \usepackage{color}

\includegraphics{example.eps}

\begin{figure}
\includegraphics[width=0.6\textwidth]{example.eps}
\caption{Example} \label{example}
\end{figure}

如果你输出的是eps_tex文件,那么导入的方式需要更改。

1
2
3
4
5
6
7
8
9
10
11
% \usepackage{graphicx}
% \usepackage{transparent}
% \usepackage{color}

\input{example.eps_tex}

\begin{figure}
\def\svgwidth{0.6\textwidth}
\input{example.eps_tex}
\caption{Example} \label{example}
\end{figure}

光速不变原理和迈克尔孙──莫雷实验

作者 Victrid
2021年7月28日 07:46

本文旨在对光速不变原理这一基础理论做出科普性质的阐述。

早在古希腊时期,人们就发现了静电现象,中国古代的文献也有利用天然磁石指明方向的记载。库仑用库仑扭秤和电摆实验解释了静带电体之间的相互作用;安培观察到通电导线使磁针偏转实验,说明了电流产生磁场;法拉第通过电磁感应实验说明了变化的磁场产生电场。这表明,电和磁之间是密不可分的。

光在此时还没有与电和磁相关联。人们当时所能稳定产生的光源几乎都是热致发光和化学发光。在天文实践中人们发现光的传播是有速度的,而在当时,设计精密的旋转齿轮实验装置也使人们初步测定了光速的大概值。麦克斯韦通过对电学和磁学的已知结果的理论分析,在理论的推导下提出了电磁波方程,并此后提出了麦克斯韦方程。而电磁波的存在也被赫兹用实验证实。通过理论计算所得的电磁波的速度与光的速度相近,麦克斯韦猜测光是一种电磁波。光能够如电磁波一样激发一些金属导体上的电子使其逸出,其能量与麦克斯韦的电磁理论基本符合。

至于物理定律的正确与否,我想起中学时期物理老师的一句话:“物理定律自从提出就自动成立,直到实验结果将其推翻为止。”如果既有的物理理论与新的物理定律存在矛盾,那么一定有实验能够证伪其中一个。麦克斯韦的电磁理论在今天仍未被推翻,通过此理论指导下的工程实践在你我的身边都随处可见──电话天线、无线充电、GPS网络等等。事实上,电磁学理论已经是当今最完善的一个物理学科之一。其精确程度甚至允许人们以此来定义米这一国际度量单位。

然而电磁场理论本身却与牛顿第一运动定律所规定的伽利略参考系相矛盾。这是电力与磁力的不同表现形式所造成的。我们以一个简单的例子来说明。对于一个“惯性系”,即满足牛顿运动定律的参考系,中摆放的两颗静止的带异种电荷小球,此时两者之间只存在静止带电体之间的作用力──库仑力。如果不将其固定,两者之间就会互相吸引。而以另一个,以垂直于两点连线的恒定速度移动的坐标系(这也是一个惯性系,是由牛顿第一定律规定的)来看,此时的运动电荷形成了电流,而反向流动的电流之间会有排斥力──洛仑兹力。这些值都是可以精确计算的,而当移动速度到达一定值(光速),两个力之间就会相互抵消。当移动速度超过这个值,合力就会变成排斥力。这显然是不满足牛顿运动定律的。

然而结果我们都知道,库仑电摆(和扭秤)实验就已经告诉我们了。这就指出,如果要坚持牛顿运动定律,修正电磁定律,那么一定存在一个“绝对”参考系,电磁力中关于速度的计算都基于这个绝对参考系。这便引入了以太。力的传播需要介质,电磁力也不例外,其介质便是以太。在绝对参考系上,以太是静止的,这样就可以不需要修正绝大多数的力学定律,只需要对电磁定律进行相应的变换即可。我们上文提出的例子是由洛仑兹设想的。麦克斯韦提出了以太,而洛仑兹本人是以太学说的拥护者,他给出了一套电磁力的变换公式来计算一个运动参考系相对于绝对参考系的电磁力,这样就能够使得电磁的总作用力相同。这个变换公式正是后来狭义相对论的经典推论──洛仑兹变换(尽管形式不同),尽管后来的应用与他设想的并不相同。

然而地球本身在以30公里每秒的速度绕着太阳转,赤道上则是以大约1600公里每小时的速度自转。这个速度的方向时刻在改变,相对于绝对参考系,地球上的速度又快又在变化,以太相对于我们以相当高的速度运动,即迎面吹来的“以太风”。这便引入了迈克尔孙──莫雷实验,这个实验的目的就是测定我们相对于绝对参考系的速度。

迈克尔孙──莫雷实验的设计思路是,分开同一束光,同时在两个垂直方向上走过一段距离再观察,通过波动干涉,我们可以得到两者走过的时间差。再旋转90度,这个时间差会发生变化,会在干涉条纹上展现出来发生了移动。通过不同角度的测量,我们就可以得出我们相对于以太的运动速度。这就类似于伸出手来感受风向,你旋转你的手,风吹的感觉就不一样。

但是这个实验做出来的结果是不存在这股风。也就是说,实验地点相对于以太静止参考系的速度是0。人们在不同海拔,不同纬度,不同的时间做了这个实验,得出如果存在以太,那么以太参考系和地球参考系是一致的。虽然没有以太风刮到地球上,但地球就是以太风的风眼,太阳上的以太风速是一千万米每秒──这已经足够观测到了。

电磁定律修正失败,爱因斯坦开始在牛顿运动定律上做手脚,最后提出了狭义相对论。准确地说,狭义相对论只有两条基本原理:

在所有惯性系中,物理定律具有相同的表达形式。

在所有惯性系中,真空光速具有相同量值,与光源的运动无关。

前者几乎是朴素的,继承了整个经典物理学的内容,而后者就是文题所述的光速不变原理。实际上,科学界对光速的测量也是相当精确的。如果存在一个随地球旋转的以太参考系,那40亿公里外(如果你觉得这个数字很大,这只有0.0004光年,海王星都比这个远)的任何星球在其发的光掠过地球表面的时候,都会像超音速飞机的音爆那样亮(而且没有质量的物体旋转是违背牛顿运动定律的)。若非如此,地球的自转足以在早晚之间提供将近1000m/s的光速测定差值。1958年人类测量光速的精度就已经在100m/s左右,这样的误差是很难逃脱实验人员的法眼的。

当代“以太”这个词语仍然在使用,例如“以太网”。虽然静止以太的观点不再存在了,但电磁的相互作用仍然需要一个介质来传递。另一个物理学分支──量子力学提供了一个令人信服的解释,感兴趣的读者可以自行查阅相关资料。


我长期认为,让高中生和本科生接触基础科学的训练,其目的并不在于能够掌握多少知识,而是培养一个人的科学素养与严谨的态度。但令我吃惊的是,尤其是一些计算机领域中颇有建树的人,包括一位编译器领域的专家,曾经在四川唯二的985高校中完成了本科(据我所知,该高校的物理教育在四川还不错,仅逊于成都七中),而如今不仅对这些高中层次的常识认知都有些偏差,连推理实证的态度都非常匮乏。

阴谋论在如今的科学领域是站不住脚的。发一篇普普通通的光速测量的论文可能连教职都拿不到,但如果能够证明光速是在变化的,就算国际大型阴谋科研集团阻挠你发表,找个三流报纸把事情捅出去,至少也是吃喝不愁。如果在某些国家,至少能够像李森科那样混个院士,实验想做几台就做几台,论文想发几篇就发几篇。也不难测,请两个物理研究生,按照这篇论文来搞,打个飞的去马尔代夫,一天就搞定了,到时候估计都是专机来接,吃香的喝辣的,还卖什么课啊。(注:如果做不出来,本文作者不提供返程票报销事宜)

调试微信内置浏览器

作者 Victrid
2021年1月23日 20:24

微信内置的浏览器在不同的平台下的实现方式不同,但都接入了微信自身的OAuth机制用于鉴权。有些时候但微信本身的调试接口藏得很深,有些实现也没有提供一个调试窗口。此处我们以获取调试中的cookie为例来探讨不同设备的异同。

X5内核

在国内下载的微信使用的是腾讯的X5内核。X5内核自带调试功能,可以由以下方法开启:

在微信中访问 http://debugx5.qq.com ,在X5调试页面开启TBS内核Inspector调试功能。

如果提示您“非x5内核”,此时您的微信应该使用的是系统原生框架。您可以参考下一部分的“系统原生框架”进行调试,但也可以在微信中访问debugmm.qq.com/?forcex5=true切换为X5内核。(访问debugmm.qq.com/?forcex5=false就可以切换回WebView,但可能并不完全。)

系统原生框架

iOS和Google Play版本的微信在浏览器实现上调用的是使用系统提供的框架。

因为Android自带的WebView调试需要在打包时传递测试参数,而iOS则需要测试证书才能够调试,对于我们此处的例子是不适用的。

最简单的方法当然是安证书抓包。但是因为一些原因不方便抓包的时候,我试验成功的办法,是:

  1. 生成自己的根证书并对需要调试的网页进行自签名。
  2. 在手机上安装此根证书。
  3. 在电脑上搭建对应网页的服务器,目的是接受手机端传入的cookie,再配置自签名的证书。
  4. 在电脑上搭建DNS服务器,将欲调试的网页的DNS结果转发到上述网页服务器上。
  5. 配置手机使用电脑上的DNS。
  6. 此时就可以接收到cookie了。

后来观察到,当我试图在使用x5内核的设备上修改DNS服务器的时候,chrome已经可以工作,但微信并未生效,一直超时。后来发现x5内核使用微信自己实现的DoH,整个过程中只会查询微信自己的系列DoH服务器域名。而系统原生框架则不支持相关操作。

欧悌甫戎篇

作者 Victrid
2020年6月29日 20:19

单独抽出这一篇来看,欧悌甫戎篇实际上可以说是很有趣的一篇哲学“相声”。可怜的神学专家欧悌甫戎和苏格拉底同有官司在身,他们在法庭门前相遇。欧悌甫戎想要控诉自己的父亲杀人。控诉自己的父亲通常被视为不虔诚,而欧悌甫戎有自己的看法。这个理论依据是朴素和经验主义的,来源是宙斯自己都推翻了父亲,所以自己告发父亲并非不虔诚。

唯一该考虑的是杀得有没有理;……如果你明知他杀了人还和他伙同一气,不把他告到法庭来清洗自己和他的罪过,那就和他同罪了。

他们说它并没有杀人,即便杀了,死者既是凶手,我就没有必要为这样一个人去费心上诉,因为儿子告父亲杀人是不虔诚的。

从这里可以看出,欧悌甫戎的目的不是去将父亲判罪,而是要证明在虔诚上“惩治杀人犯”高过“控告父亲”。欧悌甫戎是个所谓精通神学的人,他希望通过这次审判,将自己立于一个虔诚的位置,审判的目的并非让自己的父亲服刑,而是希望通过这次审判证明自己的虔诚观念,并且洗刷自己不虔诚的罪恶感。即便自己在法庭上落于下风(这是几乎肯定的,在开庭之前他就已经被苏格拉底驳倒了),也可以在道德上落于不败之地。

苏格拉底啊,他们根本不了解那判别虔诚和不虔诚的神意。

苏格拉底诱导他对虔诚与否下一个定义。这就把号称自己精通神学,却只是精通神话故事的欧悌甫戎带到苏格拉底的逻辑陷阱里面去了。

显然欧悌甫戎提出的第一个定义“神所喜爱的就是虔诚的,神所不喜爱的就是不虔诚的”在多神论的混乱希腊神话中有语病。在苏格拉底的引导下,他给出了完备的第二个定义“虔诚就是神所一致喜爱的”。这个过程顺水推舟,可以看出欧悌甫戎除了神话故事外对基本的逻辑与辩术一无所知,正是苏格拉底的“认识你自己”的好对象,整篇对话被苏格拉底牵着鼻子走是理所当然的。完成了这个命题的声明,苏格拉底也就点到为止,没有继续做文章。

其实这个问题可以更进一步,人之间的分歧归求于神,而神之间也存在同样分歧。诸神这样的分歧,虔诚与不虔诚之间的混沌,诸神之间的理念争端(比如弑父到底虔不虔诚)显然需要求于一个更高层次的统一体。宙斯虽然是所谓的众神之神,但是希腊人民丰富的想象力早就把宙斯在诸神中的公信力败坏无存了,把多神转成主神在雅典是行不通的。对于更加抽象的统一体,显然柏拉图对此有一个“至善理性”、“绝对真理”一类的答案,但苏格拉底显然不会在此点破,否则自己也会落入渎神的境地。

苏格拉底把话题岔开,开始用主被动的关系探讨虔诚与神喜爱的事物之间的关系。这是个非常有趣的话题,甚至近于诡辩。我读到一句评论“苏格拉底需要的不是明智,而是让明智登场的舞台”,此处即是体现。“事物因为被神喜爱所以成为神喜爱的”,和“事物因为是神喜爱的所以被神喜爱”,这样两句话似乎有语义重复,怎么讲都应该是对的,但是两者是完全不同的。这是个先后问题,而我们显然给不出一个神的视角下的答案。(或许这个内容在客观唯心的观念下可以找到一个比较好的解决,从客观理性的绝对唯一精神里产生出一个服从其意志的希腊式神祗)苏格拉底利用这个漏洞,把神喜爱的替换作虔诚,问题一下子就展现出来了。

这个问题当然难不倒写书的柏拉图。这个神层面的问题反而强化了他们人依理性而非神谕行事的信条,自然,理性是一切道德的源头,而希腊诸神都是其派生物,先后关系就自然解决。但是在基督教的背景下就不太圆得过去。上帝作为唯一至善的神,也是道德之源,自然就产生了上帝无能的困境。尤其是近代的残酷战争,上帝知善而为之和上帝为之而称善的选择,在毒气战、细菌战、犹太人屠杀面前都不能自圆其说。苏格拉底的理性“真神”并不能够拥有喜怒哀乐这样的人类情感、喜好,人的事务和(希腊)神的事务是相分离的。这直接从原始的泛神论升到了一个较完善的客观唯心的思想层面。

另外,有一些观点认为,这里的苏格拉底只是顺着欧悌甫戎的假设往下说,即只说明了虔诚就是神喜爱的这个观点的问题。如果我们把上面的问题视作是主动与被动的统一,被喜爱和喜爱是同时被定义的(如果换成某人喜爱和被某人喜爱,我觉得这里的问题是平凡的),那么这样一个论证只能说明虔诚不能和神的喜好等价。这并不指虔诚不能和其他任何东西等价,或者虔诚是个伪命题。

显然,我们的神学家欧悌甫戎没有找到这样一个颠覆神学的答案。他只有将其述诸诡辩,但话又确实是自己嘴里说出来的,被苏格拉底揶揄一通。

苏格拉底又开始试探虔诚和公正的关系。苏格拉底把虔诚在公正上绕来绕去,从欧悌甫戎的嘴里得到了“虔诚是一种对待神的公正”,转头就用狗和猎人、牛和牧人的例子类比,人们去“对待”一个事物,是想要给这个事物带来益处,得到虔诚就是给神灵好处,但是神灵并不能因此而得到好处。苏格拉底还是留了个台阶,欧悌甫戎马上改口说“这种公正是对神灵的服侍”。但是苏格拉底又用将军、农民类比,神要完成“又好又多”的结果,并不需要人这样的仆人。

类比实际上并不是一个逻辑上完备的证明方法。此处的类比,至少是建立在神是可以类比的基础上的。在两位的理解内,古希腊的神是拟人化的神,有人的喜怒、好恶、优点陋习,和人之间的界限大概只有血统。如果能够跳出可类比这个基础,直接否定神与普通事物的相通性,比如说,神是形而上的,对神的服侍与对人的服侍是完全不同的,苏格拉底的类比就不攻自破了。当然,希腊人的神话并没有将神与人的关系抽开。欧赫迈罗斯认为神祇来源于历史上真实的英雄,整个希腊神话对于弑父、食子这些黑暗话题也并不避讳,和亚伯拉罕诸教天选那一套完全不同。

欧悌甫戎连忙找到一套理论把这套服侍的话术(也就是虔诚和公正是包含关系)搪塞过去。苏格拉底接下话来,给出虔诚是关于祈祷和祭祀的知识,直接推出虔诚就是人神交易。但是神灵又不需要人的这种礼物。欧悌甫戎想要否定这种交易的观点,又没什么话,说神灵通过人们的尊荣、崇敬和满意来得到好处。这又把虔诚和神喜爱的东西联系起来了。自己的逻辑被全部掀翻,欧悌甫戎连忙有个急事,先走一步。

如果按照整个来看,欧悌甫戎篇是柏拉图对话集的开篇,也是通常意义上的欧悌甫戎——申辩——克里同——云四篇的第一篇。紧接着就是申辩篇,在这一篇中苏格拉底需要为自己的渎神,也就是不虔诚的罪名辩护。一些观点认为,这一篇是在阐述苏格拉底的虔诚观,来为后文的申辩铺垫。但是这里的苏格拉底显然并没有给出这样一个明确的虔诚观念,或者说,执笔的柏拉图在这个时候还给不出所谓的虔诚究竟是什么。申辩篇里对于渎神这一点也不太明确,苏格拉底用神谕把这个责难搪塞了过去。

苏格拉底对于希腊诸神的态度实际上比较调和,他也会像其他雅典公民那样献上祭品,也遵神谕(尤其是德尔菲神庙的那条),但他并不像普通的雅典人那样,将神与自身的生活完全绑定。而只有这种绑定才有虔诚的概念。苏格拉底提倡的理性与相信的“灵机”,更有一种“凯撒的归凯撒,上帝的归上帝”的人神分离原则。神的祭祀只能决定神的事务,而人的行为需要从理性出发,去追求从理性生发出来的美德。神从赋予人理性的时刻起,人就是一个在理性的指引下行动的独立个体。这样应该就没有所谓的虔诚可言了。


Cf.

[1] <游叙弗伦篇>读书笔记 douban

[2] 《游叙弗伦篇》变奏曲 douban

[3] 习读柏拉图札记——《欧悌甫戎篇》 douban

[4] 一无所知的人哪 douban

[5] 游叙弗伦困境 Wikipedia

[6] 苏格拉底的灵机

[7] 论苏格拉底的神

[8] 2016.Feb<Plato's Euthyphro> (Kiki读书讨论后记) douban

弥罗斯人的辩论

作者 Victrid
2020年6月2日 19:20

这篇文章是《古希腊文明演绎》课上的读后感[1]


这次谈判没有赢家。双方都没能达到各自的目的,甚至连可以双方所构想的让步都没能实现。

雅典的武力威慑是完全无效的,因为所谓威慑在于武力的将实施,而雅典人最后只能动用了武力,杀光所有适龄男子,孩子妇女卖成奴隶,完全相悖于最初的将弥罗斯变为一个殖民地的目的。而弥罗斯人也没能实现事态的和平解决与正义,遭到毁灭。

这次谈判陷入了所谓的“a dialogue of the deaf”。双方只是在谈判中交换了意见,鸡同鸭讲,而没有出现任何余地的妥协。这样的谈判就如修辞的辩论一样,双方都设定了唯一不变的基点,这不是谈判。谈判的目的是达成共识,方式是双方的妥协。如果双方都各自把谈判的终点设在自己的起点,而不是一个折中的、双方都能妥协的位置,等待着对方放弃一切主张来靠拢,这样就不需要谈判了。

结果的形成和教训

出现这样的无效谈判,作为谈判的主导方的雅典显然应该负有主要责任。雅典人企图在弥罗斯人中间获取利益,杀光弥罗斯人,自己派人殖民是雅典人不希望看到的。但他们全程都是在以一个所谓明理的旁观者的角度发言,他在谈论武力差异的绝对,谈论雅典的强大与无畏,谈论救兵到来的希望渺茫,奉劝识相的弥罗斯人快快屈服。但是雅典人本来就是携军队前来,地位从一开始就是不平等的。而这种发言展现出来的傲慢,直接排除了弥罗斯人的妥协可能。

弥罗斯人在这次辩论中是被动一方,但并不代表弥罗斯人无法改变辩论的局势。弥罗斯人希望保全自己的政治存在与人民,但弥罗斯人也在以一个旁观者的角度发言,谈论虚空的正义和国际影响,谈论入侵弥罗斯对雅典可能带来的坏处。弥罗斯人说:

我们做奴隶,而你们做主人,怎样有同样的利益呢?

从此完全否定了雅典提出的屈服方案,而且除了自己的自由中立没有任何的妥协余地。

可以肯定的是,弥罗斯人根本没有能力自己打退雅典人,他们谈判的基础是斯巴达的援助、神灵、正义与非正义,和其他中立国的态度的转变可能。雅典的大军已经停在弥罗斯岛旁了,而弥罗斯人还在幻想着

那么,你们不赞成我们守中立,做朋友,不做敌人,但是不做任何一边的盟邦吗?

双方都没能抓住对方的利益点,结果自然也好不到哪里去。

谈判的目的是消除隔阂、解决问题,在这样一个强弱悬殊的谈判中,如果弱势方弥罗斯人能够为雅典人指出一条既能满足雅典进攻跳板、朝贡纳税的目的,又能够保存自己的政治和人民相对自由和独立的道路,江东子弟多才俊,卷土重来未可知。谈判所需要考虑的,不是自己需要什么,而是别人需要什么。

弥罗斯人和雅典人

弥罗斯人显然站在正义的一方,但是历史不会偏袒正义,弥罗斯年底就被雅典人攻破,人要么杀要么卖作奴隶。弥罗斯人似乎也只有在谈判场上斥责对方的无耻了。但妥协显然也不是什么好的解决办法。“今日割五城,明日割十城,然后得一夕安寝。起视四境,而秦兵又至矣。”二战的时候,法国人妥协,被德国吃干抹净,波兰人反抗,照样被德苏吃干抹净。由此看来,雅典人所说的

正义的标准是以同等的强迫力量为基础的;同时……强者能够做他们有权力做的一切,弱者只能接受他们必须接受的一切。

倒是有点道理了。弥罗斯所遭遇的,只能说是弱小国家的必然结局。现实中是没有主持公义的神的。

雅典人是愚蠢的。弥罗斯一开始也并非雅典的敌人,雅典人将弥罗斯纳入联盟的目的也并不是为了单纯的占领。本可以通过谈判团结的中立势力,因

雅典人对他们施用压力,把他们的土地蹂躏,他们才公开地成为雅典的敌人。

从而又需要耗费人力精力去剿灭弥罗斯。派两个傲慢自大的将军去参加战前辩论,与其说是战前辩论,还不如说是战争预告。武力是愚蠢的人解决问题的最终手段。

当今世界的强权国家似乎和雅典人有着一样的看法,原本建立的世界谈判桌被玩成了战争借口的制造工具,而如伊拉克战争,联合国否决后,美英直接绕过联合国决议自组联军。双方实力的差距越大,谈判的效用就越弱。或许,弥罗斯人的辩论能够带给我们最大的启示,就是不要对与实力更强的对手辩论能够解决争端抱有太大信心。


  1. 本文是该课程的一次作业。 ↩︎

埃斯库罗斯作品:波斯人、阿伽门农

作者 Victrid
2020年5月19日 18:04

这篇文章是《古希腊文明演绎》课上的读后感[1]


波斯人

波斯人以波斯国王塞克塞斯带领的波斯海军中计败于雅典军队为故事的背景。故事由两个部分组成。第一个部分中,波斯军队讨伐雅典,迟迟没有军信回报,而波斯的太后、大流士的妻子阿托萨做了噩梦。通过阿托萨与歌队的对话,交代了波斯军队的情况。

此时,军队信使传回了悲报:强手云集、人数众多、船坚炮利的波斯军队全军覆没,而雅典人未动分毫。究其以少胜多的原因,是希腊人使了诡计,欺骗塞克塞斯希腊人人心涣散,都在逃跑。塞克塞斯因之前成功搭好浮桥,以为有天神相助,未加细想就命军队成合围之势。第二日希腊人趁机反攻,海战楞头青的波斯船只乱了阵脚,如赤壁之战的曹军一样,互相碰撞沉没。本等候胜利、在一旁待命的陆军也被雅典人的反攻打退,将士死伤无数,逃兵也大多丧命。

第二个部分开始。战争的悲报让波斯人心涣散。阿托萨和歌队召唤大流士的亡魂,大流士听毕状况的说明后,心痛万分。大流士强调敬神的重要,并指出塞克塞斯已经归来,衣衫褴褛。塞克塞斯失魂落魄,悲痛万分。自己为国家招致灾难,将领兵士也为之丧命。故事在悲哭声中落幕。故事除了阐明敬神的重要性之外,还警示我们不可让理智被心灵所压过。


歌队是这部戏剧不同于现代的戏剧的一点,也是古希腊戏剧中的一个核心组成部分。歌队没有人的特征——只是一个功能性的群像,又不仅仅是一个用于交代背景的纯功能性的旁白。在大流士鬼魂出现和塞克塞斯两个部分,歌队也参与了人物之间的对话。而歌队与塞克塞斯

塞 我看见大祸临头,撕破了我的王袍。

歌 哎呀!哎呀!

塞 放出那更悲痛的声音!

歌 两声不够,再唤一声。

的对白,很有钦差大臣里对观众“你们笑什么?笑你们自己”的意思,歌队又成为站在舞台上的观众了。

阿伽门农

阿伽门农是埃斯库罗斯的作品,以特洛伊战争结束,阿伽门农返回阿耳戈斯为背景。故事以守望人得到阿伽门农攻下特洛伊的消息开场。歌队进场,介绍特洛伊战争的背景,墨涅拉俄斯的妻子海伦被特洛伊王子帕里斯拐走,因此他和他的兄弟阿伽门农起兵远征特洛伊。在远征的途中,神发出逆风阻挡了阿伽门农的船队。而阿伽门农为了讨好神求顺风,非法地献祭了他的女儿。此时,王后克吕泰墨斯特拉告知歌队守望人汇报特洛伊城被攻陷的消息。在歌队为此喜悦的同时,王后担心如果军队败坏纪律会招致厄运。歌队称赞王后的冷静谨慎,开始吹捧宙斯的英明,责备特洛伊王子和海伦因为私情挑起了战争。

传令官宣告阿伽门农得胜归来。说阿伽门农借宙斯的大能夷平了特洛伊城,但返程时遭遇了可怕的风暴,墨涅拉俄斯的船队在风暴中漂散,不知去向。歌队再次谴责挑起这次战争的海伦。此后阿伽门农和俘虏的卡珊德拉上场。他赞美阿耳戈斯军队的强大,并歌颂阿耳戈斯的护卫神。王后上场,假意迎合自己的丈夫,表达对于阿伽门农的思念与阿伽门农凯旋的欣喜。她命令婢女们用紫色花毡铺一条路,请阿伽门农从毡子上回宫。她说:

至于其余的事,我的没有昏睡的心,在神的帮助下,会把它们正当的安排好,正像命运所注定的那样。

阿伽门农起初对在紫色毡子上走感到恐惧,担心冒犯天神。但王后迎合阿伽门农胜利归来的心态说服了他这样做。阿伽门农仍然担心被神嫉妒,因此踏着紫色花毡光脚走进王宫。王后道:

啊,宙斯,全能的宙斯,使我的祈祷实现吧,愿你多多注意你所要实现的事。

歌队暗示福兮祸之所倚。王后上场,宣布了自己国家体贴的奴隶政策,请卡珊德拉也进宫。但卡珊德拉不肯下车,王后和歌队都劝说她下车进宫,也不起作用。王后放下狠话,独自进宫。卡珊德拉预言自己的毁灭,指出这个家庭不敬神,充满了家人间的杀戮,并预言会发生一件莫大的祸事,而能够帮上忙的人又远在天涯。歌队先开始不理解,但结合城中沸沸扬扬的传言和卡珊德拉对妻子杀死丈夫的明示,而自己也很快死亡的预言才明白即将到来的灾难。

阿伽门农被刺。歌队听到了国王的叫喊,却在怎样救国王的争执中错失了救治的机会。此时

后景壁打开,壁后有一个活动台,阿伽门农的尸体躺在台上的澡盆里,上面盖着一件袍子,卡珊德拉的尸体躺在旁边。

王后站在台上,说她用长袍罩住阿伽门农,刺了阿伽门农三剑。王后说明,刺杀阿伽门农的原因是阿伽门农侮辱了自己的妻子,像献祭牲畜一样献祭了自己的女儿,应该为此付出代价。歌队哀叹说,国王真是不幸,为了一个女人去打仗,现在又死在另一个女人手里。

自己的兄弟被阿伽门农的父亲、自己的叔叔杀死来宴请父亲的埃癸斯托斯如愿以偿,非常高兴。歌队对此非常愤怒,双方按剑,此时王后出来,制止了这场争斗。


阿伽门农的悲剧性,更多体现在人物在道德困境中的挣扎上。克吕泰墨斯特拉的女儿被自己的丈夫无辜、非法地杀害。一方面是丈夫远征特洛伊胜利,一方面是自己为女儿的复仇,建立了克吕泰墨斯特拉的悲剧形象。阿伽门农在剧中可能更多是以一个背景与复仇的结果而存在的。他还没怎么说话,就被杀死在浴缸里。而阿伽门农作为伟大的胜利者,丧命于自己妻子的刀下,更加深了王后此举的冲突。

在英雄时代的宏大叙事下,王后用自己的决断与独立完成了自己的私人复仇。尽管通常来讲克吕泰墨斯特拉都被描述为伙同情夫杀死丈夫的恶妇,但《阿伽门农》中的王后的正面、宿命形象更加突出,更多是一个被不可抗拒的宿命所左右,只能以这种方式反抗的弱小角色。


  1. 本文是该课程的一次作业。 ↩︎

电路理论资料

作者 Victrid
2020年5月5日 19:22

网课

电路理论

电路实验

习题与答疑

第一次习题课

第二次习题课 12

第三次习题课

4.19电路答疑 12

5.17电路答疑

5.24电路答疑

6.7电路答疑1 2

书籍

需要连接VPN访问,配置方法

电路基础(第二版),陈洪亮、张峰、田社平, 高教社, 2015

电路, 邱关源, 高教社, 2006

简明电路分析基础, 李瀚荪, 高教社, 2002

Fundamentals of Electric Circuits, Charles K. Alexander, 清华大学出版社, 2000

Placement New

作者 Victrid
2020年4月1日 00:57

前几天写vector,可把我难倒了。最开始是一个测试用例,没有默认的构造函数,这个时候应该怎样去处理内存的分配。

我想到,既然一个元素的大小,即sizeof()是固定的,我们就可以先申请这个空间,等到需要使用的时候再赋值。

memcpy() 失败

虽然只学过C++化的简化版C语言,但我想到了malloc()方法,这就是一个只申请空间,不去填充空间的方法。

这对于传统的数据,就是说所谓的POD(Plain Old Data)是很好的。我的整个实现逻辑也是memcpy(),这样的处理速度快的多。

但是不是POD的类,占用了堆来存储部分内容,在他的存储空间里只存储了指向堆的指针。所以memcpy()只执行了浅拷贝,拷贝的实际上是指针,它指向的堆内存储是不变的。当原对象析构释放堆的时候,拷贝对象就被破坏了;被覆盖的对象没有进行析构而是直接被覆盖,内部申请的堆内存就被泄漏。

同样,当被覆盖的地方原本不存在对象,即只分配了内存却没有初始化内存空间时,如果直接调用类的赋值,这个赋值方法是不能检测原来的对象是否正确的。一旦有针对原指向堆内存的指针,现在是随机指针进行的操作,例如直接向动态数组发起memcpy(),就会出现段错误,或者内存的错误覆盖。

所以,凯撒的归凯撒,经过查阅资料,我找到了下面两个C++的处理方法。

placement new 成功

为什么我最开始不用new呢?因为new在调用的时候会自动调用构造函数,而一个用例没有默认构造函数,new就会出错。

怎么解决呢?经过资料的学习,我找到了placement new这个用法。实际上,new和delete是通过以下的过程实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
A* p=new A;B* t=new B[_NUM];
delete p;delete [] t;

//<==>A* p=new A;B* t=new B[_NUM];
//the first step. This is equal to malloc(size_t).
operator new(sizeof(A));operator new[](_NUM*sizeof(B));
//This is called 'placement new',
//to exec the constructor on the p location.
new(p) A();for(size_t in=0;in<_NUM;in++)
new(t+in*sizeof(B)) B();

//<==>delete p;delete [] t;
//exec destroyer explicitly.
p->~A();for(size_t in=0;in<_NUM;in++)
(t+in*sizeof(T))->~B();
//delete
operator delete p;operator delete [] t;

过程中的placement new实际上就是按需调用构造函数,而operator new/delete则是只申请内存而不初始化,和malloc/free功能是一致的。好像构造函数是不能够显式调用的,所以placement new来解决这个问题。

Powerful but Limited

作者 Victrid
2020年3月18日 05:22

这篇文章是《古希腊文明演绎》课后的感想[1]


人天生就存在着悲剧性。作为意识与思考的载体,人的躯壳并不能很好的实现保护他的容纳物免遭破坏。相较于野猪、老虎这样的生存机器,不仅有着极其强大的生存和免疫能力,让它们能够在一片山林中独当一面,配套的神经也恰到好处,没有过多的累赘消耗本就稀少的生存物资(人的大脑往往消耗人日常摄入能量的三分之一)。这些生命在健壮性上走到了极致。

而人类显然不是一个这样一个设计精密,万无一失的机器。自人类出现的六千万年以来,从树上生态的参与者,到草原生态的掠夺者,到种植作物的生产者,再到现在铅与火,光与电的操纵者。人不是一个完美的产物,而是一种能够不断地向前发展与更新的迭代。

不完美造就了人的悲剧性。“人是一颗会思想的芦草。”帕斯卡如是说。与达尔文的生存理论不同。我们不是为了生存而生存的。Powerful but limited.甚至仅在可以观测的历史里,人的改造世界的能力展现出了巨大的进步,从生态位的中下层一举跃出动物间的食物金字塔;但是从出土的早期人类化石来看,人本就弱于平均的生存能力相较于早期人类与类人猿,甚至有所下降。霍金这样的科学家,更是将生存的有限和思维的强大两个角度的撕裂展现得淋漓。

古希腊人心目中的神,一个核心特性就是不朽。单从追求不死这一点来看,古今中外都是人永恒的幻想。人的大脑有着长达120年的细胞寿命,但躯干甚至维持不到它的2/3。人生是短暂的。相较于道家对于羽化成仙的追求与武士道对生命的极端漠视,又或是三大宗教对未来的幸福的画饼式憧憬,古希腊人更多将目光投到“现世”上。从而踏踏实实接受人的命运,在此基础上从自己出发,把自己的能力展现出来。

古希腊的神不像其他地区起源的精神依托式的神那样是永远伟大光荣不朽的,他继承了早期原始信仰的黑暗与混沌,又得以在其上加以秩序的发展。这些神也有缺点,也会残暴,但却与人有着不可跨越的界限。对神的向往,与之对应的对美德的崇尚直接反映在人的行动上。他们致力于把自己的悲剧写得精彩,而不是要将每一个故事都强行圆上一个圆满的大结局。

鲁迅说“悲剧将人生的有价值的东西毁灭给人看”,关键在于有价值的人生,而非这个毁灭的过程。俄狄浦斯王所想展现的,不是杀父娶母的俄狄浦斯罪大恶极,得了多大的报应,而是俄狄浦斯以人的力量去反抗神的灾厄,却仍然难逃神示的宿命。这样的悲剧,limited的部分不再是短板,而成了人之所以为人,就像悲剧因为毁灭而称为悲剧,的重要部分了。


  1. 本文最早写作于3月16日。 ↩︎

VLC Libva Error Troubleshooting

作者 Victrid
2020年3月3日 19:58

VLC player is well-designed and powerful, and I prefer this type of software designing. (Although UNIX's politically correct answer is Keep It Simple, Stupid, In the multimedia region I prefer a powerful and what-you-see-is-what-you-get interface, just the same design of those old cassette recorder.)

VLC is preinstalled with KDE, or some basic structures. I couldn't play video, but audio is OK since then.

Problem Description

Everytime I tried to play a video, e.g. MP4 with VLC, the interface just flashed and nothing displayed.

using journalctl I got

1
kernel: vlc[33836]: segfault at 168 ip 00007fb96213e11c sp 00007fb920207320 error 4 in libva.so.2.600.0[7fb96212d000+13000]

and I tried vlc -vvv to show the verbose messages, I got

1
2
3
4
5
6
7
8
9
10
11
[00007f82180017f0] egl_x11 gl debug: EGL version 1.4 by Mesa Project
[00007f82180017f0] egl_x11 gl debug: extensions: EGL_ANDROID_blob_cache EGL_ANDROID_native_fence_sync EGL_CHROMIUM_sync_control EGL_EXT_buffer_age EGL_EXT_create_context_robustness EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers EGL_IMG_context_priority EGL_KHR_config_attribs EGL_KHR_create_context EGL_KHR_create_context_no_error EGL_KHR_fence_sync EGL_KHR_get_all_proc_addresses EGL_KHR_gl_colorspace EGL_KHR_gl_renderbuffer_image EGL_KHR_gl_texture_2D_image EGL_KHR_gl_texture_3D_image EGL_KHR_gl_texture_cubemap_image EGL_KHR_image EGL_KHR_image_base EGL_KHR_image_pixmap EGL_KHR_no_config_context EGL_KHR_reusable_sync EGL_KHR_surfaceless_context EGL_EXT_pixel_format_float EGL_KHR_wait_sync EGL_MESA_configless_context EGL_MESA_drm_image EGL_MESA_image_dma_buf_export EGL_MESA_query_driver EGL_NOK_texture_from_pixmap EGL_WL_bind_wayland_display
[00007f82180017f0] main gl debug: using opengl module "egl_x11"
[00007f8218658f00] main generic debug: looking for glconv module matching "any": 4 candidates
[00007f82180017f0] glconv_vaapi_x11 gl error: vaInitialize: unknown libva error
[00007f82180017f0] glconv_vaapi_drm gl error: vaInitialize: unknown libva error
[00007f82180017f0] glconv_vaapi_drm gl error: vaInitialize: unknown libva error
[00007f8218658f00] main generic debug: using glconv module "glconv_vaapi_drm"
[00007f82180013b0] main vout display debug: using vout display module "gl"
[1] 32678 segmentation fault (core dumped) vlc -vvv

Way to Solution

microcode problem?

Though the Archwiki page VLC has some suggestions on segmentation fault, it indicates it as a microcode problem. That's something happens as missing VLC interface. I can play audio file with VLC, after trying the solution I think it's not the microcode problem.

VLC bug?

Some reports says it's a bug, that older VLC uses strlen() on a null pointer, but they were posted years ago and I believe it's not my problem. My VLC is fully updated.

libva error: Display drivers

When checking vaapi and libva error, I found it might be a problem with NVIDIA GPU. Some articles point out that Intel uses vaapi and Nvidia uses vaapu. After updated nvidia, libva, the problem still happens.

Checking VLC's optional dependencies, I found

1
2
libva-vdpau-driver: vdpau backend nvidia
libva-intel-driver: video backend intel

not installed. It could be essential if you have these GPUs. After installing these two (my laptop uses both intel and Nvidia to display) drivers, my problem finally solved.

Ring-Fit新上手

作者 Victrid
2020年2月28日 05:25

趁着疫情时期宅在家中,新购买了健身环大冒险。

以前就见过体感游戏,研究过Kinect,也用Wii打过网球。体感很多都是一个简单参与性质的,但是没有想到竟然有以体育运动作为核心的体育游戏,甚至还有专门的体感外设。

我已经连续好几周没有活动过了。在学校里面本来也很少活动,这次疫情发生,我活动的时间就更少了(还更加理直气壮了)。这次用ringfit算是这几周来第一次主动去运动。

我在买ringfit之前,看到网上的评论有比较多的负面观点。有知乎上的健身大牛直接评价“心率起不来,连基本的运动效果都得不到满足。”但是我拿起健身环之后,还是感觉效果非常不错。尽管我调的运动强度算是非常低的数值,但是整个运动对于我来说也是中到高强度的内容。(这个评价也是因人而异,并不是说负面评价都是编造的,对于我这种平时就很少锻炼的人来讲还是比较合适。)

前面几个关卡,虽然说是简单的教学关,但对我来说还是有挑战性。我的基础代谢比较高,但是心肺功能又没有因此而带上来,导致长时间运动非常吃力,耐力很不足。最后的小boss真的是做到难以继续下去。感觉游戏自身的设定还是很好的。


从游戏说到游戏。貌似柏拉图对于游戏的评价是幼体——动物的幼崽与人类幼儿对于实际的模仿,亚里士多德提出是一种无目的的休闲形式,在今天这一点已经很不同了。尤其是生产力水平大幅提高,给了成年人充足的休息与娱乐的时间,娱乐手段也极端地多样化之后。ringfit这种内容可能还不能算上是大众的游戏,但是现在的游戏也越来越脱离现在被称为幼儿游戏的“模仿实际”,和简单无目的的休闲形式,有了新的内涵。

游戏与动画产业一样,都将曾经的“儿童”属性剥离出来,增加了自己内容的可选择面。现在的游戏受众向青少年,更多的是青年转变,也不在拘泥于幼儿也可以懂的简单叙事,而是向内容取材广度与内涵深度拓展。游戏也在向更丰富的社会功能发展,比如叙事艺术,比如思想与甚至理论的教育,再比如我们今天谈到的健身。

虽然不可避免的,当今社会的单一价值观总会将娱乐,和他的纯粹代表游戏放在道德的对立面,但是社会压力普遍增大,上升空间缩小,生活成本增加的情况下,游戏的市场获得了较大的提升,而游戏的内容也极大地丰富拓展了。不仅是逃避社会现实的工具,而更多起到了价值观丰富化的作用。文化的发展很多时候需要这一点,而单一价值观的社会总会面临多元文化衰退的危险。

我不想将问题引到体制和精神上,但是现在公开与奋斗论唱反调的人越来越多,娱乐事业的快速发展也在为这样的思想暗加砝码。缺乏前驱力的社会固然是我们不想看到的,但是马车夫的鞭子是否抽的过重了?劳动本身是快乐的,这种快乐也是高层次的、纯粹的。虽然不用鞭子和缰绳,会让“一群厨师做坏一锅汤”,但社会仍然需要的是多元价值体系的共存,而不是把其他的价值体系都一路归于钱和名利(这两者,在一定程度上是等价的)上。

我们甚至有微积分和运动的游戏。学习也是带来快乐的,运动也是带来快乐的。为什么我们要刻意将快乐与我们的道路分开呢?我们研究了两千年的教育制度,却旨在剥夺那些不是第一名的多数学生仅存的些许快乐。运动被量化在秒表,米尺和积分板上。马渴了自然会喝水,强迫也无济于事,对人这样做,还会有反作用。为什么我们要如此处心积虑地去逼着马儿喝水呢?

人有惰性,并且喜新厌旧,这是事实。但是游戏也是人在玩,很少有人指出来,游戏厂商反而还用这个赚了大笔钞票。有些时候我们只需要对事,而没有必要让每个人都成为圣人。我们需要利用人性的弱点,而不是试图用各种威严来弥补它。

动态规划——从分割等和子集入手

作者 Victrid
2020年2月21日 19:54

引入

题目:

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

每个数组中的元素不会超过 100

数组的大小不会超过 200 (Cf. LeetCode,2020)

这道题目,在动态规划里,实际上是一个典型的01背包问题。但是我并不会动态规划,大佬们讲的方法,都是默认已经掌握了动态规划问题的处理方法来讲解。

这些解法对于我来说看得很痛苦。因此,我们通过这一道题来理解动态规划中01背包问题的解决原理。

1 解决

1.1 从枚举开始

最容易想到(甚至不用想就可以得到)的是枚举法。代码我们不再赘述。这样操作的时间复杂度是 $O(n2^n)$ 。主数组中的元素,要么包含、要么不包含在子数组中。枚举每一个可能的子数组,再对数组进行求和,就得到了我们的答案。

但是这样的复杂度是很难让人(和判断程序)接受的。它甚至比指数复杂度还要大。

我们观察这样的过程,找一找可以优化的步骤。我们发现,这样进行了大量的重复求和。从{1,2,3,4,5}到{1,2,3,4,5,6},我们可以用一些方法来保存{1,2,3,4,5}的加和,就可以节约这些时间。

1.2 分治法

这里的分治,实际上仍然是对于主数组的枚举,因此在枚举上的复杂度($O(2^n)$)是不变的。请看下面这段代码:

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
//0.1 divide and conquer TLE
class Solution {
public:
bool canPartition(vector<int>::iterator begin, vector<int>::iterator end, int sum) {
if (begin == end)
return false;
if (*begin == sum)
return true;
if (*begin > sum)
return false;
return canPartition(begin + 1, end, sum - *begin) || canPartition(begin + 1, end, sum);
}

bool canPartition(vector<int>& nums) {
auto it = nums.begin();
int sum = 0;
for (; it < nums.end(); it++)
sum += *it;
//--------------------------------------------
if (sum & 1)
return false;//这里是比较小的一步优化,
//因为奇数和不可能被分割,就直接得到答案。
//--------------------------------------------
it = nums.begin();
auto end = nums.end();
return canPartition(it, end, sum >> 1);
}
};

我们通过函数的调用栈来保存了这个临时的加和。这样整个过程的复杂度就降到$O(2^n)$。

但是这样复杂度仍然非常高。O(2^n)的复杂度,在实际的运用中都很难碰到。

我们继续观察这个过程,找一找可以优化的步骤。

我们观察到:对于问题的一个实例{1,2,3,2,8,7,9,6}:

进行到第三步:这样两个子数组

{1,2,不取}和{不取,不取,3}他们的加和都是3。这样造成了重复,他们接下来的比较实际上是相同的。对于后续的数列{2,8,7,9,6},3到底是哪些数字加出来的并不会影响。如果去掉这些分支,我们就可以节约这些时间。

1.3 基于二维数组的动态规划

动态规划是什么?Quora上有一个很有趣的例子:

什么是动态规划?

*在纸上写:1+1+1+1+1+1+1+1=?* 这个式子等于多少?

*数了一下*……8!

*在式子的左边添一个“1+”*,现在呢?

等于9!

你怎么算的这么快?

因为你只是添加了一个!

所以你不需要重新加一遍,因为你记住了他以前有8个。“动态规划”只是一种“把东西记下来以节省时间”的洋盘说法。

我们可以看出,我们刚才使用基于递归的分治,就是一种隐性的“利用计算机的调用栈把数据记下来以节省时间”动态规划法,只是你不知道他的名字。

刚刚提到,分治的做法会重复的计算相同和的部分,因此我们现在这样做:

对于一个实例{1,2,3,5},我们建立如图的这样的一个二维数组表格:

Form

这个表格中的数据是怎样得到的呢?

  • 首先计算出目标和(就是分割的子集的和),建立对应的数组。
  • 将第一行(也就是空集合对应的行)全部置false。再把空集和为0的(0,0)格子置true。
  • 下一行
  • 我们先将上一行为true的对应的和的格子置true。因为这些在新集合的真子集中存在,那么他们在新集合中一定存在。(文中用==>箭头表示。)
  • 我们再将上一行为true对应的和加上新增的集合元素值所得到的和对应的格子置true。(文中用-->和圆圈[与0相加]表示)。
  • 我们判断目标和的格子是否为true。如果true出现那么返回true。(文中用方框标记。)如果true不出现,我们进入下一行,重复这个过程。
  • 全部遍历目标和格子仍然为false,我们返回false。

我们注意到,现在所需的时间就已经缩短到$O(N*sum)$.这样的时间复杂度远远好于前文所述的方法的时间复杂度。这样的一个示例代码给在下方:

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
class Solution {
public:
bool canPartition(vector<int>& nums) {
auto it = nums.begin();
int sum = 0;
for (; it < nums.end(); it++)
sum += *it;
if (sum & 1)
return false;
//计算目标和,建立数组。
bool** b = new bool*[nums.size() + 1];
for (int i = 0; i <= nums.size(); i++) {
b[i] = new bool[sum / 2 + 1];
}

//第一行的设置
b[0][0] = true;
for (int j = 1; j <= sum / 2; j++) {
b[0][j] = false;
}

//接下来的各行
for (int i = 1; i <= nums.size(); i++) {
//将上一行为true的对应的和置true。
for (int j = 0; j <= sum / 2; j++)
b[i][j] = b[i - 1][j];

//上一行为true对应的和加新增值置true
for (int j = 0; j <= sum / 2; j++)
if (b[i - 1][j] && j + nums[i - 1] <= sum / 2)
b[i][j + nums[i - 1]] = true;

//判断目标和
if (b[i][sum / 2])
return true;
}
//目标和格子仍然为false,我们返回false。
return false;
}
};

这样的操作虽然耗时大大减少了,但是空间复杂度更大了。

我们再次观察这个程序,看看有没有可以节约空间的地方。显然,这个二维数组是空间消费的大头。我们注意到,每次我们都会把上面的数组结果复制下来。因此我们可以用一个一维数组来简化这个数组。

1.4 基于一维数组的动态规划

用一维数组简化这个数组。需要注意的是在对应新增值相加的时候需要从高位向低位加,否则会出现同一个数被重复加的情况。

代码如下:

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
class Solution {
public:
bool canPartition(vector<int>& nums) {
auto it = nums.begin();
int sum = 0;
for (; it < nums.end(); it++)
sum += *it;
if (sum & 1)
return false;
//建立一个一维bool数组
bool* boolarray = new bool[sum + 1]();
sum >>= 1;
for (it = nums.begin(); it < nums.end(); it++) {
//原本为true对应的和加新增值置true
for (int j = sum; j >= 0; j--)
if (boolarray[j])
boolarray[j + *it] = 1;
//自己置true
boolarray[*it] = 1;
//判断目标和
if (boolarray[sum])
return true;
}
//没能达到目标和
return false;
}
};

整个过程是相同的,但是内存的占用量减小了很多。因为内存动态分配需要消耗大量的时间,这个做法的耗时也比原来的耗时少一半。

当然,在程序堆里分配空间非常耗时,也可以从条件出发,直接将数组定义在程序栈里,可以更节约时间和空间。

被背叛的革命 (1)

作者 Victrid
2020年2月19日 19:16

这是《被背叛的革命》(列昂·托洛茨基著)前三章的摘录与书评。

取得了什么成就

如果你们记得,社会主义的任务就是要建立一个以团结一致和一切需要得到妥善的满足为基础的无阶级的社会,那么,从这个根本的意义上来说,苏联还没有一点社会主义的影子。 >>P V<<

社会主义已经表明,它有权取得胜利,不是在《资本论》的书页上,而是在包括地球表面六分之以的工业舞台上——不是在辩证法的语言中,而是在钢、水泥、和电力的语言中。……这个不可摧毁的实施,即一个落后国家只是由于进行了无产阶级革命就在不到十年的时间力取得了史无前例的成就,也仍然还是未来的实际景象。 >>P 3<<

国内价格和世界市场价格之间的差别,就是衡量这种力量对比的主要手段之一。然而,苏联统计学家甚至连接近这个问题都遭到禁止。原因是,尽管资本主义处于停滞和腐朽的条件下,但是在技术、组织以及劳动熟练程度方面,它还是远远走在前面的。 >>P 4<<

一种理想的计划管理,所要保证的不是个别部门达到最大限度的速度,而是整个经济最适当地发挥效能,从这个立场出发,统计出来的增长率在初期是比较低的,但是整个经济,特别是消费者,将会得到好处。归根结底,总的工业动力也会得到好处。 >>P 8<<

资本主义制度的根本弊病并不在于有产阶级的奢侈生活——尽管这一点本身也许是可恶的——而在于这一事实,即资产阶级为了保障其过奢侈生活的权利,保持了它的生产资料私有制,这样就是经济制度注定了限于无政府和衰退的状态。在消费奢侈品方面,资产阶级当然拥有垄断权。但是在基本需要品方面,劳苦群众是站压倒多数的消费者。 >>P 11<<

苏维埃政权正在经历一个准备阶段,它正在输入、模仿和吸取西方的计数和文化成就。生产和消费的相对系数证明,这个准备阶段还远远没有结束。甚至在未必会有的资本主义继续完全停滞的条件下,这个阶段也还是必须经历一整个历史时期。这是我们在进一步考察以前需要的第一个极其重要的结论。 >>P 12<<

苏联在当时取得一系列的经济成就,实际上与一战结束,世界经济萧条萎缩,资本主义本身自身难保,无暇镇压颠覆新兴政权有关。沙俄本身具备一定的资本与生产基础,这是其他社会主义国家难以复制的。

资本主义社会能够消灭封建社会,其根本原因在于生产力的解放。时至十九、二十世纪,资本主义已经发展到“可以保证封建不再复辟”的程度了。而社会主义经济,尤其是从生产资料公有化到满足一切社会需要,这两点之间应该寻找怎样的路径,实际上到今天也没能得到比较妥善的解决。

资本需要利用人本性上的贪婪和欲求来追求无限的生产扩大化和多样化。日新月异、繁复诱人的新产品、新发明,实际上是现代资本主义发展设下的一个发展循环,通过绕路来延缓到达经济危机的时间。这些物品本身就是资本主义赖以生存的核心。斯巴达式的共产主义就是乌托邦。只要人的智力和想象力不枯竭,从人最自然、不受到教育和约束的愿望出发,资本主义的生活方式一定远超斯巴达式的共产主义。

尼克松与赫鲁晓夫有过一场著名的厨房辩论。

尼克松: 这是我们最新型的洗碗机。这种型号生产成千上万,被直接安装在家里。在美国,我们喜欢让女性的生活更轻松……

赫鲁晓夫:你们资本主义对女性的这种态度不会在共产主义社会发生。

但是赫鲁晓夫最终还是输了这场辩论。事实证明,苏联人民还是无法抵挡洗碗机、麦当劳十足的诱惑力。

俄国早期的共产主义者很有斯巴达精神,但是他们的孩子们不。尽管数据上苏联是,在五年计划甚至更早,就是世界上不可或缺的主要工业国,但是重工业优先的问题从那时开始,一直延续到解体。甚至今日俄罗斯仍然面临着轻工业薄弱的困境。

苏联的工业可以表述为这样一条独特的法则:越是接近群众消费的商品,通常总是越糟。 >>P 7<<

享乐在很早,就被斯巴达主义者们冠以了腐朽的标签。动摇这一点,甚至会影响苏联存在的合法性和社会主义的优越性。因此苏共的官员们不是不了解享乐——有些时候他们反而更了解这些——而是不能够公之于众。苏联的重工业越是发达,轻工业的生产者们就越是回避这一问题,这会让好看而美观的全联盟大会公报上留下一个擦不掉的污点。

中国注意到了这样的问题。但是也没有提出更加社会主义的解决方案,而是借用资本主义的方法来处理资本主义提出的问题,通过引入私人经济来解决变化不定的生活需求。早期的处理方式显得矫枉过正,但是这种处理是有效的。至少使用这种处理方法,没有像苏联那样解体。但这样做损害了斯巴达主义者和理想主义者们的热情,认为这样做“背叛了革命”。

中国也仍然在一个准备阶段,但是已经接近尾声了。不是说中国目前准备已经结束,而是世界资本并不像过去苏联的准备那样疲软,无力进行对抗。距离2008年经济危机已经过去了12年了。敌人是不会打瞌睡的。中国目前的理论合理性把太多砝码压在了经济增长上了。全世界经济下行、反全球化不断升级,互联网又将负面声音扩大化了,如何解决即将到来的理论危机,是一个难点。

经济增长和领导的左右摇摆

对于我们来说,似乎还有比要对苏维埃政府的经济政策及其左右摇摆的情况进行一次历史性的回顾,以摧毁那种人工培养出来的个人拜物教。这种拜物教认为,要取得成就——不管真的还是假的——关键在于领导的特殊品质,而不再与革命所床在的那种财产社会化的条件。 >>P 28-29<<

有一点当然还是不可理解的——至少以一种合理的态度对待历史的化——即一个思想最贫乏、犯错误最多的派别怎么会、为什么会反而占了上风,压倒了所有的其他集团,并且把一种毫无限制的权利集中在自己手里。 >>P 29<<

苏联的发展,从十月革命到斯大林经济体制的完全建立的这段过程,可以说是非常危险、而且不可以照搬的。社会主义经济的发展,一方面需要在全新的生产体制下调动经济本身的活动性,另一方面,需要在群强环伺的情况下,保卫经济安全。从军事共产主义到新经济政策,再到斯大林模式,这种经济政策截然相反的改变,在其他社会主义国家的建设过程中都是不可能如此快速地进行转换的。

托洛茨基的年代苏联还只是经济制度的变革,他没有看到的是,苏联每次政治变革,令人惊讶地没有造成太大的国内社会动荡,而真正的国内动荡,反倒是领导所愿意并主动推进的结果。苏联国家有着先天性的文化独立性,这是其他国家所不具备的。

社会主义和国家

无产阶级所需要的只是逐渐消亡的国家——即需要建立一个立刻开始消亡而且不能不消亡的国家。 >>国家与革命《列宁全集》B 22, P 390<<

同时无产阶级将采取措施来防止自己的及其转入官僚手中——“马克思和恩格斯详细分析过的办法:(一)不但实行选举制度,而且随时可以撤换,(二)薪金不得高于工人的工资,(三)立刻转到使所有的人都来执行监督和监察的职能,使所有的人暂时都变成‘官僚’,因而任何人都不能成为官僚。”你不要以为列宁所说的是十年当中的问题。不是。这是“我们应当而且必须在完成无产阶级革命方面开始“采取的第一个步骤。 >>P 35<<

因此,无产阶级专政制度从一开始就不再是原来意义的那种”国家“——即一个使大多数人服从的特殊工具。物质力量以及武器都直接立刻转入像苏维埃这样的工人组织手中。从无产阶级专政的第一天起,国家作为一个官僚工具开始消亡。之久是党纲发出的呼声——而且到现在还没有停止。奇怪的是,这种呼声听起来就像幽灵从陵墓中发出的声音。 >>P 35<<

法权永不能超过社会经济制度以及由此经济制度所决定的社会文化发展程度。 >>哥达纲领批判 《马克思恩格斯文选》(两卷集)第二卷 P 22<<

负有社会主义改造任务的国家,只要被迫用强制的方法维护不平等——即少数人在物质上的特权,那么它就依然是一个”资产阶级“国家。即使已经没有资产阶级。 >>P 37<<

如果国家没有消亡,反而变得越来越专横,如果工人阶级的全权代表们官僚化而官僚们驾于新社会之上,那么,这并不是由于过去的心里残余的次要原因造成的,而是由于只要国家不能保证真正平等就会产生并支持拥有特权的少数人这样一种铁的必然性所造成的结果。

”牛奶是乳牛的产物,而不是社会主义的产物,你们实际上是把社会主义同一个乳流成河的国家形象混为一谈了,因此才不了解一个国家即使没有相当大地提高人民群众的物质条件,也能在一个时期达到较高的发展水平。“这几句话是在可怕的饥荒正在全国猖獗的时候写的。

马克思将社会主义界定为一种初级阶段的共产主义。这与我们对于社会主义的实际定义是不同的。如果要谈所谓的社会主义初级阶段(就是共产主义的初级阶段的初级阶段(笑)),实际上也不能够说是完成了社会主义。不论从形式上:工业是多种所有制共同发展,农业是以个体农业为基础的有限度合作,商业和服务业的绝大多数经济成分都是私有的;还是从理论上:没有完成生产力的高度发达,没有完成社会矛盾与需求的完全解决。我们都不能够自称为[一个马克思主义理论框架下的]社会主义。

很多人非常反对这一观点。他们指出,在中国特色社会主义理论体系的指导下,中国的经济、文化建设都取得了显著的进步。国民的物质生活品质和精神生活都比过去得到了显著的提高。现在中国的经济发展也处于世界领先的水平。

就如书中所说的,马克思认为共产主义的低级阶段是这样一个社会,它从一开始就比最先进的资本主义经济发展水平要高些。这个发展水平高不是指比10年前发展水平更高,也不是和19世纪的德国比发展水平更高。如果要这样比,甚至很难有国家达不到这一点。这个更高,是要和今天的美国比、今天的发达资本主义国家比,并且随着时间的推移这个水平还会水涨船高。

我们国家40年来改革开放取得了非常辉煌的建设成就。一举成为世界第二大经济体,这是非常值得称赞的。但是理论的建设却是在倒退。我们搞出几条红线,只要不压线,不管黑猫白猫,抓到老鼠就是好猫。仿佛社会主义只是一个名号,甚至连名号都不是了。

中国确实取得了辉煌的成就,但这样的建设过程,从联产承包到民营企业,形式上不是社会主义的,内核上更不是社会主义的。从资本的私有制度、剩余价值的剥削到仍然低下的生产力水平——这里的低下和上文所述的一样,不是与过去比,而是和今天使用资本主义方式进行生产的最先进的同类企业比——我们实际上,是在用革命家们拼搏出的公有资产赔礼,请回被革命家们赶走的资本家,好言好语求着先进的资本家们将我们建成一个先进的资本主义国家,并且妄想着等到成为真正的资本强国,拥有最先进的资本主义生产方式之后,再和蔼有礼貌地请资本家同志们放弃他们的财产,并宣布”我们建成了社会主义!“

社会主义它先进的地方在于,通过生产资料的公有制,来减少不必要的低效能劳动,通过社会广泛的合作,来更好的利用资源和工具。而不是我们因为马克思教给我们社会主义先进,我们就先进。更不是给资本主义冠个帽子就据为己有。

我们离社会主义还有很远的距离,而现在的政治理论更是伯恩施坦化了。很明显的是,我们的官僚,如同资本主义国家的官僚一样,可以从这样的经济体系下得到更多的好处,但是社会主义完成了,他们的好处就结束了,他们要兑现过去将手按在马克思著作上而发出的誓言了。那么想必这样的经济体系持续的越久,得到的利益就越大。这样看来”我们处于并将长期处于社会主义初级阶段“可以说是一颗定心丸了。

二分法——重复情形

作者 Victrid
2020年2月13日 17:56

假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

请找出其中最小的元素。注意数组中可能存在重复的元素。(Cf. LeetCode,2020)

这道题目被标记为困难。与题目基本一致但是被标记为中等的题目的区别在于数组中存在的重复元素

这种题目类似于找零点,早在400年前,牛顿就已经给出了二分法的解决方案。而现在,我们的基本思路也是二分法,这可以让程序运行控制在 $O(lgN)$ 的时间复杂度里。但是这种方法存在一定的缺陷。在数学上,这样的二分法是失效的。比如这样一个数组:

[3,3,1,3]

我们可以看到左边界、右边界和中点(不论是哪种取整意义上的,我们还可以加一个[3,1,3,3],没有任何的区别。)都是3.这样的数组,最小元素同时可能存在于左区间、右区间或者端点值里。这个时候,需要使用一些方法来在不变换的同时缩小区间。我们可以想到这个部分开始作枚举。

下面的代码使用了这一观点,但是通过巧妙的判断,将枚举的时间最小化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int findMin(vector<int>& nums) {
if(nums.empty())return 0;
return findMin(nums.begin(), nums.end() - 1);
}
int findMin(vector<int>::iterator begin, vector<int>::iterator end) {
// *begin>=*end,or not rotated.
if (*begin < *end)
return *begin;
if(begin==end)return *begin;
if(end-begin==1)return *end;
auto mid = begin + (end - begin) / 2;
if (*mid<*end)return findMin(begin,mid);
else if(*mid>*end)return findMin(mid,end);
else return findMin(begin,end-1);
}
};

每次枚举后再进行判断,这样使得能够二分的时候都在二分,平均复杂度就更接近 $Θ(lgN)$ 一些。

为什么要写博客

作者 Victrid
2020年2月13日 00:02

很多人的博客,基本上只有一篇“如何使用jekyll/hexo搭建博客”。顶多再来两三篇,就会放弃。

博客,在其他的领域里已经算是过时的一种媒体介质了。微博、twitter,人们已经不需要使用博客这种内容正式、形式繁杂的系统来完成他们的生活分享了。就算在仍然活跃的IT领域,(IT这个领域虽然技术变革比别的行业快的多,但是在某些方面还是很怀旧的。IRC(Internet Relay Chat)、telnet BBS论坛,也几乎只有IT领域的这些东西还在继续发挥余热,或者说,热度不减。)

网络上能看的博客越来越少。cnblogs,CSDN两大host充斥着整个search ranking(我不是说这两家上面没有优秀的博客和精彩的内容,但是劣币驱逐良币,绝大多数的博客都是个人的笔记本,对于前人的成果直接二话不说全文转载,再加上这些大的host本身内容就多,更容易获得高的search ranking,导致全网很多内容几乎都是重复的。),更多的内容创作者选择了更省心的创作平台,独立博客这种DIY的方式更是不被待见。

另一方面,很多人写博客的目的,很多时候也包含了希望别人看到、希望展示自己这一方面(其实我也有)。但是在现在的网络传播形式中,尤其是这样的新博客,往往只有作者一个读者。久而久之,往往很无趣,中途就放弃了。这甚至加剧了博客这种传播方式衰落的循环。


我的记性很差。只要不是一直在用的,基本上过了两三个月就会忘光。因此我认为写博客和记笔记写日记一样,都是记忆外部化的一个过程。同时书写的过程实际上也是一种对话,在问答里,思考能够得到具象化。哲学,据我的理解,也是一种具象化一个人的思想的思维训练。我的博客的目标读者实际上就是我自己。思想如果不付诸于实体,他就会像早上醒来的梦一样转瞬即逝。

因此我的博客里会有具体的学习内容,也会有闲谈和牢骚。有大佬提到说要想写一个成功的博客就不能光想着自己,要做你的目标读者想读到的内容。这个观点对于我来说是等价的。

我也是一个成果驱动型的人。而这个博客是我从git init命令开始一个字母一个字母地打出来的。(如果npm curl wget也算命令的话)有些时候光看看自己做的这个小widget,往往可以增强学习的动力。博客的好坏是全由我主观评价,而无关于他人的。而学习面对的困难与评价是客观的,我只能做到学习,分由别人打给我。做这些事情实际上是在短路我脑中的信心回路。


我不想半途而废地写博客,因为我过去做过太多半途而废的东西了(买域名的钱都已经掏了)。只要在写,我搭建这个博客的目的就达到了。

写作的目的是写作本身。[递归](笑)

本来新冠疫情情况就是如此,我呆在家里也没啥事干。

因此我不打算写如何用hexo搭建博客。搭建这个博客本身也是很简单的事情。另外又在使用hexo-admin插件,md的语法也没有什么难度,实在不行还可以用html写。另外,我如果不写这个“如何用hexo搭建博客”,是不是就不会最多两三篇就半途而废呢?(笑)

事情如果你真心想要做,自然就可以做好。

动窗法与前缀和——简单实践

作者 Victrid
2020年1月21日 05:38

题目是:

SJTUOJ 1002.二哥种花生

二哥在自己的后花园里种了一些花生,也快到了收获的时候了。这片花生地是一个长度为L、宽度为W的矩形,每个单位面积上花生产量都是独立的。他想知道,对于某个指定的区域大小,在这么大的矩形区域内,花生的产量最大会是多少。

首先尝试用最简单的遍历:

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
#include <iostream>

using namespace std;

int cc(int **matp, int m, int l, int h);

int main()
{
int m, n, l, h;
cin >> m >> n;
int **matp = new int*[m];
for (int i = 0; i < m; i++)
{
*(matp + i) = new int[n];
}
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
cin >> matp[i][j];
}
}
cin >> l >> h;

int max = 0;
int temp = 0;
for (int i = 0; i < m-l+1; i++)
{
for (int j = 0; j < n-h+1; j++)
{
temp = cc(matp + i, j, l, h);
max = (temp >= max) ? temp : max;
}
}
cout << max;
return 0;
}

int cc(int **matp, int xa, int l, int h)
{
int ret = 0;
for (int i = 0; i < l; i++)
{
for (int j = 0; j < h; j++)
{
ret += matp[i][j + xa];
}
}
return ret;
}

发现反馈是6/10 [Time Limit Exceeded]

本身在搜索数组很大的时候,同一个数上要重新计算次,浪费了很多时间。

后来想到,可以采用动窗法来解决。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//constructed using some macro and snippets
#include <iostream>
using namespace std;

int main()
{
//using glide window method.
int mat_h, mat_l;
cin >> mat_h >> mat_l;

//DynMatName:Pmat
//Lines:mat_hrows:mat_l
int **Pmat = new int *[mat_h];
for (int i = 0; i < mat_h; i++)
*(Pmat + i) = new int[mat_l];
int *Pmat_cfg = new int(mat_h);
//End of Dynmat.

for (int i = 0; i < mat_h; i++)
for (int j = 0; j < mat_l; j++)
cin >> Pmat[i][j];
int srch_l, srch_h;
cin >> srch_h >> srch_l;

//DynMatName:Resmat
//Lines:mat_h-srch_h+1rows:mat_l-srch_l+1
int **Resmat = new int *[mat_h - srch_h + 1];
for (int i = 0; i < mat_h - srch_h + 1; i++)
*(Resmat + i) = new int[mat_l - srch_l + 1]();
int *Resmat_cfg = new int(mat_h - srch_h + 1);
//End of Dynmat.

//construct 1st window
for (int i = 0; i < srch_h; i++)
for (int j = 0; j < srch_l; j++)
Resmat[0][0] += Pmat[i][j];

//construct horiz screener
for (int j = 1; j < mat_l - srch_l + 1; j++)
{
Resmat[0][j] = Resmat[0][j - 1];//get from left
for (int i = 0; i < srch_h; i++)
{
//column process
Resmat[0][j] -= Pmat[i][j - 1];
Resmat[0][j] += Pmat[i][srch_l + j - 1];
}
}

//screen
for (int i = 1; i < mat_h - srch_h + 1; i++)
{
//1st window
Resmat[i][0] = Resmat[i - 1][0];//get from upside
for (int j = 0; j < srch_l; j++)
{
//row process
Resmat[i][0] -= Pmat[i - 1][j];
Resmat[i][0] += Pmat[i + srch_h - 1][j];
}
//screener
for (int j = 1; j < mat_l - srch_l + 1; j++)
{
Resmat[i][j] = Resmat[i][j - 1];//left
for (int z = 0; z < srch_h; z++)
{
//column
Resmat[i][j] -= Pmat[i + z][j - 1];
Resmat[i][j] += Pmat[i + z][srch_l + j - 1];
}
}
}

//search
int max = 0;
for (int i = 0; i < mat_h - srch_h + 1; i++)
for (int j = 0; j < mat_l - srch_l + 1; j++)
max = max < Resmat[i][j] ? Resmat[i][j] : max;

cout << max;

//delete dynamic variable

//Release DynMat
//Name:Resmat
for (int i = 0; i < *Resmat_cfg; i++)
delete [] *(Resmat + i);
delete [] Resmat;delete Resmat_cfg;
//End of Release.

//Release DynMat
//Name:Pmat
for (int i = 0; i < *Pmat_cfg; i++)
delete [] *(Pmat + i);
delete [] Pmat;delete Pmat_cfg;
//End of Release.

return 0;
}

仍然是9/10超时。

后来看到,如果不使用原数组,而是建立一个前缀和数组(Cf. FineArtz, 2018),用来存储从(0,0)到这个点的所有数字和。这样计算取得了比较好的效果。

容易分析得到,动窗法适合一个固定大小的窗户的情形,但是在窗户的大小会发生变化时,需要重新对整个窗户进行计算。在此题中,窗户按不同大小有n^2个,这样做效率会比较低。

而采用前缀和的方式,在经过O(n)的one-pass计算后,每次取值都是O(1)的。

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
#include <iostream>

using namespace std;

int main()
{
int m, n;
cin >> m >> n;
//DynMatName:Summat
//Lines:m+1rows:n+1
int **Summat = new int *[m + 1];
for (int i = 0; i < m + 1; i++)
*(Summat + i) = new int[n + 1]();
int *Summat_cfg = new int(m + 1);
//End of Dynmat.
int getnum;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
{
cin >> getnum;
Summat[i][j] = getnum + Summat[i - 1][j] + Summat[i][j - 1] - Summat[i - 1][j - 1];
}
int l, h;
cin >> l >> h;
int max = 0;
int total = 0;
for (int i = 0; i < m + 1 - l; i++)
for (int j = 0; j < n + 1 - h; j++)
{
total = (Summat[i + l][j + h] + Summat[i][j] - Summat[i + l][j] - Summat[i][j + h]);
max = total > max ? total : max;
}
cout << max;
//Release DynMat
//Name:Summat
for (int i = 0; i < *Summat_cfg; i++)
delete[] * (Summat + i);
delete[] Summat;
delete Summat_cfg;
//End of Release.
return 0;
}

这么久重新回来重写这篇文章,其实优化一个算法的核心就是要减少重复,拿空间换时间。(这个题目甚至没有使用更多的空间)有名的Strassen算法,仅仅是减少了一次计算,都能够带来很大的 理论上的收益。(虽然现实意义很小,还不如硬算)

❌
❌