多年以后,面對辦公室的屏幕,我會回憶起開始肝第二周OO作業的那個遙遠的下午。那時的程序是一個一兩百行的符號求導,基類與接口在包里一字排開,工整的注釋一望到底
誰能想到,接下來的十幾個小時我要經歷什么樣的噩夢(
轉自我的個人博客
問題描述
我們面臨的問題很簡單:給定一個符號表達式E,E由冪函數、正弦函數和余弦函數的乘積加和而成(嚴謹定義略),求與E等價的盡可能短的表達式E'
即:
形如2*x*sin(x)^2+sin(x)+4*cos(x)+6*sin(x)*cos(x)^-2
形式的表達式合法,而若給定
sin(x)^2+cos(x)^2
,則最優解為1
胡亂分析
這個問題,第一眼看上去是個貪心,因為很容易想到,對於像sin(x)^4+2*sin(x)^2*cos(x)^2+cos(x)^4
這樣的表達式,只要每次合並Exp*sin(x)^2 + Exp*cos(x)^2
,最后能得到最優解。合並經常是正收益的。
但是反例也很好舉出:3*sin(x)^-2 + cos(x)^-2
,類似這種表達式,合並后並非最優
實際上由於字符串長度在某因子系數為-2
,-1
,0
和1
時會因為省略輸出而變短,在合並后會發生難以控制的變化
但是簡單想想就知道,大部分時候無腦合並應該是正收益。所以這個貪心可以作為一個baseline
另外一個問題來自合並順序。考慮A和B可合並,B和C可合並,但A和C不可合並,那么優先合並哪兩項?
因此簡單的說我們要解決兩個問題:
- 合與不合
- 先合誰
十分NP了。NP的最優解只能靠搜索求
暴搜是可能T或爆棧的,剪枝又太麻煩,容易寫錯,不太想走這條路子。想起來NOIP2015 D1T3 斗地主,考場上就有人用多個策略貪心取最優解的做法騙滿。說是騙分,但是這種做法其實更適合實際生產環境應用。這個思想很像Embedding。所以決定借鑒一下
准備工作
一般地,對於所有這樣的表達式項我們可以以一個三元向量簡化表示,如
4*sin(x)^2*cos(x)
,可以以(0,2,1)
和其系數4
表示
而對於兩個可合並的項,容易證明,一定滿足其指數三元向量的差為(0,2,-2)
或(0,-2,2)
進一步的,對於一個給定的表達式,若其系數表示為root = (a,b,c)
,那么它及
sin(root) = (a,b+2,c)
cos(root) = (a,b,c+2)
這三者中的任意兩者的線性組合可以轉化為另外兩者的線性組合
結合這種表述形式,我們實現一個可哈希的三元向量類,作為一個HashMap
的鍵,將每一項的系數作為值,這樣做的首要考慮是方便合並同類項,次要考慮是方便查找和給定項存在轉化關系的項(幾乎常數級的查詢復雜度)
貪心策略
這一步主要解決第一個問題:合與不合
采用如下的組合策略:
- 對於三角函數因子系數不含
-2
、-1
的項,無腦合並,優先合並成root
,不足以補成root
的三角函數項,轉換成系數絕對值較大的三角函數因子。迭代至不能再合並為止,作為初始解 - 對於三角函數因子系數不含
-2
、-1
的項,無腦合並,檢查root+sin
、root+cos
、sin+cos
三種情況中輸出最短的情況,轉化至這種情況。迭代至不能再合並為止。若長度得到優化接收當前解作為新解 - 對於所有項,執行類似1的操作,若長度得到優化接受當前解作為新解
- 對於所有項,執行類似2的操作,若長度得到優化接受當前解作為新解
這些策略可能不是最優的,但是實測已經能卡掉絕大多數情況了,所以也沒有進一步打磨。
引入隨機化
解決第二個問題:先合誰。
實際上裸的多策略貪心性能已經不錯了,我們可以通過隨機打亂項的順序,將目前的方法改進為一個蒙特卡羅方法,進一步消除合並順序的影響。
每次求一個新解前,用鍵集合keySet
初始化一個鏈表並用Collections.shuffle()
打亂順序,然后使用上述的貪心策略求一個解,若優於既有解,接受新解。迭代這個過程
我只迭代了50次,因為這次作業的輸入規模(100)很小,這個數量應該足夠了。
進一步的討論
目前的隨機化只是簡單的打亂順序。是否可以考慮使用一些智能隨機算法,比如遺傳、蟻群、PSO、退火?
使用遺傳算法的可能做法
將項的系數與指數三元組作為項的編碼,將全部項的編碼降序排列作為表達式的編碼
- 突變算子:
- 分裂突變:隨機分裂某一項為
sin^2+cos^2
的形式,開銷是常數級 - 合並突變:隨機選擇可合並的某兩項合並,復雜度常數級。為了讓合並突變是常數級操作,需要維護一個Flag確定每一項在當前編碼中是否可合並、合並對象的索引或引用
- 分裂突變:隨機分裂某一項為
- 交叉算子:對於編碼A中的子串A'和B中的子串B',A'和B'同源(由原始表達式中的相同項合並或分裂生成),那么一定有A'和B'的補CAA'、CBB'也同源。將A'與CBB'拼接、B'與CAA'拼接,作為兩個子代。為了快速找到兩個同源的子串,需要對每個編碼維護一個並查集。
- 適應函數與選擇算子:顯而易見,選擇編碼對應的表達式字符串長度作為適應函數。
總結
這次優化設計,我的感受是,一個優秀的近似解算法可能會比一個確定解算法更實用,因為它的時空需求更少,而解的質量並不會差的太多。