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

回溯算法

回溯法:也稱為試探法,它并不考慮問題規模的大小,而是從問題的最明顯的最小規模開始逐步求解出可能的答案,并以此慢慢地擴大問題規模,迭代地逼近最終問題的解。這種迭代類似于窮舉并且是試探性的,因為當目前的可能答案被測試出不可能可以獲得最終解時,則撤銷當前的這一步求解過程,回溯到上一步尋找其他求解路徑。
為了能夠撤銷當前的求解過程,必須保存上一步以來的求解路徑,這一點相當重要。

  • 八皇后問題

8皇后問題是其經典的問題,描述為:在一個 8x8的國際象棋棋盤中,怎樣放置8個皇后才能使8個皇 后之間不會互相有威脅而共同存在于棋局中,即在8x8個格子的棋盤中沒有任何兩個皇后是在同一行、同一列、同一斜線上。
思路:用回溯法,最容易想到的方法就是有序地從第 1 列的第 1 行開始,嘗試放上一個皇后,然后再嘗試第 2 列的第幾行能夠放上一個皇后,如果第 2 列也放置成功,那么就繼續放置第 3 列,如果此時第 3 列沒有一行可以放置一個皇后,說明目前為止的嘗試是無效的(即不可能得到最終解),那么此時就應該回溯到上一步(即第 2 步),將上一步(第 2 步)所放置的皇后的位置再重新取走放在另一個符合要求的地方…如此嘗試性地遍歷加上回溯,就可以慢慢地逼近最終解。
我們用代碼模擬該步驟,比較簡單的是使用遞歸方法

 package com.fredal.structure;
public class NQueen {
   static int index=0;

   private static void show(int[] queen){
       for(int i=0;i<queen.length;i++){
           System.out.print(queen[i]+" ");
       }
       System.out.println();
       index++;
   }
   
   //k表示層數 i表示列
   public static void process(int[] queen,int k){
       if(k==8){//找到解了
           show(queen);
           return;
       }
       for(int i=0;i<8;i++){
           
           if(!check(queen, k, i)) 
               continue;
           else {
               queen[k]=i;//放置皇后k
               process(queen, k+1);//放置皇后k+1
               queen[k]=-1;//更好地表示回溯 會被覆蓋的
           }
       }
   }
   
   //新皇后放入的位置
   private static boolean check(int[] queen,int row,int col){
       for(int i=0;i<row;i++) if(queen[i]==col) return false;//垂直檢測
       for(int i=0;i<row;i++) if(row-i==Math.abs(col-queen[i])) return false;//對角線檢測
       return true;
   }
   
   public static void main(String[] args) {
       int[] queen=new int[8];
       process(queen,0);
       System.out.println("共有"+index+"種解法");
   }
}

可以得到共有92種解法,我們注意到使用遞歸方法回溯的思想體現的不那么明顯,因為遞歸是內部自動回溯的,效率也比非遞歸低一點的,所以我們又使用循環來模擬
關鍵在于如何回溯以及回溯的時機,沖突了需要回溯,同時找到一個解之后仍然需要回溯,這點值得注意!

 package com.fredal.structure;
public class NQueen {
   static int index=0;
   private static void show(int[] queen){
       for(int i=0;i<queen.length;i++){
           System.out.print(queen[i]+" ");
       }
       System.out.println();
       index++;
   }    
   private static void init(int[] queen){
       for(int i=0;i<queen.length;i++){
           queen[i]=Integer.MAX_VALUE;
       }
   }
//k表示層數,i表示列數
   public static void processS(int[] queen){
       int k=0,i=0;
       init(queen);//初始化數組
       while(k<8){
           while(i<8){
               if(check(queen, k, i)){
                   queen[k]=i;//放置皇后k
                   i=0;//探測下一行 將i清0 從下一行的第0列開始探測
                   break;
               }else{
                   ++i;//探測下一列
               }
           }
           
           if(queen[k]==Integer.MAX_VALUE){//第k行沒有找到可以放皇后的地方
               if(k==0) break;//回溯到第一行還沒有 就終止
               else{
                   --k;//回到上一行
                   i=queen[k]+1;//把上一行皇后的位置往后一列
                   queen[k]=Integer.MAX_VALUE;
                   continue;//清楚位置 重新探測
               }
           }
           
           if(k==7) {//找到解了
               show(queen);
               i=queen[k]+1;//把最后一行皇后的位置往后一列
               queen[k]=Integer.MAX_VALUE;
               continue;//清楚位置 重新探測
           }
           
           ++k;//探測下一行
       }
   }
   
   //新皇后放入的位置
   private static boolean check(int[] queen,int row,int col){
       for(int i=0;i<row;i++) if(queen[i]==col) return false;//垂直檢測
       for(int i=0;i<row;i++) if(row-i==Math.abs(col-queen[i])) return false;//對角線檢測
       return true;
   }
   
