旅行商问题的一点笔记

最近朋友跟我讨论了一个类型的旅行商问题。原版的旅行商问题是说,给定一个图,给定起点和终点,求不重复经过任何节点的最短路径,遍历全部节点。而他的问题是,仍然不许重复经过任何节点,但是不要求路径遍历全部节点,而是遍历某一个给定的关键点集,求最短路径。问题里面对图没有做任何限制,图可能包含数千节点,不过关键点集是充分小的,比如只有5个关键点,这样。

一开始我没听清楚题目,以为这个问题跟斯坦纳树有什么关系。(所以才会写了上面一篇note。。)不过后来知道不许重复经过节点,那么这种想法就没有用了。

不过既然关键点集充分小,求的又是路径,那么几个关键点必然只能依次经过。可以尝试所有关键点的排列,依次求最短路径。比如说5个关键点,也只需要算120次。还算可以接受吧。并且假如采取分支定界,其实还是可以减去很多分支的。当然可以构造出几个关键点无论怎么排列,路径都等长的情况。比如所有两两点间都有路径长度为1的任何图,都是这样。。。不过假如是工程上使用的话,可能还有其他办法可以提前退出,比如求出“足够好”的解,或者算了很长时间,一直没有找到更好的解,之类的。。这个就可以根据需求而定了。

不过朋友比较担心的是割点的问题。割点是说,一旦去掉这个点,则图会被拆成两个独立的联通分量。就是说从图的某些点出发,永远也无法到达图的另外一些点了。。之所以担心这类点,是怕一旦算法的前面走过了这类点,那么由于存在不许重复任何节点的限制,可能导致某些点无法经过了。那么这个割点至少有两种情况。就是这个割点会导致关键点被割掉,或者只会导致非关键点被割掉。

导致非关键点被割掉的那种割点,可以直接收缩为一个点。因为题目并不要求遍历非关键点。并且求出的路径绝不可能进到那个割点后面去(因为1.里面没有关键点,2. 进去就出不来了)。那么至于说会导致关键点被割掉的情况呢?暂且也把他收缩为一个点吧。如果不断这样收缩,整个图就会最终退化为不含割点的图。并且最后一次收缩的割点应该不会超过两个。假如超过两个,那么这个图是无解的。因为总有一些关键点,你要经过一个割点才能进去,然后就出不来了。至于不超过两个,那是说起点和终点可以从割点经过。当然这个是欧拉说的啦。。

比如上面的图,每个椭圆表示里面有一坨点,并且里面的每个点都至少有两个入度。假如上图的三个小椭圆里面都有关键点,那么这个题就无解了。假如任何一个小椭圆里面没有关键点,那么就可以直接去掉。

最后肯定只有不超过两个小椭圆里面是有关键点的。比如上图的红色椭圆。可以暂时收缩成一个点。

所以最后一次收缩的割点,不会超过两个。

那么其实这种收缩可以重复进行,于是可以写出递归代码来解。当然现在问题就转化成,对于没有割点的情况,怎么解这个问题。

当然其实问题的难点,也正是在没有割点的情况下吧。。还是继续前面那种遍历所有关键点顺序的那个思路,这回每次需要考虑的关键点都变少了(好吧也可能没变少,至少不会变多!!)。接下来的事情可以参考旅行商问题的一般解法来求吧。。因为不是我的题目,所以没有继续去查。。同样是做个记录留下来吧。。还是方便以后捡回来。。

斯坦纳树的一点笔记

大三的时候做了一道斯坦纳树的题,最近几天朋友跟我讨论某个问题,忽然觉得跟那时候的做法很类似,结果发现自己没有留下任何材料。想了半天把那时候的想法回忆起来了。当时没有留记录是因为想法太简单,算法性能太差。不过现在想想无论好坏,啥想法都应该记下来,以后至少可以在这个基础上继续走。

Steiner Tree, 就是说给定一个图,和他的节点的一个子集。求包含该子集内所有节点的最小生成树(不要求包含全图的所有节点)。其实是算法老师留的作业,那时候也没当回事周末随便写了点。结果发现这个居然是一个NPC问题。只好临时想了一种解法。还是沿着prim算法的思路走,每次向已生成好的树里面增加一个新节点。只不过这次,每次都只增加关节节点集里面的节点。而寻找节点的时候,需要用Dijkstra算法。并且找到需要加入的关键点后,也需要把路径上的其他非关键点也加入进正在生成的树上面去。说到Dijkstra算法,复杂度是n^2, 再加上prim算法复杂度也是n^2,复杂度岂不是到达n^4?其实没有。因为每次使用Dijkstra算法,都把某一个点到其他所有点的路径长度都求出来了。而最终的结果,最坏也不过把所有节点两两之间的最短路径求出来,相当于填了一张n×n的表,n是节点数量。用一次Dijkstra算法可以填一行。总共n行所以复杂度是n^3。恩这里算是用到了动态规划。性能还算是可以接受吧。不过这个算法明显求不出精确解。我也没有证明过他求出的解究竟能多逼近,或者在哪些条件下能求出足够逼近的解。只不过是当时老师给的样例输入正好碰上是返回正确结果,就这样交卷了。

