java 消除游戏

想了解更多数据结构以及算法题,可以关注微信公众号“数据结构和算法”,每天一题为你精彩解答。也可以扫描下面的二维码关注
在这里插入图片描述
给定一个从1 到 n 排序的整数列表。

首先,从左到右,从第一个数字开始,每隔一个数字进行删除,直到列表的末尾。

第二步,在剩下的数字中,从右到左,从倒数第一个数字开始,每隔一个数字进行删除,直到列表开头。

我们不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。

返回长度为 n 的列表中,最后剩下的数字。

示例:

输入:
n = 9,
1 2 3 4 5 6 7 8 91,3,5,7,9被删除)
2 4 6 88,4被删除)
2 62被删除)
6 (剩余6)

输出:
6

答案:

 public int lastRemaining(int n) {
     boolean left = true;
     int remaining = n;
     int step = 1;
     int head = 1;
     while (remaining > 1) {
         if (left || ((remaining & 1) == 1)) {
             head = head + step;
         }
        remaining = remaining >> 1;
        step = step << 1;
        left = !left;
    }
    return head;
}

解析:

题描述的很清晰,就是先从左往右每隔一个就删除一个数字,然后再从右往左每隔一个删除一个数字……一直这样循环下去,直到最后剩下一个数字为止。

在计算机编程中有个非常著名的算法题就是“约瑟夫环问题”,也称“丢手绢问题”,如果对约瑟夫环问题比较熟悉的话,那么今天的这道题也就很容易理解了。如果不熟悉的话也没关系,我们今天就详细分析一下这道题。关于约瑟夫环问题不在今天所讲的范围之内,后续有时间我们在单独讲解。

这题如果使用双向链表或者是双端队列很好解决,因为双向链表既可以从前往后删除也可以从后往前删除,当然这两种方式都需要先初始化,今天我们讲的这种方式是既没有使用链表也没有使用数组。

我们来看下上面的代码,直接看可能不太直观,我们可以把n想象成一个长度为n的数组,数组的元素是1,2,3,4,5……n,我们只需要记录下每次删除一遍之后数组的第一个元素即可,当remaining==1的时候就会退出while循环,最后返回数组的仅有的一个元素即可(这只是我们的想象,实际上操作的并不是数组,也没有删除,只是记录,但原理都类似)。

boolean left = true;

代码left判断是否是从左往右删除,如果为true表示的是从左往右删除,如果为false表示的是从右往左删除。

int remaining = n;
int step = 1;
int head = 1;

代码remaining表示剩余的个数。step表示每次删除的间隔的值,不是间隔的数量,比如1,2,3,4,5,6,7,8。第一次从左往右删除的时候间隔值是1,删除之后结果为2,4,6,8。第二次从右往左删除间隔值就变为2了,删除之后结果是2,6。然后第3次就变成4了。head表示的是记录的剩余数字从左边数第一个的值。

    while (remaining > 1) {
        if (left || ((remaining & 1) == 1)) {
            head = head + step;
        }
        remaining = remaining >> 1;
        step = step << 1;
        left = !left;
    }

第5-7行代码很好理解,remaining表示的是剩余个数,每次删除的时候都会剩余一半,所以除以2,也可以表示为往右移一位。step上面说了表示的是间隔值,每次循环之后都会扩大一倍,left就不在说了,一次往左一次往右……一种这样循环。

我们主要来看下第2-4行代码,如果是从这边循环,那么第一个肯定是会被删除的,第二个会成为head,而第二个值就是head+step;如果从右边开始循环,如果数组的长度是奇数,那么第一个元素head也是要被删除的,所以head值也要更新,代码remaining&1==1判断remaining是否是奇数。

我们以n=14为例,画个图来看下会更直观一些

在这里插入图片描述
上面代码变量比较多,实际上我们还可以改的更简洁一些

public int lastRemaining(int n) {
    int first = 1;
    for (int step = 0; n != 1; n = n >> 1, step++) {
        if (step % 2 == 0 || n % 2 == 1)
            first += 1 << step;
    }
    return first;
}

注意这里的step不是删除的间隔值,他是表示的是每删除一遍就会加1,比如最开始从左往右删除的时候step是0,然后再从右往左删除的时候是1,然后再从左往右删除的时候是2,然后再从右往左删除的时候是3……,一直这样累加。代码很好理解,就不在过多解释。

下面我们再来换种思路想一下

1, 当我们从左往右消除的时候,比如[1,2,3,4]第一次从左往右消除的时候结果是[2,4],也就是2*[1,2]

或者[1,2,3,4,5]第一次从左往右消除的时候结果也是[2,4],也就是2*[1,2]