   public static void main(String[] args) {
       int[] queen=new int[8];
       processS(queen);
       System.out.println("共有"+index+"種解法");
   }
}

8皇后問題擴展到n皇后問題是非常簡單的,這兒就不做了.

  • 迷宮問題

迷宮問題也是回溯法的經典應用,我們用代碼模擬過程

 package com.fredal.structure;
import java.util.HashSet;
import java.util.Set;
import com.fredal.structure.Maze.Position;
public class Maze {
   char maze[][];//存放迷宮
   Position entry;//入口
   Position exit;//出口
   Set<Position> res=new HashSet<Position>();//找到的解
   
   //位置類
   class Position{
       int row;
       int col;
       public Position(int row,int col){
           this.row=row;
           this.col=col;
       }
       public int hashCode() {
           return row*1000+col;
       }
       public boolean equals(Object obj) {
           if(obj instanceof Position==false) return false;
           Position p=(Position) obj;
           return p.row==row && p.col==col;
       }
       public String toString() {
           return "Position [row=" + row + ", col=" + col + "]";
       }
       
   }
   
   public Maze(){
       init();
   }
   
   //初始化迷宮
   private void init(){
       //硬編碼迷宮
       String[] x={
               "####A#######",
               "####....####",
               "####.####..#",
               "#....#####.#",
               "#.#####.##.#",
               "#.#####.##.#",
               "#.##.......#",
               "#.##.###.#.#",
               "#....###.#.#",
               "##.#.###.#.B",
               "##.###...###",
               "############"    
       };
       maze=new char[x.length][];
       for(int i=0;i<maze.length;i++){
           maze[i]=x[i].toCharArray();
           for(int j=0;j<maze[i].length;j++){
               if(maze[i][j]=='A') entry=new Position(i, j);
               if(maze[i][j]=='B') exit=new Position(i, j);
           }
       }
   }
   
   //核心程序
   private boolean findPath(Position cur,Set<Position> path){
       if(cur.equals(exit)) return true;//找到出口了
       path.add(cur);//path存放所有的路
       
       Position[] t={new Position(cur.row, cur.col-1),new Position(cur.row, cur.col+1),
               new Position(cur.row-1, cur.col),new Position(cur.row+1,cur.col)
       };//通過上下左右檢測
       
       for(int i=0;i<t.length;i++){
           try{
               if(maze[t[i].row][t[i].col]!='#' && path.contains(t[i])==false){//不是墻而且沒有訪問過的
                   if(findPath(t[i], path)){//遞歸 自動回溯
                       res.add(cur);//加入到結果中
                       return true;
                   }
               }
           }catch(Exception e){
               
           }
       }
       
       return false;
   }
   
   public void findPath(){
       Set path=new HashSet<Position>();//存放所有的路
       findPath(entry, path);
   }
   
   //打印迷宮
   public void show(){
       for(int i=0;i<maze.length;i++){
           for(int j=0;j<maze[i].length;j++){
               char c=maze[i][j];
               if(c=='.'&&res.contains(new Position(i, j))) c='o';//若被結果集包含就用圈表示
               System.out.print(c+" ");
           }
           System.out.println();
       }
   }
   
   public static void main(String[] args) {
       Maze m=new Maze();
       m.show();
       System.out.println();
       m.findPath();
       m.show();
   }
}

和8皇后問題一樣,我們發現遞歸其實是內部回溯,就是你在外面沒有自己去回溯,回溯思想沒有很好地體現,還有效率問題.
迷宮問題我們可以采用棧來完美地模擬,并且很好地體現了回溯的思想,我對每一步都調用了show(),所以每一步的走法和如何回退都非常清晰.

 package com.fredal.structure;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import com.fredal.structure.Maze.Position;
public class MazeL {
       char maze[][];//存放迷宮
       Position entry;//入口
       Position exit;//出口
       Position cur;//當前位置
       Stack<Position> res=new Stack<Position>();        
       //位置類
       class Position{
           int row;
           int col;
           public Position(int row,int col){
               this.row=row;
               this.col=col;
           }
           public int hashCode() {
               return row*1000+col;
           }
           public boolean equals(Object obj) {
               if(obj instanceof Position==false) return false;
               Position p=(Position) obj;
               return p.row==row && p.col==col;
           }
           public String toString() {
               return "Position [row=" + row + ", col=" + col + "]";
           }
           
       }    
       public MazeL(){
           init();
       }
       