恩后来又在网上查了很多资料,貌似斯坦纳树研究的人还是很多的。不过搜到的资料大多是些没法下载的论文。。。所以也就荒废了没有继续看下去。。今后有机会在继续看吧。这次只是把过去的思路记录一下以后方便捡起来。

协作开发和二进制管理

因为经济的原因,协作开发一直是仅做源代码管理,由开发者checkout代码后自行编译。

但在有大笔资源的情况下,时效应该是更为重要的东西,为什么不考虑二进制管理?

在公司,一个项目编译出来需要3到5个小时。。(会跑来这里写东西的原因也说出来了。。)。。反正提交代码之前每个人都必须做build, 做测试,然后才能提交的,为啥不一开始就把大家build的结果保留下来,需要用的人自取呢?

这正好可以跟系统每天的daily-build和test-suite结合起来,一起完成。

开发过程还是一样,checkout, update,之后提交。但这个时候提交的内容不要直接进main branch. 先在他checkout的版本基础上,patch上他的update,之后做buddy-build和test-suite。如不通过,打回重练。这个过程可以以天或者以周为单位。以天为例吧。每天晚上12点,系统尝试将通过测试的patch进行合并。如果合并失败的话,就把导致合并失败的patch拿掉,直至合并成功为止。如果成功合并,进main branch, 并且做daily-build和test-suite。假如合并之后导致新的bug或build break产生,应该知道这些问题是协作造成的,将受影响的文件或模块相关的patch除去,重新build-test。直至没有bug产生为止。次日早晨发邮件给没进main branch的patch的owner们,让他们解决问题。

关键在于编译。在编译脚本里面可以加上新的机制,允许从服务器下载跟本地版本相符的obj,lib,dll,exe等内容。即编译器需要和源代码管理器协作,确定即将编译的一段代码是否被edit过。如果没有被edit过,检查目前版本的代码在服务器上是否有相应的二进制目标文件。如果有就可以直接下载,如果没有才考虑自己编译。这个过程跟编译顺序相反,是自顶向下,从最终目标开始比较,尽可能下载整个的lib,dll之类,这样就可以避免下载很多可能本地根本用不到的obj文件。

当然,由于二进制文件没有有效的增量备份算法,在服务器上不可能保存所有版本的二进制文件。但至少可以保存最近几天或几周的版本的,还有历史上的几个milestone版本的。

当然在这种机制下必须要求所有人都在最新版本下工作,否则必须新开branch。每天的patch都只能接受从当天版本号基础上checkin。如果有旧版本的patch提交,可以有两种选择,一种是要求开发者更新到最新,然后重新提交patch,另一种是要求开发者开新branch, 并在新branch上做开发。具体选择哪一种看具体需求以及开branch的成本了。

毕竟当项目变大,每个人要处理的模块都是很小的一部分,其余的部分只需要lib,dll就可以了,没有必要自己编译。并且,如果不需要编译,其实开发人员的机器配置就没有必要那么高,节省的成本可以做更好的服务器机器。其实这种策略也更符合所谓“云计算”。当然更加有趣的是,可以让编译本来就发生在由所有客户机结成的计算网格上面。并且所有的二进制文件也都分布式存储在这些分散的机器上面。服务器仅仅是一台或几台管理信号的中转路由。这样做可能可以进一步降低成本,并且对于每个计算网格而言,本地编译某个模块,然后二进制文件就存储在本地,也没有太多网络开销。当然具体算法如何实现,这个恐怕要仔细研究了。(研究生课题?:)

图形系统设计心得

最近做的两个项目都跟图形系统设计有关系。慢慢的总结出来一些体会,记录下来以后方便自我批评。。

假如让我写一个操作系统的话,我绝不会把图形界面放到内核里面。否则图形界面挂起居然会导致系统死机,这太恐怖了。

然而,提供一套统一的方式让应用程序管理自己的图形接口是绝对正确的。提供API给应用程序的方法很纯粹,但是也很危险。经常遇到的各种程序挂起,不能更新界面,不相应鼠标等等问题,其实都是事务处理和图形处理过度耦合造成的。他需要事务处理和图形处理以协作的方式由应用程序自行实现。假如应用程序的winproc里面写一个死循环,那么界面更新是无论如何也做不了的了。这样的一个操作系统,自然而然会得到“不稳定”的美名。就算仅仅是他的应用程序不稳定,也是你把“不稳定”的权限交给他的。

