谷歌Kickstart 2017 F輪編程題解析|掘金技術徵文

寫在前面

據說Google的Kickstart應該是在半年前,Google Kickstart便是原APAC Test,G家的校園招聘線上筆試,不過一直沒有認真參加過,這個比賽時間通常是週日的下午1點到4點(北京時間),不過此次是後兩輪是在週日下午4點開始,而後持續12個小時,本身選擇其中的任意連續3個小時提交都有效。總的來講此次的題目比較簡單,除了C題的large外。git

本次共有4道題目,我AC了前面2道題。裏面的解法是根據個人思考結合官方給出的分析給出的一些思路。github

題目及分析

原題目傳送門:Round F - Dashboard算法

本文我會對題目進行簡單的中文描述。編程

Problem A. Kicksort

這兒有一種排序叫Kicksort,是來源於快速排序算法。快速排序算法選取一個基準,而後根據基準分爲兩組,而後在每組裏遞歸的這樣作。可是這種算法選取的基準可能會致使按照基準比較後只會產生一組而不是兩組,這違反了這種算法的目的。咱們稱這種基準爲最差基準。數組

爲了不這種狀況,咱們建立Kicksort算法,使用待排序序列的中間的值做爲基準,算法以下:bash

Kicksort(A): // A is a 0-indexed array with E elements
    If E ≤ 1, return A.
    Otherwise:
      Create empty new lists B and C.
      Choose A[floor((E-1)/2)] as the pivot P.
      For i = 0 to E-1, except for i = floor((E-1)/2):
        If A[i] ≤ P, append it to B.
        Otherwise, append it to C.
    Return the list Kicksort(B) + P + Kicksort(C).複製代碼

經過使用Kicksort算法,咱們發現Kicksort仍然存在對於每一次選取的基準,都是最差基準。好比數組:[1,4,3,2]。Kicksort算法首先選取4做爲基準,而後原數組變爲[1,3,2]。而後選取3做爲基準,原數組變爲[1,2]。選取1做爲基準,原數組變爲[2]。至此結束(Kicksort結束是數組長度爲0或者1時),能夠看出每次選取的基準都是最差基準。app

輸入
第一行是測試用例數量:T。
每一個測試用例包括兩行,N:元素個數。
接下來的一行是待排序的序列,元素大小從1到N,包括1到N。ide

輸出
Case #x: y
x表明是第幾個測試案例
y表明若是Kicksort算法一直選取最差基準則爲「YES」,其餘狀況爲「NO」。佈局

小數據集
1 ≤ T ≤ 32
2 ≤ N ≤ 4post

大數組集
1 ≤ T ≤ 100
2 ≤ N ≤ 10000

例子:
輸入:
4
4
1 4 3 2
4
2 1 3 4
2
2 1
3
1 2 3

輸出:
Case #1: YES
Case #2: NO
Case #3: YES
Case #4: NO

題目分析

這個題目和快速排序相關,思路仍是比較清晰的,個人思路是模擬快速排序,不過是選取中間的值做爲基準,而不是咱們日常在快速排序中的選取最後的值做爲基準,若是在排序的過程當中,若是存在按基準值分開爲2組,則返回NO,若是一直按基準值分開後都是一組,則返回YES。

具體代碼:

public class Problem1 {
    static boolean flag = true;
    public static List isWorstPivots(List<Integer> list) {
        if (list.size() <= 1) return list;
        List<Integer> left = new ArrayList<>();
        List<Integer> right = new ArrayList<>();
        int pivot = list.get((list.size()-1)/2);
        for (int i = 0; i < list.size(); i++) {
            if (i != (list.size()-1)/2) {
                if (list.get(i) <= pivot) {
                    left.add(list.get(i));
                } else {
                    right.add(list.get(i));
                }
            }
        }
        if (left.size() != 0 && right.size() != 0){
            flag = false;
        }
        List<Integer> ans = new ArrayList<>();
        ans.addAll(onlyWorstPivots(left));
        ans.add(pivot);
        ans.addAll(onlyWorstPivots(right));
        return ans;
    }
}複製代碼

