1. 程式人生 > >2014新浪校招筆試題:取水果(17年第一篇讓人懵逼的面試題)

2014新浪校招筆試題:取水果(17年第一篇讓人懵逼的面試題)

前言

2017 年,我還是會堅持每週一篇面試題,當然我每週看的面試題肯定是不止一篇的,我是在這周看過的面試題中,選擇一題自己認為較好的來寫。因為每一篇都寫,不現實,寫一篇部落格,需要的時間也是挺長的,所以選擇較好較大眾化的題目。

一、題目

有任意種水果, 每種水果個數也是任意的, 兩人輪流從中取出水果, 規則如下:
1) 每一次應取走至少一個水果; 每一次只能取走一種水果的一個或者全部.
2) 如果誰取到最後一個水果就勝.

給定水果種類 N 和每種水果的個數 M1, M2, …, Mn, 算出誰取勝.

二、解題

看到這個題目的時候,我整個人懵逼了,啥,到底是叫我做什麼?一臉懵逼,然後再看題目,又重新看題目,才發現,題目有個隱含的條件,就是 這兩個人足夠聰明,充分利用了規則。

可是單單憑藉這一點,還是不知道該從何下手,其實這題是 必勝策略題,可以通過用遞推的方式找一下規律解決該題。

在遞推之前,我們先來看看題目中一共給出了什麼條件:
1.N 種不同的水果
2.每種水果的個數分別為:M1, M2, …, Mn,
3.有兩個人,輪流取水果,每一次應取走至少一個水果; 每一次只能取走一種水果的一個或者全部
4.誰取到最後一個水果就勝

那好,根據上面的分析, 我們先假設兩個人分別為 A 和 B ,A 先取水果,水果的總個數為 M ,即 M = M1 + M2 + M3 + … + Mn,

(1)N = 1(只有一種水果)

A 先拿,因為知道水果的種類,所以 A 不需要考慮水果有多少個,他只要第一次拿的時候,拿完這一種水果就可以獲勝了。

結論:N = 1 ,A 必勝

(2)N = 2 (有兩種水果)

此時兩個人都不敢直接拿走一種水果, 因為那樣會送對方進入(1)的必勝局中, 自己必敗.所以 A 和 B 都只能一個一個的拿, 這樣誰拿走最後一個就由 M(水果的總個數) 的奇偶性決定。也就是說 ,M 是奇數,A 必勝,M 是偶數,B 必勝

當然我在想這個例子的時候,不小心進入了一個誤區,假如第一種水果 3 個,第二種水果 2 個,水果總數為奇數,滿足條件,假如 A 先拿第一種水果,B 再拿一個第一種水果,A 再拿一個,然後 B 拿全部第二種,B 贏。可是 A 是足夠聰明的,A 拿了第一種水果,B 跟著拿,此時 A 肯定不會接著拿第一種水果的,因為這樣自己必敗,所以他肯定會選擇拿第二種水果,這樣就能必勝了。所以還是 N = 2 的時候,水果的總數為奇數,先拿必勝,如果水果的總數為偶數,先拿必敗

結論:N = 2 ,M 是奇數, A 必勝; 否則 A 必敗

(3)N = 3 (有三種水果)

當水果種類大於 2 種時,不太好確定到底誰獲勝,需要根據各種水果數量的奇偶數來判斷,因此先按水按數量的奇偶分類,有 4 種可能:

  • 3 種水果的個數分別都是奇數個
  • 3 種水果的個數分別都是偶數個
  • 其中 2 種水果的個數是奇數個,其中 1 種水果的個數是偶數個
  • 其中 2 種水果的個數是偶數個,其中 1 種水果的個數是奇數個

無論上面是哪種情況,A 都可以立即讓 B 進入與 (2) 相反的局面(必敗的局面),比如:

  • 3 種水果的個數分別都是奇數個: A 隨便拿掉一種水果,剩餘的水果總數為偶數(奇數 + 奇數 = 偶數),剩餘兩種水果,進入了(2)的局面,水果總數為偶數,先拿必敗,所以 B 必敗,A必勝
  • 3 種水果的個數分別都是偶數個: 跟上面是一樣的,A 隨便拿掉一種水果,剩餘的水果總數為偶數(偶數 + 偶數 = 偶數),剩餘兩種水果,進入了(2)的局面,水果總數為偶數,先拿必敗,所以 B 必敗,A必勝
  • 其中 2 種水果的個數是奇數個,其中 1 種水果的個數是偶數個: A 拿走偶數個的水果的全部,也會進入(2)的局面且水果總數為偶數,A 必勝
  • 其中 2 種水果的個數是偶數個,其中 1 種水果的個數是奇數個: A 拿走奇數個的水果的全部,也會進入(2)的局面且水果總數為偶數,A 必勝

結論:N = 3 ,A 必勝

(4)N = 4 (有四種水果)

A 先取, 他肯定不會全部取走一種, 因為會送 B 進入(3)的必勝態, A 就必敗.

