12_经典趣题算法(Java、Kotlin描述)

(1)百钱买百鸡问题

    来自《算经》,大意:公鸡5文钱一只,母鸡3文钱一只,小鸡1文钱3只,如果用100文钱,买一百只鸡,那么公鸡、母鸡和小鸡各买多少只?
    分析:这是一个非常经典的不定方程问题,可能有多个解。假设x、y和z,分别代表公鸡、母鸡和小鸡的数量,那么应该满足:1)x + y + z = 100; 2)5x + 3y + z/3 = 100;
    Java版本算法如下:

    /**
     * 计算m钱买n鸡问题
     * @param m
     * @param n
     */
    void hundredBuyChicken(int m ,int n){
        int x,y,z;
        for (x = 0;x<=n;x++){
            for (y = 0;y<=n;y++){
                z = n - x -y;
                if (z>0 && z %3 == 0 && x*5 + y*3+z/3 == m){
                    System.out.printf("公鸡:%d只,母鸡:d%只,小鸡:d%只",x,y,z);
                }
            }
        }
    }

    Kotlin版本:

    /**
     * 计算m钱买n鸡问题
     * @param m
     * @param n
     */
    fun hundredBuyChicken(m: Int, n: Int) {
        var x: Int
        var y: Int
        var z: Int
        x = 0
        while (x <= n) {
            y = 0
            while (y <= n) {
                z = n - x - y
                if (z > 0 && z % 3 == 0 && x * 5 + y * 3 + z / 3 == m) {
                    System.out.printf("公鸡:%d只,母鸡:d%只,小鸡:d%只", x, y, z)
                }
                y++
            }
            x++
        }
    }

(2)猴子吃桃问题

    大意是:某天一只猴子摘了一堆桃子,具体数量未知。猴子每天吃掉其中的一半然后再多吃一个,第二天吃剩余的一半然后再多吃一个,······,直到第10天,猴子发现只有一个桃子。问猴子在第一天摘了多少桃子?
    思路:假设a_n表示第n天剩余的桃子数量,那么有关系:
        a_1 = (a_2+1) * 2;
        a_2 = (a_3+1) * 2;
        ······
        a_9 = (a_{10}+1) * 2;
        a_{10} = 1;
    Java版本:

    int peach(int n){
        int num;
        if (n == 1){
            return 1;
        }else {
            num = (peach(n-1)+1)*2;
        }
        return num;
    }

    Kotlin版本:

    fun peach(n: Int): Int {
        val num: Int
        num = if (n == 1) {
            return 1
        } else {
            (peach(n - 1) + 1) * 2
        }
        return num
    }

(3)舍罕王赏麦问题

    一个著名的级数求和问题,大意是:在国际象棋的8 x 8 = 64格子里,第一个格子放1粒麦子,第二个格子放2粒麦子,第三个格子放4粒麦子,······,以此类推,直到64个格子。每个格子里的麦子数是它前一个格子的2倍。
    Java版本的算法如下:

    long computeWheat(int n){
        long tmp, sum;
        tmp =1;
        sum = 0;
        
        for (int i = 1;i<=n;i++){
            sum = sum + tmp;
            tmp = tmp *2;
        }
        return sum;
    }

    Kotlin版:

    fun computeWheat(n: Int): Long {
        var tmp: Long
        var sum: Long
        tmp = 1
        sum = 0
        for (i in 1..n) {
            sum = sum + tmp
            tmp = tmp * 2
        }
        return sum
    }

(4)汉诺塔问题

    汉诺塔问题来自于古印度,又称河内塔问题。大意是:有3根柱子,分别为A、B、C。柱子A上有64个大小不一的圆形金片,其中最大的金片在最底下,其余的依次叠上去,且一个比一个小。柱子B和柱子C上开始没有金片。要求将柱子A上的金片,移动到柱子B或者柱子C上。规定一次只能移动一个金片,且金片放到柱子上时,大的只能放到小的下面。在此过程中,可以使用另外一个柱子作为辅助。
    基本思想:1)将A柱上的n-1个圆盘移到B柱上;2)将A柱上的1个圆盘移到C柱上;3)将B柱上的n-1个圆盘移到C柱上。整个过程使用了递归算法。
    算法的Java版本:

    int count = 0;

    /**
     * 汉诺塔问题
     * @param n 圆盘数量
     * @param a 初始柱子名称,例如A
     * @param b 移动的辅助柱子名称,例如B
     * @param c 移动的目标柱子名称,例如C
     */
    void hanoiTower(int n, char a, char b, char c) {
        if (n == 1) {
            System.out.printf("第%d次移动:\t圆盘从%c柱移动到%c柱\n", ++count, a, c);
        } else {
            hanoiTower(n - 1, a, c, b);
            System.out.printf("第%d次移动:\t圆盘从%c柱移动到%c柱\n", ++count, a, c);
            hanoiTower(n - 1, b, a, c);
        }
    }

    Kotlin版本:

    var count = 0

    fun hanoiTower(n: Int, a: Char, b: Char, c: Char) {
        if (n == 1) {
            System.out.printf("第%d次移动:\t圆盘从%c柱移动到%c柱\n", ++count, a, c)
        } else {
            hanoiTower(n - 1, a, c, b)
            System.out.printf("第%d次移动:\t圆盘从%c柱移动到%c柱\n", ++count, a, c)
            hanoiTower(n - 1, b, a, c)
        }
    }

(5)窃贼问题

    大意:有一个窃贼带着一个背包去偷东西,房屋中共有5件物品,其重量和价值如下:

物品1:6公斤,48元;
物品2:5公斤,40元;
物品3:2公斤,12元;
物品4:1公斤,8元;
物品5:1公斤,7元;

    窃贼希望拿最大价值的东西,而窃贼的背包最多可装重量8公斤的物品,那么窃贼应该装上面哪些物品才能达到要求呢?
    主要思路:

  1. 首先,将物品 i 试着添加到方案中;
  2. 然后判断是否超重,如果未超,则继续添加下一个物品,重复第一步;
  3. 如果超重,则将该物品排除在方案之外;并判断此时所有未排除物品的价值是否小于已有最大值;如果满足,就不必再尝试后续物品了。

    Java版本算法:

    class ThingType { //物品结果
        double value;//价值
        double weight;//重量
        boolean isSelect;//是否选中到方案
    }

    static double maxValue; //方案最大价值 
    static double totalValue; //物品总价值
    static double maxWeight;//窃贼能拿的最大重量
    static int num;//物品数量
    static boolean[] selectedArray; //临时选中数组

    void thiefSolve(ThingType[] goods, int i, double weight, double value) {
        if (weight + goods[i].weight <= maxWeight) { //判断当前方案i重量是否超出限制
            selectedArray[i] = true;
            if (i < num - 1) { //不是最后一个物品
                thiefSolve(goods, i + 1, weight + goods[i].weight, value);
            } else {
                for (int k = 0; k < num; k++) {
                    goods[k].isSelect = selectedArray[k];
                }
                maxValue = value;//保存当前方案的最大价值
            }
        }
        selectedArray[i] = false;//取消物品i的选择状态
        if (value - goods[i].value > maxValue) { //还可以继续添加物品
            if (i < num - 1) {
                thiefSolve(goods, i + 1, weight, value - goods[i].value);
            } else {
                for (int k = 0; k < num; k++) {
                    goods[k].isSelect = selectedArray[k];
                }
                maxValue = value - goods[i].value;//保存当前方案的最大价值
            }
        }
    }

    Kotlin版本:

    internal inner class ThingType {
        //物品结果
        var value //价值
                = 0.0
        var weight //重量
                = 0.0
        var isSelect //是否选中到方案
                = false
    }

    fun thiefSolve(goods: Array<ThingType>, i: Int, weight: Double, value: Double) {
        if (weight + goods[i].weight <= maxWeight) { //判断当前方案i重量是否超出限制
            selectedArray[i] = true
            if (i < num - 1) { //不是最后一个物品
                thiefSolve(goods, i + 1, weight + goods[i].weight, value)
            } else {
                for (k in 0 until num) {
                    goods[k].isSelect = selectedArray[k]
                }
                maxValue = value //保存当前方案的最大价值
            }
        }
        selectedArray[i] = false //取消物品i的选择状态
        if (value - goods[i].value > maxValue) { //还可以继续添加物品
            if (i < num - 1) {
                thiefSolve(goods, i + 1, weight, value - goods[i].value)
            } else {
                for (k in 0 until num) {
                    goods[k].isSelect = selectedArray[k]
                }
                maxValue = value - goods[i].value //保存当前方案的最大价值
            }
        }
    }

    companion object {
        var maxValue //方案最大价值
                = 0.0
        var totalValue //物品总价值
                = 0.0
        var maxWeight //窃贼能拿的最大重量
                = 0.0
        var num //物品数量
                = 0
        var selectedArray //临时选中数组
                : BooleanArray
    }

    只看上述的算法,并不容易理解。还需要看初始化及传入的数据,这里用窃贼问题的数据做几点说明:

  1. 初始化数据,物品数量num = 5,goods是一个size=5的数组;物品总价值totalValue = 48 + 40 +12 + 8+ 7 = 115 ,maxValue初始值为0 ,maxWeight = 8;
  2. 开始调用thiefSolve()方法时,传入的参数是:thiefSolve(goods, 0, 0.0, totalValue); weight = 0.0,value = totalValue;
  3. 可以看出,weight表示当前方案的总重量;value表示当前方案的总价值,不过它并不是从0开始,而是反过来计算,初始值为最大值totalValue,然后随着 i 的增加减去不在方案中的物品价值。

(6)鸡兔同笼问题

    鸡兔同笼问题于1500年前,由《孙子算经》所记载。大致是:在一个笼子里关着若干鸡和兔,从上面数有35个头,从下数共有94只脚。问鸡、兔各有多少只?
    穷举法的Java版本:

    int chicken;
    int rabbit;
    
    boolean computeChickenRabbit(int head,int foot){
        int i,j;
        for (i =0;i<=head;i++){
            j = head - i;
            
            if (i*2 + 4*j == foot){
                chicken = i;
                rabbit = j;
                return true;
            }
        }
        return false;
    }

    Kotlin版本:

    var chicken = 0
    var rabbit = 0
    fun computeChickenRabbit(head: Int, foot: Int): Boolean {
        var i: Int
        var j: Int
        i = 0
        while (i <= head) {
            j = head - i
            if (i * 2 + 4 * j == foot) {
                chicken = i
                rabbit = j
                return true
            }
            i++
        }
        return false
    }

    还有一种不使用穷举法的求解算法,它更为简单,主要思想是:一只鸡有一个头和两只脚,一只兔子有一个头和四只脚,那么所有的脚减去2倍的头,就是多余的兔子的另外两只脚。Java版本如下:

    void computeChickenRabbit2(int head,int foot,int rabbitNum,int chickenNum){
        rabbitNum = (foot - head*2)/2;
        chickenNum = head - rabbitNum;
    }

