【算法特訓總結】計算機經典算法的核心思想及獨特角度的解讀

計算機經典算法的核心思想及獨特角度的解讀


在1月1日新年之日開始的"算法特訓"(一月一日~二月十日)終於結束了,對於這本<<算法競賽經典>>,除了第十章(在上個暑假末期的"離散數學特訓"已經覆蓋)和第十二章(屬於進階範圍還沒看)之外已經搞定了其他部分,但是仍然也有許多沒明白的地方(做了標記後續討論);在這裏做一個收尾總結,肯定不求寫那種流水賬的知識概括,而偏重於精華思想的提取和獨特角度的解讀;

在這裏插入圖片描述


成爲算法高手的11個技巧(Tricks)

在這裏容許我模仿一下<<C++ Cookbook>>的口吻來敘述一些書裏的精華思維;這些思維其實零散地分佈在每一章和每一道習題裏,<<算法競賽經典>>的組織形式是數據結構基礎、暴力求解法、動態規劃… …這樣的算法方法分章節撰寫,既然它已經這樣做了,這篇總結裏我想將書中一些有亮點的思維用1111個相互獨立的技巧來敘述如下:

Trick 1:c++技巧
  • c++技巧:c++是實現算法最順手的一門語言,c++的技巧遍佈正本書的範圍,一些必要的使用方式可以大大簡化算法的表達,使其清晰易讀;

使用函數指針來實現導數的運算,使得其的表達是更加直觀的數學語言形態:

double loss_function(VectorXd* X)
{
    return (W*(*X)).norm();
}

double derivation(double(*loss_function)(VectorXd*),VectorXd* X)
{
    VectorXd X_new;    
    X_new.array() = X->array() + step;    
    cout<<"Now the loss is:"<<loss_function(X)<<endl;    
    return (loss_function(&X_new)-loss_function(X))/step;
}

使用泛型,屏蔽底層的運算法則細節,專心編寫上層的邏輯;這裏舉一個排序的例子,考慮如下的泛型類Node,它的加法++的和比較方法>>實現細節可以用不同的法則定義,而只需要編寫其排序的實現:

template<class Node>
struct node_wrap {    
        Node* ptr;
        node_wrap(Node* p = 0) : ptr(p) { }
         
        Node& operator* const { return *ptr; }
        Node* operator-> const { return ptr; }
        node_wrap& oeprator++()    
        node_wrap operator+(int)        
        bool operator== (const node_wrap& i)    
        bool operator> (const node_wrap& i)    
        bool operator!= (const node_wrap& i)
};

可以預編寫一些"頭"來定義一些常用的功能:比如數據類型的轉換,c++可能不像很多動態語言那樣一個強制類型轉換就了事了,好在c++可以很方便地rename啊或者簡化一些原生語句:

template<class T>inline string toString(T x) 
{
    ostringstream sout;    
    sout << x;    
    return sout.str();
}
... ...
typedef unsigned int uI;
typedef long long LL;
typedef unsigned long long uLL;
typedef queue< int > QI;
typedef priority_queue< int > PQIMax;
typedef priority_queue< int, VI, greater< int > > PQIMin;
const double EPS = 1e-8;
const LL inf = 0x7fffffff;
const LL infLL = 0x7fffffffffffffffLL;
const LL mod = 1e9 + 7;
const int maxN = 1e4 + 7;
const LL ONE = 1;
const LL evenBits = 0xaaaaaaaaaaaaaaaa;
const LL oddBits = 0x5555555555555555;
Trick 2:問題分解
  • 問題分解:複雜的問題分解成若干個獨立簡單的問題,這是一個相當通用的思想:事實上分治法,雙向搜索,降維法等等的本質都是複雜問題的分解,下面列舉一些代表性問題和解決思路;

(UVa1605 - Building for UN)有nn個國家,要求你設計一棟樓併爲這n個國家劃分房間,要求國家的房間必須連通,且每兩個國家之間必須有一間房間是相鄰的;

將這個問題分解成"兩層樓的房間規劃",然後令第一層第ii行全是國家ii,令第二層第jj行全是國家jj,則\rightarrow此時\forall國家i,ji,j是通過兩層之間相鄰的;

(uva11134 - Fabled Rooks)你的任務是在n×nn \times n的棋盤上放置nn輛車,使得任意兩輛車不互相攻擊,且第ii輛車在一個給定的矩形RiR_i以內;