最近做的项目是widget嘛,其实这个东西完全可以做整个操作系统的图形界面。让图形界面和事务处理在两个分离的线程里面完成,这是非常必要的。当然你必须提供一套合适的信号系统,让两个线程得以合理的通讯。这会遇到很多复杂或者可能会有很多bug的问题,不过这些问题可以慢慢研究嘛。我们先看好的方面。

首先就是可以用类html的东西来定义显示形式,首先html已经非常成熟,有大批熟练程序员,所以用这个东西的成本最低廉,另外这种东西也方便修改,同时修改又对已经编译好的程序没有影响,也就是说界面换肤会成为一种非常低成本的事情。应用程序内部逻辑其实是完全的事务处理,做一些重要的事情。然后界面和这些事务处理采用js之类的脚本语言控制,方便开发维护。

是否需要一个脚本-二进制粘合层是一个可以商量的问题。其实最基本的就是让应用程序定义一套界面接口的名称,并在内操纵这些接口。这些接口可不是原始的图形渲染逻辑,决不能让应用程序去做渲染的实质工作,这套接口应该是充分抽象的。比如应该定义出listbox, textbox, checkbox等类型,要避免应用程序自己去考虑绘制的实际问题。应用程序只需要说明逻辑。向某个列表里面插入数据,或者显示某幅图片。具体的渲染逻辑要放到图形引擎里面去做。

为什么要充分抽象。因为渲染是一个全局的事情。尤其是加上半透明处理之后。渲染不能光顾着自己的程序,还要和其他的窗体融合。单独的应用程序是不应该知道,除了自己之外操作系统还有那些其他窗口的。他应该只顾着自己。但是图形绘制又需要跟其他窗口耦合,因此这两部分事情绝不能在一个单一的模块里面完成,否则一定会出问题。哪怕应用程序已经死循环了,当鼠标移动到他的界面上的各个按钮的时候,按钮仍然应该拥有原有的视觉效果,mouseover, mousedown, 这些跟应用程序无关。窗口应该还是可以拖动,可以随时关掉。不能仅仅因为应用程序在buzy,就连最基本的图形显示都做不到了。只有把图形渲染的任务交给另外一个线程来做,这才是有可能的。

并且将listbox之类的图形接口由统一的引擎来完成,还会有很多额外的好处。就让我们关注listbox吧,因为要包含大量数据的东西,或者说“容器”,需要考虑的东西往往比第一反应能想到的东西要多。比如你的listbox还有10K数据,但一次只能显示100条,你会一次就把这些数据都读取到内存吗?显然不能。我们会采用cache。然后所有cache相关的东西都要往里面放,什么时候读,一次读多少条,什么时候写回,脏比特位,等等。然后listbox还有滚动条。滚动的时候应该怎么做。动画怎么显示。等等等等。每个含有这类逻辑的应用程序都去重新实现一遍这套逻辑是不可思议的。你会说用库嘛,用MFC, 用ATL,问题没那么简单。因为这些东西的实现是复杂的,需要考虑的问题很多。往往是使用不当的时候你又不知道究竟哪里用得不当。文档永远是不够详细的,同时,用户是永远都不会看文档的。并且文档越详细,用户越不会看(太长了)。因此文档是没有意义的。需要提供服务,而非类库。让这些复杂难解的事情由别人直接解决掉,从一开始就不要把跟渲染相关的东西或编译或link到应用程序里面来,从物理上就要做decoup,应用程序只需提供数据和逻辑就好了。

当然这可能又会产生新的问题。比如数据提供的接口怎么定义。同是listbox,数据可能来自内存,可能来自文件,可能来自数据库,可能来自网络,等等。 需要提供一个通用接口绑定界面和数据,要保证使用字符串就能定位到相应的属性名(比如可以采取类似IDispatch的模式)。之后又应用程序自己实现这个接口,实现内部就可以随心所欲了,网络怎么取,数据库怎么访问,等等。但是从性能考虑,你还不能仅仅定义一个单独item数据访问的接口。你需要让应用程序一次就返回给你批量的数据。比如你有10K数据,但你要应用程序只给你从第200条到第300条。需要有这类区间型的接口。另外还需要有数据写回的接口,比如某个属性可以设置成可写的。然后数据就变成一段cache。当析构函数被调用的时候,先写回数据,然后析构。

一旦这些接口被定义好,让脚本来控制接口就变得简单。无非是在不同接口之间不断回调嘛。并且这样的脚本还可以编译成二进制代码段。其实也就是把一个一个的func call组合起来而已了。

