1. 程式人生 > >五大常用演算法一(回溯,隨機化,動態規劃)

五大常用演算法一(回溯,隨機化,動態規劃)

  • 蒙特卡洛演算法

首先要講一下,隨機化演算法之間並不是涇渭分明的,像之前隨機投點法求π也算蒙特卡洛演算法,只有蒙特卡洛演算法與拉斯維加斯演算法有著比較明顯的區別,前者是以高概率給出正確解,但無法確定那個是不是正確解.後者是給出的解一定是正確的,但可能給不出...夠明顯的區別了吧...
基本思想:當所要求解的問題是某種事件出現的概率,或者是某個隨機變數的期望值時,它們可以通過某種“試驗”的方法,得到這種事件出現的頻率,或者這個隨機變數的平均值,並用它們作為問題的解。
公式推導啥的維基百科去,直接上例子
主元素問題:問題描述標準版自行谷歌,由於沒弄好LaTeX,我感性地描述一下,就是一個元素要有很多重複,而且重複的數量超過整個陣列的一半了,他就是主元素...
編碼很簡單:

package com.fredal.structure;
public class Major {
 static Random r=new Random();
 private static boolean MajorMC(int[] a){        
     int random=r.Random(a.length-1);
     int x=a[random];//隨記選取元素
     int index=0;
     for(int i=0;i<a.length;i++){
         if(a[i]==x)
             index++;
     }
     if(index>(a.length/2)){
         System.out.println(x);//順便把主元素輸出了
         return (index>(a.length/2));//如果是主元素 概率大於1/2
     }
     return false;
 }

 public static boolean MajorMC(int[] a,double e){
     int k = (int) Math.ceil(Math.log(1.0/e) / Math.log(2.0));//e表示錯誤的概率 
     for(int i=0;i<k;i++){
         if(MajorMC(a)) return true;//重複的呼叫MajorMC().有一次成功說明有主函式
     }
     return false;
 }

 public static void main(String[] args) {
     int a[]={5,4,3,5,6,5,7,5,5,5,7,1,5,5};
         System.out.println(MajorMC(a, 0.001));
 }
}

思路很簡單,就是隨機選取一個數,如果有主元素的話,那麼這個數不是主元素的概率小於1/2.至於重複選取多少次呢,我們程式中e為錯誤的概率,那麼顯然我們只調用一次的話,出錯概率為0.5.想要降低到e的概率,那麼應該呼叫log(1/e)次演算法(以2為底).可以自行推導.
然後講一講素性測試,在前面,我們講過了篩法求素數,參考篩法求素數.
素性測試基於兩個定理:費馬小定理,以及關於平方探測定理.
費馬小定理:如果P是素數,且0<A<P,那麼A^(P-1) mod P=1.證明不在這寫了,首先我們可以隨機 選取一個數A,如果A^(P-1) mod P=1的話,那麼宣佈P為素數,否則肯定不是素數. 嗯,有些數不是素數但是它的大部分A的選擇都可以通過驗證,這些數集叫Carmichael數.最小的是561. 於是我們需要**平方探測定理**:如果P是素數且0<X<P,那麼X² mod P=1僅有兩個解X=1,P=1.證明很 簡單. 還是寫程式碼吧,但是之前先考慮一個問題,如何求a^(n-1)次方呢,用Math.power()麼,這個是可以但是 我們所需的空間太龐大.這裡我們有種巧妙的方法. 對於m=41=101001,b5b4b3b2b1b0=101001,可以這樣來求a^m: 初始C=1. b5=1:C=C^2(=1),∵b5=1->C=a
C(=a);
b5b4=10:C=C^2(=a^2),∵b4=0,不做動作;
b5b4b3=101:C=C^2(=a^4),∵b3=1,做C=a
C(=a^5);
b5b4b3b2=1010:C=C^2(=a^10),∵b2=0,不做動作;
b5b4b3b2b1=10100:C=C^2(=a^20),∵b1=0,不做動作;
b5b4b3b2b1b0=101001:C←C^2(=a^40),∵b0=1->C=a*C(=a^41)。
完了之後我們還要對a^(n-1)對n求模,那麼顯然我們可以在每一步動作後就求模,而不用等全部算完才求模.還有一點,中間的算平方步驟可以完美地進行平方探測.一舉兩得.
package com.fredal.structure;
 public class IsPrime {
 //計算a^i mod n  
  private static long witness( long a, long i, long n )
     {
         if( i == 0 )
             return 1; //二進位制最高位,開始回退

         long x = witness( a, i / 2, n );//遞迴呼叫            
         if( x == 0 ){//如果遞歸回來的是0 說明之前平方探測失敗 直接返回就行了
             return 0;               
         }
         long y = ( x * x ) % n;//順帶平方探測!,注意是順帶,二進位制求次方的關鍵
         if( y == 1 && x != 1 && x != n - 1 )//表示平方探測失敗
             return 0;
         if( i % 2 != 0 )
             y = ( a * y ) % n;//二進位制如果是1 就再乘以一次a再取模
         return y;
     }
     static Random r = new Random( );
     public static boolean isPrime( long n )
     {
         for( int counter = 0; counter < 10; counter++ )//反覆呼叫10次
             if( witness( r.randomLong( 2, n - 2 ), n - 1, n ) != 1 )
                 return false;
         return true;
     }
     public static void main( String [ ] args )
     {
        for(int i=500;i<600;i++){
            if(isPrime(i))
                System.out.println(i);
        }
     }
}