(7)八皇后问题

    八皇后问题是1850年高斯提出的,是一个经典的回溯算法问题。大意是:在国际象棋的64个单元格中,摆放8个皇后,使其不能互相攻击,也就是说任意两个皇后不能处于同一列、同一行或者同一斜线上。问共有多少种摆法?每种是怎么摆的?
    简略介绍一下国际象棋:它起源于亚洲,具体来自古印度,还是中国,尚有争论;同中国象棋一样,国际象棋共有32个棋子,每方16个,没有中国象棋的士、炮,增加了王后。王、王后各1个,车、马、象各2个,再加8个兵。
    主要思路:采用递归的方式来求解,思路如下:

  1. 首先在棋盘的某个位置放置一个皇后;
  2. 放置下一个皇后,判断该皇后是否与前面的皇后形成互相攻击,若不互相攻击,则继续放下一个皇后;若相互攻击,则调整位置,再继续判断;
  3. 当放置完8个不形成攻击的皇后,就形成一个解,将其输出。

    八皇后问题共有92个解,下面的算法会输出所有的解。为了便于理解,对输出结果做一点说明,如输出:1 6 8 3 7 4 2 5 ,表示第一个皇后放在第1行,第1列;第二个皇后放在第2行,第6列,第三个皇后放在第3行,第8列,······,依此类推。
    Java版本:

/**
 * 八皇后问题
 */
public class EightQueen {

    final static int MAX = 8;
    static int[] posArray = new int[MAX];
    static int count;

    public static void main(String[] args) {
        eightQueen(0);
        System.out.println("Solution count : " + count);
    }

    static void eightQueen(int n) {
        if (n == MAX) {
            printSolution();
            return;
        }
        for (int i = 0; i < MAX; i++) {
            posArray[n] = i; //在第n行的i列上放置
            if (!willAttack(n)) {//不会引起冲突,就继续下一行
                eightQueen(n + 1);
            }
        }
    }

    /**
     * 和已有皇后是否相互攻击
     *
     * @param n
     * @return
     */
    static boolean willAttack(int n) {
        for (int i = 0; i < n; i++) {
            //如果同一列,或者列、行差值相等即斜率相等(对角线),会互相攻击
            if (posArray[i] == posArray[n] || Math.abs(n - i) == Math.abs(posArray[i] - posArray[n])) {
                return true;
            }
        }
        return false;
    }

    /**
     * 打印输出结果,下标加1进行调整
     */
    static void printSolution() {
        count++;
        for (int i = 0; i < posArray.length; i++) {
            System.out.print((posArray[i] + 1) + " ");
        }
        System.out.println();
    }

}

    Kotlin版本:

object EightQueen {
    const val MAX = 8
    var posArray = IntArray(MAX)
    var count = 0
    @JvmStatic
    fun main(args: Array<String>) {
        eightQueen(0)
        println("Solution count : " + count)
    }

    fun eightQueen(n: Int) {
        if (n == MAX) {
            printSolution()
            return
        }
        for (i in 0 until MAX) {
            posArray[n] = i //在第n行的i列上放置
            if (!willAttack(n)) { //不会引起冲突,就继续下一行
                eightQueen(n + 1)
            }
        }
    }

    /**
     * 和已有皇后是否相互攻击
     *
     * @param n
     * @return
     */
    fun willAttack(n: Int): Boolean {
        for (i in 0 until n) {
            //如果同一列,或者列、行差值相等即斜率相等(对角线),会互相攻击
            if (posArray[i] == posArray[n] || Math.abs(n - i) == Math.abs(
                    posArray[i] - posArray[n]
                )
            ) {
                return true
            }
        }
        return false
    }

    /**
     * 打印输出结果,下标加1进行调整
     */
    fun printSolution() {
        count++
        for (i in posArray.indices) {
            print((posArray[i] + 1).toString() + " ")
        }
        println()
    }
}

    初看这个算法时,觉得这不是只输出一个解吗?printSolution()也才打印一次啊?怎么会输出92个解呢?注意这是一个递归调用,对于皇后每一行、每一列的可能位置都做了遍历。就拿放置第一个皇后为例,它可能出现在任意一列,针对每一个可能位置,对第二个皇后的放置有不同影响,依此类推。每一种可能,都处理到第8个皇后为止。所以,这个算法会输出所有的解。

(8)假银币问题

    大意是这样的:有8枚银币,其中一枚是假币。但是,从外观和做工上无法分辨真假,只知道假币的重量比真币稍轻。要求使用一个天平,以最少的步骤寻找假银币?
    主要思路:采用递归分治的思想来求解这个问题,步骤如下:

  1. 将所有的银币等分成2份,放在天平的两边;
  2. 因为假币的分量较轻,因此天平较轻的一侧中一定包含假币;
  3. 再将较轻的一侧中的银币等分为两份,重复上述的做法;
  4. 直到剩下最后两枚银币,可以直接找出。

    关于等分,这里有一个问题,如果银币总数是奇数,是没法等分的。有一些书籍上,对奇偶情况分别判断,分别处理,使得算法既长,又重复,一点也不简练。在此,稍微改动一下,并不进行等分,而是进行3分,分3组。前2组银币个数相等,第三组个数不定。如果前2组重量一样,说明假币在第三组。
    Java版本:

    /**
     * 假银币问题
     *
     * @param coinArray 包含假银币的数组
     * @param low       起始位置
     * @param high      结束位置
     * @param n         个数
     * @return 假银币的在数组中的下标,从0开始
     */
    int falseCoin(int[] coinArray, int low, int high, int n) {
        int num1, num2, num3;
        int sum1 = 0, sum2 = 0;
        if (n == 1) {
            return low;
        }
        if (n % 3 == 0) {
            num1 = num2 = n / 3;
        } else {
            num1 = num2 = n / 3 + 1;
        }
        num3 = n - num1 - num2;

        for (int i = 0; i < num1; i++) {
            sum1 += coinArray[i];
        }

        for (int i = 0; i < num2; i++) {
            sum2 += coinArray[i];
        }

        if (sum1 < sum2) { //假币在第一组
            return falseCoin(coinArray, low, low + sum1 - 1, sum1);
        } else if (sum1 > sum2) { //假币在第二组
            return falseCoin(coinArray, low + num1, low + sum1 + num2 - 1, sum2);
        } else {//假币在第三组
            return falseCoin(coinArray, low + num1 + num2, high, num3);
        }
    }

    kotlin版本:

    fun falseCoin(coinArray: IntArray, low: Int, high: Int, n: Int): Int {
        val num1: Int
        val num2: Int
        val num3: Int
        var sum1 = 0
        var sum2 = 0
        if (n == 1) {
            return low
        }
        if (n % 3 == 0) {
            num2 = n / 3
            num1 = num2
        } else {
            num2 = n / 3 + 1
            num1 = num2
        }
        num3 = n - num1 - num2
        for (i in 0 until num1) {
            sum1 += coinArray[i]
        }
        for (i in 0 until num2) {
            sum2 += coinArray[i]
        }
        return if (sum1 < sum2) { //假币在第一组
            falseCoin(coinArray, low, low + sum1 - 1, sum1)
        } else if (sum1 > sum2) { //假币在第二组
            falseCoin(coinArray, low + num1, low + sum1 + num2 - 1, sum2)
        } else { //假币在第三组
            falseCoin(coinArray, low + num1 + num2, high, num3)
        }
    }

