热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

约瑟夫问题——算法优化

在华为的OJ自学平台上有个约瑟夫问题,不过它不是原来意义上的约瑟夫问题,而是其变体,做了这个题之后,有一点关于算法优化的小想法,因此想写下来。问题的描述如下:

在华为的OJ自学平台上有个约瑟夫问题,不过它不是原来意义上的约瑟夫问题,而是其变体,做了这个题之后,有一点关于算法优化的小想法,因此想写下来。

问题的描述如下:

 功能: 约瑟夫问题众所周知,原始的约瑟夫问题是这样的:有n个人,编号为1,2,..., n,站成一圈,

 每次第m个将会被处决,直到只剩下一个人。约瑟夫通过给出m来决定赦免其中的一个人。

 例如当n=6,m=5时,5,4,6,2,3将会被依次处决,而1将会幸免。

 假如有k个好人,和k个坏人,所有人站成一圈,前k个人是好人,后k个人是坏人,

 编写程序计算一个最小的m,使k个坏人都被处决,而不处决任何好人。

     

 输入: k 为正整数   

 输出: 

 返回: 最小的m,使k个坏人都被处决,而不处决任何好人。

一开始拿到这个题还真有点摸不着头脑,想来想去也不知如何下手,因为我在纸上随便试了几个数,感觉给定的k越大,所需的m值就可能会呈指数幂增长,即使能做出来,提交后肯定会计算超时。

不过,先管不了那么多了,上网搜一搜看有没有现成的代码,符合这个题目的没找到,解原始约瑟夫问题的到是有一大堆,这里的一篇不错,就先拿去分析了一下代码。核心代码就是以下这段了:

//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
while(len>0){
if(a[i%n]>0){
if(j%m==0){//找到要出圈的人,并把圈中人数减一
System.out.print(a[i%n]+" ");
a[i%n]=-1;
j=1;
i++;
len--;
}else{
i++;
j++;
}
}else{//遇到空位了,就跳到下一位,但j不加一,也就是这个位置没有报数
i++;
}
}


这个算法的基本思路就是:先创建一个从1到n的递增数组,对数组进行遍历,找到要圈出的人,即计数器j为m的整数倍(j%m=0),将其打印出来,标记为-1,计数器重新回到1,索引加一,数组长度-1(实际长度并未减);若计时器j不是m的整数倍,则将计数器和索引i各自增1进入下一次循环。

