约瑟夫问题(优化优化再优化)
1 什么是约瑟夫问题
约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。
从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;
依此规律重复下去,直到圆桌周围的人全部出列。
2 如何求最后一个出列的人
1、模拟方法
2、数学方法
3 模拟方法
模拟方法就是所谓的一个个模拟,一个一个出列。这个方法比较多,可以直接用数组模拟,也可以直接建一个循环链表模拟,
总之这个很好实现,但是复杂度却是O(nm),如果n和m都是10000,要求1s计算出结果,估计就不行了。
这个算法实现,网上一大堆:随便给出两个:
struct ListNode { int num; //编号 ListNode *next; //下一个 ListNode(int n = 0, ListNode *p = NULL) { num = n; next = p;} }; //自定义链表实现 int JosephusProblem_Solution1(int n, int m) { if(n < 1 || m < 1) return -1; ListNode *pHead = new ListNode(); //头结点 ListNode *pCurrentNode = pHead; //当前结点 ListNode *pLastNode = NULL; //前一个结点 unsigned i; //构造环链表 for(i = 1; i < n; i++) { pCurrentNode->next = new ListNode(i); pCurrentNode = pCurrentNode->next; } pCurrentNode->next = pHead; //循环遍历 pLastNode = pCurrentNode; pCurrentNode = pHead; while(pCurrentNode->next != pCurrentNode) { //前进m - 1步 for(i = 0; i < m-1; i++) { pLastNode = pCurrentNode; pCurrentNode = pCurrentNode->next; } //删除报到m - 1的数 pLastNode->next = pCurrentNode->next; delete pCurrentNode; pCurrentNode = pLastNode->next; } //释放空间 int result = pCurrentNode->num; delete pCurrentNode; return result; }
//使用标准库 int JosephusProblem_Solution2(int n, int m) { if(n < 1 || m < 1) return -1; list<int> listInt; unsigned i; //初始化链表 for(i = 0; i < n; i++) listInt.push_back(i); list<int>::iterator iterCurrent = listInt.begin(); while(listInt.size() > 1) { //前进m - 1步 for(i = 0; i < m-1; i++) { if(++iterCurrent == listInt.end()) iterCurrent = listInt.begin(); } //临时保存删除的结点 list<int>::iterator iterDel = iterCurrent; if(++iterCurrent == listInt.end()) iterCurrent = listInt.begin(); //删除结点 listInt.erase(iterDel); } return *iterCurrent; }
4 数学方法-优化
由于上面O(nm)的方法很容易超时,所以这里的数学方法可以做到O(n).
问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。
我们知道第一个人(编号一定是m%n-1) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m%n的人开始): k k+1 k+2 ... n-2, n-1, 0, 1, 2, ... k-2,并且从k开始报0。
现在我们把他们的编号做一下转换:
1 |
k --> 0 |
2 |
k+1 --> 1 |
3 |
k+2 --> 2 |
4 |
... |
5 |
... |
6 |
k-2 --> n-2 |
7 |
k-1 --> n-1 |
变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗?!!变回去的公式很简单,相信大家都可以推出来:x'=(x+k)%n。
如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ---- 这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:
令f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]。
递推公式:
1 |
f[1]=0; |
2 |
f[i]=(f[i-1]+m)%i; (i>1) |
有了这个公式,我们要做的就是从1-n顺序算出f[i]的数值,最后结果是f[n]。因为实际生活中编号总是从1开始,我们输出f[n]+1。
int f(int n, int m) { int f1 = 0,f2; for(int i = 2; i <= n; i++) {
f2 = (f1 + m) % i;
f1=f2;
} return f2+ 1; }
5 在优化还能优化吗?-再优化
今天碰到一个题目,n <= 10^18,m<=1000,时间1s,这想想O(n)肯定超时,没得说。
但是我么可以看看上面的规律,
f[i] = (f[i-1]+m)%i,通过这个式子,我们发现,到一定程度,m会远远小于i的,所以每次不是仅仅加一个m,我可以一下子加X*m,从而跳过X个i,事实证明,这样做的效率非常高。
当然只有当m远远小于n的时候,效率会比较高。如果m>n那么效率也就接近O(n)了。
对于当前的i,如果f1+m <i,那么表示,很有可能可以跳过下一个i,这里我们假设f1+X*m=i,那么至少可以跳过X=(i-f1)/m,然后i+=X即可,这样就不用求i到i+X之间的数据了。
什么时候结束呢?
如果i+X>=n,那么就证明这次已经超过了n,这里只需要令f2=f1+(n-i)*m,并且i=n跳出循环即可。
具体代码及注释如下:
#include <iostream> using namespace std; //数据范围n<=10^18,m<=1000,时间几十ms __int64 N,M; int main() { while (cin >> N >> M) { __int64 f1 = 0; __int64 f2; __int64 X; if (M == 1) { cout << N <<endl; } else { for (__int64 i = 2; i <= N; ++ i) { if (f1 + M < i)//表示很有可能跳过X个i { X = (i - f1) / M;//能跳过多少个 if (i + X < N)//如果没有跳过n,就是i<=N { i = i + X;//i直接到i+X f2 = (f1 + X*M);//由于f1+X*M肯定<=i,所以这里不用%i f1 = f2; } else//如果跳过了n,那么就不能直接加X了,而是只需要加(N-i)个M即可 { f2 = f1+(N-i)*M; f1 = f2; i = N; } } f2 = (f1 + M) % i;//如果f1+M>=i或者跳过上面的一些i之后还是要继续当前i对于的出列的人 f1 = f2; } } cout << f2+1 <<endl; } return 0; }