美文网首页
约瑟夫环解法大全(C语言版)

约瑟夫环解法大全(C语言版)

作者: M_lear | 来源:发表于2019-04-15 11:24 被阅读0次

前言: 约瑟夫环不愧是一道经典的算法题,原来也经常看到,但一直没有动手编码研究。最近又被同学提起这个问题,就研究了一下,发现这个问题可以挖掘的东西有很多,怪不得一直是面试的热门问题。

解法一,使用链表模拟:

使用链成环的单链表完全模拟这个计数淘汰的过程,计数到要淘汰的结点时,直接删除该结点。

typedef struct NODE{
    int num;
    struct NODE* next;
}PN;

int cycle0(int n, int m){ // 使用链表实现
    PN* head = (PN*)malloc(sizeof(PN));
    head->num = 0;
    head->next = NULL;
    PN* tail = head; // 恒指向链表尾 
    for(int i = 1; i < n; ++i){
        tail->next = (PN*)malloc(sizeof(PN));
        tail = tail->next;
        tail->num = i;
        tail->next = NULL;
    }
    tail->next = head; // 链成环
    PN* p = head;
    int j = 0; // 报数器 
    while(p->next != p){ // 如果 p 的下一个结点指向自己,说明环中只剩一个结点 
        if(j == m-2){ // 每次报到 m-2 删除当前 p 指向结点的下一个结点 
            PN* q = p->next;
            p->next = q->next;
            free(q); // 释放内存 
            p = p->next;
            j = 0;
        }else{
            p = p->next;
            ++j;
        }
    } 
    return p->num;
}

解法二,使用数组模拟:

数组模拟法用数组下标对应人的编号。最简单直白的模拟方式,就是使用数组值来表示存活或者淘汰,一般我们用 0 和 1 来表示,如果数组元素值对应淘汰,则在计数时跳过该元素。

int cycle1(int n, int m){ // 使用数组实现 1 代表活 0 代表淘汰(反过来也可以) 
    int a[n];
    for(int i = 0; i < n; ++i){
        a[i] = 1;
    }
    int i = 0, j = 0, count = 0;
    while(count < n-1){
        i %= n;
        if(a[i] == 1){
            if(j == m-1){
                j = 0;
                a[i] = 0;
                ++count;
            }else ++j;
            ++i;
        }else while(a[(++i)%n] == 0){} // 跳过淘汰者,这些人不计入报数
    }
    for(int i = 0; i < n; ++i){
        if(a[i] == 1){
            return i;
        }
    }
}

上面的代码写了一个循环跳过淘汰者,代码形式上太不美观。我们可以通过借助标志值来计数,优化代码形式,让代码看起来更美观,但并未优化代码效率。

int cycle2(int n, int m){ // 优化数组模拟法的代码 0 代表活 1 代表淘汰(反过来也可以)
    int a[n] = {0};
    int i = 0, j = 0, count = 0;
    while(count < n-1){
        if(a[i] == 0 && j == m-1){
            j = 0;
            a[i] = 1;
            ++count;
        }
        j += 1-a[i]; // 利用 a[i] 的值来对 j 进行计数
        i = (i+1)%n;
    }
    for(int i = 0; i < n; ++i){
        if(a[i] == 0) return i;
    }
}

上面的代码虽然没有能提升效率,但给了一个优化思路,即充分利用数组元素的值。我们可以利用数组值来辅助定位到当前存活者的下一存活者,尽量跳过中间的淘汰者。辅助定位的方法是,当数组元素值等于元素下标时,表示此人存活,当需要淘汰当前的人时,就用后面一个元素的数组值覆盖当前的元素值,这样当前元素值和下标不等,表示当前这个人已被淘汰,还可以借助数组值定位到下一个可能存活的人身上。