       //初始化迷宮
       private void init(){
           //硬編碼迷宮
           String[] x={
                   "####A#######",
                   "####....####",
                   "####.####..#",
                   "#....#####.#",
                   "#.#####.##.#",
                   "#.#####.##.#",
                   "#.##.......#",
                   "#.##.###.#.#",
                   "#....###.#.#",
                   "##.#.###.#.B",
                   "##.###...###",
                   "############"    
           };
           maze=new char[x.length][];
           for(int i=0;i<maze.length;i++){
               maze[i]=x[i].toCharArray();
               for(int j=0;j<maze[i].length;j++){
                   if(maze[i][j]=='A') entry=new Position(i, j);
                   if(maze[i][j]=='B') exit=new Position(i, j);
               }
           }
           cur=entry;//初始化為起點
       }
       
       //核心程序
       public boolean findPath(){
           while(!cur.equals(exit)){
               //當前位置入棧
               res.push(cur);
               maze[cur.row][cur.col]='x';//標記為已經過
               //開始上下左右搜索
               if(cur.col-1>0&&maze[cur.row][cur.col-1]!='#'&&maze[cur.row][cur.col-1]!='x'){
                   cur=new Position(cur.row, cur.col-1);
                   show();
               }
               else if(cur.col+1<12&&maze[cur.row][cur.col+1]!='#'&&maze[cur.row][cur.col+1]!='x'){
                   cur=new Position(cur.row, cur.col+1);
                   show();
               }
               else if(cur.row-1>0&&maze[cur.row-1][cur.col]!='#'&&maze[cur.row-1][cur.col]!='x'){
                   cur=new Position(cur.row-1, cur.col);
                   show();
               }
               else if(cur.row+1<12&&maze[cur.row+1][cur.col]!='#'&&maze[cur.row+1][cur.col]!='x'){
                   cur=new Position(cur.row+1, cur.col);
                   show();
               }
               else{//四條路都走不通
                   show();
                   if(!res.isEmpty()){
                       res.pop();//回溯
                       cur=res.peek();
                       res.pop();//彈兩次是因為一開始還要push進來
                   }
               }
           }            
           return false;
       }
       //打印迷宮
       public void show(){
           for(int i=0;i<maze.length;i++){
               for(int j=0;j<maze[i].length;j++){
                   char c=maze[i][j];
                   if(res.contains(new Position(i, j))) c='o';//若被結果集包含就用圈表示
                   if(c=='x') c='.';//把走過的那些岔路重新用.表示
                   System.out.print(c+" ");
               }
               System.out.println();
           }
           System.out.println();
       }
       
       public static void main(String[] args) {
           MazeL m=new MazeL();
           m.findPath();
           m.show();
       }
}

隨機化算法

隨機化算法,在算法中使用隨機函數,其中決策依賴于某種隨機事件,基本特征是同一個實例用統一隨機化算法得到可能完全不同的結果.

  • 隨機數生成器

首先當然要產生隨機數啦.隨機數在概率算法中扮演著十分重要的角色,現實計算機上無法產生真正的隨機數,都是在一定程度上隨記的,即偽隨機.
產生隨機數最常用的是線性同余法.數x1,x2,...的生成滿足:Xi+1=A Xi mod M.剛開始給出的值X0稱為種子.這里我們一般取M為31比特數即2147483647,取A為48271這個素數.據此我們可以設計算法

  package com.fredal.structure;
public class Random {

   private static final int A=48271;
   private static final int M=Integer.MAX_VALUE;
   private static final int Q=(M/A);
   private static final int R= (M%A);
   
   private int state;
   
   public Random(){//使用系統時間與inf的余數作為種子
       state=(int) (System.currentTimeMillis()%Integer.MAX_VALUE);
   }
   
   public int RandomInt(){
       int tmpState=A*(state%Q)-R*(state/Q);
       if(tmpState>=0) state=tmpState;
       else state=tmpState+M;
       return state;
   }
   
   public double Random0_1(){//產生0-1之間的隨機數
       return (double)RandomInt()/M;
   }
   
   public static void main(String[] args) {
       Random m=new Random();
       for(int i=0;i<20;i++){
          System.out.println(m.Random0_1());
       }
   }
}

這里最關鍵的問題在于Q和R是啥,還有RandomInt()的里面按照公式直接返回(A*state)%M不行么,嗯,這個問題在于乘法可能溢出.解決溢出雖然可以使用64比特的long,但是減慢計算速度.
于是Schrage給出了算法,計算M/A的商和余數并分別定義為Q和R,那么按照程序中的算法,可以確保不溢出.推導可以查相關資料.
注: 接下去本節默認使用上面實現的隨機數產生器

  • 數值概率算法

顧名思義,這個,就是最簡單直接的隨機化算法.
比較有趣的例子是用概率模擬去求π的值,眾所周知,在邊長為a的正方形內部畫一個最大的四分之一圓,面積是πa2/4,那么與正方形面積之比是π/4,很容易算出π的值.
怎么算面積只比呢,那就是概率模擬咯

  package com.fredal.structure;
