Saturday, May 18, 2019

约瑟夫问题







约瑟夫问题的两个O(log n)解法


转自:http://maskray.me/blog/2013-08-27-josephus-problem-two-log-n-solutions
(同时添加了自己的一些心得)

这个是学习编程时的一个耳熟能详的问题了:
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语言实现:





1
2
3
4
5
6
7
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





1
2
2 3 x 4 5 x 0 1 (B)
___

(B)的答案等于规模缩小后的情况n'=6时最后一个人的编号s。然后根据s算出圈子状态(A)的最后一个人的编号。
如果s在最后一个x后面(带下划线),即s < n%m,那么s2 = s-n%m+n;否则s2 = s-n%m+(s-n%m)/(m-1)
注意如果n < m,那么报数回到第一个人时还没死过人。因为子问题的规模没有减小,所以上述方法不适用。需要用之前时间复杂度O(n)的方法递推。下面是C语言实现:





1
2
3
4
5
6
7
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)。程序中在子问题计算后还需要做一些处理,无法尾递归,所以空间复杂度也等于这个。
参考:

另一种说明方式, 转自 https://zh.wikipedia.org/wiki/%E7%BA%A6%E7%91%9F%E5%A4%AB%E6%96%AF%E9%97%AE%E9%A2%98


对于,可以将上述方法推广,将杀掉第k2k、……、个人视为一个步骤,然后把号码改变,可得如下递推公式, , 运行时间为


这里的第三个公式,把s < n%m 与 s >= n%m,两种情况综合了起来。(公式中的k即为m)
为了理解这个公式,需要注意两个事实:

其一,下面例子中m=3,即每三个删除一个,然后从新标号得到第二行。(这里不是用约瑟夫游戏


1
2
0 1 2 3 4 5 6 7 8 9 10 11 12 13
0 1 2 3 4 5 6 7 8 9

要从第二行的数得到与之对应的第一行的数,可以发现第一行的数是每三个一组,而第二行的数是每两个一组。假设第二行的一个数为v,那么v/(m-1) 可得到组号,再乘以m,即m*v/(m-1)可得到之对应的第一行的数的大概位置。然后再验算一下,发现这个结果好于预期,居然可以算出第一行的数的精确位置!

其二,约瑟夫游戏时,第二行是按递归考虑时的重新编号,

1
2
0 1 2 3 4 5 6 7 8 9 10 11 12 13
2 3 4 5 6 7 8 9 0 1

我们的目的是要从第二行的数得到与之对应的第一行的数,这是我们可以利用其一中的事实了,只要我们把第二行的数转为其一中的第二行的数就行了,即:

1
2
0 1 2 3 4 5 6 7 8 9
2 3 4 5 6 7 8 9 0 1

公式为 (v-2)%n, 这里n=10

-----------------------------------------------------

把上面的递归公式转为代码后,发现结果不正确,会返回一个负数。经检查,问题出在
(f(n',k)- n%k) mod n' 上。原来c++的模运算和数学意义上的还是有些差别。


std::cout << (-7 % 3) << std::endl;
std::cout << (7 % -3) << std::endl;
give different results:
-1
1
解释:
From ISO14882:2011(e) 5.6-4:
The binary / operator yields the quotient, and the binary % operator yields the remainder from the division of the first expression by the second. If the second operand of / or % is zero the behavior is undefined. For integral operands the / operator yields the algebraic quotient with any fractional part discarded; if the quotient a/b is representable in the type of the result, (a/b)*b + a%b is equal to a.
The rest is basic math:
(-7/3) => -2
-2 * 3 => -6
so a%b => -1

(7/-3) => -2
-2 * -3 => 6
so a%b => 1
Note that
If both operands are nonnegative then the remainder is nonnegative; if not, the sign of the remainder is implementation-defined.

就是说当f(n',k)- n%k 为负值时,(f(n',k)- n%k) mod n' 也为负。解决这个问题只需在代码中用(f(n',k)- n%k+n') mod n' 就可以了。


时间O(log n),空间O(1)

三年前我还看到过一段更巧妙的代码,具体写法不可考了,当时盯着式子思考了好久呢。其等价的形式长这样:





1
2
3
4
5
6
int kth(int n, int m, int k)
{
if (m == 1) return n-1;
for (k = k*m+m-1; k >= n; k = k-n+(k-n)/(m-1));
return k;
}

这个函数解决了第二种问法的一个扩展问题,即第k个(从0数起)自杀的人的编号。如果取k = n-1那么就得到了最后一个自杀的人的编号。
这个算法的思想是第k*m+m-1次报数的人就是第k个自杀的人,追踪他之前每次报数的时刻来确定他的编号。
考虑n=5, m=3, k=4,一共有15次报数(每报m次数死一个人,一共有k+1个人要死,所以需要(k+1)*m次报数),每一次报数的人的编号如下:





1
2
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
0 1 2 3 4 0 1 3 4 1 3 1 3 3 3

报到2、5、8、11、14的人自杀。
设第p次(从0数起)报数的人是y,令p = m*a+b (0 <= b < m)
经过前p次报数,一共死了floor(p/m) = a人,圈子里还剩下n-a人。
如果y本次报数后没有自杀,那么他下次报数是在圈子里其他n-a人都报数之后,
也就是第q = p+n-a = n+(m-1)*a+b次报数。
这是后继的计算式,逆用即可计算前驱。
y本次报数后还存活知b < m-1,故a = floor((q-n)/(m-1)),【注】
p = q-(n-a) = q-n+floor((q-n)/(m-1))
我们要求第k个自杀的人,他是第(k+1)*m-1次报数后自杀的,用计算前驱的方式求出这个人之前一次报数是在什么时候,不断地重复这一过程,直到知道他是第k' (0 <= k' < n)次报数的人,那么k'就是这个人的编号。