(9)三色旗问题

    大意是这样的:在一条绳子上挂有白、红、蓝三种颜色的多面旗子,这些旗子的排列是无序的。现要将绳子上的旗子按蓝、白、红三种颜色进行归类排列,每次可以调换两个旗子,问如何采用最少的步骤来完成?
    主要思路:使用三个变量blue、white、red来表示旗子的顺序下标,初始时,blue、white均为0,red为旗子总数减1;在[ 0, blue-1]放置蓝色旗子,在[ blue, white-1 ]放置白色旗子,在[ red+1, length-1]放置红色旗子;[ white, red ]这个区间放置还未处理的旗子。每一次都处理变量write指向位置的元素,分3种情况:

  1. 如果white所在的位置是白旗,直接white++ ;
  2. 如果white所在的位置是蓝旗,将white处的元素与blue对调,然后blue++、white++;
  3. 如果white所在的位置是红旗,将white处的元素与red对调,然后red-- ;

    Java版本:

    /**
     * 三色旗问题
     *
     * @param color 由'b'、'w'、'r'组成的无序字符数组
     */
    void threeFlags(char[] color) {
        int blue, white, red;
        blue = white = 0;
        red = color.length - 1;

        while (color[white] == 'b') { //蓝旗
            blue++;
            white++;
        }
        while (color[red] == 'r') { //红旗
            red--;
        }
        while (white <= red) {
            if (color[white] == 'r') {
                swap(color,white,red);
                red--;

                while (color[red] == 'r'){
                    red--;
                }
            }

            if (color[white] == 'w'){
                white++;
            }

            if (color[blue] == 'b'){
                swap(color,white,blue);
                white++;
                blue++;
            }
        }
    }

    void swap(char[] charArray, int x, int y) {
        char tmp = charArray[x];
        charArray[x] = charArray[y];
        charArray[y] = tmp;
    }

    Kotlin版本:

    /**
     * 三色旗问题
     *
     * @param color 由'b'、'w'、'r'组成的无序字符数组
     */
    fun threeFlags(color: CharArray) {
        var blue: Int
        var white: Int
        var red: Int
        white = 0
        blue = white
        red = color.size - 1
        while (color[white] == 'b') { //蓝旗
            blue++
            white++
        }
        while (color[red] == 'r') { //红旗
            red--
        }
        while (white <= red) {
            if (color[white] == 'r') {
                swap(color, white, red)
                red--
                while (color[red] == 'r') {
                    red--
                }
            }
            if (color[white] == 'w') {
                white++
            }
            if (color[blue] == 'b') {
                swap(color, white, blue)
                white++
                blue++
            }
        }
    }

    fun swap(charArray: CharArray, x: Int, y: Int) {
        val tmp = charArray[x]
        charArray[x] = charArray[y]
        charArray[y] = tmp
    }