這個代碼使用flag做爲每一次的標誌,若是爲TRUE,則返回「YES「,反之則返回「NO「」。這種算法的時間複雜度爲$O(n^2)$。

下面介紹一種時間複雜度爲O(n)的算法,這是官方題解介紹的。
首先咱們考慮返回YES的不一樣長度序列,咱們能夠發現它們使用的索引都是同樣的。舉個例子,
對於長度爲6的序列,首先會選取索引爲(N-1)/2對於的值,即索引爲2的值做爲索引,而後剩餘的部分組成一個長度爲5的序列,這時選取索引爲2的值,這個對應於原序列的索引3,若是咱們繼續這樣下去,選取的值的索引在原序列中的全部依次爲2,3,1,4,0,5。即咱們開始於(N-1)/2,而後向右移動1,而後向左移動2,而後向右移動3......這個不依賴於序列裏的值。

對於長度爲奇數的狀況,也是相似的,最開始從(N-1)/2開始,而後向左移動1,而後向右移動2,而後向左移動3......

這樣咱們就按照這索引規律,在每次索引的時候判斷該索引對應的值是否是最大值或則最小值。若是一直都是最大值或者最小值,則最後返回「YES」,反之則返回「NO」。

對於本題來講,因爲序列的元素是1~N,因此最小值是1,最大值是N,在代碼裏維護好這兩個值就能夠了。

具體代碼:

public static boolean isWorstPivots(int[] nums) {
    int min = 1;
    int max = nums.length;
    int start = (nums.length - 1) / 2;
    int[] move = new int[2];
    for (int i = 0; i <= (nums.length - 1) / 2; i++) {
        if ((nums.length & 1) == 1) {
            move[0] = -1;
            move[1] = 1;
        } else {
            move[0] = 1;
            move[1] = -1;
        }
        for (int j = 0; j < move.length; j++) {
            if ((start + move[j] * i) > -1 || (start + move[j] * i) < nums.length ) {
                if (nums[start + move[j] * i] == min) {
                    min++;
                } else if (nums[start + move[j] * i] == max) {
                    max--;
                } else {
                    return false;
                }
                if (i == 0) break; //當i = 0時,即爲初始位置,循環一次
            }
        }
    }
    return true;
}複製代碼

這種思路的代碼就比較簡潔清晰了。我在作這個題的時候採起的是第一種思路(比較容易想到)。

Problem B. Dance Battle

這個題目是關於在「舞蹈戰爭」中獲取最大分數。首先你的團隊有E點能量,0點的榮譽值。你會面對大小爲N個對手團隊的陣容,第i個團隊他們的跳舞技能點爲Si

在每一輪與對手對戰中,你能夠採起下面這些策略:

  • 跳舞:你會損失能量值,大小爲對手的Si,得到1點榮譽值,對手不會退回陣容中。你的能量值損失後不能小於等於0。
  • 推遲:你的能量值和榮譽值不改變,對手會退回陣容中。
  • 休戰:你的能力值和榮譽值不改變,對手不退回陣容中。
  • 招募:你招募對手到你的團隊裏,對手不退回陣容中。你增長對手的Si。你會損失1點榮譽值。你的榮譽值不能少於0。

這場戰爭結束直到對手陣容裏沒有對手團隊了。求你能得到的最大榮譽值。

輸入
第一行:T,表示測試用例數。
接下來E N,E表示你的初始能量數,N表示對手的數量。
接着是N個對手對應的跳舞技能點Si

輸出
每行輸出:Case #x: y。
x表明第幾個測試用例。
y表明得到最大的榮譽值。

限制:
1 ≤ T ≤ 100。
1 ≤ E ≤ 106。
1 ≤ Si ≤ 106, for all i。

小數據集:
1 ≤ N ≤ 5

大數據集:
1 ≤ N ≤ 1000

栗子:
輸入:
2
100 1
100
10 3
20 3 15

輸出:
Case #1: 0
Case #2: 1

題目分析