行列可以分開考慮,它們互不影響,單單考慮行的話,其實就是給定若干個區間,然後給每個區間都分配一個點,使得點不重複的情況下都能落在相應區間中,可以對區間按右端點升序排序,右端點相等時按左端點升序排序,從左往右看每個區間,儘量往區間的左端點分配即可(問題分解+貪心法).

(UVA - 12627 Erratic Expansion)一開始有一個紅氣球.每小時後,一個紅氣球會變成33個紅氣球和11個藍氣球,而1個藍氣球會變成44個藍氣球.如圖所示分別是經過0,1,2,30,1,2,3小時後得情況.經過kk小時後,第A BA~B行一共有多少個紅氣球;

在這裏插入圖片描述

由圖分析,每次把圖分爲四個部分,右下角的部分全爲藍氣球,不用去管他,剩下三部分都是一樣的並且和前一小時的圖形是一樣的,這樣的話我們可以計算出每個時刻紅氣球的總數.既然每次可以分爲四部分,那麼很明顯的就是用分治法來解決.分別計算出BB行之前和A1A-1行之前的紅氣球總數,那麼ABA \sim B行的氣球總數就是兩者相減.

Trick 3:映射
  • 映射:這本來是一個數學概念,但是也是一個相當通用的思想:事實上一些極其複雜並且看似難以下手的問題往往可以通過將輸入映射到一個特徵量來解決,下面列舉一些代表性問題和解決思路;

(uva1103 - Ancient Messages)給出一幅黑白圖像,每行相鄰的四個點壓縮成一個十六進制的字符.然後還有題中圖示的6種古老的字符,按字母表順序輸出這些字符的標號(其實關鍵就是識別這些符號).

在這裏插入圖片描述

圖像是被壓縮過的,所以我們要把它解碼成一個0101矩陣.而且我們還要在原圖像的四周加一圈白邊,這樣圖中的白色背景都連通起來了.黑色連通塊的個數就是字符的個數.觀察題中字符樣式可知,每種字符中包裹的“白洞”的個數是不同的,所以我們可以根據每個字符中的“白洞”的個數來區別這些字符.

因此核心思想是:圖像\mapsto“白洞”的個數,“白洞”數即映射的特徵量;

(uva1451 - Average)給出一個0101串,選一個長度至少爲LL的連續子串,使得串中數字的平均值最大;

在這裏插入圖片描述

首先預處理子串的前綴和SS,如果在座標系中描出(i,Si)(i, S_{i})這些點的話.所求的平均值就是兩點間的斜率了,具體來說,在連續子串[a,b][a, b]中,有SbSa1S_{b}-S_{a-1}11,長度爲ba+1b-a+1,所以平均值爲(SbSa1)/(ba+1)(S_{b}-S_{a-1})/(b-a+1);所以就把問題轉化爲:求兩點橫座標之差至少爲L1L-1,能得到的最大斜率.

因此核心思想是:數字的平均值\mapsto兩點間的斜率(特徵量);

(QT中的事件機制)這裏談一個題外話,但是和程序設計密切相關;信號與槽(Signal & Slot)是Qt編程的基礎,也是Qt的一大創新.因爲有了信號與槽的編程機制,在Qt中處理界面各個組件的交互操作時變得更加直觀和簡單.

信號(Signal)就是在特定情況下被映射的事件,例如PushButton 最常見的信號就是鼠標單擊時映射的 clicked() 信號,一個 ComboBox 最常見的信號是選擇的列表項變化時映射的 CurrentIndexChanged()信號.GUI程序設計的主要內容就是對界面上各組件的信號的響應,只需要知道什麼情況下映射哪些信號,合理地去響應和處理這些信號就可以了.

槽(Slot)就是對信號響應的函數.槽就是一個函數,與一般的C++函數是一樣的,可以定義在類的任何部分(public、private或protected),可以具有任何參數,也可以被直接調用.槽函數與一般的函數不同的是:槽函數可以與一個信號關聯,當信號被映射時,關聯的槽函數被自動執行.信號與槽關聯是用QObject::connect() 函數實現的,其基本格式是:

QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
Trick 4:巧用數據結構
  • 巧用數據結構:首先說明,什麼BFS使用隊列,處理任務使用優先隊列這些傳統思路不屬於我們的討論範疇,"巧用"強調的是使用簡單的數據結構簡化處理看似毫不相干的複雜問題(比如並查集就是巧妙聯繫了聯通分量集合和樹結構),下面列舉一些代表性問題和解決思路;

(uva297 - Quadtrees)用四分樹來表示一個黑白圖像:最大的圖爲根,然後按照圖中的方式編號,從左到右對應4個子結點.如果某子結點對應的區域全黑或者全白,則直接用一個黑結點或者白結點表示;如果既有黑又有白,則用一個灰結點表示,並且爲這個區域遞歸建樹.

在這裏插入圖片描述

利用遞歸建樹,因爲是4分樹,所以遞歸時,當遇見pp(非葉節點)就遞歸分別4個位置,每個位置記錄左上角,再利用此次遞歸的邊長即可得到本塊的大小,邊長每次縮小2倍… …其解決思路不展開細講,巧妙的是用樹結構作爲黑白圖像表達的方式;

(uva1624 - Knots)給出一個橡皮筋,有兩種操作,問是否可以將它還原;

在這裏插入圖片描述

初見這道題的時候很有代數拓撲的感覺,結果和高深的數學沒有必然聯繫,其實使用"環形鏈表"的數據結構即可解決;先用鏈表把每一個節點串起來,並對有覆蓋的地方進行標記.模擬解鎖操作,如果一個節點和它所覆蓋的節點之間沒有其他結,那麼進行逆self loop操作.同理進行逆passing操作.如果能把所有的結都解開,則答案是有解.

(八數碼問題:藉助鏈表的Hash來判重)在3×33 \times 3的棋盤上,擺有八個棋子,每個棋子上標有1至8的某一數字.棋盤中留有一個空格,空格用0來表示.空格周圍的棋子可以移到空格中.要求解的問題是:給出一種初始佈局(初始狀態)和目標佈局,找到一種最少步驟的移動方法,實現從初始佈局到目標佈局的轉變.

在這裏插入圖片描述