(10)渔夫捕鱼问题

    大意是这样的:某天,A、B、C、D、E 五个渔夫合伙捕了很多鱼,天黑后到岸边各自休息。第二天早晨,渔夫A第一个醒来,他将鱼分作五份,把多余的一条扔回河中,拿自己的一份回家去了。渔夫B第二个醒来,也将鱼分作五份,扔掉多余的一条,拿走自己的一份······以此类推,直到最后一个渔夫E醒来,也将鱼分作五份,扔掉多余的一条,拿走自己的一份。问五渔夫至少捕到多少条鱼呢?
    先来看看一个错误的分析,这种分析非常地误导人。网络和书籍上的某些作者,都陷入了这个陷阱中,包括开始的自己。

  1. 每个渔夫醒来的时候,鱼的数量都应该是5的倍数再加1。先来看看最后一个渔夫E,鱼的数量最少应该是6。在他扔掉一条鱼之后,仍然可以平均分为5份;
  2. 渔夫D醒来的时候,鱼的数量应该是:6*5 + 1 = 31;
  3. 渔夫C醒来的时候,鱼的数量应该是:31*5+1 = 156;
  4. 渔夫B醒来的时候,鱼的数量应该是:156*5+1 = 781;
  5. 渔夫A醒来的时候,鱼的数量应该是:781*5 + 1 = 3906;那么,渔夫至少合伙捕到3906条鱼。

    由上得到,这是一个递推的式子,公式:S_{n-1} = 5S_n+1;

    这个分析乍一看,好像没什么问题。但拿笔在草稿本上算一算、画一画,就感到哪里不对劲了。让我们先来跟着它的分析,找到矛盾之处。

    先假设上述分析没有问题,渔夫E醒来后的鱼数是6,渔夫D醒来后的鱼数是31,只看这两个数据就够了。从问题中,渔夫D比渔夫E先醒来,他将鱼数分作5份,扔掉多余的一条,拿走自己的一份。31条鱼,扔掉一条,还有30条,分作5份,每份6条,他拿走自己的一份,剩下应该有24条。随后,渔夫E醒来,此时鱼的数量应该是24,并不是6。这就是矛盾所在,所以上述分析是错误的,得出的公式也是错误的。

    由上可知,如果渔夫E醒来后鱼的数量是6,渔夫D醒来后的鱼数肯定不是31,那么就想问一问,渔夫D醒来后的正确鱼数是多少?

    假设渔夫D醒来后的正确鱼数是N,那么从问题描述中可以得到:N - 1 - \frac{N-1}{5} = 6 ,由它得出N = \frac{15}{2} + 1;这显然不对,因为鱼的条数不可能是小数。所以,很显然,渔夫E醒来后的鱼数不可能为6。初始感觉是错误的。

    我们先来推导一下正确的公式,将问题倒过来递推,以渔夫E醒来后的鱼数作为第一项。

    假设S_n表示第n个渔夫醒来后的鱼数,那么:
         S_{n+1} - 1 - \frac{S_{n+1}-1}{5} = S_{n}
    由此可得:
         S_{n+1} = \frac{5*S_{n}}{4} + 1 , n\epsilon{1,2,3,4,5}

    在本问题中,因为只有5个渔夫,第一项S_1,第二项S_2,第三项S_3,第四项S_4,都必须能被4整除,而第五项S_5无此要求。
    那么,现在第一项S_1的值是多少呢?显然不能再靠感觉了。由上可知:

  1. S_1的值必须被4整除,因此它的最小值为4;
  2. 因为S_2、S_3、S_4也必须被4整除;所以如果发现有不能被整除的情况,那么可以推断,S_1的值不对;
  3. 重新设置S_1的值,直到满足整除条件为止;
  4. S_5可以不被4整除。

    Java版本算法:

/**
 * 5渔夫捕鱼问题
 */
public class FishProblem {
    static int initValue = 4;

    /**
     * 5渔夫捕鱼问题求解
     * @return 渔夫捕鱼问题的最少鱼数
     */
    static int fiveFishmanProblem(){
        int sum = initValue;

        while (true){
            for (int i=0;i<4;i++){ //只需要处理前4个值,保证被4整除
                if (sum % 4 != 0){
                    break;
                }
                sum = sum *5/4+1;
            }

            if (sum%4 == 0){
                break;
            } else {
                initValue += 4; //重置s1的值
                sum = initValue;
            }
        }
        sum = sum *5/4+1; //第5个值单独计算,不需要满足被4整除
        return sum;
    }

    public static void main(String[] args) {
        int result = fiveFishmanProblem();
        System.out.println("result = "+result +" , S1 = "+ (initValue *5/4 +1));
    }

    运行这个程序,可以得出:

result = 3121 , S1 = 1276

    从结果得知,渔夫E醒来时,鱼的数量是1276,他扔了一条,带走了255条;五渔夫总共捕获了至少3121条。
    Kotlin版本:

/**
 * 5渔夫捕鱼问题
 */
object FishProblem {
    var initValue = 4

    /**
     * 5渔夫捕鱼问题求解
     * @return 渔夫捕鱼问题的最少鱼数
     */
    fun fiveFishmanProblem(): Int {
        var sum = initValue
        while (true) {
            for (i in 0..3) { //只需要处理前4个值,保证被4整除
                if (sum % 4 != 0) {
                    break
                }
                sum = sum * 5 / 4 + 1
            }
            if (sum % 4 == 0) {
                break
            } else {
                initValue += 4 //重置s1的值
                sum = initValue
            }
        }
        sum = sum * 5 / 4 + 1 //第5个值单独计算,不需要满足被4整除
        return sum
    }

    @JvmStatic
    fun main(args: Array<String>) {
        val result = fiveFishmanProblem()
        println("result = " + result + " S1 = " + (initValue * 5 / 4 + 1))
    }
}

    让我们继续深入一下,上述算法是否可以扩充到任意个渔夫?很显然,简单的传递一个渔夫数量参数是不行的,因为不同的渔夫数量,得出的推导公式是不同的。让我们来分别考虑。
    二渔夫情况:这个很简单,S_1=3,S_2=7,两个人最少捕获7条鱼。
    三渔夫情况:可以推导获得以下公式:

    假设S_n表示第n个渔夫醒来后的鱼数(n\epsilon{1,2,3}),那么:
         S_{n+1} - 1 - \frac{S_{n+1}-1}{3} = S_{n}
    由此可得:
         S_{n+1} = \frac{3*S_{n}}{2} + 1 , n\epsilon{1,2,3}

    可以得出这样的结论:S_1、S_2必须被2整除,S_3无此限制。
    Java版本:

/**
 * 3渔夫捕鱼问题
 */
public class FishProblem {
    static int initValue = 2;

    static int threeFishmanProblem(){
        int sum = initValue;

        while (true){
            for (int i=0;i<2;i++){ //只需要处理前2个值,保证被2整除
                if (sum % 2 != 0){
                    break;
                }
                sum = sum *3/2+1;
            }

            if (sum%2 == 0){
                break;
            } else {
                initValue += 2; //重置s1的值
                sum = initValue;
            }
        }
        sum = sum *3/2+1; //第3个值单独计算,不需要满足被2整除
        return sum;
    }

    public static void main(String[] args) {
        int result = threeFishmanProblem();
        System.out.println("result = "+result +" , S1 = "+ (initValue *3/2 +1));
    }
}