這個題目實際上是比較好想的,首先經過延時策略咱們能夠對對手按照Si進行由小到大排序。而後使用兩個指針,初始一個指向最左邊,一個指向最右邊,首先從左邊開始,若是當前的能量值大於Si,則進行跳舞策略,直到不知足爲止。此時從右邊開始(這個時候要確保至少還存在2個對手,若只剩一個對手時,直接執行休戰策略便可),進行招募策略,招募後再移動左邊的指針......整個過程保證能量值E大於0和榮譽值不小於0便可。

具體代碼:

public static int findMaxHonor(int energy, int[] skills) {
    int honor = 0;
    Arrays.sort(skills);
    int size = skills.length - 1;
    for (int i = 0; i < skills.length; i++) {
        if (skills[i] != 0) {
            if (energy > skills[i] && energy - skills[i] > 0) {
                energy -= skills[i];
                honor++;
                skills[i] = 0;
            } else if (energy <= skills[i] && honor > 0 && size > i) {
                energy += skills[size];
                honor--;
                skills[size] = 0;
                size--;
                i--;
            }
        }
    }
    return honor;
}複製代碼
Problem C. Catch Thme All

這個題目是以Pokemon GO抓取寵物小精靈爲背景,這兒有一款遊戲叫Codejamon GO,去街道上抓取寵物小精靈。

你的城市有N個位置從1~N,你開始在位置1。這兒有M條雙向的道路(1~M)。第i條道路鏈接兩個不一樣的位置(Ui, Vi),而且須要花Di分鐘從一邊到另外一邊。保證從位置1能夠到其餘任何位置。

在時間爲0時,一個寵物小精靈會等機率出如今除了你當前位置(時間:0,位置:1)的其餘任何位置。即每一個位置出現寵物的機率爲1/(N-1)。寵物一出現,你就會馬上抓住它,即抓取過程沒有時間花費。每次只有一個寵物小精靈存在,只有抓住了存在的這個纔會出現下一個。

給你城市佈局,計算抓取P個寵物小精靈的指望時間,假設你在兩個位置走的是最快的路線。

輸入
第一行:T,表示測試用例數目。
每一個測試用例第一行爲: N M P,N表明位置數目,M表明道路數目,P表明要抓取的寵物小精靈數目
接下來的是M條道路信息:Ui Vi Di,表示從位置Ui到Vi存在道路,且花費時間爲Di

輸出
每行輸出Case #x: y。
x表明第幾個測試用例
y表明花費的指望時間,若是在正確答案的10-4的絕對或相對偏差範圍內,您的答案將被視爲正確。

限制:
1 ≤ T ≤ 100.
N - 1 ≤ M ≤ (N * (N - 1)) / 2.
1 ≤ Di ≤ 10, for all i.
1 ≤ Ui < Vi ≤ N, for all i.
For all i and j with i ≠ j, Ui ≠ Ui and/or Vi ≠ Vi

小數據集
2 ≤ N ≤ 50.
1 ≤ P ≤ 200

大數據集
2 ≤ N ≤ 100.
1 ≤ P ≤ 109

栗子
輸入:
4
5 4 1
1 2 1
2 3 2
1 4 2
4 5 1
2 1 200
1 2 5
5 4 2
1 2 1
2 3 2
1 4 2
4 5 1
3 3 1
1 2 3
1 3 1
2 3 1

輸出:
Case #1: 2.250000
Case #2: 1000.000000
Case #3: 5.437500
Case #4: 1.500000

題目分析
首先咱們能夠想到圖的最短路算法,好比使用Dijkstra算法或Floyd-Warshall算法來計算兩點的最短路徑,在這個題目裏便是時間最短的路徑。求出全部點之間的花費時間最少的路徑的時間,後面會用到。而後咱們能夠根據動態規劃來計算指望時間,令dp[K, L]表明從位置L出發抓取K個精靈的指望時間,而後咱們能夠獲得狀態轉移方程:
dp[K, L] = Σi!=L(dp[K-1, i] + dis[L, i]) / (N-1).

而dp[K, L] = 0 (K = 0)

具體代碼:
圖的頂點類:

public class Vertex implements Comparable<Vertex>{
    private int path;    // 最短路徑長度
    private boolean isMarked;    // 節點是否已經出列(是否已經處理完畢)

    public int getPath() {
        return path;
    }

    public void setPath(int path) {
        this.path = path;
    }

    public Vertex() {
        this.path = Integer.MAX_VALUE;
        this.isMarked = false;
    }

    public Vertex(int path) {
        this.path = path;
        this.isMarked = false;
    }

    @Override
    public int compareTo(Vertex o) {
        return o.path > path ? -1 : 1;
    }

    public void setMarked(boolean mark) {
        this.isMarked = mark;
    }

    public boolean isMarked() {
        return this.isMarked;
    }
}複製代碼

計算指望時間:

public class Problem3 {
    // 頂點
    private List<Vertex> vertexs;
    // 邊
    private int[][] edges;
    // 沒有訪問的頂點
    private PriorityQueue<Vertex> unVisited;

    public Problem3(List<Vertex> vertexs, int[][] edges) {
        this.vertexs = vertexs;
        this.edges = edges;
        initUnVisited();
    }

    // 初始化未訪問頂點集合
    private void initUnVisited() {
        unVisited = new PriorityQueue<Vertex>();
        for (Vertex v : vertexs) {
            unVisited.add(v);
        }
    }

    // 搜索各頂點最短路徑
    public void search() {
        while (!unVisited.isEmpty()) {
            Vertex vertex = unVisited.peek();
            vertex.setMarked(true);
            List<Vertex> neighbors = getNeighbors(vertex);
            updatesDistabce(vertex, neighbors);
            unVisited.poll();
            List<Vertex> temp = new ArrayList<>();
            while (!unVisited.isEmpty()) {
                temp.add(unVisited.poll());
            }
            for (Vertex v : temp) {
                unVisited.add(v);
            }
        }
    }

    // 更新全部鄰居的最短路徑
    private void updatesDistabce(Vertex vertex, List<Vertex> neighbors) {
        for (Vertex neighbor: neighbors) {
            int distance = getDistance(vertex, neighbor) + vertex.getPath();
            if (distance < neighbor.getPath()) {
                neighbor.setPath(distance);
            }
        }
    }

    private int getDistance(Vertex source, Vertex destination) {
        int sourceIndex = vertexs.indexOf(source);
        int destIndex = vertexs.indexOf(destination);
        return edges[sourceIndex][destIndex];
    }

    private List<Vertex> getNeighbors(Vertex vertex) {
        List<Vertex> neighbors = new ArrayList<Vertex>();
        int position = vertexs.indexOf(vertex);
        Vertex neighbor = null;
        int distance;
        for (int i = 0; i < vertexs.size(); i++) {
            if (i == position) {
                continue;
            }
            distance = edges[position][i];
            if (distance < Integer.MAX_VALUE) {
                neighbor = vertexs.get(i);
                if (!neighbor.isMarked()) {
                    neighbors.add(neighbor);
                }
            }
        }
        return neighbors;
    }

    public static double calculateExpectedTimeSmallDataSet(int locations, int codejamGo, List<String> graph) {
        int[][] edges = new int[locations][locations];
        for (int i = 0; i < locations; i++) {
            for (int j = 0; j < locations; j++) {
                edges[i][j] = Integer.MAX_VALUE;
            }
        }
        for (int i = 0; i < graph.size(); i++) {
            String edge = graph.get(i);
            int source = Integer.valueOf(edge.split(" ")[0]);
            int destination = Integer.valueOf(edge.split(" ")[1]);
            int value = Integer.valueOf(edge.split(" ")[2]);
            edges[source - 1][destination - 1] = value;
            edges[destination - 1][source - 1] = value;
        }
        List<Vertex> vertexs = new ArrayList<>();

        for (int begin = 0; begin < locations; begin++) {
            for (int i = 0; i < locations; i++) {
                vertexs.add((i == begin) ? new Vertex(0) : new Vertex());
            }

            Problem3 problem3 = new Problem3(vertexs, edges);
            problem3.search();
            for (int i = 0; i < vertexs.size(); i++) {
                edges[begin][i] = vertexs.get(i).getPath();
            }
            vertexs.clear();
        }

        double[][] dp = new double[codejamGo + 1][locations];
        Arrays.fill(dp[0], 0);

        for (int i = 1; i < codejamGo + 1; i++) {
            for (int j = 0; j < locations; j++) {
                double sum = 0;
                for (int k = 0; k < locations; k++) {
                    if (k != j) {
                        sum += dp[i - 1][k] + edges[j][k];
                    }
                }
                dp[i][j] = sum / (locations - 1);
            }
        }

        return dp[codejamGo][0];
    }
}複製代碼

這種方式對於小數據集是徹底可以解決的,但對於大數據集就不適用了,這個時候咱們須要對狀態轉移方程進行簡化。咱們能夠發現,對於每一個dp[K, L],都是dp[K-1, i]的線性表達式,定義 Σj!=i(dis[i, j])。則咱們能夠寫成以下的形式:

狀態轉移方程化簡
狀態轉移方程化簡

FK表明dp[K, i]向量,A表明轉移矩陣,咱們能夠獲得
FK = A FK-1 = $A^K$ F0
在計算$A^K$時,咱們可使用快速冪算法。

具體代碼:
快速冪算法:

public class MatricQuickPower {

    public static double[][] multiply(double [][]a, double[][]b){
        double[][] arr = new double[a.length][b[0].length];
        for(int i = 0; i < a.length; i++){
           for(int j = 0; j < b[0].length; j++){
               for(int k = 0; k < a[0].length; k++){
                   arr[i][j] += a[i][k] * b[k][j];
               }  
           }  
        }  
        return arr;  
    }

    public static double[][] multiplyPower(double[][] a, int n){
        double[][] res = new double[a.length][a[0].length];
        for(int i = 0; i < res.length; i++){
            for(int j = 0; j < res[0].length; j++) {
                if (i == j)
                    res[i][j] = 1;
                else
                    res[i][j] = 0;
            }
        }  
        while(n != 0){
            if((n & 1) == 1)
                res = multiply(res, a);
            n >>= 1;
            a = multiply(a, a);
        }  
        return res;  
    }
}複製代碼

計算指望時間:

public static double calculateExpectedTimeLargeDataSet(int locations, int codejamGo, List<String> graph) {
    int[][] edges = new int[locations][locations];
    for (int i = 0; i < locations; i++) {
        for (int j = 0; j < locations; j++) {
            edges[i][j] = Integer.MAX_VALUE;
        }
    }
    for (int i = 0; i < graph.size(); i++) {
        String edge = graph.get(i);
        int source = Integer.valueOf(edge.split(" ")[0]);
        int destination = Integer.valueOf(edge.split(" ")[1]);
        int value = Integer.valueOf(edge.split(" ")[2]);
        edges[source - 1][destination - 1] = value;
        edges[destination - 1][source - 1] = value;
    }
    List<Vertex> vertexs = new ArrayList<>();
    int[] distanceSum = new int[locations];
    for (int begin = 0; begin < locations; begin++) {
        for (int i = 0; i < locations; i++) {
            vertexs.add((i == begin) ? new Vertex(0) : new Vertex());
        }

        Problem3 problem3 = new Problem3(vertexs, edges);
        problem3.search();
        int totalDistance = 0;
        for (int i = 0; i < vertexs.size(); i++) {
            totalDistance += vertexs.get(i).getPath();
        }
        distanceSum[begin] = totalDistance;
        vertexs.clear();
    }

        double[][] zeroCodeJam = new double[locations + 1][1];
        for (int i = 0; i < zeroCodeJam.length; i++) {
            for (int j = 0; j < zeroCodeJam[i].length; j++) {
                zeroCodeJam[i][j] = 0;
            }
        }
        zeroCodeJam[locations][0] = 1;
        double[][] matris = new double[locations + 1][locations + 1];
        for (int i = 0; i < matris.length; i++) {
            if (i != locations) {
                for (int j = 0; j < matris[i].length; j++) {
                    if (i == j) {
                        matris[i][j] = 0;
                    } else if (j == locations) {
                        matris[i][j] = distanceSum[i] * 1.0/(locations - 1);
                    } else {
                        matris[i][j] = 1.0/(locations - 1);
                    }
                }
            }
            if (i == locations) {
                for (int j = 0; j < matris[i].length; j++) {
                    matris[i][j] = j == locations ? 1 : 0;
                }
            }
        }

        matris = MatricQuickPower.multiplyPower(matris, codejamGo);
        double ans = 0;
        for (int i = 0; i < zeroCodeJam.length; i++) {
            ans += matris[0][i] * zeroCodeJam[i][0];
        }
        return ans;
}複製代碼

這個題目還有其餘的一些解法,更多解法移步ProblemC Analysis

Problem D. Eat Cake

這個題目是關於吃蛋糕的,蛋糕的高度都是同樣的,上下兩面都是正方形,數量不限制。如今
Wheatley想吃麪積爲N的蛋糕,可是爲了健康,想吃的蛋糕塊數最少,求最少蛋糕塊數。

輸入
第一行:T,測試用例數
接着每行表明一個測試用例:N,表示吃的蛋糕的面積

輸出
Case #x: y
x表明第幾個測試用例
y表明最小蛋糕塊數

小數據集
1 ≤ T ≤ 50
1 ≤ N ≤ 50

大數據集
1 ≤ T ≤ 100
1 ≤ N ≤ 10000

栗子
輸入:
3
3
4
5
輸出:
Case #1: 3
Case #2: 1
Case #3: 2

題目分析
首先咱們發現小數據集的數據是很小的,咱們能夠用暴力的方法。

具體代碼

public static int findCakeNumsByArea(int area) {
    if (area == 0) return 0;
    int ans = Integer.MAX_VALUE;
    for (int i = 1; i <= Math.sqrt(area); i++) {
        ans = Math.min(ans, findCakeNumsByArea(area - i * i) + 1);
    }
    return ans;
}複製代碼

對於大數據集,咱們能夠採起動態規劃DP。將每次計算的結果保存起來,這個代碼比較簡單,你們能夠看下下面的僞代碼:

public class Problem4 {

    public boolean[] alreadyComputed;
    public int[] dp;

    public Problem4() {
        this.alreadyComputed = new boolean[10000 + 1];
        this.dp = new int[10000 + 1];
        Arrays.fill(alreadyComputed, false);
    }

    // dp
    public int findCakeNumsByAreaDp(int inputArea) {
        if (inputArea == 0) return 0;
        if (alreadyComputed[inputArea]) return dp[inputArea];
        dp[inputArea] = Integer.MAX_VALUE;
        alreadyComputed[inputArea] = true;
        for (int i = 1; i <= Math.sqrt(inputArea); i++) {
            dp[inputArea] = Math.min(dp[inputArea], findCakeNumsByAreaDp(inputArea - i * i) + 1);
        }
        return dp[inputArea];
    }
}複製代碼

這種方法在使用前,須要提早將1~10000計算下,獲得dp這個數組,而後就能夠計算給的數據集。

這兒我想介紹一種很簡單的方法,就是根據拉格朗日四平方和定理,即任何一個正整數均可以表示成不超過四個整數的平方之和。若是知道這個定理,那這個題目就簡單了。代碼以下:

public static int findCakeNumsByArea(int area) {
    for (int i = 1; i <= Math.sqrt(area); i++) {
        if(i * i == area) return 1;
    }
    for (int i = 1; i <= Math.sqrt(area); i++) {
        for (int j = i; j <= Math.sqrt(area); j++) {
            if (i * i + j * j == area) return 2;
        }
    }
    for (int i = 1; i <= Math.sqrt(area); i++) {
        for (int j = i; j <= Math.sqrt(area); j++) {
            for (int k = j; k <= Math.sqrt(area); k++) {
                if (i * i + j * j + k * k == area) return 3;
            }
        }
    }
    return 4;
}複製代碼

總結

以上的全部題目的完整代碼可在2017-round-F下載。此次題目難度不大,考了一些基礎了算法,好比快速排序算法,最短路算法等,還有涉及了較多的動態規劃,平時還須要多多練習基礎算法,多多刷題。

歡迎你們批評指正與交流。

ps. 文中可能公式顯示不正確,原文請訪問個人博客:谷歌Kickstart 2017 F輪編程題解答(Java版)
附掘金徵文大賽連接

相關文章
相關標籤/搜索