同样的,类html的解析其实也是可以编译出来的。你解析过一次html之后,生成了一段图形类的thunk,你只需把这段thunk二进制写进某个dll,要用的时候直接读进来就可以显示了,无需反复parse html,性能其实跟纯用系统调用写出来的程序性能不会差很多。

当然这样的应用程序就不再是一个exe文件。他其实是一个dll。或者通用的说,它是一个服务。整个操作系统都是基于服务的。系统底层提供访问硬件的各种服务,图形引擎提供图形处理的服务,应用程序提供事务处理和交互逻辑的服务。而exe其实是一个简单的binder。他把不同的服务启用起来,在之间传递各种消息(因为不同的服务是跑在不同的线程里面的,所以需要消息)。前面说的那么多图形处理和数据访问的接口,都应该定义在这个exe里面,实质上就是向不同的服务发送不同的请求,并且返回应答而已。这样设计的结果不仅仅减少了耦合,增加了系统鲁棒性,减少了程序员的压力和重复代码,并且也大大增加了程序的灵活性。比如一个阅读器,本来是读取硬盘上的文件的。界面无需变化,只需要实现一个新的获取网络数据的服务,就可以变成一个读取网络图书的在线阅读器。如此之类的可能性还有很多。并且应用程序因为都采取统一的接口,因此进程间通讯也会变得更方便,增加新的UI也会更简便(不是说GUI),比如原来是鼠标点击的程序,要想增加一套用键盘或者用啥啥新式设备的交互逻辑,也会很简单。

好吧,其实以上说的这么多东西也没有什么创建性,所谓MVC嘛,几十年前就有了。只不过从来没有仔细想过它究竟意味着什么。这次实习还有写毕设,慢慢让我对这套东西理解深刻了。只是自我记录记录。以后发现有错了回过头来批评批评,之类的。

以上

论牛人

记后记:本文纯属个人心灵探索…观点纯属个人愚见,没有妄加评论别人或者妄图要求别人按照自己想法去做的任何意图…

最近跟几个同学聊天。。都碰上了牛人这个话题。。

先是在buzz上不小心跟不认识的学弟言语冲突。。之后又跟人聊天时对牛人产生了鄙视情绪。。

虚荣心。。虚荣心在做怪。。

膜拜牛人是一种病。当然,有些牛人是不得不拜的。他们达到了神的境界。但是我现在已经堕落到见牛就拜的程度了…承认一个人很牛,表示自己对自己达到他的实力缺乏信心,如果仅是没有胆量或者方向不同(比如程序员崇拜一个诗人)还情有可原,否则其实一种惰性。是的,他很强,我比不上他,我认输。表面上看着很谦虚很大度。其实仅仅是懒惰而逃避罢了。

但是反过来讲,否认牛人,装出一副鄙视全世界的样子,一样是病。“切,那算什么”。其实自己根本做不到。这纯粹就是装B。。。

应该对牛人抱有什么样的态度呢。。应该是对对手的尊敬。要敢于挑战,也要勇于认输。但是认输不是服输。“我早晚要超越你”。

牛人有很多种类。有些牛人确实是牛。他们有伟大的想法,他们创造了奇迹,他们构造了整个世界赖以生存的基础,他们的贡献使每个人受益。对这些牛人,我们深深的膜拜。

但并非所有的牛人都是这样。很多牛人并非是看起来那么牛。之所以如此,是因为人们看到了他的成绩和辉煌,却没有看到他们达成成绩的过程。过程是艰辛的。假如你看到他达到辉煌的困苦历程,你对他的态度不会是崇拜,而会是尊敬。就像魔术,你不知道原理的时候,以为是超自然力,或者魔法,或者什么不可思议的奇迹创造了这一切。你别无选择,对魔术师产生了深深的崇拜,以为他们无所不能。但当你看到他们练习魔术的艰苦,设计魔术的苦思冥想,以及了解了魔术实现的原理,你或许不再崇拜他。他从神坛上走下,他是人。但你仍然保有对他的尊敬,因为他的智慧和勤奋。

然而魔术师不会告诉你魔术的背后。当然这没有错。因为他们依靠这种信息不平等造就的魔力吸引观众。这是他们的职业。

某些种类的牛人也是一样。他们强大,可他们的背后有艰辛,有运气,还有别人的帮助。假如你知道他是怎么做的,并且你付出跟他相同的努力和代价,或许(这要看运气)也能达到他的程度。等你知道这一切,你不再把他当作膜拜的对象,而是敬佩他的付出和代价。