因此 A 只能取一個

  • 若 A 取走一個,變成了三種水果,就是變成 (3) 了, 說明 4 種水果都只有一個(否則 A 足夠聰明,可以避免這種情況) 即 M 為偶數 4 , A 必敗
  • 若 A 取完這一個還剩 4 種水果, 那 B 同上分析也只敢取一個,依次類推, 誰最後面對變成 (3) 的情況就必敗了.

也就是說 M - 4 必須是奇數,這樣 A 才會讓 B 進入最終的必敗局面,所以勝負由 M - 4 的奇偶性決定, 也就是說勝負由 M 的奇偶性決定

結論:N = 4 ,M 是奇數, A 必勝; 否則 A 必敗

通過上面的遞推,我們基本可以看到規律了:

  • N 為奇數,A 必勝
  • N 為偶數,如果 M 為奇數,A 必勝;如果 M 為偶數,A 必敗

三、程式設計


package com.liangdianshui;

import java.util.Scanner;


/**
 * <p>
 *   有任意種水果,每種水果個數也是任意的,兩人輪流從中取出水果,規則如下:
 *   1)每一次應取走至少一個水果;每一次只能取走一種水果的一個或者全部
 *   2)如果誰取到最後一個水果就勝
 *    給定水果種類N和每種水果的個數M1,M2,…Mn,算出誰取勝。
 *   (題目的隱含條件是兩個人足夠聰明,聰明到為了取勝儘可能利用規則)
 * </p>
 * 
 * @author liangdianshui
 *
 */
public class TakeTheFruit {
    private static final String EXIT = "q";

    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        String input;
        int[] fruitNums;

        do {
            System.out.println("假設 A 和 B 兩個人,A 先取水果");
            System.out.println("請輸入每種水果的個數(空格或回車分隔):");
            System.out.println("輸入 Q 或  q 退出");

            if (EXIT.equalsIgnoreCase(input = scanner.nextLine())) {
                System.out.println("Exit");
                break;
            }

            input = input.trim();
            if (input.length() != 0) {
                fruitNums = initFruitNums(input);
                boolean isWin = takeTheFruitGame(fruitNums, fruitNums.length);
                if(isWin){
                    System.out.println("A 贏");
                }else{
                    System.out.println("B 贏");
                }
                System.out.println("--------------------------------------------");
            }
        } while (true);
    }

    /**
     * 初始化每種水果的個數
     * 
     * @param input
     * @return
     */
    private static int[] initFruitNums(String input) {
        String[] nums = input.split("\\s+");
        int[] fruitNums = new int[nums.length];
        int num;
        for (int i = 0; i < nums.length; i++) {
            num = Integer.parseInt(nums[i]);
            if (num <= 0) {
                throw new IllegalArgumentException("水果數量不能為 0 或負數:" + num);
            }

            fruitNums[i] = num;
        }

        return fruitNums;
    }

    /**
     * 遞迴法
     * 
     * @param fruitNums
     * @param numOfTypes
     * @return
     */
    private static boolean takeTheFruitGame(int[] fruitNums, int numOfTypes) {

        //當水果種類為1的時候,必勝
        if (numOfTypes == 1) {
            return true;
        }

        // 當水果種類為2的時候
        if (numOfTypes == 2) {
            return sumOfTwoFruitNums(fruitNums) % 2 == 1;
        }

        // 當水果種類大於等於3的時候
        int num;
        for (int i = 0; i < fruitNums.length; i++) {
            num = fruitNums[i];
            if (num == 0)
                continue;

            fruitNums[i] = 0;
            if (!takeTheFruitGame(fruitNums, numOfTypes - 1)) {
                fruitNums[i] = num;
                return true;
            }
            if (num > 1) {
                fruitNums[i] = num - 1;
                if (!takeTheFruitGame(fruitNums, numOfTypes)) {
                    fruitNums[i] = num;
                    return true;
                }
            }

            fruitNums[i] = num;
        }

        return false;
    }

    /**
     * <p>
     *  通過結論直接輸出結果
     *  N 為奇數,A 必勝
     *  N 為偶數,如果 M 為奇數,A 必勝;如果 M 為偶數,A 必敗
     * </p>
     * @param fruitNums
     * @return
     */
    private static boolean takeTheFruitGame2(int[] fruitNums) {
        if (fruitNums.length % 2 == 1) {
            return true;
        }

        return sumOfFruitNums(fruitNums) % 2 == 1;
    }

    private static int sumOfTwoFruitNums(int[] fruitNums) {
        int num1 = 0;
        int num2 = 0;

        for (int num : fruitNums) {
            if (num > 0) {
                if (num1 == 0) {
                    num1 = num;
                } else {
                    num2 = num;
                    break;
                }
            }
        }

        return num1 + num2;
    }

    private static int sumOfFruitNums(int[] fruitNums) {
        int sum = 0;

        for (int num : fruitNums) {
            sum += num;
        }

        return sum;
    }
}