public class Pi {
   //假設邊長為1的正方形
   public static double go(Random r,long n){
       long k=0;
       for(long i=0;i<n;i++){
           double x=r.Random0_1();
           double y=r.Random0_1();
           if(x*x+y*y<1) k++;//當與原點距離小于1就認為在四分之一圓內
       }
       
       return 4*k/(double)n;
   }
   
   public static void main(String[] args) {
       Random r=new Random();
       System.out.println(go(r, 1000));
       System.out.println(go(r, 10000));
       System.out.println(go(r, 100000));
       System.out.println(go(r, 1000000));
       System.out.println(go(r, 10000000));
       System.out.println(go(r, 100000000));
   }
}

比較有趣的一點是,除了100次1000次這種實在不太靠譜之外,并不是次數越多就越精確的噢!!不相信可以多實驗幾次.

  • 舍伍德算法(Sherwood)

舍伍德算法的公式推導,復雜度計算啥的可以參考維基百科,這里說一下基本思想:在一般輸入數據的程序里,輸入多多少少會影響到算法的計算復雜度。這時可用舍伍德算法消除算法所需計算時間與輸入實例間的這種聯系.
舍伍德算法總能求得問題的一個解,且所求得的解總是正確的。當一個確定性算法在最壞情況下的計算復雜性與其在平均情況下的計算復雜性有較大差別時,可以在這個確定算法中引入隨機性將它改造成一個舍伍德算法,消除或減少問題的好壞實例間的這種差別。舍伍德算法精髓不是避免算法的最壞情況行為,而是設法消除這種最壞行為與特定實例之間的關聯性。
它可以獲得很好的平均性能,很典型的例子就是之前我們說的快速排序,參考快速排序.選取標桿時第一種是無腦選取第一個,還有是我們采用了三數中值法,當然隨記選取標桿也是很自然的想法.
這里現在上面寫的隨記類中加一個方法

  //0-n的隨記整數
   public int Random(int n){
       return (int) (Random0_1()*(n+1));
   }

可以開始寫了

  package com.fredal.structure;
public class Sherwood {
   
    public static void main(String[] args) {
       Random r=new Random();
       int[] a={14,7,2,34,6,95,27,9,54,12,103};
       quickSort(a, 0, a.length-1,r);
       for(int i=0;i<a.length;i++){
           System.out.print(a[i]+" ");
       }
   }

   public static void quickSort(int[] a,int left,int right,Random r){
       if(left<right){//遞歸出口條件
           if(left<right){//遞歸出口條件
               int i=left;//左指針
               int j=right;//右指針
               int random=left+r.Random(right-left);//隨機化選取標桿
               System.out.println(random);
               int tmp=a[left];//交換
               a[left]=a[random];
               a[random]=tmp;
               
               int x=a[left];
               while(i<j){
                   while(i<j && a[j]>=x) j--;//從右向左找第一個小于x的數
                   if(i<j) a[i++]=a[j];
                   while(i<j && a[i]<x) i++;//從左向右找第一個大于等于x的數
                   if(i<j) a[j--]=a[i];
               } 
               a[i]=x;//插入標尺
               quickSort(a,left,i-1,r);//遞歸左邊
               quickSort(a, i+1, right,r);//遞歸右邊
           }
       }
   }
}

當然舍伍德算法有局限性,很多情況下所給的確定性算法無法直接改造成舍伍德算法,這時候可以借助隨機預處理技術,僅對輸入進行隨機洗牌,同樣可以達到舍伍德算法的效果.
還是就快速排序說,隨記選取標桿確實是一個方法,但是在排序前對數組隨機洗牌一次,再選第一個作為標桿,也是一樣的嘛..

  package com.fredal.structure;
import java.util.Arrays;
public class Sherwood {
   
    public static void main(String[] args) {
       Random r=new Random();
       int[] a={14,7,2,34,6,95,27,9,54,12,103};
       quickSort(a, 0, a.length-1,r);
       show(a);
   }
   
   //隨記洗牌算法
   public static void shuffle(int[] a,Random r){
       int len=a.length;
       for(int i=0;i<len;i++){
           int j=r.Random(len-1);
           if(i!=j){
               int tmp=a[i];
               a[i]=a[j];
               a[j]=tmp;
           }
       }
   }
   
   static void show(int[] a)
   {
       for(int i=0; i<a.length; i++) System.out.print(a[i] + " ");
       System.out.println();
   }