甚至还有一类牛人,他们的牛完全是装出来的。他们采取别人听不懂的措辞,他们故作高深。他们深居简出,不让别人看到他们。于是别人以为他们高深莫测,就认为他们是牛人。(在此强调,并非所有深居简出的牛人都是装牛。。而是说某些种类的装牛会采取此策略。。)对于这类牛,尊重他们装牛的选择,既不鸡肚他们不费什么力就得到牛人的光环,也不非要让他们下不了台,以此为乐,那就对了。因为鸡肚是源于自己的惰性(“如果我像他那样省力讨好该多好”),而扯破面皮则是自己的虚荣心在做怪了(“凭什么我不如他”)。

之前说了句,觉得真正的牛人应该是傻乎乎的那种类型。这是电影看多了吧。。不过我的话,确实是欣赏那些不摆架子,不故弄玄虚的牛人。他们会在自己力所能及的范围内帮你(他们强大的力所能及啊。。),甚至热心的授之以渔,但超出他们的范围的时候他们简单的道歉,并且告诉你解决问题的途径(某个领域专家,图书馆或网络,继续练习和吸取经验,或者只是简单的不可能)。而不是在力所能及的时候卖弄炫耀,在力所不及的时候故弄玄虚。

至于我自己。我承认自己虚荣心重得搬不动。。被人哄一句牛人就轻飘飘了。。说不定他还是冷嘲热讽我都没听出来。。举世誉之而不加劝,举世毁之而不加沮。。这确实很难做到啊。。被人哄也没啥可高兴的,被人讽也没啥可不高兴的。总归自说自话自做自事就对了。

有的时候在想,为什么我这么争强好胜,为什么对“牛人”这个话题这么敏感。我努力学习,努力工作,是不是只是为了让大家觉得我很牛,尊敬我,甚至崇拜我呢。我是不是故意把自己光彩的一面给人看,而藏起自己的窘状和搓态呢。这就是在装逼啊。。。包括我在写这篇博客,都是在掩饰自己的虚荣心啊。不是吗。这篇博,不就是因为自己对“牛人”这个词敏感了,虚荣了,鄙视了,才写出来的吗。。

不能这个样子。。。

学习,工作。是为了什么呢?

付出。

绝对是付出。

这个问题想了好久了。人要吃饭。人要穿衣。人要体面,人要虚荣。但仅仅是为了要这些东西而活着吗。那人跟怪兽有什么区别。[人形怪兽出没注意]

吃饭穿衣是为了活着。活着是为了付出。体面虚荣,游戏音乐,是为了高品质的活着。高品质的活着是为了高品质的付出。归根结底,人是为了付出而活着的。

一定是这样。[我无法证明这一点诶。。摊手]

真正的牛人不是别人怎么看待他,而是他付出的数量和品质。

我不是牛人[因为还没付出过啥],也不想[不应该想]当牛人[是指受人追捧的那类气场型牛人]。但是我要尽力付出更多。为了这个目的,要开开心心的活着。

SC AI 估值函数

SC AI还是没有进展。

最近在想办法找出合适的估值函数。生命值,兵数,总输出。这些固然没有问题了。然而最重要的位置信息,却得不出合适的估值函数。如果不能建立好的模型的话,一切算法都是空谈。。。

今天忽然想到的,其实是跟以前的想法一致,不过换了一种方式在思考问题。

micro radar for the SC AI
图中环状圈表示中心的蓝色单位距离每个红色(敌军)单位的距离。在这个距离内,每增加一个敌对单位减1分,每增加一个友方单位加1分

假如能够测量出每个单位攻击一轮所需要的时间,该单位能够移动多少距离,则更好。以此可以计算出优势或者劣势能够保持多少轮,从而得出优势或者劣势多少单位的HP。

另外忽然发现开全图的比赛情况,你甚至可以知道对手每个单位的目标是什么。这样甚至就无需猜测,并且被设置成敌军目标的单位,其实反而掌握了主动权——他往哪里移动,敌人就会追到哪里。并且由于大家的移动速度都是相等的,被追击的单位有权利选择在什么位置开战——他停在哪里,就在哪里开战。因此问题就可以简化成,(对某一个单兵而言)在什么位置开战,对我方更有利(比如获得更大的优势或者使敌人获得更大的劣势)。

还有最近在考虑的就是一些图论的模型是否可以放到这个里面来。比如最小生成树。最小生成树在这个AI里面看似有某种意义。至少他指示了所有兵力集结到同一个地点的最短方案。他也可以表示友军之间互相援助的行动路线。但是其他的意义呢。现在还看不清楚。

另一个图论的模型是steiner tree,  它表示了将一坨兵拆成两个组的方法,使得拆开后的两个组的最小生成树最小。或许这个模型可以用于暴露敌人的弱点。当然steiner tree是NPC问题,所以把它拿进来可能对比赛也没什么帮助。不过话又说回来比赛过程中出现的单位数量也不会特别多,尤其是开始的时候。所以即使使用最蠢的算法,或者使用较平庸的近似算法,对AI有帮助也是有可能的。

