题目链接:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=88
题意:有几幢楼分别从1~n编号,现在1号楼首先断网,从下一幢未断网的楼从1开始数(数的是未断网的楼)到m,对数到m的楼进行断网,求当有n幢楼时,m是多少2号楼是最后断网的。
例:当n=10时,m=3,2号楼是最后断网的,断网顺序:1;4;7;10;5;9;6;3;2
解题思路:
有两种做法,一种是暴力的进行模拟,时间效率不高,另一种是用数学的方法做,也就是约瑟夫环问题,
//模拟做法
//Judge Status Problem ID Language Run Time(ms) Run Memory(KB)
// Accepted 1088 C++ 520 272
#include <iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int main()
{
int n,i;
bool cut[155];
while(scanf("%d",&n) && n!=0)
{
bool find=false;//表示是否成功
int m=1;//m从1开始模拟
while(!find)
{
memset(cut,true,sizeof(cut));//每次模拟对所有楼设为未断网
cut[1]=false;//1号楼首先断网
int before=1;//记录前一幢断网的楼号
int k=m;//k用于辅助
int num=n-1;//表示未断网的楼的数量
while(num!=1)//当未断网的楼只有一幢时此次m值的模拟结束,判断未断网的楼是否为2号来判断
{ //这个m值是否成功
for(i=1;i<=k;i++ )
if(!cut[(before+i)%n])//在数的过程中遇到以断网的楼那么k的值需要加1,来跳过这幢楼
k++;//对于%n,因为是循环
before=(before+k)%n;//记录前一幢断网的楼
cut[before]=false;//把那幢楼状态设为断网
num--;//未断网楼数减1
if(cut[2]==false)//如果在模拟过程中遇到2号楼断网了那么这个m是错误的直接进行下一次模拟
break;
k=m;//在模拟中为了跳过以断网的楼k值也许已经改变,但是再数下一幢时k要变成m再数
}
if(cut[2]==true && num==1)//判断成功的条件
find=true;
else
m++;
}
printf("%d\n",m);
}
return 0;
}
约瑟夫环:
首先明确约瑟夫环问题:约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。
好了下面开始推公式:
给出一个序列,从0~n-1编号。其中,k代表出列的序号的下一个,即k-1出列。
a 0, 1, …, k-1, k, k+1, …, n-1
k-1出列后得到
b 0, 1…k-2,.k, k+1 ,….., n-1
对b进行转化一下,把0~k-2这串数列挪到k~n-1后面得到
b* k ,k+1 ,….n-1, 0, 1,….k-2
对b*数列进行一种映射:
k -----> 0
k+1 ------> 1
k+2 ------> 2
…..
n-1 ------>n-k-1
0 ------>n-k
1 ------>n-k+1
...
...
k-2 ------> n-2
也就形成c数列
C 0 ,1, 2,…., n-k-1, n-k, n-k+1, …….n-2
可以看出右边的值加个k对其取余(%n)就是左边的的值
这是一个n -1个人的问题,如果能从n -1个人问题的解推出 n 个人问题的解,从而得到一个递推公式,那么问题就解决了。假如我们已经知道了n -1个人时,最后胜利者的编号为x,那么他原本的编号就是(x+k)%n,也就是胜利者的编号。
其中k等于m % n。代入(x + k) % n <=> (x + (m % n))%n <=> (x%n +(m%n)%n)%n <=> (x%n+m%n)%n <=> (x+m)%n
假设第三轮的开始数字为B,那么这n - 2个数构成的约瑟夫环为B, B + 1, B + 2,......B - 3, B - 2.。继续做映射。
B -----> 0
B+1 ------>1
B+2 ------>2
...
...
B-2 ------> n-3
这是一个n - 2个人的问题。假设最后的胜利者为y,那么n -1个人时,胜利者为 (y + B) % (n -1 ),其中B等于m % (n -1 )。代入可得 (y+m) %(n-1)
要得到n - 1个人问题的解,只需得到n- 2个人问题的解,倒推下去。只有一个人时,胜利者就是编号0。下面给出递推式:
f [1] = 0;
f [ i ] = ( f [i -1] + m) % i;(i>1)
也许你会迷惑为什么%n变成公式中f[i]=(f[i-1]+m)%i中的%i?其实这个稍微想想就能明了。我们%n就是为了从序列c转换到序列b*——这是在n-1序列转换成n序列时%n;那么从n-2转换到n-1呢?不是要%(n-1)了吗?所以这个值是变量,不是常量。
好了公式推导完毕,下面回到这道题,依旧以n=10,m=3为例子,对他的映射转换过程具体分析。
首先1号楼直接断网,排除1号楼,映射关系从2号楼开始
红色数字表示2号楼在映射过程编号的变化,黄色数字表示下个断网楼号映射后的编号
第一次映射
2 - 0
3 - 1
4 - 2
5 - 3
6 - 4
7 - 5
8 - 6
9 - 7
10 - 8
第二次映射
3 0
4 1
5 2
6 3
7 4
8 5
0 6
1 7
第三次映射
3 0
4 1
5 2
6 3
7 4
0 5
1 6
第四次映射
3 0
4 1
5 2
6 3
0 4
1 5
第五次映射
3 0
4 1
5 2
0 3
1 4
第六次映射
3 0
4 1
0 2
1 3
第七次映射
3 0
0 1
1 2
第八次映射
0 0
1 1
第九次映射
1 0
最后总结:
2号楼最后映射关系后的编号肯定为0,因为只有他这么一幢楼,然后就是由后往前推,9次推后(n-1)它是否依旧是0
//公式
//Judge Status Problem ID Language Run Time(ms) Run Memory(KB)
//Accepted 1088 C++ 0 272
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n;
int judge(int m)
{
int f=0;
//由于开始XWBy一号大楼开始直接出队,所以从2~n进行编号0~n-2
for(int i=2;i<n;i++)
f=(f+m)%i;
if(f==0)//编号为0,对应2号大楼
return 1;
else
return 0;
}
int main()
{
while(scanf("%d",&n) && n!=0)
{
int m=1;
while(!judge(m))
m++;
printf("%d\n",m);
}
return 0;
}