    运行此程序,得到的结果是:

result = 25 , S1 = 10

    可以得知,三渔夫最少捕获25条鱼。

    类似的,四渔夫推导公式:
         S_{n+1} - 1 - \frac{S_{n+1}-1}{4} = S_{n}
    由此可得:
         S_{n+1} = \frac{4*S_{n}}{3} + 1 , n\epsilon{1,2,3,4}
    它的Java实现就省略了,和上面的大同小异。

    综上所述,可以得到m个渔夫推导公式:
         S_{n+1} - 1 - \frac{S_{n+1}-1}{m} = S_{n}
    由此可得:
         S_{n+1} = \frac{m*S_{n}}{m-1} + 1 , n\epsilon{1,2,......,m}

    它的Java代码实现:

/**
 * m渔夫捕鱼问题
 */
public class FishProblem {
    static int initValue = 0;

    /**
     * m渔夫捕鱼问题
     *
     * @return
     */
    static int mFishman(int m) {
        if (m == 1) {
            return 3;
        }
        initValue = m - 1;
        int sum = initValue;

        while (true) {
            for (int i = 0; i < m - 1; i++) { //只需要处理前m-1个值,保证被m-1整除
                if (sum % (m - 1) != 0) {
                    break;
                }
                sum = sum * m / (m - 1) + 1;
            }

            if (sum % (m - 1) == 0) {
                break;
            } else {
                initValue += (m - 1); //重置s1的值
                sum = initValue;
            }
        }
        sum = sum * m / (m - 1) + 1; //第m个值单独计算,不需要满足被m-1整除
        return sum;
    }

    public static void main(String[] args) {
        int m = 6; //随便设置
        int result = mFishman(m);
        System.out.println("result = "+result +" S1 = "+ (initValue *m/(m-1) +1));
    }
}

    Kotlin版本就省略了,毕竟这一小节已经很长了。

(11)爱因斯坦的阶梯

    大意是这样的:有一个楼,其两层之间有一个很长的阶梯。如果一个人每步上2阶,最后剩1阶;如果每步上3阶,最后剩2阶;如果每步上5阶,最后剩4阶;如果每步上6阶,最后剩5阶;如果每步上7阶,最后刚好一阶不剩。问这个阶梯至少有多少阶?
    主要思路:阶数与上面这些数字,存在特定的关系,比如是7的倍数、奇数等,通过遍历,找到满足这些关系的数。
    Java版本:

    int computeEinsteinStairs() {
        int count = 7;
        for (int i = 0; i < 100; i++) {
            if (count % 2 == 1 && count % 3 == 2 && count % 5 == 4 && count % 6 == 5) {
                break;
            }
            count = 7 * (i + 1);
        }
        return count;
    }

    Kotlin版本:

    fun computeEinsteinStairs(): Int {
        var count = 7
        for (i in 0..99) {
            if (count % 2 == 1 && count % 3 == 2 && count % 5 == 4 && count % 6 == 5) {
                break
            }
            count = 7 * (i + 1)
        }
        return count
    }

(12)常胜将军问题

    大意是这样的:甲乙两人玩轮流抽火柴的游戏,共有21根火柴。每个人每次最多取4根火柴,最少取1根,取到最后一根火柴的判输。甲让乙先抽,结果每次都是甲赢。这是为什么?
    分析:把问题倒过来看,不从21开始分析,以乙拿火柴时的可能数量倒序分析:

  1. 如果乙拿火柴时火柴数为1,那么按照规则,乙必输,甲要争取这种情况;
  2. 如果乙拿火柴时火柴数是2-5,那么他总可以留下一根,则乙必胜,甲要避免这种情况;

    分析火柴数大于5的情况。如果乙取火柴时,火柴数是6,那么:

  1. 乙取1根,剩5根,甲取4根,留1根,甲胜;
  2. 乙取2根,剩4根,甲取3根,留1根,甲胜;
  3. 乙取3根,剩3根,甲取2根,留1根,甲胜;
  4. 乙取4根,剩2根,甲取1根,留1根,甲胜;

    可以看到:只要甲取的火柴数,等于5减去乙取的火柴数,那么甲必胜。也就是说,如果火柴数是6,遵照这个规则,那么谁先拿,谁必输。
    由此可以推断,如果火柴数是7、8、9、10,那么先拿的一方可以让剩下的火柴数等于6,从而获胜。即谁先拿,谁必胜。
    再考虑火柴数是11,此时不论拿几根火柴,都会让剩下的数量在区间[ 7, 10 ],必输。即谁先拿,谁必输。
    同样的道理,如果火柴数在区间[ 12, 16 ],那么先拿者总可以让剩下的火柴数等于11,必胜。即谁先拿,谁必胜。

    综上所述,可以得到更一般的规律:如果火柴初始数量等于(5n+1),n\epsilon {1,2,3,......},那么谁先拿,谁必输;如果不等,那么谁先拿,谁必胜。
    实现方案:如果火柴初始数量S=(5n+1),先拿者拿走m个,其中m\epsilon {1,2,3,4},不论m是多少,后拿者只要拿走(5 - m)个,就可以让剩余火柴数朝着1递进,从而实现必胜的目标,即先拿必输。如果火柴初始数量S\neq(5n+1),那么先找到一个值K,K满足\begin{cases}K=(5n+1) \\K<S \\K+5>S \end{cases},然后先拿者只要拿走( S - K )根火柴,就可以保证必胜,即先拿必胜。

    回归到本小节最开始的问题,火柴的初始值是21,满足(5n+1),其中n=4,所以根据上面的规律:先拿必输。乙先拿,乙必输,这也是甲每次都赢的原因。
    Java版本:

import java.util.Random;

/**
 * 常胜将军问题
 */
public class AlwaysWinQ {
    static String personA = "甲";
    static String personB = "乙";