但是究竟怎样帮助。。暂时还没想清楚。。

过年回来没干劲了

博客也荒废了

不想干事情的状态再持续下去的话,整个人就会完全废掉了。

其实需要干的事情很多很多啊。结果都扔在那里。然后自己一个人呆在一边发呆。

需要动力啊。

最近在做图形库的接口抽象。之前做的widget引擎和图形库的耦合过于紧密了,现在想要加上DX的支持的话就相当于整个项目重写一遍。没有办法,只好想办法把图形库的通用接口抽离出来。之后用DX实现一下那套接口。说起来貌似挺容易。但是从哪个层次抽离接口呢。由于我们原来使用的是Cairo图形库,最初的想法是把Cairo的每一个API都当作是接口的一个函数。这样做改动不大,原来的代码可能几乎不怎么动就能把接口抽离。问题在于抽离了接口之后,怎么去实现…Cairo是一套平面矢量库,DX的API跟它差的十万八千里。要用非常别扭的方法才能实现那些接口。那样做的结果只有性能下降。本来用DX是要提高性能的,这样做实在得不偿失。

于是只好在较高的层面来做抽象。分析widget里面每个DOMObject衍生类使用到Cairo的函数,发现主要使用到的其实只有render和translate两个函数(其实translate本来也是属于render的一部分,后来因为通用性强才抽出来做独立的函数的),除此之外还有一些使用到的,比如getCurentRect,或者img::setSRC,这些函数其实本可以不使用Cairo,比如setSRC完全可以只是设置一个字符串路径。 到render的时候才去读取文件。这样一方面能提高些许效率(假如有人反复setSRC却不显示那个图片,其实本不需要读取那个文件),另一方面也能减少DOMObject和图形库的耦合。至于getCurrentRect之类,应该在每次render的时候将位置,大小还有mask(用于hittest)的信息存储在某处,想要获取的时候直接去用就好了。这样做就几乎把所有跟图形库的耦合全都缩小到render函数里面了。之后只需写一个公共接口,然后用工厂方法对不同的DOMObject子类生成不同的render实现传指针进去就好了。这样在有DX支持的系统上,可以使用DX,而在没有DX支持的板子上,程序可以动态适应,换用Cairo版的render实现。比较灵活。

这两天总算拖着拖着做了这么一点,接口基本上抽离出来了,但是抽出来之后发生了一些bug, 主要是TextArea和Colorize部分出了问题。争取尽快把bug解决掉好提交代码吧。。还有太多太多事情等着我做呢!!

SC AI – 微操

这个命题想了好久。实在不容易定义一个准确清晰的模型来描述这个问题。

从简单的开始吧。

首先SC地图是米字格拼成的,从每个位置向外都有八个方向。当然很传统的,每个位置都可以视为可通过或不可通过。除此之外,在可通过的位置上还有一些比如高度,是否掩护之类的一些属性需要考虑。暂时先讨论开阔地吧。

从最简单的情况开始考虑。比如红蓝两方各有2个狂徒,如何微操可以尽快消灭敌人呢?进一步简化问题,令红方谋求进攻,而蓝方决定防守。当然是将两个狂徒同时攻击对手的同一个狂徒。同时尽可能防止对手的两个狂徒攻击自己的同一个。如何做到这一点呢。假如红方在不断移动,而蓝方仅仅是站着,直到红方攻击蓝方才给予反击,那么,红方应该移动到怎样的位置时就可以决定攻击了呢?应该是红方的两个狂徒都能移动并攻击蓝方的1号狂徒,而蓝方的2号狂徒移动并加入战斗所需时间至少比1号开始同时遭受红方两个狂徒攻击晚一轮攻击所需时间。这样红方就能在攻击中占得至少一次攻击的便宜。由此看来,蓝方必须保证自己的两个狂徒之间的距离不超过红方任何狂徒到己方两个狂徒的最大值的最小值(好吧我语无伦次了)。解释一下就是红方两个狂徒到蓝方一个确定的狂徒之间的距离取最大值,而在以两个蓝方狂徒为目的地的距离之间取最小值。

橙色线表示的就是红方两个狂徒到蓝方狂徒距离的较大值。要在这个较大值中再取较小值。

显然为了做到这一点,蓝方必须移动。蓝方必须移动而保证两个狂徒间的距离没有对手到自己的距离远。这很容易做到。只需两个狂徒尽可能贴近就好了。此外当红方试图接近其中一个蓝方狂徒的时候,蓝方受到威胁的狂徒(被取了最小值的那个)向后移动绕到另一个狂徒侧面,从而使得两个距离变得尽可能相等。