int cycle3(int n, int m){ // 继续优化数组模拟法的代码,数组值 等于 下标表示存活 
    // 使用数组值引导到下一个人的下标
    int a[n];
    for(int i = 0; i < n; ++i){
        a[i] = i;
    }
    int i = 0, j = 0, count = 0;
    while(count < n-1){
        i %= n;
        if(a[i] == i){
            if(j == m-1){
                j = 0;
                a[i] = a[(i+1)%n];
                ++count;
            }else ++j;
            ++i;
        }else i = a[i]; // 优化的点 
    }
    for(int i = 0; i < n; ++i){
        if(a[i] == i){
            return i;
        }
    }
}

最后沿袭这个思路还可以进一步优化算法,还是通过数组值来确定下一存活者,但这次是精准定位到下一存活者。与上一方法不同的是,数组值存储的是下一个存活者的编号,使用两个索引,分别为 p 和 c,p 为上一个存活者的编号,其数组值为当前存活者编号,c 为当前存活者的编号,其数组值为下一个存活者的编号,当需要淘汰当前存活者时,令 a[p] = a[c] 即可,即上一存活者指向的是下一存活者,当前存活者的编号被覆盖,相当于在数组中删除了当前存活者。这种改进算法不仅不需要每次判断数组值等于多少,而且可达的数组值一定表示的是真实的下一个存活者,大大提升了上一算法的效率。

// 你可以类比二叉树的双亲表示法(使用数组表示二叉树)来理解这个算法
// 这个算法的本质是,相当于使用数组来模拟链表,数组值就是指针,覆盖数组值就相当于链表中的删除结点操作。
int cycle4(int n, int m){ // 继续优化数组模拟法的代码
    int a[n];
    a[n-1] = 0;
    for(int i = 0; i < n-1; ++i){
        a[i] = i+1;
    }
    int c = 0, p = n-1, j = 0, count = 0;
    while(count < n-1){
        if(j == m-1){
            j = 0;
            a[p] = a[c]; // 删除当前存活者,p 此时指向的就是下一存活者,所以 p 指针不需要移动。
            ++count; 
        }else{
            ++j;
            p = c;
        }
        c = a[c];
    }
    return c;
}

如果你理解了上述算法的本质是模拟链表,那么就像我们给出的第一个链表模拟法的算法一样,我们使用一个指针便可以完成遍历和删除结点的操作,并不需要使用 p,c 两个索引来配合遍历和删除操作。

int cycle5(int n, int m){
    int a[n];
    a[n-1] = 0;
    for(int i = 0; i < n-1; ++i){
        a[i] = i+1;
    }
    int c = n-1, j = 0, count = 0;
    while(count < n-1){
        if(j == m-1){
            j = 0;
            a[c] = a[a[c]]; // 删除当前存活者 
            ++count; 
        }else{
            ++j;
            c = a[c];
        }
    }
    return c;
}

解法三,动态规划:

优点是代码简洁,时间复杂度仅为 O(n)。缺点是只能获得最后存活者的编号,无法像模拟法一样可以获取淘汰过程中的编号序列。
假设有 n 个人,编号为 0,1,\cdots,n-1,每报数 m(m<n) 次淘汰一个人。
先给出状态转移方程:
f(n) 表示 n 个人中最终存活者的编号。

  • f(1) = 0
  • f(n) = (f(n-1)+m)\%n

n 个人,编号为 0,1,\cdots,n-1,从 编号为 0 的人开始报数,第一个被淘汰的人,编号为 m-1。此时剩下 n-1 个人,下一次报数从编号为 m 的人开始。将这剩下的n-1 个人按报数顺序一字排开,序列为:m,m+1,\cdots,n-1,0,1,\cdots,m-2,对比总人数为 n-1 个人时的编号序列:0,1,\cdots,n-2,可以得到两者的对应关系为f(n) = (f(n-1)+m)\%n。可以认为这个递推公式就是通过上述找规律的方式看出来的。
这就意味着,如果我们已知总人数为 n-1 时最终存活者的编号,就可以得到这个人在总人数为 n 时对应的编号。
上面为了方便,我们假设的是 m<n,其实当 m>n 时,递推式子不变。因为当 m>n 时,每报数 m 次淘汰一人,相当于每报数 m\%n 次淘汰一人,所以有 f(n) = (f(n-1)+m\%n)\%n=(f(n-1)+m)\%n,式子不变。

