[M模拟] lc1823. 找出游戏的获胜者(模拟+约瑟夫环+周赛236_2)

1. 题目来源

链接:lc1823. 找出游戏的获胜者

相关题目:[剑指-Offer] 62. 圆圈中最后剩下的数字(数学、环形链表、约瑟夫环、巧妙解法)

2. 题目解析

约瑟夫环问题。一般拿环形链表模拟可做,也是在数据结构一章中环形链表的经典应用,在竞赛中一时没想起来,直接数组暴力模拟也过了,数据范围太小了。但是写了一个很丑的代码,却好在 1a 了。

数组模拟思路:

  • 一个大小为 n 的数组,用 0 1 标记该位置是否有效。总共循环 n - 1 次,每次循环遍历 k 个有效的位置,将其标记,代表其被枪毙。然后再从该位置寻找到下一个有效的位置将其作为起点即可。

递归思路:

  • f(n, k)n 个人,跳 k 步的最后胜利者的编号,在此要求是从下标 0 开始的,即它的下标是 0,1,2,3,...,n-1
  • 考虑 f(n-1, k) 此时下标应该从 k 开始,可写作 k, k+1, k+2,...,0,1,2,...,k-2,总共 n-1 个人,现在从下标 k 开始报数。
  • 但是 f(n-1,k) 的定义是 n-1 个人从下标 0 开始报数最后的幸存者,这两者并不等价,即 f(n,k)!=f(n-1,k)
  • 那么 f(n-1,k) 是从下标 0,1,2,3,...,n-2 开始构成的一个约瑟夫环,环内 n-1 个点,我们可以将这个环转 k 下,让 0 对应到 k,后面的均将一一对应。
  • 故考虑做下标映射,即:将 k, k+1, k+2,...,n-1,0,1,2,...,k-2,总共 n-1 个人,映射为 0,1,2,3,...,n-2,0 和 k 一一对应,1 和 k+1 一一对应,k-2n-2 一一对应。
  • 此时 f(n-1,k) 就是下标 0,1,2,3,...,n-2 得到的胜利者的编号了,但是我们需要将这个结果映射到 k, k+1, k+2,...,n-1,0,1,2,...,k-2 中,观察可以发现每一项就是多加了个 k,为了保证下标是在合法范围内,再 %n,将下标再映射到 [0,n-1]其实在此有个疑问就是为啥不是映射到 %(n-1) 中呢,毕竟只有 n-1 个数啊,数的范围就是 [0,n-2] 啊。但是实际上,这里的映射 %n 并不是针对 0,1,2,...,n-2 的,而是针对 f(n,k)n 个数作的映射,同理 f(n-1,k) 就得 %(n-1) ,这个模数是随着第一个参数递减的。
  • f(n-1,k) 做完下标映射之后,就是以下标为 k 开始的 n-1 个人中的最后胜利者,这就和 f(n,k) 的胜利者完全等价了。故 f(n,k)= (f(n-1,k)+k)%n,这就是一个非常简洁的递推式了,可以递推、递归实现。
  • 边界情况就是当 n==1 的时候,返回下标 0 即可,即自己直接胜出。
  • 最后注意题目要求是从下标 1 开始,结果需要加 1。

要注意一点,这个 %n 随着 f(n-1,k) 的不断递减,模数也会不断减少!这点在递归改迭代的过程中相当重要!

  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

代码:
自己写的繁琐的模拟代码

class Solution {
public:
    int findTheWinner(int n, int k) {
        vector<int> q(n);
        int cnt = n, index = 0;
        for (int i = 1; i < n; i ++ ) {
            for(int i = index, cnt = 0; cnt < k; i ++ ) {
                if (i == n) i = 0;
                if (q[i] == 0) cnt ++;
                index = i;
            }
            
            q[index] = 1;
            for (int i = index; i <= n; i ++ ) {
                if (i == n) {
                    i = 0;
                }
                if (q[i] == 0) {
                    index = i;
                    break;
                }
            }
        }
        for (int i = 0; i < n; i ++ ) 
            if (q[i] == 0)
                return i + 1;
        return 0;
    }
};

大佬简洁的模拟写法:

class Solution
{
public:
    int findTheWinner(int n, int k) 
	{
		vector<int> a(n);
		for (int i = 0; i < n; ++i) {		// 0~n-1 代表序号,1~n 对应它的数值
			a[i] = i + 1;					// 真是及其巧妙的数据组织!
		}
		int at = 0;
		while (a.size() > 1) {
			at = (at + k - 1) % a.size();	// 找到下标,取模防止越界处理
			a.erase(a.begin() + at);
		}
		return a[0];
	}
};

约瑟夫经典递归:

// 需要最后下标 +1,不方便写一行
class Solution {
public:
    int f(int n, int k) {
        if (n == 1) return 0;
        return (f(n - 1, k) + k) % n;
    }
    int findTheWinner(int n, int k) {
        return f(n, k) + 1;
    }
};

约瑟夫迭代写法:

class Solution {
public:
    int findTheWinner(int n, int k) {
        int ans = 0;   // i=1 的情况,ans=0,循环也可以从i=1开始,因为 %1=0,ans无变化
        for (int i = 2; i <= n; i ++ ) ans = (ans + k) % i;
        return ans + 1;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值