假如蓝方使用这种防御方式,则红方将很难找到理想的攻击位置。但是由于蓝方为了保证距离相等必须不断移动。在移动过程中总会出现与红方距离不和谐的状态。红方只需抓住机会尽快冲上去就好了。当然蓝方鉴于此,一定不会简单的同红方对拼。而会不断后退避免冲突。无论怎样。这个最大值的最小值可以作为一个合理的AI指导原则。至于进一步的讨论要等实际测试之后再做分析了。

接下来讨论一下更多的狂徒群P的情况。想要找出一个完美的模型非常不易。可以用一条边将处于视野范围内的狂徒跟自己连起来。这样所有距离较近的狂徒就能连成一个无向图。边长表示狂徒的距离。前面说到的最大值的最小值可以用一些图论算法找出来。可是现在的问题变得非常复杂。因为之前仅考虑两个狂徒同时攻击某个单独的狂徒。而现在却需要考虑令1~n个狂徒同时攻击一个狂徒。这有n的组合种方式,还要乘以敌对狂徒的数量。这过于复杂,不可能实时计算。

采取机器学习很可能是必要的。可是如何构造决策树和估值函数却非常困扰。

现在能想到的一种简单的策略是将手下的狂徒两两结对。之后每两个一组去捕猎对手的某一个狂徒。当然过程中可以决定切换目标或者撤退。但是这种策略很可能无法发挥狂徒数量多的优势。或许可以进一步将每两组狂徒编为一个小队,每个小队去尝试捕猎对手的某2个狂徒,然后再把这两个目标分配到小组。这就形成了树状结构,像军队那样层层管理。或许这样的主意会不错。至于如何编制,以及各个级别的AI如何实现,还是等测试一段时间再总结讨论吧。

我和我的编译器

“要不然…要不然…今晚再编译一次?明早我过来看结果,好不好?”

“想编就编呗”,她漫不经心的说。

5个小时的等待…在得到确定答复之前,你永远都不会知道结果。“她编译通过了吗?”,你嚼着米饭在想。“她编译通过了吗?”,你一边走路一边想。“通过啦!通过啦!”,你从梦中发出会心的微笑。然而第二天去那滴滴答答闪着光标的屏幕前,你一定能发现一行行刺眼的红字。

我发现我的编译器是个女孩子,无论我怎么追求她,她总是拒绝我。

Don’t know how to build #^$&@.lib

光标假装认真的一下一下的闪着。“我跟你说过多少次了!”,这次我真的有点发火,“按照makefile执行!”她一定知道怎么编译的。她只是想试试我。我这样暗自想着,并且忍住偷笑。可是抬头看着那闪烁的光标,这次看起来她是认真的。她真的不知道怎么编译了。为什么在服务器上她每次都能顺利编译通过。在我这里就不行。或许她只是想要一个更刚猛的CPU,更宽敞的内存和更顺畅的网络。这是我所没有的。

重新检查,makefile, sources还有dirs,我完全没有碰过,原原本本的从服务器上拷贝下来。可是根据编译脚本本该编译出来的lib就这样神奇般的在编译进几十个exe之后,从硬盘上消失了,连一比特的碎片都没有留下。这不可能。这没有道理。要不我去单独编译一下lib?那不行,那需要太多依赖了。他们都在之前的编译环节中被删除了。我也许可以跟别人借一个lib过来。不过编译环境选的不一样,这样编出来的东西很可能不能跑。

“要不然…要不然…今晚再编译一次?”故事就这样进行着。

“现在我正编一个debug版,但是retail版也需要编译一下。一起编译可以吗?”

“没问题”,她望着窗外。

我没有自信。不过她看起来没什么不愉快。“那劳驾你了。”

第二天,不仅retail没编出来,连新来的debug也fail掉了。

Can’t open file ^*%%&*, check if other progress have opened it.

她不能容忍在我的机器上同时运行其他编译进程。

近来我发现,原来奇迹般的清除编译好的lib文件的,都是该死的杀毒软件!我错了。我不该让她跟杀毒软件呆在一起的。我把杀毒软件彻彻底底的从硬盘清除掉。就如同他删除那些lib文件一样,不留一比特碎片。还好她不会不依不饶,事情过了也就原谅了我。

我最喜欢卡夫卡的树,在雪地上横躺着一棵树。他看起来漂浮在雪面,似乎轻轻一推就会滑落山崖。不对。他已经深扎大地,深埋雪底。不过,那也仅仅是看起来如此而已。

你永远不知道下一次能否顺利编译成功。

不过,经历这些事情,我更加牢记了程序员的三大定律:

1. 她永远不会犯错

2. 如果犯错都是我的错

3. 我一定会犯错

以上

SC经济学

今天考虑了一下最基本的SC经济学。

我把这个问题设定为:在给定的时间Te之内,怎样做才能得到最大化的采矿量。

