【例题描述】
由M只猴子围成一圈,从1到M进行编号,打算从中选出一个大王,经过协商,决定选出大王的规则:从第一个开始循环报数,
数到N的猴子出圈,下一个猴子从1开始报数。
【输入样例】
3 2
【输出样例】
3
方法一:
//模拟法
#include<iostream>
using namespace std;
#define MAX 100
long a[MAX+1];
long m,n,i,Count,k;
int main()
{
cin>>m>>n;
for(i=1;i<=m;i++)
a[i]=1;
k=m; //报数前,指向该数的前一只猴子
for(i=1;i<=m;i++)//出圈M只猴子
{
Count=0; //报数前,个数为0
while(Count<n)//当前报数<n,继续报数
{
if(k==m)//从最后一个位置移动到第一个位置
k==1;
else
k++;
if(a[k]==1)//当前猴子在圈内,报数
Count++;
}
a[k]=0;//猴子K出圈
}
cout<<k<<endl;
return 0;
}
分析:上程序发现:当猴子出圈后,程序也会一一检查是否出圈,在这里花了大量的时间。
方法二:利用数组存放下一个该报数的编号,以8只猴子为例,a[1]的值为2,意思是指2号猴子,
如下所示
a[1] | a[2] | a[3] | a[4] | a[5] | a[6] | a[7] | a[8] |
---|---|---|---|---|---|---|---|
2 | 3 | 4 | 5 | 6 | 7 | 8 | 1 |
这样构成了一个环,如果5号猴子出圈就用语句a[4]=a[a[4]],以后报到4号猴子时,就直接指向6号猴子。
#include<iostream>
using namespace std;
#define N 100000
long a[MAX+1]
long m,n,i,Count,k;
int main()
{
cin>>m>>n;
for(i=1;i<=m-1;i++)
a[i]=i+1;
a[m]=1;//最后一只猴子指向第一只
k=m;//报数前,指向该报数的前一只猴子
for(i=1;i<=m;i++)//出圈M只猴子
{
for(Count=1;Count<=n-1;Count++)//只需报数n-1次,a[k]指向该出圈瘊子
k=a[k];
a[k]=a[a[k]];
}
cout<<k<<endl;//最后一只出圈的是大王
}
为了更好的理解我列出来第一次小循环后的数组值:
a[1] | a[2] | a[3] | a[4] | a[5] | a[6] | a[7] | a[8] |
---|---|---|---|---|---|---|---|
3 | 3 | 4 | 5 | 6 | 7 | 8 | 1 |
所以每次点名到1的时候回跳过二号值,外循环进行到M次时要出去的k值即为大王的位置。
方法三:
倒退算法:如果我们知道这个子问题的答案:设X是猴王,那么根据对应关系就有以下的对应关系,F[i]=(F[i-1]+k)%i;(k为每次需要喊的次数)
//倒退算法
#include<iostream>
using namespacestd;
int main()
{
int i,m,k,ans;
cin>>m>>k;
ans=0;
for(i=2;i<m=;i++)
ans=(ans+k)%i;
cout<<ans+1<<endl;//因为这里的位次是从0开始的k%k=0.
}
【例题描述】变形约瑟夫问题
给出K与N,求K%i(1<=i<=N),即求出K%1+K%2+K%3+K+…+K%N的值。
【输入格式】
两个整数K与N(k>=1,N<109)。
【输出格式】
一个正整数ans=K%i+
【样例输入】
10 10
【样例输出】
13
方法一:
#include<iostream>//变形约瑟夫问题解法一
using namespace std;
int main()
{
int n,k,s=0;
cin>>n>>k;
for(int i=1;i<=n;i++)
s=s+k%i;
cout<<s<<endl;
}
但由于K和N数据规模过大(K>=1,N<109),所以我们要进行优化。由于题目中没有指明K与N的大小比较,所以可能的情况有以下三种:
(1) 1<K<N
(2) 1<N=K
(3) 1<N<K
当为情况(1)时我们可以把方程划分为前后两部分
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
K%i | 0 | 0 | 1 | 0 | 1 | 4 | 2 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
可以发现当i>K/2时,K%i的值为等差数列。
因此,我们可以将前半部分划分为前后两部分。
后面部分是一个公差为1的等差数列,即(K/2)
-1,(K/2)-2,…4,3,2,1,0,直接使用等差数列求和公式算出即[K%2-1]+[K%2-2]+[K%2-3]+…+[1]+[0].
继续讲前半部分划分当i>K/3时,是一个公差为2的等差数列,但需要注意的是,由于K/2
取整,所以其首项K%(K/2)可能为0也可能为2得等差数列,但需要注意的是,由于取整K/2取整,所以其首项K/(K%2)可能为0也可能为1。
需要注意的是,当拆分到1=<i<=sqrt(K)时,该段数据已无规律可循,所以该段直接使用朴素算法即可。
这样即使数据量再大,通过这种数学方法,它的时间复杂度仅为sqrt(K);
#include<iostream>
#include<cmath>
using namespace std;
int main()
{
long long N,K;
while(cin>>N>>K)
{
long long S,T,i,s,e,ans=0;
S=sqrt((double)K);
T=K/S;
//该段为朴素算法
for(i=1,ans=0;i<=N&&i<=T;i++)
ans+=K%i;
if(N>K)
ans+=(N-K)*K;
//该段为优化算法
for(i=S;i>1;i--)
{
s=K/i;
e=K/(i-1);
if(N<s)
break;
if(N<e)
e=N;
ans=ans+(e-s)*(K%e+K%(s+1))/2;
}
cout<<ans<<endl;
}
}
//递归算法
#include<iostream>
using namespace std;
int m,k;
int Josephus(int m)
{
int ans;
if(m==1)
return 1;
else
ans=(Josephus(m-1)+k-1)%m+1;
return ans;
}
int main()
{
cin>>m>>k;
cout<<Josephus(m)<<endl;
return 0;
}
这种算法效率是极高的但还可以对它进行优化,使他与侯子数无关。
将上面的递归程序中的第12行进行注释去除,再次编译运行观察输出的ans值,例如当m=30,k=3时如表6.2所示
ans | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|
值 | 2 | 2 | 1 | 4 | 1 | 4 | 7 | 1 | 4 | 7 | 10 |
ans | … | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
值 | … | 2 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 26 | 29 |
可以发现从ans21到30之间的值处于一种等差递增的状态。通过分析ans=(ans+k-1)%i+1,可以看出,当i比较大而ans+k-1比较小的时候,ans就处于一种公差为K的等差递增状态,而这一部分我们可以跳过。
设中间变量x列出等式:ans+k×x-1=i+x。
解出x,令ans=ans+k*x,将i+x直接赋值给i,这样就跳过了中间共x重的循环,从而节省了等差递增得时间开销。
当然,我们还需要注意几种特殊情况:
1 当k=1时,最终结果就是(k+n-1)%M。
2 当k!=1时,最终结果就是(k+n-1)%M;
参考程序如下所示:
#include<iostream>
using namespace std;
int main()
{
int x,M,ans=0,K;
cin>>M>>K;
for(int i=1;i<=M;i++)
{
if(ans+K<i)
{
x=(i+1-ans)/(K-1)-1;//该处减一是因为,循环的末尾有一个进位需要执行所以这里需要减一
if(i+x<M)
{
ans=ans+k*x;//跳过一些不必要的轮次
i=i+x;
}
else//该次跳跃可以执行到末尾
{
ans=ans+K*(M-i);
i=M;//循环结束
}
}
ans=(ans+K-1)%i+1;
}
cout<<ans<<endl;//输出猴王的位置
return 0;
}
算法分析:算法优化之处就是通过观察规律而得到的,其实就通过计算x的值,减少了循环当中一些不必要的轮次,从而大大的节省了时间,以至于算法本身与M是没有关系的。