【注】对于a,b,c三个数,其中b,c确定为正,这里需要证明:
if b<c, then (a-b)/c == a/c (注意,这里不考虑小数部分)
证明:
let a-b = k*c+x,     0<=x<c
let a= k'*c+y,  0<=y<c
then
k*c+x+b=k'*c+y  then,
(k-k')c=y-b-x
here,  -b<=y-b<c-b,   -c<-x<=0, then
-2c<-b-c<y-b-x<c-b<c
but (k-k')c must be a multiple of c, so y-b-x must be 0, that means k==k'

不知道还有没有更简洁的证明方法。








Thursday, March 10, 2016

Rotate Array

from here

1. 问题描述

将一个具有n个元素的向量,向左旋转i个位置。
例如,具有8个元素的向量abcdefgh,向左旋转3个元素的结果是defghabc。



第二种解法,这种解法来自于C++ STL的rotate调用的源码,代码如下:
template <class ForwardIterator>  
  void rotate ( ForwardIterator first, ForwardIterator  middle,     
                 ForwardIterator last )     
{   
  ForwardIterator next = middle;    
  while (first!=next)      
  {     
     swap (*first++,*next++);   
     if (next==last) next=middle; //注意,这两个if不能交换位置  
      else if (first == middle) middle=next;      
  }  
}  
上面的伪代码可能不太好理解,我们来分析一下。
假设数组有N个元素,旋转i位,且(N>i),我们首先考虑i < N-i的情况,我们可以把数组分成三部分,分别是 ab1 b2,其中,a的长度等于b1的长度,然后算法执行一轮以后,就得到 b1ab2,剩下要做的事情就是交换ab2的顺序,且中点应该在ab2交接的地方,所以有middle=next,假设此时b2的长度小于a,则将划分为a1a2b2,然后再执行一次while循环,整个数组就变成了b1b2a2a1,现在是要交换a2和a1的顺序即可。
交换a2和a1的顺序其实和交换a和b的顺序是一样的,只是元素更少了。
完整的代码如下所示:
#include <stdio.h>
#include <string.h>

void swap(char *p, char *q)
{
    if ( p == NULL || q == NULL) return;

    char temp = *p;
    *p = *q;
    *q = temp;
}


void rotate(char* first, char* middle, char* last)
{
    char *next = middle;
    while( first != next)
    {
        swap( first++, next++);
        if (next == last){ next = middle;}
        else if ( first == middle ) { middle = next;}
    }
    return;
}


int main(int argc, char* argv[])
{

   char str[] = "abcdefgh";
   int n = strlen(str);
   int i = 3;
   rotate( str, str + i, str + n);
   printf("%s\n", str);
   return 0;
}

Tuesday, June 16, 2015

发信人: fbrefer (fbrefer), 信区: JobHunting
标  题: facebook面试准备
发信站: BBS 未名空间站 (Tue Jul 22 11:01:17 2014, 美东)


关于面试流程
社招的话
电面1-2轮,一般就是coding
onsite一般是4轮,2轮coding,1轮design,1轮behavior+coding
校招的话,那轮design也变成coding了



关于准备
1) algo/coding
建议大家刷一下leetcode,基本上cover到了大多数常见面试题,而且有可能碰到原题
。需要注意的是,仅仅解出来,做到bug free可能是不够的。代码的质量和速度也非常
重要。网上有一些别人给出的答案可以参考,尽量做到代码简洁清晰。速度上leetcode
上所有题都做到10分钟以内写完。


2) design
解这种题是个*交流*的过程,或者说是给出方案然后获取反馈的不断循环的过程。
一般的流程:
首先你要问清楚requirement;
然后可以讲一下high level architecture,就是分成哪几个component,互相之间如果
interact,在白板上画一画;
之后面试官可能会让你深入某个component detail讨论;
也有可能变换requirement让你重新设计