首先不考虑卡人口卡矿的问题,就是说一直不停的造农民,不造水晶,造农民的时候钱也总是够用。那么需要知道的几个时间是造农民的时间Tscv,农民采到以块矿的时间Tg,他还细分成移动时间Tgm和采集时间Tgg。于是矿从和基地的距离Lm和农民的移动速度Vscv成为需要知道的参数。Tgm = Lm/Vscv。Tgg和Tscv可以在游戏中实测得到。那么我们的第一个问题就是一个矿从需要多少农民。根据经验的话,当基地和矿从的距离最近的时候,一个矿从3个农民比较合适。如果用计算的话,就需要知道,采矿时同一个矿从的几个农民之间的距离是多少。想象两个农民都挤在一个矿从前面,一个开始采集,一个等待。当采集的那个农民离开的时候,等待的开始采集。经过了Tgg时间,离开的农民移动了Tgg Vscv距离,而等待的农民恰好开始移动。所以一个矿从所能容纳的农民数量是2Lm/(Tgg Vscv)。向下取整是经济值(没有农民等待),向上取整是最大值(每个矿从最多有一个农民等待)。更多就是浪费了。

那么在这个理想模型下,到达时间Te能采多少矿呢?到矿从饱和之前,假设训练了n个农民。这一共花了n Tscv时间。在这段时间内的采矿量是个等差数列求和,因为从第5个农民开始,每个农民的采矿量都比前面一个少Tscv时间所能采的矿量。总共少了(n-4)(n-5)/2这么多。所以总的采矿时间就是T’ = Tscv(n·n – (n-4)(n-5)/2)。在加上矿从饱和之后的采矿时间n(Te-T’), 可求出总采矿时间Ttotal。再用采矿量M=Mg Ttotal/Tg可求出总的采矿量(Mg=8,每个农民一次的采矿量)。

现在我们来考虑到Te时矿从没有达到饱和的情况。这个样的话应该什么时候停止造农民呢?因为一个造一个农民的消耗是Mscv=50。而一个农民一次采Mg=8的矿。因此要让这个农民“盈利”,需要他采Tg(Mscv/Mg)的时间,向上取整是7Tg。也就是说,假如准备造第n个农民的时候,所剩的时间小于7Tg,则应该停止造农民开始憋矿。这样的话就应该造(Te-7Tg)/Tscv个农民,向下取整。仍然可以用前面的采矿时间-采矿量方式计算总量。

我们继续考虑卡人口卡钱问题。。啊我前面农民都用scv表示了,就这么将就吧。接下来以神族为主,因为神族的建筑方式比较简单,并且我们参赛也将以神族为主。其实经验告诉我们,当Te比较大时(也就是说不快攻时),8BP是最好选择。这是因为8BP卡钱不多,并且Tscv的时间加上因为卡钱等第9个农民的时间跟一个Tpylon的时间差不多,因此卡到第10个农民的时间也不多。不过这里面的计算确实开始复杂起来。要仔细分析的话,首先需要知道任意时间Tr的时候手上的现钱有多少。根据前面的分析,这个时候的总采矿量已经知道,再减去造农民和水晶花掉的钱,就是手上的现钱Mtr。用Tpylon/Tscv得到造一个水晶的时间相当于造多少个农民。这里向下取整后,把余下的时间也记录下来。因为考虑到卡钱的问题,时间很可能刚好凑够。用Mtr-Mpylon 得到从这个时间开始造pylon剩下的钱,并计算Mtr-Mpylon-Mscv得到卡钱的量,并以当前拥有的农民数量估算卡钱时间(理论上讲,根据农民的出生时间和选择采矿的矿从可以计算出来他的位置和状态,并且精确的计算每个卡钱的时间。不过这些计算实在复杂,并且我们有API嘛,这些数据可以游戏时实时得到,就无需我在这里静态的去计算了……不过以后可能还是会把这里的一些公式补上,因为我们还是需要估计对手的矿量吧)。最后总的采矿量就是前面理想模型的总矿量减去这里卡钱卡人口时间里一个农民的采矿量(被卡的那个农民)。当Te较大的时候,采取这个策略总是最经济的,可以称之为经济策略。因为假如矿不够硬憋钱憋到Mpylon然后造水晶,之后又需要憋钱等农民,卡农民时间必然更长。并且经验告诉我们等到Mtr够Mpylon的时候,一般来说不会出现卡人口的情况(比如8BP,神族基地提供9人口)。但是当Te较小的时候(快攻),很有可能为了快出狂徒而停农民憋钱(比如经典的6BP)。因为这里我没有考虑这个问题,所以暂时不推了。

呜写到这里也累了,下次再来考虑血量伤害等的计算公式吧。