    static void alwaysWinQ(int matchNum) {

        int n = (matchNum - 1) / 5;
        int r = (matchNum - 1) % 5;
        int leftNum = matchNum;
        int bGetNumPerTime, aGetNumPerTime;
        int count = 0;
        Random random = new Random();
        if (r == 0) {
            System.out.println("满足(5n+1),结论:先拿者必输。实际过程如下:");
            while (leftNum > 1) {
                //乙先拿,随机拿几个
                bGetNumPerTime = 1 + random.nextInt(4);
                aGetNumPerTime = 5 - bGetNumPerTime;
                System.out.println("第" + (++count) + "次循环," + personB + "拿走了" + bGetNumPerTime
                        + "根火柴,剩下" + (leftNum - bGetNumPerTime) + "根," + personA + "拿走了" 
                        + aGetNumPerTime + "根火柴,剩下" + (leftNum - bGetNumPerTime - aGetNumPerTime));
                leftNum -= aGetNumPerTime + bGetNumPerTime;
            }
            if (leftNum == 1) {
                System.out.println("最后一次循环,剩余火柴数量为1," + personB + "拿走," + personA + "获胜!");
            }
        } else {
            System.out.println("不满足(5n+1),结论:先拿者必胜。实际过程如下:");
            int bGetNumFirstTime = matchNum - (5 * n + 1);
            System.out.println(personB + "先拿,拿走" + bGetNumFirstTime + "根火柴。剩余:" + (leftNum - bGetNumFirstTime));
            leftNum -= bGetNumFirstTime;
            while (leftNum > 1) {
                //乙已经拿过了,现在轮到甲拿,数量随机
                aGetNumPerTime = 1 + random.nextInt(4);
                bGetNumPerTime = 5 - aGetNumPerTime;
                System.out.println("第" + (++count) + "次循环," + personA + "拿走了" + aGetNumPerTime
                        + "根火柴,剩下" + (leftNum - aGetNumPerTime) + "根," + personB + "拿走了" 
                        + bGetNumPerTime + "根火柴,剩下" + (leftNum - bGetNumPerTime - aGetNumPerTime));
                leftNum -= aGetNumPerTime + bGetNumPerTime;
            }
            if (leftNum == 1) {
                System.out.println("最后一次循环,剩余火柴数量为1," + personA + "拿走," + personB + "获胜!");
            }
        }
    }

    public static void main(String[] args) {
        int matchNum = 21;
        System.out.println("轮流抽火柴游戏,火柴总数:" + matchNum + ",规则:" + personB + "先拿,每次最少拿1根,最多拿4根;拿到最后一根的判输。");
        System.out.println();
        int repeatNum = 5;
        System.out.println("重复玩" + repeatNum + "次,看看结果是否一致");
        for (int i = 0; i < repeatNum; i++) {
            System.out.println("********************");
            alwaysWinQ(matchNum);
            System.out.println();
        }

    }
}

    当matchNum=21时,以下是部分输出:

    轮流抽火柴游戏,火柴总数:21,规则:乙先拿,每次最少拿1根,最多拿4根;拿到最后一根的判输。
    满足(5n+1),结论:先拿者必输。实际过程如下:
    第1次循环,乙拿走了1根火柴,剩下20根,甲拿走了4根火柴,剩下16
    第2次循环,乙拿走了2根火柴,剩下14根,甲拿走了3根火柴,剩下11
    第3次循环,乙拿走了3根火柴,剩下8根,甲拿走了2根火柴,剩下6
    第4次循环,乙拿走了3根火柴,剩下3根,甲拿走了2根火柴,剩下1
    最后一次循环,剩余火柴数量为1,乙拿走,甲获胜!

    当matchNum=22时,以下是部分输出:

    轮流抽火柴游戏,火柴总数:22,规则:乙先拿,每次最少拿1根,最多拿4根;拿到最后一根的判输。
    不满足(5n+1),结论:先拿者必胜。实际过程如下:
    乙先拿,拿走1根火柴。剩余:21
    第1次循环,甲拿走了1根火柴,剩下20根,乙拿走了4根火柴,剩下16
    第2次循环,甲拿走了4根火柴,剩下12根,乙拿走了1根火柴,剩下11
    第3次循环,甲拿走了4根火柴,剩下7根,乙拿走了1根火柴,剩下6
    第4次循环,甲拿走了2根火柴,剩下4根,乙拿走了3根火柴,剩下1
    最后一次循环,剩余火柴数量为1,甲拿走,乙获胜!

    从输出结果,验证了上面总结的规律。
    鉴于本小节有点长,Kotlin版省略。

(13)新郎和新娘问题

    大意是这样的:有三对新婚夫妇参加集体婚礼,三个新郎为A、B、C,三个新娘为X、Y、Z。主持人一时忘了谁应该和谁结婚,于是他便问这6个人中的三个。这三个人开起了玩笑,全都给了错误的答案,如下:

  1. 新郎A说他将和新娘X结婚;
  2. 新娘X说她将和新郎C结婚;
  3. 新郎C说他将和新娘Z结婚。