如果感觉上面描述的实在不好理解,可以自己找个例子用这个递推的式子实战一下,应该就有点感觉了。例如我们要求 n=5,m=3 的情况,dp 过程是这样的:

  1. f(1)=0,即人数为 1 时,最终存活者的编号为 0。
  2. f(2)=(f(1)+3)\%2=(0+3)\%2=1,即那个在人数剩 1 个人时最终存活者的编号在人数为 2 时,对应的编号为 1。
  3. 同理 f(3)=(1+3)\%3=1,即最终存活者的编号对应到总人数为 3 时,编号为 1。
  4. 同理 f(4)=(1+3)\%4=0,对应为总人数为 4 时,编号为 0。
  5. 同理 f(5)=(0+3)\%5=3,对应为总人数为 5 时,编号为 3。递推结束。

递归解法如下:

// dp 的递归解法 
int cycle6(int n, int m){
    if(n == 1) return 0;
    return (cycle5(n-1,m) + m)%n;
}

递推解法如下:

// dp 的递推解法 
int cycle7(int n, int m){
    int alive = 0; // 对应 i = 1 的结果 
    for(int i = 2; i <= n; ++i){
        alive = (alive + m)%i;
    }
    return alive;
}

最后附上完整的测试代码:

#include<stdio.h>
#include<stdlib.h>
typedef struct NODE{
    int num;
    struct NODE* next;
}PN;

int cycle0(int n, int m){ // 使用链表实现
    PN* head = (PN*)malloc(sizeof(PN));
    head->num = 0;
    head->next = NULL;
    PN* tail = head; // 恒指向链表尾 
    for(int i = 1; i < n; ++i){
        tail->next = (PN*)malloc(sizeof(PN));
        tail = tail->next;
        tail->num = i;
        tail->next = NULL;
    }
    tail->next = head; // 链成环
    PN* p = head;
    int j = 0; // 报数器 
    while(p->next != p){ // 如果 p 的下一个结点指向自己,说明环中只剩一个结点 
        if(j == m-2){ // 每次报到 m-2 删除当前 p 指向结点的下一个结点 
            PN* q = p->next;
            p->next = q->next;
            free(q); // 释放内存 
            p = p->next;
            j = 0;
        }else{
            p = p->next;
            ++j;
        }
    } 
    return p->num;
}

int cycle1(int n, int m){ // 使用数组实现 1 代表活 0 代表淘汰(反过来也一样) 
    int a[n];
    for(int i = 0; i < n; ++i){
        a[i] = 1;
    }
    int i = 0, j = 0, count = 0;
    while(count < n-1){
        i %= n;
        if(a[i] == 1){
            if(j == m-1){
                j = 0;
                a[i] = 0;
                ++count;
            }else ++j;
            ++i;
        }else while(a[(++i)%n] == 0){}
    }
    for(int i = 0; i < n; ++i){
        if(a[i] == 1){
            return i;
        }
    }
}

int cycle2(int n, int m){ // 优化数组的代码 0 代表活 1 代表淘汰(反过来也一样)
    int a[n] = {0};
    int i = 0, j = 0, count = 0;
    while(count < n-1){
        if(a[i] == 0 && j == m-1){
            j = 0;
            a[i] = 1;
            ++count;
        }
        j += 1-a[i]; // 利用 a[i] 的值来对 j 进行计数
        i = (i+1)%n;
    }
    for(int i = 0; i < n; ++i){
        if(a[i] == 0) return i;
    }
}