   public static void quickSort(int[] a,int left,int right,Random r){
       if(left<right){//遞歸出口條件
           if(left<right){//遞歸出口條件
               int i=left;//左指針
               int j=right;//右指針

               int[] shuffle=Arrays.copyOfRange(a, left, right);//注意不要把整個a拿來shuflle了~
               shuffle(shuffle, r);
               
               int x=a[left];
               while(i<j){
                   while(i<j && a[j]>=x) j--;//從右向左找第一個小于x的數
                   if(i<j) a[i++]=a[j];
                   while(i<j && a[i]<x) i++;//從左向右找第一個大于等于x的數
                   if(i<j) a[j--]=a[i];
               } 
               a[i]=x;//插入標尺
               quickSort(a,left,i-1,r);//遞歸左邊
               quickSort(a, i+1, right,r);//遞歸右邊
           }
       }
   }
}

  • 拉斯維加斯算法(Las Vegas)

同樣只說一下基本思想:拉斯維加斯算法不會得到不正確的解。一旦用拉斯維加斯算法找到一個解,這個解就一定是正確解。但有時用拉斯維加斯算法找不到解.所以通常用一個布爾函數來表示

void Obstinate(InputType x, OutputType y){
   // 反復調用拉斯維加斯算法LV(x, y),直到找到問題的一個解
   boolean success= false;
   while (!success) 
        success = LV(x,y);
}

設p(x)是對輸入x調用拉斯維加斯算法獲得問題的一個解的概率,t(x)是算法obstinate找到具體實例x的一個解所需的平均時間,s(x)和e(x)分別是算法對于具體實例x求解成功或求解失敗所需的平均時間.
容易得到:t(x)=p(x)s(x)+(1-p(x))(e(x)+tx(x)),可以解得t(x)=s(x)+((1-p(x))/p(x))e(x)
我們在回溯法的時候講過8皇后問題,之前是從0往后遞增地放,并采用回溯.但其實每個皇后在棋盤山位置無規律,不具有系統性,用拉斯維加斯算法十分自然.
從第0行開始,每一行得皇后擺放的位置都是隨記的,如果擺放不了沖突了就全盤否定掉重新開始,而不是采用回溯往前退一步.由于我們是采用隨記的算法,所以仍然有較高的概率找到解的.
要講的是這兒設計算法注意的問題.每一步放皇后的位置是隨機的,很容易想到這么寫:

 while(!check(queen,k,i)){
    i=r.Random(7);
 }

其實這么寫很容易理解,就是每一步檢驗通不過的時候就一直采取新的隨機數.
但是會有一個問題,前幾行也許沒問題,但是假如這一行沒有可以放的列,我們就一直死循環了,那怎么辦,設置循環多少次之后就默認找不到了然后重新開始?
好吧剛開始確實掉死胡同里去,其實可以先遍歷一遍這一行的所有列,在那些可行的列里面再進行隨記選取.

  package com.fredal.structure;
public class LVQueen {
   private static void show(int[] queen){
       for(int i=0;i<queen.length;i++){
           System.out.print(queen[i]+" ");
       }
       System.out.println();
   }
    static Random r=new Random();//隨機數產生器
   //拉斯維加斯算法
   public static boolean QueenLV(int[] queen){
       int k=0,i=0;//k表示行 i表示列
       int[] can = new int[8];//用來保存可以遍歷的那些列
       int count=1;
       while(k<8&&count>0){//每一層
           count=0;//置為0
           for(int j=0;j<8;j++){//遍歷所有列
               if(check(queen, k, j))
                   can[count++]=j;
           }
           if(count>0)//說明可以找到存放的地方
               queen[k++]=can[r.Random(count-1)];//可能存放的列里面隨便選一個
       }
       return (count>0);
   }
   //測試新皇后放入的位置
   private static boolean check(int[] queen,int row,int col){
       for(int i=0;i<row;i++) if(queen[i]==col) return false;//垂直檢測
       for(int i=0;i<row;i++) if(row-i==Math.abs(col-queen[i])) return false;//對角線檢測
       return true;
   }
   public static void main(String[] args) {
       int[] queen=new int[8];
       while(!QueenLV(queen)){//如果找不到可以存放的位置的話就重新來過(count=0)
           
       }
       show(queen);
   }
}

有興趣的還可以測試一下成功率.當然其實純粹采用拉斯維加斯算法也不是最好的,我們可以采取拉斯維加斯算法與回溯法相結合,即前幾行用隨機化,后幾行開始用回溯法.

  package com.fredal.structure;