使用hash判重(參考博客https://blog.csdn.net/u012283461/article/details/79078653),將狀態數字串通過某種映射f(x)f(x)012345678876543210012345678-876543210這樣一個大集合,映射到128128M範圍之內;這裏採用簡單的hash,取模一個大質數,只要這個質數大於9!9!即可;

當然這裏可能出現衝突,也就是key1!=key2key_1!=key_2但是f(key1)==f(key2)f(key_1)==f(key_2),hash算法只能減少衝突不能避免衝突.這裏如何減少衝突呢?掛鏈表,當key1!=key2key_1!=key_2但是f(key1)==f(key2)f(key_1)==f(key_2),則將key_2掛到key_1後面;

int hashTable[M];//hashtable中key爲hash值,value爲被hash的值 
int next[M];//next表示如果在某個位置衝突,則衝突位置存到hashtable[next[i]] 
int hash(int n)
{
       return n%N; 
}

bool tryInsert(int n)
{
       int hashValue=hash(n);      
       while(next[hashValue]) //如果被hash出來的值得next不爲0則向下查找           
       {
               if(hashTable[hashValue]==n)//如果發現已經在hashtable中則返回false                  
               return false;               
               hashValue=next[hashValue];      
       }//循環結束hashValue指向最後一個hash值相同的節點        
       if(hashTable[hashValue]==n)//再判斷一遍              
           return false;       
       int j=N-1;//在N後面找空餘空間,避免佔用其他hash值得空間造成衝突        
       while(hashTable[++j]);//向後找一個沒用到的空間         
       next[hashValue]=j;      
       hashTable[j]=n;     
       return true; 
}
Trick 5:最優子結構
  • 最優子結構:最優子結構嚴格來說不是一種"解決問題的思維",而是"一類問題具備的性質".最優子結構是依賴特定問題和子問題的分割方式而成立的條件.各子問題具有最優解,就能求出整個問題的最優解,此時條件成立.比如求廣州到北京的最短距離,假設這個路徑必經過中間的南京,那麼先把路徑分割爲(廣州,南京)和(南京,北京).分別求出子路徑的最短距離然後再連接,就可以得到廣州到北京的最短路徑;下面列舉一些代表性問題和解決思路;

(uva11054 - Wine trading in Gergovia)直線上有nn個等距的村莊,每個村莊要麼買酒,要麼賣酒.設第ii個村莊對酒的需求爲ai,ai>0a_i,a_i>0表示買酒,ai<0a_i<0表示賣酒,所有村莊供需平衡.把kk個單位的酒從一個村莊運到相鄰村莊需要kk個單位的勞動力.計算所需最少勞動力.

從左邊第一個開始分析,如果它賣酒,則可以把它全賣給第二個村莊,如果它買酒,可以從第二個村莊那裏買酒,依次下去分析第二個直到最後一個村莊.這樣的話每次買酒和賣酒的距離都是最短的,勞動力肯定也是最少的(每次只考慮最左邊的村莊及其右側村莊構成的子結構最優).

(uva348 - Optimal matrix chain multiplication)給出NN個矩陣(A1,A2,...,An)(A_1,A_2,...,A_n),求完全括號化方案,使得計算乘積(A1A2...An)(A_1A_2...A_n)所需乘法次數最少.並輸出方案.

要求的是[0,n1][0,n-1]的最小代價.且大區間的決策依賴於小區間.矩陣連乘的最後一定有一個最後一次乘法,假設最後一個乘號在第kk個矩陣後,也就是P=A1×A2×...AkP=A_1\times A_2\times...A_kQ=Ak+1×Ak+2×...×AnQ=A_{k+1}\times A_{k+2}\times...\times A_n.只需分別求出P,QP,Q的最優方案(最優子結構).

Trick 6:空間換時間
  • 空間換時間:這句聽起來像斯大林行事作風的技巧甚至被用在了微信的聊天記錄搜索裏(快速的Hash,用O(n)O(n)的空間複雜度換取至少O(n)O(n)的時間複雜度),下面列舉一些代表性問題和解決思路,比如刷表法;

(uva1583 - Digit Generator)如果xx加上xx的各個數字之和得到yy,就說xxyy的生成元.給出n(1<=n<=100000)n(1<=n<=100000),求最小生成元.無解輸出00.例如,n=216,121,2005n=216,121,2005時的解分別爲198,0,1979198,0,1979.

可以利用打表的方法,提前計算出以ii爲生成元的數,設爲dd,並保存在a[d]中(a[d]=i),反覆枚舉,若是初次遇到或遇到更小的則更新(寫入表);

(uva1025 - A Spy in the Metro)某城市地鐵是線性的,有n(2n50)n(2\le n\le 50)個車站,從左到右編號1n1 \sim n.有M1M_1輛列車從第11站開始往右開,還有M2M_2輛列車從第nn站開始往左開.列車在相鄰站臺間所需的運行時間是固定的,因爲所有列車的運行速度是相同的.在時刻00,Mario從第11站出發,目的在時刻T(0T200)T(0\le T\le 200)會見車站nn的一個間諜.在車站等車時容易被抓,所以她決定儘量躲在開動的火車上,讓在車站等待的時間儘量短.列車靠站停車時間忽略不計,且Mario身手敏捷,即時兩輛方向不同的列車在同一時間靠站,Mario也能完成換乘.

在這裏插入圖片描述

狀態定義:dp[i][j]:到時刻ii的時候(出發的時候時刻爲0,約定時間爲時刻time),從jj號車站開往NN號車站,在車站等待的最少的時間.這個人當前的策略:

1.在車站等待一個單位的時間(該站此時沒有發車時應該這麼做);
2.坐上開往左邊的火車;
3.坐上開往右邊的火車;

狀態轉移方程(我們可以做一個乘車時刻表(空間換時間)來記錄i時刻j車站是否有車經過.):

dp[i][j] = min(dp[i+1][j]+1,dp[i+t[j]][j+1],dp[i+t[j-1]][j-1]);
Trick 7:遞推
  • 遞推:事實上計算機專業的應該在大一的離散數學/組合數學裏學過遞推方程,其實不光只有組合問題需要用到遞推思維;下面列舉一些代表性問題和解決思路,比如動態規劃的狀態轉移思想其實也是在已知一些邊界條件情況下做遞推;

(uva580 - Critical Mass)有UULL兩種盒子,數量足夠多,要把nn個盒子排成一行,但至少要有3個UU放在一起,問有多少種方法.

f[i]f[i]ii個盒子的合法方案數,g[i]g[i]ii個盒子的非法方案數.對於f[n]f[n],考慮第一次出現三個U連續的情況是在i,i+1,i+2i,i+1,i+2,則i1i-1(如果存在)必須是LL,之前不能出現三個UU連續,之後隨便選.總方案數爲g[i2]2(ni2)g[i-2]*2^{(n-i-2)}.另外在特殊處理一下i1i-1不存在的情況,即i=1i=1,此時的方案數爲2(n3)2^{(n-3)}. 綜上所述,f[n]=2(n3)+i=2...n2g[i2]2(ni2)f[n]=2^(n-3)+\sum_{i=2...n-2}g[i-2]2^{(n-i-2)},g[n]=2nf[n]g[n]=2^n-f[n];

(uva12034 - Race)兩匹馬比賽有三種比賽結果,求nn匹馬比賽的所有可能結果總數;

現在設ii匹馬佔有jj個名次,問所有可能的情況;dp[i][j]表示ii匹馬佔有jj個名次的組合情況,然後考慮ii匹馬和i+1i+1匹馬的關係(也就是多了一匹馬要放在哪個位置)這匹馬和前ii匹馬中至少一匹馬的成績相同(jj個名次就有jj種情況),這匹馬獨佔了一個成績(可以放入jj個位置)所以可以得到遞推式:

dp[i][j] = dp[i-1][j] * j + dp[i-1][j-1] * j;
Trick 8:剪枝
  • 剪枝:剪枝不僅僅針對有關樹結構出現的問題,一切有關狀態空間搜索的問題,包括但不僅限於貪心法、回溯法、動態規劃、圖算法等都會用到剪枝;簡單來說就是及時檢查,來停止某個分支方向的搜索,來避免不必要的搜索浪費(有"及時止損"的感覺);下面列舉一些代表性問題和解決思路;

(uva140 - Bandwidth)輸入一行數據,其中有nn個字符節點和節點間的連通關係,輸出一組排列,節點ii的帶寬爲ii和相鄰節點在排列中的最遠距離,所有帶寬的最大值就是該排列的帶寬.按字典序輸出帶寬最小的排列.

思路(參考博客https://www.cnblogs.com/luruiyuan/p/5847706.html):
1.建立雙射關係(回憶映射思維):從字符A到字符Z遍歷輸入的字符串,用strchr函數將輸入中出現的字符找出,並將找出的字符進行編號,用letter和id分別存儲字符和對應的編號;
2.降維:輸入中給出的,是類似於鄰接表形式的二維形式,如果我們用二維數據結構,將增加處理時對於輸出細節的處理難度,用2個vector將輸出降低到1維,簡化了計算Bandwidth時的代碼,實際上讓我們更加有的放矢;
3.存儲必要信息,位置:數組pos每個下標代表字母編號,存儲的是對應的位置下標,便於計算時尋找位置;
4.剪枝:減去不必要的計算(雖然對於本題而言不是必須的);

(uva1354 - Mobile Computing)就是首先給一個房間的寬度rr,之後有ss個掛墜,第ii個掛墜的重量是wiw_i,設計一個儘量寬,但是不能寬過房間的寬度;

在這裏插入圖片描述

(主要關注一下如何判重來剪枝)自頂向下,把集合分爲左右子集(分別爲左右子樹所含的掛墜集合),在遞歸調用左右子集.枚舉子集的思路用的是二進制枚舉集合的思路,每個二進制數分別對應掛墜集合能組成的所有天平的左右臂長度,用vector node[MAXN]儲存,[]內是二進制數.還用到了二進制&,^運算來處理集合間的關係.

(uva690 - Pipline Scheduling)有一臺包含55個工作單元的計算機,還有10個完全相同的程序需要執行.每個程序需要n(n<20)n(n<20)個時間片來執行,可以用一個55nn列的保留表(reservation table)來表示,其中每行代表一個工作單元(unit0~unit4),每列代表一個時間片,行iijj的字符爲XX表示“在程序執行的第jj個時間片中需要工作單元ii”.例如,如圖所示就是一張保留表,其中程序在執行的第0,1,2,...0,1,2,...個時間片中分別需要unit0,unit1,unit2…同一個工作單元不能同時執行多個程序,因此若兩個程序分別從時間片0011開始執行,則在時間片55時會發生衝突(兩個程序都想使用unit0),如圖所示.輸入一個55n(n<20)n(n<20)列的保留表,輸出所有1010個程序執行完畢所需的最少時間,例如,對於圖中的保留表,執行完1010個程序最少需要3434個時間片.

使用二進制表達壓縮狀態,加上剪枝:每次移動只需要判斷原來的狀態向後移與程序的保留表是否有衝突,如果沒有,將這兩個取並作爲新的狀態(我最後看明白的辦法是做表記錄那些程序間的間距時間是可行的,然後對不可行的方案剪枝);

Trick 9:仿真/演繹
  • 仿真/演繹:仿真/演繹思想其實是兩個對立的但互相依存的思維,"仿真"是針對無從下手的複雜問題,但是知道有限的邊界條件,於是把握其中的規則來編寫仿真過程求解;"演繹"思想是能通過問題很好地預知很多過程,這時就可以剔除很多不必要的分支可能,針對性編寫程序;

(uva1609 - Foul Play)給一羣隊伍,隊伍11至少能擊敗一半的隊伍(令爲白隊),且不能擊敗另外的隊伍(令爲黑隊),每隻隊伍11不能擊敗的黑隊都有另一隻白隊能擊敗他.給一個比賽安排讓一號隊奪冠;

(這個題目屬於中間過程的可推導性比較好的,因此可以使用構造思維求解)構造之後的遞歸就相對比較簡單了.構造的方式分爲四個階段(能夠證明按照這樣的策略打過一輪之後,剩下的隊伍還滿足初始條件,因此可以遞歸求解):
1.把滿足條件的隊伍A和B配對,其中隊伍11打不過A,隊伍11能打過B,並且B能打過A.
2.把隊伍11和剩下的它能打過的隊伍配對.
3.把隊伍11打不過的隊伍相互配對.
4.把剩下的隊伍配對.

(uva10603 - Fill)設33個杯子的容量爲abcabc,起初只有第三個杯子裝滿了cc升水.其它兩個杯子均爲空.最少要倒多少升水可以讓某一個杯子裏有dd升水.如果無法做到dd升水.就讓某個杯子裏有dd'升水,其中d<dd' < d而且儘量接近d(1a,b,c,d200)d(1 \le a,b,c,d\le 200)要求輸出最小的倒水量和目標水量(dd或者是dd');

(這道題和上一題相反,難以推測其中的事件細節,適合仿真地解決;據說是美團的算法崗面試題)使用廣度優先搜索BFS,可以解決狀態轉移或者是決策問題.而這道題33個杯子,假設在某一時刻第一個杯子裏有v1v_1升水.第二個杯子有v2v_2升水,第三個杯子有v3v_3升水.而這個時候可以說是在某一時刻的狀態爲(v1,v2,v3)(v_1,v_2,v_3),而每個狀態之間都可以通過某種方式進行轉換,也就是在狀態圖GG中進行BFS;這道題就是通過倒水轉移.

Trick 10:歸約
  • 歸約:歸約思想是邏輯學裏的一個概念(歸約是使用解題的"黑盒"來解決另一個問題的思維方式),就是將問題AA轉變爲問題BB;其好處是可以把一個陌生的問題轉換爲一個已經有成熟固定套路的解法的問題(在圖論問題中尤爲常見);

(uva753 - A Plug for UNIX)有若干個電器設備需要不同的適配器才能接上電源,現在你要讓儘可能多的電氣設備接上電源.首先你手中有nn個適配器和適配器的型號,再告訴你有mm個電器和他們分別對應的適配器的型號,最後還有一個商店提供買不同型號的適配器轉換器,轉換是單向的ABA\rightarrow B表示能把AA接口轉換成BB接口(就是原來需要用AA適配器的現在可以用BB適配器當然也可以用原來的不變)超市提供的轉換器數量是沒有限制的,可以無限買.

節點表示插頭類型,邊表示轉換器,然後使用floyd算法,計算出任意一種插頭類型能否轉換成另外一種插頭類型.額外添加一個源點ss,從ss到設備device[i]連接一條容量爲11的邊,再額外加一個匯點tt,從插座target[i]到tt連接一條容量爲11的邊.然後只要device[i]能夠轉換成target[i]就在兩者間添加一條容量爲INF的邊,表示允許任意多設備從device[i]轉換成target[i].最後求s-t最大流(規約),m減去最大流就是所要求的答案.

(uva247 - Calling Circles)如果兩個人互相打電話(直接或間接),則說他們在同一個電話圈裏.例如,a打給b,b打給c,c打給d,d打給a,則這四個人在同一個電話圈裏;如果e打給f但f不打給e,則不能推出e和f在同一個電話圈裏.輸入n(n25)n(n\le 25)個人的mm次電話,找出所有的電話圈.人名只包含字母,不超過2525個字符,且不重複.

用map存下人名,然後用floyd算法跑一遍連通性就行了.因爲floyd算法是解決任意兩點之間的最短距離,這裏我們可以用此特性來判斷連通性(歸約爲求\forall兩點之間最短路).

Trick 10:謂詞
  • 謂詞:謂詞也是現代邏輯學裏的一個概念(歸約是使用解題的"黑盒"來解決另一個問題的思維方式),在這本書裏這是最核心的一個思維(前文中很多方法也有這個思維的影子),一切狀態和描述狀態的本質都是謂詞,可以說除了絕對靜態的概念(比如時間,整數…)外"一切都可以看作謂詞"(在動態規劃中尤爲常見,狀態描述函數就是謂詞,而狀態轉移方程其本質就是謂詞的動態作用),在這裏我還不想把它說得太抽象,下面看一些例子(可以看出不同描述方法的謂詞函數的選取和謂詞描述範圍因素(也就是狀態函數的維度)會對問題的解決產生決定性影響);

(uva12186 - Another Crisis)一個公司有11個老闆和nn個員工,nn個員工中有普通員工和中級員工,現在進行一次投票,若中級員工管理的普通員工中有T%T\%的人投票,則中級員工也投票並遞交給上級員工;求最少需要多少個普通員工投票,投票才能到達老闆處;

用一個vector存儲結點的子節點,設f[i]表示(謂詞函數)爲了讓信息傳到ii,需要的最少人數;設結點uu的子節點有kk個,則至少需要人數:

c=(kT1)/100+1c=(k*T-1)/100+1

把所有的子結點的f[i]值排序,選最小的cc個加起來就是當前點的"最少需要員工投票數量";

(uva1220 - Party at Hali-Bula)公司的員工成樹形分佈,每個人只有一個直屬上司,現在要開個party,不能讓一個人和他的直接老闆同時出現在party上,問最多能選多少人,並問選擇是否唯一;

用dp[i][j]表示最大人數(謂詞函數),其中ii爲第ii個點,其中jj可以爲00或者爲11,表示第ii個人選或者不選,即選或者不選ii的以ii爲根的子樹的最優值,另一個f[i][j]表示選擇唯不唯一,iijj的含義dp數組一樣;那麼只需要寫出狀態轉移的細節即可(考察節點uu):

void dpp(int u)
{
    if(v[u].empty())
    {
            dp[u][1]=1;        
            dp[u][0]=0;        
            return ;   
    }    
    int son=v[u].size();    
    for(int i=0;i<son;i++)
    {
            int to=v[u][i];
            dpp(to);        
            dp[u][1]+=dp[to][0];        
            if(dp[to][0]>dp[to][1])
            {
                dp[u][0]+=dp[to][0];            
                if(f[to][0]==0) f[u][0]=0;        
            }else
            {   dp[u][0]+=dp[to][1];            
                if(f[to][1]==0) f[u][0]=0;            
                if(dp[to][0]==dp[to][1]) f[u][0]=0;        
            }    
     }    
     dp[u][1]++;
}

11個技巧(Tricks)的數學內涵

  • c++技巧:和數學有任何關係嗎?泛型屏蔽底層運算細節規則的設定,是不是和羣論裏忽略底層加法運算規則但是抽象出代數結構和對稱性的思想有異曲同工之妙?

  • 問題分解:用貝葉斯思想考察問題QQ,其因果描述可以寫作P(Qq1,...,qn)\mathbb{P}(Q|q_1,...,q_n),其中q1,...,qnq_1,...,q_nnn個解決問題的關鍵考察因素,那麼問題可以獨立拆分爲P(Qqi)\mathbb{P}(Q|q_i)的形式當前僅當P(Qq1,...,qn)=iP(Qqi)\mathbb{P}(Q|q_1,...,q_n) = \prod_i \mathbb{P}(Q|q_i);

  • 映射:映射的思維本質是抽象對應,在解決問題中通過構造"映射"往往能夠簡化問題;
    一些有意義的特定的映射是:

函數:表示爲y=f(x)y=f(x),把具有元素xx的標量空間XX映射到具有元素yy的標量空間YY;
泛函:表示爲ρ=ϕ(f)\rho=\phi(f),把具有元素ff的函數空間映射到具有元素ρ\rho的標量空間AA(函數集合到數集上的映射:定義域爲函數,而值域爲實數的"函數");
算子:表示爲g=L(f)g=L(f),把一個函數空間映射到自己中,即f,gf,g是同一函數空間的元素(函數空間到函數空間上的映射O:XXO:X \rightarrow X.廣義上的算子可以推廣到任何空間,如內積空間等);

  • 巧用數據結構:合理的數據結構的套用本質上是一種數學建模;

  • 最優子結構:當且僅當局部最優解是蘊含(\Rightarrow)全局最優解時可以用最優子結構看待問題;

  • 空間換時間:構造一個表T(x)=f(x)T(x)=f(x)來緩存每次要計算的和xx相關的函數值f(x)f(x)即爲該思想的形式化描述;

  • 遞推:當fn=F(fn1,...,f1)f_n = F(f_{n-1},...,f_{1})形式的關係成立時,可以用遞推的思維解決關於fif_i這樣的函數所描述的問題;

  • 剪枝:當沿着分支ii進行下去的搜索"不划算時",也就是fi+L^i>L~f_i + \hat{L}_i > \tilde{L}時,終止這個分支的搜索;其中fif_i是沿着分支ii進行到當前的代價值,L^i\hat{L}_i是沿着ii搜索下去最樂觀的代價(一般需要估計一個下界),L~\tilde{L}是當前全局最優代價;

  • 仿真/演繹:當我們無法洞悉問題QQ的內部狀態集合{si}\{s_i\}時,我們可以構建一個根據問題規則描述的作用f()f(\cdot),從狀態s0s_0開始用f(0)f(0)作爲初始開始仿真並記錄下狀態集合{si}\{s_i\};

  • 歸約:歸約其實是理論計算機裏計算複雜度的一個概念;假設有一個複雜的問題PP,而它看起來與一個已知的問題QQ很相似,可以試着在兩個問題間找到一個歸約(reduction,或者transformation),記作PQP \prec Q.對於問題的先後,歸約可以達到兩個目標:

i) 已知QQ的算法,那麼就可以把使用了QQ的黑盒的PP的解決方法轉化成一個PP的算法.

ii) 如果PP是一個已知的難題,或者特別地,如果PP的下限,那麼同樣的下限也可能適用於QQ.前一個歸約是用於獲取PP的信息;而後者則是用於獲取QQ的信息.

  • 謂詞:考慮這樣一個代數結構(pi,j,xl)(p_i,\odot_j,x_l),其中1in,1jm,1lk1\le i \le n,1\le j \le m,1\le l \le k,且piP,jO,xlXp_i \in P,\odot_j \in O,x_l \in X,滿足:

i) pi(x1...xc)p_i(x_1...x_c)是一個映射:x1...xcxdXx_1...x_c \mapsto x_d \in X;
ii) pijplPp_i \odot_j p_l \in P;
iii) 存在pePp_e \in P,滿足對pi,j\forall p_i,\odot_jpijpe=pip_i \odot_j p_e=p_i;

我們將其稱之爲一個謂詞結構(這是筆者初步構思的一個可以解決一些實際問題的代數結構);


總結

即使在經歷瞭如此多的有意義的之後,我還是感到知識上的信心不足,但是好在我覺得我至少具備不錯的學習能力;算法的訓練後,在計算機算法方面(我們會嘗試參加比賽)算是至少入門了,接下來幾年大量的訓練必不可少;

從明天開始我將轉入一個桌面仿真軟件的開發(QT c++),那將會持續到二月底,順便我會持續閱讀一些數學書;在稍早些時候(2017年的夏天)我開發了一些Linux上的期貨交易算法程序,是時候結合新的數學思維和計算機編程能力重回那個戰場,找回一些失落的希望了;

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章