int cycle3(int n, int m){ // 继续优化数组的代码,数组值 等于 下标表示活 
    // 使用数组值引导到下一个人的下标
    int a[n];
    for(int i = 0; i < n; ++i){
        a[i] = i;
    }
    int i = 0, j = 0, count = 0;
    while(count < n-1){
        i %= n;
        if(a[i] == i){
            if(j == m-1){
                j = 0;
                a[i] = a[(i+1)%n];
                ++count;
            }else ++j;
            ++i;
        }else i = a[i]; // 优化的点 
    }
    for(int i = 0; i < n; ++i){
        if(a[i] == i){
            return i;
        }
    }
}
// 本质是使用数组模拟链表
int cycle4(int n, int m){ // 继续优化数组的代码 不需要每次都判断 数组值
    // 和上一种方法不同的是,数组值表示的是下一个存活的人
    int a[n];
    a[n-1] = 0;
    for(int i = 0; i < n-1; ++i){
        a[i] = i+1;
    }
    int c = 0, p = n-1, j = 0, count = 0;
    while(count < n-1){
        if(j == m-1){
            j = 0;
            a[p] = a[c]; // 删除当前存活的人 即 p 索引的数组值 
            ++count; 
        }else{
            ++j;
            p = c;
        }
        c = a[c];
    }
    return c;
}

// 使用单索引实现
int cycle5(int n, int m){
    int a[n];
    a[n-1] = 0;
    for(int i = 0; i < n-1; ++i){
        a[i] = i+1;
    }
    int c = n-1, j = 0, count = 0;
    while(count < n-1){
        if(j == m-1){
            j = 0;
            a[c] = a[a[c]]; // 删除当前存活者 
            ++count; 
        }else{
            ++j;
            c = a[c];
        }
    }
    return c;
}

// dp 的解法只能输出最后存活者的序号,无法输出淘汰序列 
int cycle6(int n, int m){ // dp 的递归解法 
    if(n == 1) return 0;
    return (cycle5(n-1,m) + m)%n;
}

int cycle7(int n, int m){ // dp 的递推解法 
    int alive = 0; // 对应 i = 1 的结果 
    for(int i = 2; i <= n; ++i){
        alive = (alive + m)%i;
    }
    return alive;
}

int main(){
    int n = 10000, m = 3;
    printf("%d\n",cycle0(n,m));
    printf("%d\n",cycle1(n,m));
    printf("%d\n",cycle2(n,m));
    printf("%d\n",cycle3(n,m));
    printf("%d\n",cycle4(n,m));
    printf("%d\n",cycle5(n,m));
    printf("%d\n",cycle6(n,m));
    printf("%d\n",cycle7(n,m));
    return 0;
}

相关文章

  • 约瑟夫环解法大全(C语言版)

    前言: 约瑟夫环不愧是一道经典的算法题,原来也经常看到,但一直没有动手编码研究。最近又被同学提起这个问题,就研究了...

  • 链表环与链表交点

    1.约瑟夫环问题 示例代码: 2. 链表节点 解法一:空间O(1) 空间O(M*N) 实现代码: 解法二: 解法三...

  • 约瑟夫环问题

    参考文章 约瑟夫环之二(用递归的思想解决Josephus问题) 解释 解法 初始情况: 0, 1, 2 ........

  • 约瑟夫环(c++)

    上面有点问题,用递归变得简单

  • 约瑟夫环类问题的简单解法

    原创 猴子选大王 一群猴子要选新猴王。新猴王的选择方法是:让N只候选猴子围成一圈,从某位置起顺序编号为1~N号。从...

  • 约瑟夫问题

    约瑟夫问题 一、数组解法 二、循环队列 三、数学解法

  • 约瑟夫环问题(c++)

    百度百科: 约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆...

  • 约瑟夫环问题

    约瑟夫环问题约瑟夫环描述:约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3…n分别表示)围...

  • 循环单链表实现约瑟夫环(C语言)

    约瑟夫环

  • 约瑟夫环

    题目:100名学生围成一个圈, 编号从1到100,从第一名学生开始报数,从1-9报数 每报出9就退出,直到所有学生...

网友评论

      本文标题:约瑟夫环解法大全(C语言版)

      本文链接:https://www.haomeiwen.com/subject/adzywqtx.html