public class LVQueen {
   private static void show(int[] queen){
       for(int i=0;i<queen.length;i++){            System.out.print(queen[i]+" ");
       }
       System.out.println();
   }
    static Random r=new Random();//隨機數產生器
   //拉斯維加斯算法
   public static boolean QueenLV(int[] queen,int stopVeags){
       int k=0,i=0;//k表示行 i表示列
       int[] can = new int[8];//用來保存可以遍歷的那些列
       int count=1;
       while(k<stopVeags&&count>0){//每一層
           count=0;//置為0
           for(int j=0;j<8;j++){//遍歷所有列
               if(check(queen, k, j))
                   can[count++]=j;
           }
           if(count>0)//說明可以找到存放的地方
               queen[k++]=can[r.Random(count-1)];//可能存放的列里面隨便選一個
       }
       return (count>0);
   }
   
   //回溯法
   public static void BackTrack(int[] queen,int k){
       if(k==8){//找到解了
           show(queen);
           return;
       }
       for(int i=0;i<8;i++){            
           if(!check(queen, k, i)) 
               continue;
           else {
               queen[k]=i;//放置皇后k
               BackTrack(queen, k+1);//放置皇后k+1
               queen[k]=-1;//更好地表示回溯 會被覆蓋的
           }
       }
   }
   
   //測試新皇后放入的位置
   private static boolean check(int[] queen,int row,int col){
       for(int i=0;i<row;i++) if(queen[i]==col) return false;//垂直檢測
       for(int i=0;i<row;i++) if(row-i==Math.abs(col-queen[i])) return false;//對角線檢測
       return true;
   }
   
   //為了測試成功率方便 寫一個函數
   public static void nQueen(int[] queen,int stopVeags){//stopVeags表示前多少行用拉斯維加斯,后面的用回溯
       while(!QueenLV(queen, stopVeags)){
       }
       BackTrack(queen, stopVeags);
   }
   
   public static void main(String[] args) {
       int[] queen=new int[8];
       nQueen(queen, 2);//試試前面兩行隨機化,后面回溯
   }
}

有興趣地測試一下選取的stopVeags不同對成功率以及解的個數的影響,還有性能.籠統的說,當stopVeags為1和8的時候成功率應該是1,1的時候解有92個,8的時候就是1個.從2到7遞增的話成功率是遞減,解必然遞減.性能的話按照我之前測試貌似是取2的時候最好.
除了著名的8皇后問題,來說一下尋找第k小的數這個問題,我們采用拉斯維加斯算法,很容易得到

  package com.fredal.structure;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FindTheKMin {    
   public static void main(String[] args) {
       Integer[] a={12,56,73,45,9,51,43,67,11};
       List<Integer> lst=Arrays.asList(a);
       while(!find(lst, 2)){        
       }
   }
   public static boolean find(List<Integer> a,int index){
       //Random r=new Random();//放到外面去...
       int random=(int)(Math.random()*a.size());
       int mid=a.get(random);//隨記選擇一個數
       List<Integer> s1=new ArrayList<Integer>();
       
       for(Integer x:a){//記錄比mid數小的
           if(x<mid)
               s1.add(x);
       }
       
       if(s1.size()==index-1){//比mid數小的數量等于index-1 說明找到了
           System.out.println(mid);
           return true;
       }
       
       return false;
   }
}

這里有點小問題,不知道為什么用我自己寫的隨機數產生器性能低得令人發指(好吧我知道了,我怎么把隨機數生成器放到方法里去了,隨機性沒了).但這個思路是沒錯的,就是隨機選一個,看看是不是符合要求,不符合重新來一遍...
當然這個太暴力了,而且當我們要求的數有很多重復值的時候,成功率降低很多,我們還是加點優化,加點分治法之類的...

  package com.fredal.structure;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FindTheKMin {    
   public static void main(String[] args) {
       Integer[] a={12,56,73,45,9,51,43,67,11};
       List<Integer> lst=Arrays.asList(a);
       find(lst, 7);
   }
   static Random r=new Random();
   public static void find(List<Integer> a,int index){
       int random=r.Random(a.size()-1);
       int mid=a.get(random);//隨記選擇一個數
       List<Integer> s1=new ArrayList<Integer>();
       List<Integer> s2=new ArrayList<Integer>();
       List<Integer> s3=new ArrayList<Integer>();
       
       for(Integer x:a){//記錄比mid數小的,相等的,大的
           if(x<mid)
               s1.add(x);
           else if(x==mid)
               s2.add(x);//可能有重復值
           else if(x>mid)
               s3.add(x);
       }
       
       if(s1.size()>=index)
           find(s1,index);//說明在s1內
       else if(s1.size()+s2.size()>=index){
           System.out.println(mid);//說明在s2內
           return;
       }else if(s1.size()+s2.size()<index)
           find(s3, index-s1.size()-s2.size());//說明在s3內
   }
}

其實這個我認為和拉斯維加斯算法差得有點遠了,但是算法不用拘泥.當然也可以用舍伍德隨即洗牌之類的算,每次先shuffle一下,一樣的.

  • 蒙特卡洛算法(Monte Carlo)