3.動態規劃(Dynamic Programming)

動態規劃是通過拆分問題,定義問題狀態和狀態之間的關係,使得問題能夠以遞推(或者說分治)的⽅式去解決.
動態規劃最重要的兩個要點:

  1. 狀態(狀態不太好找,可先從轉化方程分析)
  2. 狀態間的轉化方程(從問題的隱含條件出發尋找遞推關係)

動態規劃:適用於子問題不是獨立的情況,也就是各子問題包含公共的子子問題,鑑於會重複的求解各子問題,DP對每個問題只求解一遍,將其儲存在一張表中,從而避免重複計算.

  • 自頂向下求最短路徑

如圖求自頂向下的最短路徑,可以知道最短路徑為2-3-5-1首先我們用二維陣列triangle來儲存,變成了[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
我們設f(x,y)表示從(0,0)到(x,y)的最短路徑和,那麼狀態轉移方程為;
f(x,y) = min{f(x − 1,y),f(x − 1,y − 1)} + triangle[x][y],初始狀態為f(0,0).
當然我們也可以選擇自底向上考慮,f(x,y)表示出發走到最後一行的最短路徑和,那麼狀態轉移方程為:
f(x,y) = min{f(x + 1,y),f(x + 1,y + 1)} + triangle[x][y],初始狀態為f(n-1,y).
我們可以依次編碼實現,首先是自頂向下:
package com.fredal.structure;
import java.util.Arrays;
public class TopToBottom {
 public static int minimum(int[][] t){
     int n=t.length;
     int[][] result=new int[n][n];//存放結果

     result[0][0]=t[0][0];//初始化條件

     for(int i=1;i<n;i++){
         for(int j=0;j<=i;j++){
             if(j==0)
                 result[i][j]=result[i-1][j];//第一列時候 就等於本列上一行的結果
             if(j==i)
                 result[i][j]=result[i-1][j-1];//最後一列 等於前一列上一行的結果
             if(j>0 && j<i)
                 result[i][j]=min(result[i-1][j],result[i-1][j-1]);//取最小值
             result[i][j]+=t[i][j];//加上自身的數值
         }
     }

     int sum=Integer.MAX_VALUE;
     for(int i=0;i<n;i++){
         sum=min(sum, result[n-1][i]);//在最後一行取最小值
     }
     return sum;
 }
 private static int min(int i, int j) {
     return i<j?i:j;
 }    
 public static void main(String[] args) {
     int t[][]={
             {2},
             {3,4},
             {6,5,7},
             {4,1,8,3}
     }        System.out.println(minimum(t));
 }
}

接著是自底向上考慮,按照轉狀態移方程可得:
package com.fredal.structure;
public class BottomToTop {
 public static int minimum(int[][] t){
     int n=t.length;
     int[][] result=new int[n][n];//存放結果

     for(int i=0;i<n;i++){//初始化條件
         result[n-1][i]=t[n-1][i];
     }

     for(int i=n-2;i>=0;i--){
         for(int j=0;j<=i;j++){
             result[i][j]=min(result[i+1][j], result[i+1][j+1])+t[i][j];//狀態轉移方程
         }
     }

     return result[0][0];//頂部就是最小值
 }

 private static int min(int i, int j) {
     return i<j?i:j;
 }

 public static void main(String[] args) {
     int t[][]={
             {2},
             {3,4},
             {6,5,7},
             {4,1,8,3}
     };        System.out.println(minimum(t));
 }
}
這種方式雖然思維逆向一點,但編碼方便一點.
  • LCS(最長公共子序列)

該問題描述如下:一個數列 S,如果分別是兩個或多個已知數列的子序列,且是所有符合此條件序列中最長的,則 S 稱為已知序列的最長公共子序列。
例如:輸入兩個字串 BDCABA 和ABCBDAB,字串 BCBA 和 BDAB 都是是它們的最長公共子序列,則輸出它們的長度 4,並列印任意一個子序列.
稍加推理可以得出遞迴結構的方程:


2


設C[i,j]記錄Xi和Yj的最長子序列的長度,則可以得到如下狀態轉移方程:


3


算出的c[i][j]陣列以及如何選擇子序列如圖:


4


用程式碼模擬可得所有子序列:

package com.fredal.structure;
import java.util.LinkedList;
public class LCS {
 private static Character[] result;

 public static int[][] LCS(char[] X,char[] Y){
     int[][] c=new int[X.length+1][Y.length+1];//存放最長子序列長度,長度加1是因為 第一行第一列拿來初始化了

     //第一行第一列 自動初始化為0
     for(int i=1;i<=X.length;i++){
         for(int j=1;j<=Y.length;j++){
             if(X[i-1]==Y[j-1])
                 c[i][j]=c[i-1][j-1]+1;
             else if(c[i-1][j]>=c[i][j-1])
                 c[i][j]=c[i-1][j];
             else
                 c[i][j]=c[i][j-1];
         }
     }

     return c;
 }
 //列印最長子序列 使用遞迴
 public static void print(int[][] c,char[] x,char[] y,int i,int j,int len){
     if(i==0||j==0){//找到解了 就進行輸出
         for(int k=0;k<c[x.length][y.length];k++){//遍歷結果陣列
             System.out.print(result[k]);
         }
         System.out.println();
         return;
     }
     //結果陣列是空 就初始化
     if(result==null)
         result=new Character[c[x.length][y.length]];

     if(x[i-1]==y[j-1]){//斜著遞迴
         len--;
         result[len]=x[i-1];//倒序加入結果陣列
         print(c, x, y, i-1, j-1,len);
     }
     else if(c[i-1][j]>c[i][j-1])
         print(c, x, y, i-1, j,len);
     else if(c[i-1][j]<c[i][j-1])
         print(c, x, y, i, j-1,len);
     else {//說明橫著和豎著都行  那就依次遞迴            
         print(c, x, y, i, j-1,len);
         print(c, x, y, i-1,j,len);
     }
 }

 public static void main(String[] args) {
     char[] x ={'A','B','C','B','D','A','B'}; 
     char[] y ={'B','D','C','A','B','A'}; 
     int[][] c =LCS(x,y);

     int len=c[x.length][y.length];
     System.out.println("最長子序列長度:"+len);

     print(c, x, y, x.length, y.length,len);
 }
}
要注意的是存放長度的陣列應為字元陣列長度加1,因為多餘一列拿來初始化狀態.還有列印所有結果的時候,本來想用連結串列儲存子序列,這樣push,pop比陣列方便一點,但是連結串列在各個遞迴之間會相互影響,用陣列則不會出現這種問題.
  • 最大子段和

就是一段數字陣列,求出連續的和最大的欄位.注意要連續,設b[j]為子段和,a[j]為每個數,那麼很簡單的得出狀態方程是
b[j]=max(b[j-1]+a[j],a[j]),1<=j<=n
主要就看當b[j-1]>0時b[j]=b[j-1]+a[j],否則b[j]=a[j]

package com.fredal.structure;
import java.util.LinkedList;
public class MaxSubSum {
 public static void maxSum(int[] a){
     int n=a.length;
     int sum=0,b=0;//初始化最大子段和為0
     LinkedList<Integer> start=new LinkedList<Integer>();
     int flag=0,end=0;//設定子段位置引數
     for(int i=0;i<n;i++){
         if(b>0)
             b+=a[i];
         else{
             b=a[i];
             start.push(i);//更新開始下標
             flag=1;
         }
         System.out.println(b);
         if(b>sum){
             sum=b;//更新最大子段和
             end=i;
             flag=0;
         }
         if(flag==1)//如果更新了開始下標,但卻沒有改變sum值,說明是錯誤的更新
             start.pop();
     }
     System.out.println("最大子段和是:從"+(start.pop()+1)+"到"+(end+1)+",和為"+sum);
 }

 public static void main(String[] args) {
     int[] a={1,2,6,-7,-3,-4};
     maxSum(a);
 }
}

這問題求子段和不難,求兩端座標想了好一會兒,要注意b=a[i]時候更新開始下標有可能是錯誤的!