由于采用数组,有一点不好的地方就是它的长度不可变,已经“删除”的数下次还会被判断,如果数组比较大,删除的数较多时,就会有大量无用的判断,因此决定采用用Java里的list试一试。为了不同实现方法的对比,将上面的核心算法提取为放方法,代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
*使用数组实现约瑟夫环问题
*由m个人围成一个首尾相连的圈报数。
*从第一个人开始,从1开始报数,报到n的人出圈,
*剩下的人继续从1开始报数,直到所有的人都出圈为止。
*对于给定的m和n,求出所有人的出圈顺序.
*/
public class RingTest{
public static void main(String[] args){
System.out.println("程序说明如下:");
System.out.println("由m个人围成一个首尾相连的圈报数。从第一个人开始,从1开始报数,报到n的人出圈,剩下的人继续从1开始报数,直到所有的人都出圈为止。对于给定的m和n,求出所有人的出圈顺序.");

//提示输入总人数
System.out.println("请输入做这个游戏的总人数:");
Scanner sca=new Scanner(System.in);
int n=sca.nextInt();
//提示输入要出圈的数值
System.out.println("请输入要出圈的数值:");
int k=sca.nextInt();
System.out.println("按出圈的次序输出序号:");
long time1=System.nanoTime();
test1(n,k);
long time2=System.nanoTime();
System.out.println("用时1:"+(time2-time1));
test2(n,k);
long time3=System.nanoTime();
System.out.println("用时2:"+(time3-time2));
} private static void test1(int n,int m){
//创建有m个值的数组
int[] a=new int[n];
//初始长度,以后出圈一个,长度就减一
int len=n;
//给数组赋值
for(int i=0;i a[i]=i+1;
//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
while(len>0){
if(a[i%n]>0){
if(j%m==0){//找到要出圈的人,并把圈中人数减一
System.out.print(a[i%n]+" ");
a[i%n]=-1;
j=1;
i++;
len--;
}else{
i++;
j++;
}
}else{//遇到空位了,就跳到下一位,但j不加一,也就是这个位置没有报数
i++;
}
}
}
private static void test2(int n,int m){
//创建有m个值的数组
List list=new ArrayList();
//初始长度,以后出圈一个,长度就减一
//给数组赋值
for(int i=0;i list.add(i+1);
}
//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
while(n>0){
if(j%m==0){//找到要出圈的人,并把圈中人数减一
System.out.print(list.get(i%n)+" ");
list.remove(i%n);
j=1;
n--;
}else{
i++;
j++;
}
if(i>=n&&n>0)
i=i%n;
}
}
}

对于数据量比较少时,明显是数组占优势,因为list移除一个值之后,后面的值都会移动,但数据量大了后这个弱点就会被无效比较给抵消掉。

以下贴出将数组改为List的的核心算法:

//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
while(n>0){
if(j%m==0){//找到要出圈的人,并把圈中人数减一
System.out.print(list.get(i%n)+" ");
list.remove(i%n);
j=1;
n--;
}else{
i++;
j++;
}
if(i>=n&&n>0)
i=i%n;
}相对于之前用数组,这里面少了个判断值是否小于0 if 条件语句,不过加了两句

if(i>=n&&n>0)
i=i%n;当然,上面的list.get(i%n)也可以改为list.get(i),除此之外,区别不大。以下还是贴出两者对比的完整代码:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
*使用数组实现约瑟夫环问题
*由m个人围成一个首尾相连的圈报数。
*从第一个人开始,从1开始报数,报到n的人出圈,
*剩下的人继续从1开始报数,直到所有的人都出圈为止。
*对于给定的m和n,求出所有人的出圈顺序.
*/
public class RingTest{
public static void main(String[] args){
System.out.println("程序说明如下:");
System.out.println("由m个人围成一个首尾相连的圈报数。从第一个人开始,从1开始报数,报到n的人出圈,剩下的人继续从1开始报数,直到所有的人都出圈为止。对于给定的m和n,求出所有人的出圈顺序.");

//提示输入总人数
System.out.println("请输入做这个游戏的总人数:");
Scanner sca=new Scanner(System.in);
int n=sca.nextInt();
//提示输入要出圈的数值
System.out.println("请输入要出圈的数值:");
int k=sca.nextInt();
System.out.println("按出圈的次序输出序号:");
long time1=System.nanoTime();
test1(n,k);
long time2=System.nanoTime();
System.out.println("用时1:"+(time2-time1));
test2(n,k);
long time3=System.nanoTime();
System.out.println("用时2:"+(time3-time2));
} private static void test1(int n,int m){
//创建有m个值的数组
int[] a=new int[n];
//初始长度,以后出圈一个,长度就减一
int len=n;
//给数组赋值
for(int i=0;i a[i]=i+1;
//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
while(len>0){
if(a[i%n]>0){
if(j%m==0){//找到要出圈的人,并把圈中人数减一
System.out.print(a[i%n]+" ");
a[i%n]=-1;
j=1;
i++;
len--;
}else{
i++;
j++;
}
}else{//遇到空位了,就跳到下一位,但j不加一,也就是这个位置没有报数
i++;
}
}
}
private static void test2(int n,int m){
//创建有m个值的数组
List list=new ArrayList();
//初始长度,以后出圈一个,长度就减一
//给数组赋值
for(int i=0;i list.add(i+1);
}
//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
while(n>0){
if(j%m==0){//找到要出圈的人,并把圈中人数减一
System.out.print(list.get(i%n)+" ");
list.remove(i%n);
j=1;
n--;
}else{
i++;
j++;
}
if(i>=n&&n>0)
i=i%n;
}
}
}到了这里,我在想既然用了List链表,这里面就不用判断list里面的值了,毕竟点到的都被移出了,其余的都是有用的。而我们的目的是找到第m个,把它移出,然后接着找下一个第m个, 因此,我为什么还要一次一次i++、j++呢,干嘛不一次i+=m-1,j+=m-1,这样不是一次性跳到了下一个m个么?
因此,代码稍作更改,速度几乎减半。以下为修改后的方法:

private static void test2(int n,int m){
//创建有m个值的数组
List list=new ArrayList();
//初始长度,以后出圈一个,长度就减一
//给数组赋值
for(int i=0;i list.add(i+1);
}
//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
while(n>0){
if(j%m==0){//找到要出圈的人,并把圈中人数减一
System.out.print(list.get(i)+" ");
list.remove(i);
j=1;
n--;
}else{
i+=m-1;
j+=m-1;
}
if(i>=n&&n>0)
i=i%n;
}
}这个原始约瑟夫算法的优化就先到此了。接着就按题目的要求答题了。
题目的要求是前k个是好人,后k个是坏人,只处决坏人而不出决绝好人,因此这个方法里就不需打印移除的数了,同时while里的循环也不用循环整个数组了,只需循环数组长度的一半就行了,还有这个方法的返回类型就可以改为boolean类型了,移除数的时候,只要一遇到好人,提供的m值就不满足条件,需要重新找m值了。
(注:鉴于问题有变化,以上的参数需做更改了:原来的n换为k,链表长度为2k)
以下就是再次修改后的代码了:

private static boolean isFit(List list,int k){
//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
int len=list.size();
int n=len/2;
while(len>n){
if(j%k==0){
int value=list.get(i);
if(value<=n)
return false;
list.remove(i);
j=1;
len--;
}else{
i+=k-1;
j+=k-1;
}
if(i>=len)
i=i%len;
}
return true;
}接下来就是要通过给定不同的k&#20540;来找最小的m&#20540;。找最小&#20540;,最容易想到的就是对于给定的k&#20540;,从1开始试,试到该方法返回true时即为所需的k&#20540;。

于是程序的实现如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class RingTest{
public static void main(String[] args){
System.out.println("程序说明如下:");
System.out.println("由m个人围成一个首尾相连的圈报数。从第一个人开始,从1开始报数,报到n的人出圈,剩下的人继续从1开始报数,直到所有的人都出圈为止。对于给定的m和n,求出所有人的出圈顺序.");

//提示输入总人数
System.out.println("请输入做这个游戏的好人数:");
Scanner sca=new Scanner(System.in);
int k=sca.nextInt();
//提示输入要出圈的数值
System.out.println("最小的m:");
boolean flag=false;
int c=0;//cycle
int m=1;
while(!flag){
List tmp=new ArrayList();
for(int j=0;j<2*k;j++){
tmp.add(j+1);
}
if(isFit(tmp,m)){
flag=true;
break;
}else{
m++;
}
c++;
}
System.out.print(m);
}
private static boolean isFit(List list,int k){
//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
int len=list.size();
int n=len/2;
while(len>n){
if(j%k==0){//找到要出圈的人,并把圈中人数减一
int value=list.get(i);
if(value<=n)
return false;
list.remove(i);
j=1;
len--;
}else{
i+=k-1;
j+=k-1;
}
if(i>=len)
i=i%len;
}
return true;
}
} 这种找m的方法简直是太笨了,至少你可以给它划点范围啊,直接从m开始显然不明智。很明显,对于题目的要求时第一个要处决的是坏人,因此m的第一个尝试&#20540;的取&#20540;范围至少要在[k&#43;1,2k]的范围之内啊,若果这个范围不合适,那就说明m得从大于2k(即至少绕一圈)的&#20540;开始了。因此,我们可以对这个m取一个初略的范围限定,这样isFit方法的调用次数就再一次减半了。因此,可以对main方法里的代码稍作更改了,实现代码如下:

public static void main(String[] args){
System.out.println("程序说明如下:");
System.out.println("由m个人围成一个首尾相连的圈报数。从第一个人开始,从1开始报数,报到n的人出圈,剩下的人继续从1开始报数,直到所有的人都出圈为止。对于给定的m和n,求出所有人的出圈顺序.");

//提示输入总人数
System.out.println("请输入做这个游戏的好人数:");
Scanner sca=new Scanner(System.in);
int k=sca.nextInt();
//提示输入要出圈的数值
System.out.println("最小的m:");
boolean flag=false;
int c=0;//cycle
int m=0;
while(!flag){
for(int i=(2*c+1)*k+1;i<=2*k*(c+1);i++){
List tmp=new ArrayList();
for(int j=0;j<2*k;j++){
tmp.add(j+1);
}
if(isFit(tmp,i)){
flag=true;
m=i;
break;
}
}
c++;
}
System.out.print(m);
}

这段代码里加了一个c(即圈数),指的时从1输到m时绕好坏人围成圈的圈数。m的取&#20540;就被限定在[2k*c&#43;k&#43;1,2k*c&#43;2k]范围内。

终于,代码提交后顺利通过自学平台提供的测试用例。

写到这里,代码基本上达到了目的,不过,这个算法的优化并没有结束,因为当k的&#20540;达到14时,计算出m的结果就得好几分钟了。

完整实现代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class RingTest{
public static void main(String[] args){
System.out.println("程序说明如下:");
System.out.println("由m个人围成一个首尾相连的圈报数。从第一个人开始,从1开始报数,报到n的人出圈,剩下的人继续从1开始报数,直到所有的人都出圈为止。对于给定的m和n,求出所有人的出圈顺序.");

//提示输入总人数
System.out.println("请输入做这个游戏的好人数:");
Scanner sca=new Scanner(System.in);
int k=sca.nextInt();
//提示输入要出圈的数值
System.out.println("最小的m:");
List list=new ArrayList();
for(int i=0;i<2*k;i++){
list.add(i+1);
}
boolean flag=false;
int c=0;//cycle
int m=0;
while(!flag){
for(int i=(2*c+1)*k+1;i<=2*k*(c+1);i++){
List tmp=new ArrayList();
for(int j=0;j<2*k;j++){
tmp.add(j+1);
}
if(isFit(tmp,i)){
flag=true;
m=i;
break;
}
}
c++;
}
System.out.print(m);
} private static boolean isFit(List list,int k){
//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
int len=list.size();
int n=len/2;
while(len>n){
if(j%k==0){//找到要出圈的人,并把圈中人数减一
int value=list.get(i);
if(value<=n)
return false;
list.remove(i);
j=1;
len--;
}else{
i+=k-1;
j+=k-1;
}
if(i>=len)
i=i%len;
}
return true;
}
}

目前,我个人的思路有两个:

其一是每次循环都要建立一个List数组,这里可以考虑在循环外面建立一个list后,在循环里通过java提供的复制函数来复制,这应该比每次新建list要快;

其二是继续压缩m的取&#20540;范围,上面只提到第一次处决一个坏人时的m范围,处决第二个坏人也是有范围限制的,因此可以从这里入手。

思路就说到这里,具体实现就不细说了,读者如果有兴趣可以自己尝试着去完善!(*^__^*) 嘻嘻……


附OJ自学平台里的demo实现源码:

import java.util.ArrayList;
import java.util.List;
public final class Demo {
/*
功能: 约瑟夫问题众所周知,原始的约瑟夫问题是这样的:有n个人,编号为1,2,..., n,站成一圈,
每次第m个将会被处决,直到只剩下一个人。约瑟夫通过给出m来决定赦免其中的一个人。
例如当n=6,m=5时,5,4,6,2,3将会被依次处决,而1将会幸免。
假如有k个好人,和k个坏人,所有人站成一圈,前k个人是好人,后k个人是坏人,
编写程序计算一个最小的m,使k个坏人都被处决,而不处决任何好人。
输入: k 为正整数
输出:
返回: 最小的m,使k个坏人都被处决,而不处决任何好人。
*/
public static int getMinimumM(int K)
{
boolean flag=false;
int c=0;//cycle
int m=0;
while(!flag){
for(int i=(2*c+1)*K+1;i<=2*K*(c+1);i++){
List tmp=new ArrayList();
for(int j=0;j<2*K;j++){
tmp.add(j+1);
}
if(isFit(tmp,i)){
flag=true;
m=i;
break;
}
}
c++;
}
return m;
}
private static boolean isFit(List list,int k){
//i为元素下表,j代表当前要报的数
int i=0;
int j=1;
int len=list.size();
int n=len/2;
while(len>n){
if(j%k==0){
int value=list.get(i);
if(value<=n)
return false;
list.remove(i);
j=1;
len--;
}else{
i+=k-1;
j+=k-1;
}
if(i>=len)
i=i%len;
}
return true;
}
}

约瑟夫问题——算法优化,布布扣,bubuko.com


推荐阅读
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • 本文内容为asp.net微信公众平台开发的目录汇总,包括数据库设计、多层架构框架搭建和入口实现、微信消息封装及反射赋值、关注事件、用户记录、回复文本消息、图文消息、服务搭建(接入)、自定义菜单等。同时提供了示例代码和相关的后台管理功能。内容涵盖了多个方面,适合综合运用。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
author-avatar
手机用户2502898863
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有