首先要講一下,隨機化算法之間并不是涇渭分明的,像之前隨機投點法求π也算蒙特卡洛算法,只有蒙特卡洛算法與拉斯維加斯算法有著比較明顯的區別,前者是以高概率給出正確解,但無法確定那個是不是正確解.后者是給出的解一定是正確的,但可能給不出...夠明顯的區別了吧...
基本思想:當所要求解的問題是某種事件出現的概率,或者是某個隨機變量的期望值時,它們可以通過某種“試驗”的方法,得到這種事件出現的頻率,或者這個隨機變數的平均值,并用它們作為問題的解。
公式推導啥的維基百科去,直接上例子
主元素問題:問題描述標準版自行谷歌,由于沒弄好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,那么X2 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=aC(=a);
b5b4=10:C=C2(=a2),∵b4=0,不做動作;
b5b4b3=101:C=C2(=a4),∵b3=1,做C=a
C(=a^5);
b5b4b3b2=1010:C=C2(=a10),∵b2=0,不做動作;
b5b4b3b2b1=10100:C=C2(=a20),∵b1=0,不做動作;
b5b4b3b2b1b0=101001:C←C2(=a40),∵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);
          }
       }
}

動態規劃(Dynamic Programming)

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

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

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

  • 自頂向下求最短路徑

1

如圖求自頂向下的最短路徑,可以知道最短路徑為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]時候更新開始下標有可能是錯誤的!.

  • LIS(最長遞增子序列)

問題描述:找出一個n個數的序列的最長單調遞增子序列: 比如A = {5,6,7,1,2,8,3,4,5} 的LIS是1,2,3,4,5.
我不得不吐槽,我認為很多書,網上的博客都存在錯誤,而會對初學者造成誤導.
首先LIS[i] 是以arr[i]為末尾的LIS序列的長度(這個概念并不正確,只是為了解決問題設置的一個量)
可以得到LiS[i]=max(1,max(LIS(j)+1));j<i,arr[j]<arr[i](很多博客連這兒都寫錯,直接寫成了LiS[i]=max(LIS(j))+1,醉了.回過頭來雖然這個狀態轉移方程對于解決問題是對的,只是和上面的概念是有沖突的)
那么按照序列是5,6,7,1,2,8,3,4,5,根據狀態方程計算LIS[1...i]應該為1,2,3,1,2,4,3,4,5.看到沖突的地方了么,按照概念到1為止即LIS[4]應該為3,但是按照方程應該為1,而且只有按照后者計算結果才是對的.不啰嗦了給代碼:

  package com.fredal.structure;
public class LIS {
   public static void LIS(int[] a){
       int[] result=new int[a.length];
       int maxlen=0;//LIS長度
       for(int i=0;i<a.length;i++){
           result[i]=1;//找不到比自己小的就設為1
           for(int j=0;j<i;j++){
               if(a[i]>a[j] && result[i]<result[j]+1){
                   result[i]=result[j]+1;
               }
           }
           if(result[i]>maxlen)
               maxlen=result[i];
       }        
       System.out.println("最長上升子序列長度為:"+maxlen);
   }
   
   public static void main(String[] args) {
       int[] a={5,6,7,1,2,9,3,4,5};
       LIS(a);
   }
}

這個算法的復雜度是O(n2),也不方便給出LIS序列(可以設置前驅什么的),下面給出復雜度是O(n log n)的算法.
我們需要一個數組B[]來記錄LIS的長度以及序列中最大的值.初始化B[0]為arr[0],接著如果arr數組中的數比B數組中最大的數,即末尾數大的話就添加到后面,否則就在數組B中找一個剛好比自己大的數(用二分實現),取代它.B數組的長度就是LIS的長度,但注意B數組并不是LIS...

  package com.fredal.structure;
import java.util.Arrays;
public class LIS {
   public static void show(int[] a){
       for(int i=0;i<a.length;i++){
           System.out.print(a[i]+" ");
       }
       System.out.println();
   }
   
   public static int LCSS(int[] arr){
       int[] B=new int[arr.length];
       int blen=1;
       B[0]=arr[0];//初始化
       show(B);
       for(int i=1;i<arr.length;i++){
           if(arr[i]>B[blen-1]){
               System.out.println(arr[i]+"插入");
               B[blen++]=arr[i];//比B中最大的元素大 就接在后面
               show(B);
           }
           else{    
               System.out.println(arr[i]+"替換");
               B[binarySearch(B, arr[i],blen)]=arr[i];//找一個剛剛比其大的元素,取代他的位置
               show(B);
           }
       }
       return blen;
   }
   // 在數組中查找一個元素剛剛大于arr[i]
   // 返回這個元素的index
   public static int binarySearch(int []arr, int n,int blen){
       int begin=0;
       int end=blen-1;
       while(begin<=end){
           int mid = begin+(end-begin)/2;
           if(arr[mid]==n)
               return mid;
           else if(arr[mid]>n)
               end=mid-1;
           else
               begin=mid+1;
       }
       return begin;
   }    
   public static void main(String[] args) {
       int[] a={5,6,7,1,2,9,3,4,5};
       System.out.println("最長上升子序列長度為"+LCSS(a));
   }
}

