OO第1.2次作業·魔鬼的三角函數化簡


多年以后,面對辦公室的屏幕,我會回憶起開始肝第二周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-101時會因為省略輸出而變短,在合並后會發生難以控制的變化

但是簡單想想就知道,大部分時候無腦合並應該是正收益。所以這個貪心可以作為一個baseline

另外一個問題來自合並順序。考慮A和B可合並,B和C可合並,但A和C不可合並,那么優先合並哪兩項?

因此簡單的說我們要解決兩個問題:

  1. 合與不合
  2. 先合誰

十分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的鍵,將每一項的系數作為值,這樣做的首要考慮是方便合並同類項,次要考慮是方便查找和給定項存在轉化關系的項(幾乎常數級的查詢復雜度)

貪心策略

這一步主要解決第一個問題:合與不合

采用如下的組合策略:

  1. 對於三角函數因子系數不含-2-1的項,無腦合並,優先合並成root,不足以補成root的三角函數項,轉換成系數絕對值較大的三角函數因子。迭代至不能再合並為止,作為初始解
  2. 對於三角函數因子系數不含-2-1的項,無腦合並,檢查root+sinroot+cossin+cos三種情況中輸出最短的情況,轉化至這種情況。迭代至不能再合並為止。若長度得到優化接收當前解作為新解
  3. 對於所有項,執行類似1的操作,若長度得到優化接受當前解作為新解
  4. 對於所有項,執行類似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'拼接,作為兩個子代。為了快速找到兩個同源的子串,需要對每個編碼維護一個並查集。
  • 適應函數與選擇算子:顯而易見,選擇編碼對應的表達式字符串長度作為適應函數。

總結

這次優化設計,我的感受是,一個優秀的近似解算法可能會比一個確定解算法更實用,因為它的時空需求更少,而解的質量並不會差的太多。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM