算法進階面試題07——求子數組的最大異或和(前綴樹)、換錢的方法數(遞歸改dp最全套路解說)、紙牌博弈、機器人行走問題

主要講第五課的內容前綴樹應用和第六課內容暴力遞歸改動態規劃的最全步驟算法

 

第一題

給定一個數組,求子數組的最大異或和。數組

一個數組的異或和爲,數組中全部的數異或起來的結果。緩存

 

簡單的前綴樹應用dom

 

暴力方法:函數

 

 

先計算必須以i結尾的子數組的異或和,而後再計算機i+1的,以此類推...學習

 

最暴力的解優化

    public static int getMaxEor1(int[] nums) {
        int maxEor = Integer.MAX_VALUE;
        for (int i = 0; i < nums.length; i++) {
            for (int start = 0; start <= i; start++) {
                int curEor = 0;
                for (int k = start; k <= start; k++) {
                    curEor ^= nums[k];
                }
                Math.max(maxEor, curEor);
            }
        }
        return maxEor;
    }

 

怎麼優化?spa

 

異或的運算法則爲:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同爲0,異爲1),這些法則與加法是相同的,只是不帶進位,因此異或常被認做不進位加法。設計

0~i = eor3d

0~start-1 = eor2

strart~i = eor^eor2

例如

1 0 0 1 1 1 0 1 1 eor

1 0 0 1 1 0 0 0 0 eor2

0 0 0 0 0 1 0 1 1

 

優化方案,準備一個dp輔助數組記錄結果。(增長空間)

    //記憶化搜索優化(利用以前的計算結果)
    public static int getMaxEor2(int[] nums) {
        int maxEor = Integer.MAX_VALUE;
        int[] dp = new int[nums.length];
        int eor = 0;
        for (int i = 0; i < nums.length; i++) {
            eor ^= nums[i];
            Math.max(maxEor,eor);
            for (int start = 0; start <= i; start++) {
                int curEor = eor ^ dp[start - 1];
                Math.max(maxEor,curEor);
            }
            dp[i] = eor;
        }
        return maxEor;
    }

 

結論:

 

 

O(n)的方法

思路:求以i結尾,異或和最大的子數組。0~i、1~i、2~i所有計算出來獲得結果,效率很低。

黑盒直到i,裏面存了0~0、0~一、0~2...0~i-1的結果,0~i的結果在eor時刻更新的,i但願黑盒能夠告訴他,這裏面哪一個值和eor異或出來最大,那就是答案。

例如eor和0~3異或和是最大的,那麼以i結尾的,4~i就是最大的。

 

 

黑盒能夠告訴你,0~start ^ eor(0~i) 是最大的,就能得出start^i是最大的。

黑盒用前綴樹作,以4位二進制舉例,假設0~0、0~一、0~2的值,分別加入到前綴樹中

假設求以3結尾狀況下,最大異或和,0~3異或結果爲0010,我特別但願異或後符號位仍是0,後面的儘可能1,因此就在前綴樹裏面尋找適合的路。

 

 

符號位儘可能爲0,後面的位是0走1,1走0,沒得選就將就着走,儘可能保持最大化值。

按前綴樹的走法,每次選最優,能夠找到最大值。

 

符號位爲1的狀況下,求補碼,取反再加一

例如:

1 1 1 1

1 0 0 0 + 1

-1

1 0 1 1

1 1 0 0 + 1

-5

當符號位是1的時候,但願選1的路,讓其1^1變成0,除了符號位選擇的路有講究以外,無論正負,接下來的選擇是同樣的,後面的位儘可能都變成1。

因此選擇符號位的時候,但願是和符號位的值是同樣的,1選1,0選0

 

    public static class Node {//前綴樹節點
        public Node[] nexts = new Node[2];//只有兩個路,0/1
    }

    public static class NumTrie {//前綴樹
        public Node head = new Node();

        public void add(int num) {
            Node cur = head;
            //位移,整數是31位
            for (int move = 31; move >= 0; move--) {
                //提取出每一個進制裏面的數字
                //例如:0101 >> 3 = 0
                //在和1進行與運算
                //0 0 0 0
                //0 0 0 1
                //0 0 0 0 //取出了第一位爲0
                int path = ((num >> move) & 1);
                //查看是否有路,沒有就新建
                cur.nexts[path] = cur.nexts[path] == null ? new Node() : cur.nexts[path];
                cur = cur.nexts[path];
            }
        }

        //num 0~i eor結果,選出最優再返回
        public int maxXor(int num) {
            Node cur = head;
            int res = 0;
            for (int move = 31; move >= 0; move--) {
                int path = (num >> move) & 1;
                //若是考察符號位但願和path是同樣的 1^1=0 0^0=0
                //其餘位置,但願是相反的 1^0=1 0^1=1
                int best = move == 31 ? path : (path ^ 1);//期待
                best = cur.nexts[best] != null ? best : (best ^ 1);//實際
                //當前位的最優選擇,左移當前位的數值後,加入結果(或一下)
                res |= (path ^ best) << move;//設置每一位的答案
                cur = cur.nexts[best];//下一層
            }
            return res;
        }

    }

    public static int maxXorSubarray(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int max = Integer.MIN_VALUE;
        int eor = 0;
        NumTrie numTrie = new NumTrie();
        numTrie.add(0);//0和0異或
        for (int i = 0; i < arr.length; i++) {
            eor ^= arr[i];// 0 .. i
            //這個黑盒超好用
            //放入0~i eor,返回以i結尾下最大的異或和子數組的異或值
            max = Math.max(max, numTrie.maxXor(eor));
            numTrie.add(eor);
        }
        return max;
    }

    // for test
    public static int comparator(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            int eor = 0;
            for (int j = i; j < arr.length; j++) {
                eor ^= arr[j];
                max = Math.max(max, eor);
            }
        }
        return max;
    }

    // for test
    public static int[] generateRandomArray(int maxSize, int maxValue) {
        int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
        }
        return arr;
    }

    // for test
    public static void printArray(int[] arr) {
        if (arr == null) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    // for test
    public static void main(String[] args) {
        int testTime = 500000;
        int maxSize = 30;
        int maxValue = 50;
        boolean succeed = true;
        for (int i = 0; i < testTime; i++) {
            int[] arr = generateRandomArray(maxSize, maxValue);
            int res = maxXorSubarray(arr);
            int comp = comparator(arr);
            if (res != comp) {
                succeed = false;
                printArray(arr);
                System.out.println(res);
                System.out.println(comp);
                break;
            }
        }
        System.out.println(succeed ? "Nice!" : "Fucking fucked!");
    }

 

 

但願每次都走最優的,符號位但願是0。

 

開始第六課內容 dp最全套路解說

暴力遞歸怎麼改動態規劃(基礎班最後一節有方法論的講述)

換錢的方法數

【題目】

給定數組arr,arr中全部的值都爲正數且不重複。每一個值表明

一種面值的貨幣,每種面值的貨幣可使用任意張,再給定一

個整數aim表明要找的錢數,求換錢有多少種方法。

【舉例】

arr=[5,10,25,1],aim=0。

組成0元的方法有1種,就是全部面值的貨幣都不用。因此返回1。

arr=[5,10,25,1],aim=15。

組成15元的方法有6種,分別爲3張5元、1張10元+1張5元、1張

10元+5張1元、10張1元+1張5元、2張5元+5張1元和15張1元。所

以返回6。

arr=[3,5],aim=2。

任何方法都沒法組成2元。因此返回0。

嘗試版本寫出後,後面就是搭積木。

 

使用0張200的,後面湊出1000的方法數a

使用1張200的,後面湊出 800的方法數b

使用2張200的,後面湊出 600的方法數c

使用3張200的,後面湊出 400的方法數d

使用4張200的,後面湊出 200的方法數e

...

所有加起來就是答案。

 

    public static int coins1(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }
        return process1(arr, 0, aim);
    }

    //index:任意使用index以後的全部錢
    public static int process1(int[] arr, int index, int aim) {
        int res = 0;
        if (index == arr.length) {
            res = aim == 0 ? 1 : 0;
        } else {
            for (int zhang = 0; arr[index] * zhang <= aim; zhang++) {
                res += process1(arr, index + 1, aim - arr[index] * zhang);
            }
        }
        return res;
    }

 

 

process1暴力遞歸方法

 

 

返回值同樣,都要重複計算

 

 

第一個優化版本,若是index和aim固定的,只要是後面要計算600那返回值必定是肯定的,是個無後效性問題,前面怎麼選擇不影響後面的操做。

利用一個map存儲以前的結果(緩存)。下次調用,直接取出,不用這麼暴力的重複計算。(記憶化搜索

 

    //--map優化版本
    //key index_aim
    //value 組合數
    public static HashMap<String, Integer> answer = new HashMap<>();

    public static int processMap(int[] arr, int index, int aim) {
        int res = 0;
        if (index == arr.length) {
            res = aim == 0 ? 1 : 0;
        } else {
            for (int zhang = 0; arr[index] * zhang <= aim; zhang++) {
                int nextAim = aim - arr[index] * zhang;
                String nextCalc = (index + 1) + "_" + nextAim;
                if (answer.containsKey(nextCalc)) {
                    res += answer.get(nextCalc);
                } else {
                    res += processMap(arr, index + 1, nextAim);
                }
            }
        }
        answer.put(index + "_" + aim, res);
        return res;
    }

 

 

接下來直接計算全部的變化(dp),準備一個二維表 0~n是index的長度,n是終止位置。

能夠裝下全部返回值。

 

 

最終須要獲得0 aim的值

 

 

接下來思考一下,哪些位置的值是能夠直接肯定的,不依賴其餘位置的(看遞歸中的basekey

 

 

接着開始看位置依賴,也就是怎麼調用遞歸的

index + 1, aim - arr[index] * zhang

一個具體的[index,aim]位置,須要他的下一行(假設是五塊)

Aim - 0張5塊、1張5塊、2張5塊...一直到越界的因此位置累加起來,就是他的值。

和題目已經沒什麼關係,就是看遞歸函數能夠畫出這個圖。

 

 

從最後一行逐行逐個推算,能夠算出最後的答案就是,他的下面+下面位置-5+下面位置-5*2...一直到越界,由於最上面一列的金額數是5,因此最後結果是1+1+2=4。

 

    public static int coins3(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }
        int[][] dp = new int[arr.length][aim + 1];
        for (int i = 0; i < arr.length; i++) {
            dp[i][0] = 1;
        }
        for (int j = 1; arr[0] * j <= aim; j++) {
            dp[0][arr[0] * j] = 1;
        }
        int num = 0;
        for (int i = 1; i < arr.length; i++) {
            for (int j = 1; j <= aim; j++) {
                num = 0;
                for (int k = 0; j - arr[i] * k >= 0; k++) {
                    num += dp[i - 1][j - arr[i] * k];
                }
                dp[i][j] = num;
            }
        }
        return dp[arr.length - 1][aim];
    }

 

最後一步的優化(創建起空間感後)

計算A,須要對B12345進行累加,其實A的前三個位置的同層C,已經計算過12345,那其實只須要進行C+B就行。

 

 

二維三維四維問題徹底同樣。

 

    public static int coins4(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }
        int[][] dp = new int[arr.length][aim + 1];
        //填好能夠直接設置的值
        for (int i = 0; i < arr.length; i++) {
            dp[i][0] = 1;
        }
        //例如第一行是5,那麼五、十、15...位置確定爲1
        for (int j = 1; arr[0] * j <= aim; j++) {
            dp[0][arr[0] * j] = 1;
        }
        //代碼實現是從表格的上往下填
        for (int i = 1; i < arr.length; i++) {
            for (int j = 1; j <= aim; j++) {
                dp[i][j] = dp[i - 1][j];//每一個位置必定包含下面的和
                //加上左邊的位置。
                dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0;
            }
        }
        return dp[arr.length - 1][aim];
    }

    //最優解,連dp都是一維的,如今理解起來有點超天然
    public static int coins5(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }
        int[] dp = new int[aim + 1];
        for (int j = 0; arr[0] * j <= aim; j++) {
            dp[arr[0] * j] = 1;
        }
        for (int i = 1; i < arr.length; i++) {
            for (int j = 1; j <= aim; j++) {
                dp[j] += j - arr[i] >= 0 ? dp[j - arr[i]] : 0;
            }
        }
        return dp[aim];
    }

    public static void main(String[] args) {
        int[] coins = {10, 5, 1, 25};
        int aim = 2000;

        long start = 0;
        long end = 0;
        start = System.currentTimeMillis();
        System.out.println(coins1(coins, aim));
        end = System.currentTimeMillis();
        System.out.println("cost time : " + (end - start) + "(ms)");

        start = System.currentTimeMillis();
        System.out.println(coinsOther(coins, aim));
        end = System.currentTimeMillis();
        System.out.println("cost time : " + (end - start) + "(ms)");

        aim = 20000;

        start = System.currentTimeMillis();
        System.out.println(coins2(coins, aim));
        end = System.currentTimeMillis();
        System.out.println("cost time : " + (end - start) + "(ms)");

        start = System.currentTimeMillis();
        System.out.println(coins3(coins, aim));
        end = System.currentTimeMillis();
        System.out.println("cost time : " + (end - start) + "(ms)");

        start = System.currentTimeMillis();
        System.out.println(coins4(coins, aim));
        end = System.currentTimeMillis();
        System.out.println("cost time : " + (end - start) + "(ms)");

        start = System.currentTimeMillis();
        System.out.println(coins5(coins, aim));
        end = System.currentTimeMillis();
        System.out.println("cost time : " + (end - start) + "(ms)");

    }

 

 

以前是經過遞歸獲取值,如今是在dp裏面獲取值。

 

解決相似題目:

排成一條線的紙牌博弈問題

【題目】

給定一個整型數組arr,表明數值不一樣的紙牌排成一條線。玩家A和玩家B依次拿走每張紙牌,規定玩家A先拿,玩家B後拿,可是每一個玩家每次只能拿走最左或最右的紙牌,玩家A和玩家B都絕頂聰明。請返回最後獲勝者的分數。

【舉例】

arr=[1,2,100,4]。

開始時玩家A只能拿走1或4。若是玩家A拿走1,則排列變爲[2,100,4],接下來玩家B能夠拿走2或4,而後繼續輪到玩家A。若是開始時玩家A拿走4,則排列變爲[1,2,100],接下來玩家B能夠拿走1或100,而後繼續輪到玩家A。玩家A做爲絕頂聰明的人不會先拿4,由於拿4以後,玩家B將拿走100。因此玩家A會先拿1,讓排列變爲[2,100,4],接下來玩家B無論怎麼選,100都會被玩家A拿走。玩家A會獲勝,分數爲101。因此返回101。

arr=[1,100,2]。

開始時玩家A無論拿1仍是2,玩家B做爲絕頂聰明的人,都會把100拿走。玩家B會獲勝,分數爲100。因此返回100。

 

 

先想暴力,加入緩存,創建空間感,再改動態規劃。

練遞歸,學習怎麼去試。底下的優化全都是套路。

 

分析:  f(i, j) 表示 在arr[i~j]中A 先手時可以得到的最大分數,s(i, j) 表示 A後手時可以得到的最大分數。

首先分析f(i, j)。 A可先取arr[i], 取完後剩餘arr[i+1, j]。此時至關於A後手在[i+1, j]的狀況了。

也可先取arr[j], 取完後剩餘arr[i, j - 1]。 此時至關於A後手在[i, j -1]的狀況了。

則 f(i, j) = max{arr[i] + s(i+1, j), arr[j] + s(i, j-1)}

再分析s(i, j)。B可先取arr[i] 或 arr[j] 。取完後至關於A先手的狀況了。只是在這種狀況下,B會留下最差解。

s(i, j) = min{arr[i] + f(i+1, j), arr[j] + f(i, j-1)};

 

    public static int win1(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
    }

    public static int f(int[] arr, int i, int j) {//先拿的
        //若是i == j,即arr[i...j]上只有一張紙牌,固然會被先拿紙牌的人拿走,因此能夠返回arr[i];
        if (i == j) {
            return arr[i];
        }
        //拿了其中一個以後,當前玩家成了後拿的那我的
        //由於當前的玩家會作出最好的選擇,因此會拿走最好的
        return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
    }

    public static int s(int[] arr, int i, int j) {//後拿的
        //若是i == j,即arr[i...j]上只有一張紙牌,做爲後拿的人必然什麼也得不到,因此返回0;
        if (i == j) {
            return 0;
        }
        //由於對手會拿走最好的,因此當前玩家只能拿最差的
        return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
    }

 

 

改動態規劃,兩張表。分析遞歸函數,i和j的變化範圍(index變化範圍)。

i確定不會大於j(下半區域直接不用填),先看目標位置(打星星的地方),而後填上固定的值,i==j是對角線,填上對應的值。

 

 

而後看廣泛位置是怎麼依賴的。

F(i,j)依賴與他相對稱的S(i,j)的

s(arr, i + 1, j), s(arr, i, j - 1);

同理S的點也依賴F的一樣位置。

 

 

經過對角線一行行直到把終止位置(那個小星星)推出來。

    public static int win2(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int[][] f = new int[arr.length][arr.length];
        int[][] s = new int[arr.length][arr.length];
        //一邊設置對角線,一邊計算值,對角線慢慢向上走
        for (int j = 0; j < arr.length; j++) {
            f[j][j] = arr[j];
            //設計的很好,以列爲首要遍歷條件,再逐行向下計算
            for (int i = j - 1; i >= 0; i--) {
                f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
                s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
            }
        }
        return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
    }

    public static void main(String[] args) {
        int[] arr = { 1, 9, 1 };
        System.out.println(win2(arr));
        System.out.println(win1(arr));

    }

 

 

 

2017年阿里的題目:

一個長度爲N的路,1~N

一個機器人在M位置,他能夠走P步,若是在1只能走右,在N只能走左,請問機器人走P步後他停在K位置上的走法有多少種。

 

 

 

    public static int walk(int N, int curPosition, int remainSteps, int K) {
        if (N < 1 || curPosition < 1 || curPosition > N || remainSteps < 0 || K > N) {
            return 0;
        }

        if (remainSteps == 0) {
            return curPosition == K ? 1 : 0;
        }
        int count = 0;
        if (curPosition == 1) {
            count = walk(N, curPosition + 1, remainSteps - 1, K);
        } else if (curPosition == N) {
            count = walk(N, curPosition - 1, remainSteps - 1, K);
        } else {
            count = walk(N, curPosition + 1, remainSteps - 1, K) + walk(N, curPosition - 1, remainSteps - 1, K);
        }
        return count;
    }

 

 

改動態規劃,可變參數是M(機器人位置)和P(能夠走的步數)

 

 

最後須要獲取的位置是(M,P)

廣泛依賴,

 

curPosition + 1, remainSteps - 1
curPosition - 1, remainSteps - 1

 

 

楊輝三角形

 

 

計算到最後一排,看落到M上的數是幾就返回幾。

會撞牆的楊輝三角形(指的是在1和N的狀況)

 

    public static int dpWalk(int N, int curPosition, int remainSteps, int K) {
        int[][] dp = new int[remainSteps + 1][N + 1];
        dp[0][K] = 1;

        for (int i = 1; i <= remainSteps; i++) {
            for (int j = 1; j <= N; j++) {
                dp[i][j] += j - 1 < 1 ? 0 : dp[i - 1][j - 1];
                dp[i][j] += j + 1 > N ? 0 : dp[i - 1][j + 1];
            }
        }

        return dp[remainSteps][curPosition];
    }

    public static void main(String[] args) {
        System.out.println(walk(5, 3, 0, 3));
        System.out.println(walk(5, 3, 2, 3));
        System.out.println(dpWalk(5, 3, 0, 3));
        System.out.println(dpWalk(5, 3, 2, 3));
    }

 

只要你會試,什麼都無所謂。

 

首先是遞歸試法,發現有多餘的計算浪費,衍生出記憶化搜索,加上空間感後,用空間換了時間,便是動態規劃。

相關文章
相關標籤/搜索