另外,f家还喜欢让你估算机器之类的,做一些back-of-envelopme calculation。所以
最好对一些计算机相关的基本常数,fb的用户量等等有个大概的了解。

准备的时候建议看看fb的design高频题。一方面有可能面试的时候刚好碰到这几个
topic,另一方面其实很多design都是相通的。
之前有个帖子讲这个,原帖已经被删了,这儿有个备份http://blog.csdn.net/sigh1988/article/details/9790337

另外补充一点我收集的材料

a) 首先你可以从整体上了解一下facebook的architecture
http://www.quora.com/Facebook-Engineering/What-is-Facebooks-arc
http://www.ece.lsu.edu/hpca-18/files/HPCA2012_Facebook_Keynote.
http://www.quora.com/Facebook-Engineering/What-have-been-Facebo
除了下面给出的一些资料,fb engineering page里还有很多不错的内容
https://www.facebook.com/Engineering

b) news feed
这里有个talk
http://www.infoq.com/presentations/Facebook-News-Feed
对应的slides
http://readme.skplanet.com/wp-content/uploads/2012/11/0-3_Faceb
还有一些quora上的讨论
http://www.quora.com/Activity-Streams/What-are-the-scaling-issu
http://www.quora.com/What-are-best-practices-for-building-somet
http://www.quora.com/What-is-the-best-storage-solution-for-buil

c) facebook chat
这里有两个notes,其中第二个里面还有相应的tech talk links
https://www.facebook.com/notes/facebook-engineering/facebook-chat/
14218138919
https://www.facebook.com/notes/facebook-engineering/chat-stability-and-
scalability/51412338919

d) typeahead search & graph search
关于typeahead search的tech talk和notes
https://www.facebook.com/video/video.php?v=432864835468
https://www.facebook.com/note.php?note_id=365915113919
https://www.facebook.com/note.php?note_id=389105248919

关于graph search的paper, tech talk, notes。其中paper很值得一看。
http://db.disi.unitn.eu/pages/VLDBProgram/pdf/industry/p871-cur
https://newsroom.fb.com/Photos-and-B-Roll/4362/Graph-Search-Whiteboard
https://www.facebook.com/note.php?note_id=10151240856103920
https://www.facebook.com/note.php?note_id=10151347573598920
https://www.facebook.com/note.php?note_id=10151361720763920
https://www.facebook.com/note.php?note_id=10151432733048920
https://www.facebook.com/note.php?note_id=10151755593228920

e) facebook messages
两个tech talks
http://www.youtube.com/watch?v=XAuwAHWpzPc
http://www.infoq.com/presentations/HBase-at-Facebook
以及eng notes
https://www.facebook.com/note.php?note_id=10150148835363920
https://www.facebook.com/note.php?note_id=10150162742108920

f) photo storage
相关的papers和notes
https://www.usenix.org/conference/osdi10/finding-needle-haystack-facebooks-
photo-storage
https://www.usenix.org/legacy/events/osdi10/tech/full_papers/Beaver.pdf
https://www.usenix.org/legacy/events/osdi10/tech/slides/beaver.pdf
https://www.facebook.com/note.php?note_id=76191543919

g) social graph data store
相关的note, video, paper
https://www.facebook.com/notes/facebook-engineering/tao-the-power-of-the-
graph/10151525983993920
https://www.usenix.org/conference/atc13/technical-sessions/presentation/
bronson
http://www.cs.cmu.edu/~pavlo/courses/fall2013/static/papers/117

h) tiny URL
这里有一些讨论
http://n00tc0d3r.blogspot.com/2013/09/big-data-tinyurl.html
http://stackoverflow.com/questions/742013/how-to-code-a-url-sho
http://stackoverflow.com/questions/3376163/what-are-the-things-

i) POI
参考这里
http://www.slideshare.net/mmalone/scaling-gis-data-in-nonrelati
http://www.mitbbs.ca/article_t/JobHunting/32476139.html


3) behavior,建议大家了解一下fb的culture,准备一下常见的behavior questions,
面试之前rehearsal一下。

最后面试临近的时候,可以再刷刷面经,找找感觉。像glassdoor, mitbbs/jobhunting
, careercup,这些上面就有很多。