剑指offer46_孩子们的游戏(圆圈中最后剩下的数)_题解

孩子们的游戏(圆圈中最后剩下的数)

题目描述

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)

如果没有小朋友,请返回-1

示例1

输入

5,3

返回值

3

分析

方案一:用环形链表模拟圆圈

创建一个总共有 \(n\) 个节点的环形链表,每次在这个链表中删除第 \(m\) 个节点。

使用模板库中的 \(list\) 来模拟一个环形链表,每当迭代器扫描到链表末尾,就把迭代器移到链表的头部,这样就相当于按照顺序在一个圆圈里遍历了。

代码

/**
1.时间复杂度:O(n^2)
每删除一个数字需要 m 步运算,总共有 n 个数字,总的时间复杂度是 O(mn)
2.空间复杂度:O(n)
辅助链表占用O(n)大小的额外空间
**/
class Solution
{
public:
    int LastRemaining_Solution(int n, int m)
    {
        if (n <= 0)
            return -1;
        list<int> l;
        //编号从0到n
        for (int i = 0; i < n; ++i)
            l.emplace_back(i);
        int index = 0;
        //当链表中剩余人数超过1时循环
        while (n > 1)
        {
            index = (index + m - 1) % n;
            auto it = l.begin();
            advance(it, index); //让it向后移动index个位置
            l.erase(it);
            --n;
        }
        return l.back();
    }
};

方案二:用队列模拟圆圈

/**
1.时间复杂度:O(n^2)
每删除一个数字需要 m 步运算,总共有 n 个数字,总的时间复杂度是 O(mn)
2.空间复杂度:O(n)
辅助队列占用O(n)大小的额外空间
**/
class Solution
{
public:
    int LastRemaining_Solution(int n, int m)
    {
        if (n <= 0)
            return -1;
        queue<int> q;
        //编号从0到n-1
        for (int i = 0; i < n; i++)
        {
            q.push(i);
        }
        //当队列中剩余人数超过1时循环
        while (q.size() > 1)
        {
            for (int j = 0; j < (m - 1) % q.size(); j++)
            {
                int top = q.front();
                q.pop();
                q.push(top);
            }
            q.pop();
        }
        return q.front();
    }
};

方案三:递归

设函数 \(f(n,m)\) 表示每次在 \(n\) 个数字中删除第 \(m\) 个数字后最后剩下的数字

\(n\) 个数字中第一个被删除的数字是 \((m-1)\%n\) ,记为 \(k\)

删除 \(k\) 之后剩下的数字为 \(0,1,...,k-1,k+1,...,n-1\)

并且下一次删除从数字 \(k+1\) 开始计数,相当于剩下的序列为 \(k+1,...,n-1,0,1...k-1\)

设函数 \(f'(n-1,m)\) 表示在 \(n-1\) 个数字中删除第 \(m\) 个数字后最后剩下的数字

则有 \(f(n,m)=f'(n-1,m)\) 即最初序列最后剩下的数字与删除一个数后的序列最后剩下的数字相同

接下来把 \(n-1\) 个数字的序列 \(k+1,...,n-1,0,1...,k-1\) 映射到 \(0,1...n-2\)

k+1 -> 0
k+2 -> 1
...
n-1 -> n-k-2
0   -> n-k-1
1   -> n-k
...
k-1 -> n-2

把映射定义为 \(p\),则 \(p(x)=(x-k-1)%n\),该映射的逆映射是 \(p(x)=(x+k+1)\%n\)

由于映射之后的序列和最初的序列具有相同的形式,所以依然可以适用于函数 \(f\)

故映射之前的序列中最后剩下的数字 \(f(n-1,m)=p[f(n-1,m)]=[f(n-1,m)+k+1]%n\)

\(k=(m-1)\%n\) 代入得到 \(f(n,m)=f(n-1,m)=[f(n-1,m)+m]%n\)

\(n=1\) 时,也就是序列中开始只有一个数字 \(0\),那么显然最后剩下的数字就是 \(0\)

最终得到递归方程如下:

\[f(n,m)= \begin{cases} 0\\ [f(n-1,m)+m]\%n \end{cases} \]

代码

/**
1.时间复杂度:O(n)
2.空间复杂度:O(1)
**/
//递归
class Solution
{
public:
    int LastRemaining_Solution(int n, int m)
    {
        if (n == 1)
            return 0;
        int x = LastRemaining_Solution(n - 1, m);
        return (m + x) % n;
    }
};
//迭代
class Solution
{
public:
    int LastRemaining_Solution(int n, int m)
    {
        if (n < 1 || m < 1)
            return -1;

        int last = 0;
        for (int i = 2; i <= n; i++)
            last = (last + m) % i;

        return last;
    }
};
posted @ 2021-01-23 11:26  RiverCold  阅读(80)  评论(0)    收藏  举报