    到底谁应该和谁结婚呢?
    主要思路:遍历可能的组合,并将错误的答案排除。
    分析:这个问题,通过直观分析就可以获得答案。CX、CZ都是错误的组合,所以只能是CY。AX是错误的组合,所以只能是AZ,剩下一个BX。结果是AZ、BX、CY这种结婚方案。但是如果夫妇数量扩展了,直观分析就变得麻烦了。借助于程序,处理起来就要方便得多。首先,还是基于3对夫妇进行编程,后续再扩展。
    有些算法使用了2个数组,分别是新郎、新娘数组,bridegroomArray[] = {'A','B','C'},brideArray[] = {'X','Y','Z'}。这里简化一下,只使用一个新娘数组brideArray[] = {'X','Y','Z'}。新郎的顺序A、B、C设定不变,对应的下标index固定为0,1,2。i、j、k分别代表着A、B、C对应新娘的下标,可以得出下面的条件:

brideArray[ i ] \neq 'X' ;
brideArray[ k ] \neq 'X';
brideArray[ k ] \neq 'Z';

    Java版本:

    static void groomBrideQ() {
        char[] brideArray = {'X', 'Y', 'Z'};
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                for (int k = 0; k < 3; k++) {
                    if (i != j && j != k && k != i) {
                        if (brideArray[i] != 'X' && brideArray[k] != 'X' && brideArray[k] != 'Z') {
                            System.out.println("A*" + brideArray[i]);
                            System.out.println("B*" + brideArray[j]);
                            System.out.println("C*" + brideArray[k]);
                        }
                    }
                }
            }
        }
    }

    输出:

A*Z
B*X
C*Y

    Kotlin版:

    fun groomBrideQ() {
        val brideArray = charArrayOf('X', 'Y', 'Z')
        for (i in 0..2) {
            for (j in 0..2) {
                for (k in 0..2) {
                    if (i != j && j != k && k != i) {
                        if (brideArray[i] != 'X' && brideArray[k] != 'X' && brideArray[k] != 'Z') {
                            println("A*" + brideArray[i])
                            println("B*" + brideArray[j])
                            println("C*" + brideArray[k])
                        }
                    }
                }
            }
        }
    }

    扩展到4对新婚夫妇,新郎为A、B、C、D,新娘为X、Y、Z、W。如果不额外添加错误条件,还用上面的,是有很多个组合的。下面的算法额外添加了3个错误条件,使得组合只有一个。错误条件的文字描述就省略了,看代码应该可以推测出来。
    Java版本:

    static void groomBrideQ() {
        char[] brideArray = {'X', 'Y', 'Z', 'W'};
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                for (int k = 0; k < 4; k++) {
                    for (int l = 0; l < 4; l++) {
                        if (i != j && j != k && k != l && l != i && i != k && i != l && j != l) {
                            if (brideArray[i] != 'X' && brideArray[k] != 'X' && brideArray[k] != 'Z'
                                    && brideArray[i] != 'W' && brideArray[j] != 'X' && brideArray[k] != 'W') {
                                System.out.println("A*" + brideArray[i]);
                                System.out.println("B*" + brideArray[j]);
                                System.out.println("C*" + brideArray[k]);
                                System.out.println("D*" + brideArray[l]);
                            }
                        }
                    }

                }
            }
        }
    }

    输出:

A*Z
B*W
C*Y
D*X

(14)三色球问题

    大意是这样的:一个黑盒中放着3个红球、3个黄球和6个绿球,如果从其中取出8个球,那么取出的球中有多少种颜色搭配?
    分析:这是一个经典的排列组合问题,下面分别从数学和编程角度来分析它。
    数学角度,以红球作为出发点,有下面几种可能:

  1. 无红球:只有两种搭配,即2黄6绿,或者3黄5绿,记为C_1 = 2;
  2. 1个红球:问题变成从3黄球6绿球中取出7球,那么至少有一个黄球,故有3种搭配,记为C_2 = 3;
  3. 2个红球:问题变成从3黄球6绿球中取出6球,那么可以没有黄球,比2)要多一种可能,故有4种搭配,记为C_3 = 4;
  4. 3个红球:问题变成从3黄球6绿球中取出5球,同3)一样,可以没有黄球,也有4种搭配,记为C_4 = 4;

    因此,得出的搭配总数是:C_1 + C_2 + C_3 + C_4 = 13 ,即取出的球中有13种颜色搭配。
    作为人,很容易在每种情况里,把8这个要求时刻放到脑海里。计算机虽然可以处理庞大的数据,但要求它每种情况都记住并分别计算,那代码写起来就要冗长、困难得多,可读性也差。所以在编程时,更倾向于通用的方式来求解。可以多处理一些数据,范围也可以更大,只要能用通用的条件来过滤这些数据,最终求得正确结果就行。
    那么,现在从编程角度分析三色球问题。先不考虑8这个总数要求,单论每种球的极限可能:红球有4种可能(0、1、2、3)、黄球有4种可能、绿球有7种可能。针对这些可能的各种组合(4*4*7=112种),再用总数为8作为过滤条件,就可以得出正确的结果。举个例子,0红0黄0绿这种方案也在这112种可能当中,但它显然不满足条件,被过滤掉。对计算机来说,多一定数量的计算,无伤大雅。
    Java版本:

/**
 * 3色球问题
 */
public class ThreeBallQ {

    static int threeBallQ(int red, int yellow, int green, int n) {
        int count = 0;
        for (int i = 0; i <= red; i++) {
            for (int j = 0; j <= yellow; j++) {
                for (int k = 0; k <= green; k++) {
                    if (i + j + k == n) {
                        count++;
                    }
                }
            }
        }
        return count;
    }

    public static void main(String[] args) {
        int count = threeBallQ(3, 3, 6, 8);
        System.out.println("有" + count + "种搭配方案");
    }
}

    输出结果:

有13种搭配方案

    结果符合预期。但从算法实现来看,并没有像数学那样分不同情况分别计算。而是使用一种通用的方式,在极限可能的范围并满足特定的条件下求解。这显示出了人脑思维和编程思维的极大不同。这一点在计算机世界是随处可见,面对编程时常常需要将人脑思维切换到编程思维。希望以本小节为例,可以加深你的理解。
    Over !

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容