(转)約瑟夫問題的兩個O(log n)解法
這個是學習編程時的一個耳熟能詳的問題了:
n
個人(編號爲0,1,...,n-1
)圍成一個圈子,從0
號開始依次報數,每數到第m
個人,這個人就得自殺,
之後從下個人開始繼續報數,直到所有人都死亡爲止。問最後一個死的人的編號(其實看到別人都死了之後最後剩下的人可以選擇不自殺……)。
這個問題一般有兩種問法:
- 給出自殺順序。不少數據結構初學書都會以這個問題爲習題考驗讀者對線性表的掌握。
比較常見的解法是把所有存活的人組織成一個循環鏈表,這樣做時間複雜度是O(n*m)
的。
另外可以使用order statistic tree(支持查詢第k小的元素以及詢問元素的排名)優化到O(n log n)
。
另外有篇1983年的論文An O(n log m) Algorithm for the Josephus Problem,但可惜我沒找到下載鏈接。 - 求出最後一個人的編號。可以套用上一種問法的解法,但另外有更加高效的解法,下文展開討論。
時間O(n),空間O(1)
設f(n)
爲初始有n
人時最後一個自殺的人的編號,那麼有如下遞推式:
1 |
f(n) = (f(n-1) + m) mod n |
以n=5, m=3
爲例,一開始有這麼5個人:
1 |
0 1 2 3 4 |
第一輪報數後2
號死亡,圈子裏剩下來的人的編號是這些:
1 |
3 4 0 1 |
這裏把3
號寫在最前面了,因爲輪到3
開始報數。
如果我們有辦法知道n=4
時的答案,即初始圈子爲:
1 |
0 1 2 3 |
時的答案,那麼可以把n=4
的初始圈子的所有數x
變換成(x+3) mod 5
,得到:
1 |
3 4 0 1 |
這個恰好就是n=5
時第一輪結束後的圈子狀態,也就是說我們可以根據n=4
的答案推出n=5
時的答案。
手工演算一下就能發現n=z
時的圈子第一輪結束後(即m-1
號自殺後)的圈子狀態,
可以由n=z-1
的初始圈子施以變換x -> (x+m) mod z
得到。
於是我們可以從n=1
開始(此時的答案是0),推出n=2
的答案,再推出n=3
,直到計算到所要求的n
。
下面是C語言實現:
1234567 |
int f(int n, int m){ int s = 0; for (int i = 2; i <= n; i++) s = (s + m) % i; return s;} |
時間O(log n),空間O(log n)
換一個遞推思路,考慮報數過程又回到第0
個人時會發生什麼。這時有floor(n/m)*m
個人都已經報過數了,並且死了floor(n/m)
人。
之後一輪的報數會數過m
個人,又會回到第0
個人。
我們以n=8, m=3
爲例看看會發生什麼。一開始:
1 |
0 1 2 3 4 5 6 7 |
floor(n/3)*3
個人都報過數後:
1 |
0 1 x 3 4 x 6 7 (A) |
即2號和5號死亡,接下來輪到6號開始報數。因爲還剩下6個人,我們設法做一個變換把編號都轉換到0~5
:
12 |
2 3 x 4 5 x 0 1 (B) ___ |
如果我們能求出n'=6
時的答案s
,即初始圈子爲:
1 |
0 1 x 2 3 x 4 5 (C) |
時最後一個人的編號,那麼就可以據此算出圈子狀態(B)
的最後一個人的編號。
如果去掉圈子(B), (C)
中的x
,(B)
可以看作是(C)
旋轉左移兩位(n%m=8%3=2
)得到的。
所以(C)
的答案s
可以通過運算s2=(s-n%m+n)%n
得到(B)
的答案。
然後要根據(B)
的答案計算出(A)
的答案。
如果(B)
的答案是在最後一個x
後面(帶下劃線),即s-n%m < 0
,那麼s3 = s2 = s - n % m + n
;否則s3 = s2 + s2 / (m-1)
;
注意如果n < m
,那麼報數回到第一個人時還沒死過人,需要用之前時間複雜度O(n)
的方法遞推。
下面是C語言實現:
1234567 |
int rec(int n, int m){ if (n == 1) return 0; if (n < m) return (rec(n - 1, m) + m) % n; int s = rec(n - n / m, m) - n % m; return s < 0 ? s + n : s + s / (m-1);} |
n
每次變爲n * (m-1) / m
,即以一個常數因子衰減到剛好小於m
,然後換用線性複雜度的遞推算法,
總的时间复杂度为O(m + log_{m/(m-1)}{n/m})
,如果把m
看作常數的話就是O(log n)
。
程序中在子问题计算后还需要做一些处理,无法尾递归,所以空间复杂度也等於这个。
參考:
- Time Improvement on Josephus Problem (2002) by Fatih Gelgi
- http://stackoverflow.com/questions/4845260/josephus-for-large-n-facebook-hacker-cup
時間O(log n),空間O(1)
三年前我還看到過一段更巧妙的代碼,具體寫法不可考了,當時盯着式子思考了好久呢。其等價的形式長這樣:
12345 |
int kth(int n, int m, int k){ for (k = k*m-1; k >= n; k = k-n+(k-n)/(m-1)); return k;} |
這個函數解決了第二種問法的一個擴展問題,即第k
個自殺的人的編號。如果取k = n
那麼就得到了原問題的解。
如果設法追蹤k
在計算過程中的所有取值,會發現第k
個報數的人都是同一個人。
比如n=5, m=3, k=5
,那麼一共有15次報數(每報m
次數死一個人,一共有k
個人要死,所以需要k*m
次報數),每一次報數的人的編號如下:
12 |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 140 1 2 3 4 0 1 3 4 1 3 1 3 3 3 |
報到2、5、8、11、14的人自殺。
下面直接給出一個結論:
設第p
次報數的人是y
,令p = m * a + b (0 <= b < m)
,
那麼如果這個人還沒自殺,那麼他下次報數是第q = n + (m-1) * a + b
次報數。
反過來,根據q
也可以算出這個人上一次報數的時刻p
,這裏允許報完q
後自殺,
但我們可以看到時刻p
這個人上一次報數時肯定還沒死。
這是爲什麼呢?a
其實是第p
次報數前自殺的人數。q - p = n - a
,這個是兩次報數的間隔。因爲報完p
後圈子裏還剩下n - a
人,
所以再經過n - a
次報數後又會輪到同一個人。
我們要求第k
個自殺的人,他是第k*m-1
次報數後自殺的。
根據之前q
的定義式我們可以求出這個人之前一次報數p
是在什麼時候,
不斷地重複這一過程,直到知道他是第k' (0 <= k' < n)
次報數的人,
而這個k'
就是這個人的編號。
(转)約瑟夫問題的兩個O(log n)解法的更多相关文章
- JAVA演算法---約瑟夫問題
1 public class Josephus { public static int[] arrayOfJosephus( int number, int per) { 3 int[] man = ...
- 有關AWS EC2 (EBS 收費)的問題
有關AWS EC2 (EBS 收費)的問題 之前一陣子的時候,公司在使用Amazone Web Service (AWS)的 EC2 (Amazon Elastic Compute Cloud).不過 ...
- ASP.NET MVC Identity 兩個多個連接字符串問題解決一例
按照ASP.NET MVC Identity建立了一個用戶權限管理模塊,由于還要加自己已有的數據庫,所以建立了一個實體模型,建立了之后,發現登錄不了: 一直顯示“Login in failed for ...
- [亂數] <細說> C/C++ 亂數基本使用與常見問題
陸陸續續寫了 EA 一.二年,以前亂數引導文回頭看時才發現,怎麼有這麼多細節的錯誤.沒系統. 這篇文章主要引導初學者使用亂數,同時附上常被翻出來討論的議題,C/C++適用,唯以 C 語言撰之. 也由 ...
- 何解決 LinqToExcel 發生「無法載入檔案或組件」問題何解決 LinqToExcel 發生「無法載入檔案或組件」問題
在自己的主機上透過 Visual Studio 2013 與 IISExpress 開發與測試都還正常,但只要部署到測試機或正式機,就是沒辦法順利執行,卡關許久之後找我協助.我發現錯誤訊息確實很「一般 ...
- [C#] 與Android共舞–透過GET方式傳資料給Server(含解決中文編碼問題) (转帖)
上一篇文章分享了透過POST 方式傳資料回Server,這一篇來談談有關於透過GET的方式傳遞 首先,如我預期的一樣,透過網址傳遞,會產生編碼問題,這邊我就順代解掉,希望有碰到的人 可以不用為此煩惱. ...
- 搜索提示時jquery的focusout和click事件沖突問題完美解决
在主流的搜索引擎上搜索時,輸入內容,往往會彈出智能提示.輸入框为input,智能提示區域为suggest.接下來一般有兩種操作: 1.選擇某一提示,則把內容复制到input中 ...
- ROHS無鉛問題解答!ROHS IPC SGS
無鉛smt(smd)問題1. 問Maxim關于無鉛的定義是什么?答無鉛表示在封裝或產品制造中不含鉛(化學符號為Pb).IC封裝中,Pb在外部引腳拋光或電鍍中很常見.對于晶片級封裝(UCSP和倒裝芯片) ...
- 【Java算法學習】斐波那契數列問題-兔子產子經典問題
/** * 用遞推算法求解斐波那契數列:Fn = Fn-2 +Fn-1; */ import java.util.*; public class Fibonacci { public static v ...
随机推荐
- Flask —— 使用Python和OpenShift进行即时Web开发
最近Packtpub找到了我,让我给他们新出版的关于Flask的书写书评.Flask是一个很流行的Python框架.那本书是Ron DuPlain写的<Flask 即时Web开发>.我决定 ...
- IE8中JSON.stringify方法对自动转换unicode字符的解决方案
IE8内置了JSON对象,用以处理JSON数据.与标准方法的不同,IE8的JSON.stringify会把utf-8字符转码: var str = "我是程序员" var json ...
- jquery easyui datebox单击文本框显示日期选择
jquery easyui的datebox日历控件,实现单击文本框出现日历选择,如下图: 代码: 修改jquery.easyui.min.js第9797行函数(jQuery EasyUI 1.3.2) ...
- linux 下的使用 ln 创建 软链接 和 硬链接
linux 下的一个指令 ln 作用: 创建软链接或者硬链接 Linux 系统下每创建一个文件,系统都会为此文件生成一个 index node 简称(inode) ,而每一个文件都包含用户数据(use ...
- hdu 1042 N!
题目连接 http://acm.hdu.edu.cn/showproblem.php?pid=1042 N! Description Given an integer N(0 ≤ N ≤ 10000) ...
- Karaf 依赖equinox and felix,karaf 本Apache的很多项目作为基础框架
6月17日是Apache Karaf作为Apache顶级项目.Karaf是个运行时包,包含了一个OSGi框架(Equinox或Felix).一个命令shell(Felix Gogo)及默认情况下内置的 ...
- 50.ISE布局布线错误
ERROR:Pack:1654 - The timing-driven placement phase encountered an error. 原因:时钟输出引脚直接接在I/O上了: 方法:在时钟 ...
- Pintos修改优先级捐赠、嵌套捐赠、锁的获得与释放、信号量及PV操作
Pintos修改优先级捐赠.嵌套捐赠.锁的获得与释放.信号量及PV操作 原有的优先级更改的情况下面没有考虑到捐赠的情况,仅仅只是改变更改了当前线程的优先级,更别说恢复原本优先级了,所以不能通过任何有关 ...
- QWidget设置为模态问题
设置QWidget的Qt::WindowModality属性为Qt::WindowModal和Qt::ApplicationModal,发现窗体仍然不会模态,网上查了一下,有人说改属性只对window ...
- meta 页面元信息定义
禁止页面缓存 1.name属性 name属性主要用于描述网页,与之对应的属性值为content,content中的内容主要是便于搜索引擎机器人查找信息和分类信息用的. meta标签的name属性语法格 ...