17年網易筆試-跳石板:廣度優先搜索

前言

17年網易筆試有一道編程題目《跳石板》,newcoder OJ鏈接如下:
跳石板-牛客網
題目描述如下:

小易來到了一條石板路前,每塊石板上從1挨着編號爲:1、2、3…….
這條石板路要根據特殊的規則才能前進:對於小易當前所在的編號爲K的 石板,小易單次只能往前跳K的一個約數(不含1和K)步,即跳到K+X(X爲K的一個非1和本身的約數)的位置。 小易當前處在編號爲N的石板,他想跳到編號恰好爲M的石板去,小易想知道最少需要跳躍幾次可以到達。
例如:
N = 4,M = 24:
4->6->8->12->18->24
於是小易最少需要跳躍5次,就可以從4號石板跳到24號石板

沒有實戰過圖搜索的題,考試中沒啥思路,下來趕緊看看了解答,隨後總結爲此文。

廣度優先搜索

Breadth First Search (BFS)是一種圖搜索算法,BFS首先從源定點s開始,沿着s節點的寬度遍歷節點,當所有跟s節點臨接的節點都被訪問過以後,再繼續向下一層訪問。
如下圖所示
這裏寫圖片描述
遍歷順序即爲:1-2-3-4-5-6-7-8-9-10-11-12

想法一

首先最樸素的想法就是枚舉N的所有約數(除了1和其本身),然後將所有約數與X的和作爲N的子孫,構建一棵樹以後,第一個值等於M的葉子節點的深度,即爲最小跳躍次數。
節點可以通過一個類來定義,比如:

class Node{
    int val;
    Node parent;
}

或者

class Node{
    int val;
    int depth; 
}

這樣通過一個隊列Q一步步廣搜即可,但是實現想法以後,不論如何增加限制條件都Timeout,當M大於1000以後就基本上GG了,速度非常非常慢,爲什麼呢?OK,我們換個思路,繼續。

想法二

既然簡單的使用構建搜索樹的方法行不通,我們換個思路。既然是廣度搜索,每次遍歷一層,那麼我們就嘗試每次循環記錄一層的數字進行計算。
1、聲明一個List curr存儲一層節點,通過約數,計算下一層節點(比如一開始List只有一個數字N=4,通過計算約數,下一層就是有一個數字4+2=6)。
2、通過一個臨時List tmp來存儲計算的加過約數的和。
3、下一個循環用tmp替換curr。
類似於一下結構

List curr = new ArrayList<>();
curr.add(N);
while(flag){
    List tmp = new ArrayList<>();
    tmp.add(所有求得的約數);
    curr = tmp;
}

這時輸出每一次循環的curr發現,每一層有很多重複數值的節點,顯然這些重複數值的節點再次計算是很浪費時間的。比如:

從n=4開始,第三層:
[8, 9, 8]
這就有兩個8,那麼這兩個8繼續分別向下計算,顯然重複,因此可以合併一層中所有重複的值。
[8, 9]

OK,讓我們上傳OJ運行試試看…Sad,仍然Timeout。這是爲什麼呢~

想法三

在想法二的基礎上,把M設置的大一些,不斷地輸出每一層的值就會發現,除了一層中可能出現的重複情況,各個層之間也會出現很多的相同值得節點,對呀,這就像題目中說的上樓梯,每一層樓梯都是固定的,到每一層階梯的最小值通過廣搜已經計算過了,再重複計算並沒有什麼用啊!這就回歸到了DP的思想了。
因此最終改良版本,只需定義一個合適大小的數組,這個數組大小大於M,初始值設置爲-1,表示沒有到過,對所有沒有到過的,只記錄一次首次到達的前進次數(層數)即可。隨後這個位置的兒子節點都無需再遍歷了!
OK,最終代碼如下,終於通過,感人,TAT

import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;

public class Main {
    public void run(){
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();

        int[] dst = new int[2*m];
        for(int i=0;i<dst.length;i++)
            dst[i] = -1;

        dst[n] = 0;

        Queue<Integer> Q = new LinkedList<>();
        Q.offer(n);

        while(!Q.isEmpty()){
            int x = Q.poll();
            for(int i=2; i*i <= x ; i++){
                if(x%i == 0){
                    if(x+i<= m && dst[x+i] == -1) {
                        Q.offer(x + i);
                        dst[x+i] = dst[x] + 1;
                    }
                    if(x+x/i <= m && dst[x+x/i] == -1){
                        Q.offer( x + x/i);
                        dst[x+ x/i] = dst[x] + 1;
                    }
                }
            }
        }

        System.out.println(dst[m]);

        sc.close();
    }

    public static void main(String[] args){
        new Main().run();
    }
}

約數的計算

通過上一節的程序我們可以看到,計算x的約數無需從1遍歷到x,而是隻需從1遍歷到sqrt(x)
隨後,x的約數就是2~sqrt(x)之間的i,還有對應的x/i
這樣就降低了搜索空間。

總結

1、算法複雜度太高,首先一點就是能不能減少無謂的重複計算?
2、減少重複計算的方式就是拿空間換時間,通過記錄來避免重複部分的計算。

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