為了理解方便,每一步驟都進行輸出,結果如下:

5

  • 01背包問題

如果有4個物品[2, 3, 5, 7].如果背包的大小為11,可以選擇[2, 3, 5]裝入背包,最多可以裝滿10的空間.如果背包的大小為12,可以選擇[2, 3, 7]裝入背包,最多可以裝滿12的空間.已知背包空間,函數需要返回最多能裝滿的空間大小
這是個典型的01背包問題,設F[i,v]表示前i件物品恰好放入容量為v的背包所獲得的最大價值,第i件物品大小為Ci,價值為Wi,則其狀態轉移方程為:
F[i,v]=max(F[i-1,v],F[i-1,v-Ci]+Wi
只考慮第i件物品的策略(放或不放),那么就可以轉化為一個只和前i ? 1件物品相關的問題。如果不放第i件物品,那么問題就轉化為“前i ? 1件物品放入容量為v的背包中”,價值為F[i ? 1, v];如果放第i件物品,那么問題就轉化為“前i ? 1件物品放入剩下的容量為v ? Ci的背包中”,此時能獲得的最大價值就是F[i ? 1, v ? Ci]再加上通過放入第i件物品獲得的價值Wi。
而在此題中,物品的大小與價值相當于是相等的.我們用代碼模擬該問題:

  package com.fredal.structure;
public class BackPack {
   public static int backpack(int[] a,int v){
       final int M=v;//背包空間
       final int N=a.length;
       int[][] f=new int[N+1][M+1];
       
       for(int i=0;i<N;i++){
           for(int j=0;j<=M;j++){//遍歷所有小于背包空間的所有空間情況 
               if(a[i]>j)//即j-a[i]<0,放不下 選擇不放
                   f[i+1][j]=f[i][j];
               else
                   f[i+1][j]=Math.max(f[i][j], f[i][j-a[i]]+a[i]);//看看放和不放哪個更優
           }
       }
       
       return f[N][M];
   }
   
   public static void main(String[] args) {
       int[] a={2,3,5,7};
       System.out.println(backpack(a, 11));
   }
}

仍然是四件物品,大小為[2, 3, 5, 7],它們的價值為[1, 5, 2, 4], 問如果是空間為10的背包,怎么裝可以獲得最大的價值.
這道題和前面那道差不多,多就多在他們的價值不等于他們的大小了.也很簡單

  package com.fredal.structure;
public class BackPack {
   public static int backpack2(int[] a,int[] w,int v){
       final int M=v;//背包空間
       final int N=a.length;
       int[][] f=new int[N+1][M+1];
       
       for(int i=0;i<N;i++){
           for(int j=0;j<=M;j++){//遍歷所有小于背包空間的所有空間情況 
               if(a[i]>j)//即j-a[i]<0,放不下 選擇不放
                   f[i+1][j]=f[i][j];
               else
                   f[i+1][j]=Math.max(f[i][j], f[i][j-a[i]]+w[i]);//看看放和不放哪個更優
           }
       }
       
       return f[N][M];
   }    
   public static void main(String[] args) {
       int[] a={2,3,5,7};
       int[] w={1,5,2,4};
       System.out.println(backpack2(a,w,10));
   }
}

背包問題還有特別多可以講,完全背包,多重背包...以后有時間肯定要再細致好好地寫一下算法.
更多內容以及相關下載訪問擴展閱讀

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,769評論 0 33
  • 貪心算法 貪心算法總是作出在當前看來最好的選擇。也就是說貪心算法并不從整體最優考慮,它所作出的選擇只是在某種意義上...
    fredal閱讀 9,279評論 3 52
  • Java經典問題算法大全 /*【程序1】 題目:古典問題:有一對兔子,從出生后第3個月起每個月都生一對兔子,小兔子...
    趙宇_阿特奇閱讀 1,908評論 0 2
  • SwiftDay011.MySwiftimport UIKitprintln("Hello Swift!")var...
    smile麗語閱讀 3,857評論 0 6
  • 爸爸特別喜歡科幻玄幻類小說,尤其的,喜愛倪匡的書,所以兒時我的對小說最初的印象,就是衛斯理系列,當然還有什么《世界...
    張蘇閱讀 1,523評論 1 1