所以我们只需要计算数组前面一半的结果然后再乘以2即可。

2, 当我们从右往左消除的时候,如果数组是偶数,比如[1,2,3,4,5,6]消除的结果是[1,3,5],也就是2*[1,2,3]-1,如果数组是奇数的话,比如[1,2,3,4,5,6,7]消除的结果是[2,4,6],也就是2*[1,2,3]。所以明白了这点,代码就很容易想到了

 public int lastRemaining(int n) {
     return leftToRight(n);
 }
 
 private static int leftToRight(int n) {
     if (n <= 2)
         return n;
     return 2 * rightToLeft(n / 2);
 }

private static int rightToLeft(int n) {
    if (n <= 2)
        return 1;
    if (n % 2 == 1)
        return 2 * leftToRight(n / 2);
    return 2 * leftToRight(n / 2) - 1;
}

我们再来思考一个问题,可以找一下规律

1,当n个数的时候,假设我们从左往右执行,剩下的数字记为f1(n)(从数组[1,2,……n]开始),从右往左执行,剩下的数字是f2(n)(从数组[n,n-1,……1]开始)。

2,如果我们记f1(n)在数组[1,2,……n]中的下标为k,那么f2(n)在数组中[n,n-1,……1]的下标也一定是k。所以我们可以得到f1(n)+f2(n)=n+1。

3,对于n个元素,执行一次从左往右之后,剩下的[2,4,……n/2]就应该从右往左了,我们记他执行,剩下的数字是f3(n/2),所以我们可以得到f1(n)=f3(n/2),f3(n/2)=2*f2(n/2);

4,根据上面的3个公式

(1):f1(n)+f2(n)=n+1

(2):f1(n)=f3(n/2)

(3):f3(n/2)=2*f2(n/2)

我们可以得出f1(n)=2*(n/2+1-f1(n/2));并且当n等于1的时候结果就是1,所以代码如下,非常简单

public int lastRemaining(int n) {
    return n == 1 ? 1 : 2 * (1 + n / 2 - lastRemaining(n / 2));
}

对于这道题的理解我们还可以来举个例子,比如[1,2,3……n],如果从左开始结果是k,那么从右开始结果就是n+1-k。比如[1,2,3,4,5,6,7,8,9,10]第一遍从左到右运算之后是[2,4,6,8,10],假如[1,2,3,4,5]从左到右的结果是f(5),那么他从右到左的结果就是5+1-f(5),也就是6-f(5),所以[2,4,6,8,10]从右到左的结果就是2*(6-f(5)),所以我们可以得出f(10)=2*(6-f(5)),所以递推公式就是f(n)=2*(n/2+1-f(n/2))。

我们还可以把上面递归的代码改为非递归,这个稍微有一定的难度

 public int lastRemaining(int n) {
     Stack<Integer> stack = new Stack<>();
     while (n > 1) {
         n >>= 1;
         stack.push(n);
     }
     int result = 1;
     while (!stack.isEmpty()) {
         result = (1 + stack.pop() - result) << 1;
    }
    return result;
}

下面再来思考一下,看能不能再优化一下,我们让left(n)=left[1,2,3,……n]表示从左往右执行之后,剩下的数字,right(n)=right[1,2,3,……,n]表示从右往左执行之后,剩下的数字,所以我们可以得出一个结论

1,left(1)=right(1)=1;

2,left(2k)=left[1,2,3,……2k]=right[2,4,6,……2k]=2*right(k);

3,left(2k+1)=left[1,2,3,……2k,2k+1]=right[2,4,6,……2k]=2*right(k);

4,right(2k)=right[1,2,3,……2k]=left[1,3,5,……2k-1]=left[2,4,6,……2k]-1=2*left(k)-1

5,right(2k+1)=right[1,2,3,……2k,2k+1]=left[2,4,6,……2k]=2*left(k)。

6,left(4k)=left(4k+1)=4*left(k)-2;

7,left(4k+2)=left(4k+3)=4*left(k);

搞懂了上面的规律,代码就呼之欲出了,下面我们来看下代码

public int lastRemaining(int n) {
    if (n < 4)
        return (n == 1) ? 1 : 2;
    return (lastRemaining(n / 4) * 4) - (~n & 2);
}

我们再来看最后一种解法,也是一行代码搞定

public int lastRemaining(int n) {
    return ((Integer.highestOneBit(n) - 1) & (n | 0x55555555)) + 1;
}

如果对约瑟夫环问题比较熟练的话,那么这种解法就比较好理解了,其实他就是约瑟夫环中k=2的一个问题。

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