打赏

约瑟夫环问题

最近遇到一个算法题, 题目描述为

已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列,求出剩下的人的编号。

按照直观的思路, 这应该是一个循环链表问题。思路大概如下

  1. 初始化链表
  2. 插入节点
  3. 遍历链表(步长为m)
  4. 遇到编号为m的节点则删除

定义节点

class Node {
public $elem; // 节点数据
public $next; // 下一个节点指针

    public function __construct($elem) {
        $this->elem = $elem;
        $this->next = null; 
    }
}

循环链表实现

class CircularList {
    public $currentNode;
    public $head;

    public function __construct(Node $node) {
            $this->head = $node;
            $this->head->next = $this->head;
            $this->currentNode = $this->head;
    }

    /**
    在节点之后插入某节点
    */
    public function insert($elem, $item) {
        $curr = $this->find($item);
        $node = new Node($elem);
        $node->next = $curr->next;
        $curr->next = $node;
    }

    /**
        找到某节点
    */
    public function find($elem) {
        $currentNode = $this->head;
        while ($currentNode->elem != $elem) {
            $currentNode = $currentNode->next;
        }

        return $currentNode;
    }

    /**
        查找当前节点的前一节点
    */
    public function findPrev ($elem) {
        $currentNode = $this->head;
        while ($currentNode->next->elem != $elem) {
            $currentNode = $currentNode->next;
        }

        return $currentNode;
    }

    public function delete ($elem) {
        $node = $this->findPrev($elem);
        if (isset($node->next->next)) {
            $node->next = $node->next->next;
        }
    }

    /**
        前进n步
    */
    public function advance($n) {
        while ($n > 0) {
            if ($this->currentNode->next->elem == 'head') {
                $this->currentNode = $this->currentNode->next->next;
            }else{
                $this->currentNode = $this->currentNode->next;
            }
            $n--;
        }
    }

    /*
    * 列表长度
    */
    public function count() {
        $currentNode = $this->head;
        $count = 0;
        while (isset($currentNode->next) && $currentNode->next->elem != 'head') {
            $currentNode = $currentNode->next;
            $count++;
        }

        return $count;
    }

    // 遍历打印链表
    public function display() {
        $curr = $this->head;

        while (isset($curr->next) && $curr->next->elem != 'head') {
            echo $curr->next->elem . " ";
            $curr = $curr->next;
        }
    }
}   

有上面的代码实现, 解决这个问题就简单了,代码如下

$list = new CircularList(new Node('head'));

for ($i = 1; $i <= $n ; $i++) {
    $list->insert($i, $i == 1 ? 'head' : $i - 1);
}

$total = $list->count();
while ($total > $m -1 ) {
    $list->advance($m);
    $list->delete($list->currentNode->elem);
    $total--;
    $list->display();
    echo "<br/><br/>";
}

上面的代码过程都体现出来了, 但复杂度为O(mn).如果只需要得到最后的编号, 不需要这么复杂, 第一次报数, m出列, 则当前顺序为m+1, m+2, ...n, n-1, n-2, ...,m-1, 设为f'(n-1, m).对此重新编号则为1,2,...,n-1, 设为f(n-1, m).

则原问题f(n,m)转化为f'(n-1, m)的解, 我们只需要得出f'(n-1, m)与f(n-1, m)的关系即可。
第一个出列的人编号为m%n, 则他前面的人(m-1)%n, 后面的人为(m+1)%n, f与f'的对应关系为f'(n-1, m) = (f(n-1, m) + m)%n, 得到递推关系, 则此问题就简单了。

function Joseph($n,$m) {  
    $r=0;  
    for($i=2; $i<=$n; $i++) {
        $r = ($r + $m) % $i;  
    }
    return $r+1;  
}
posted @ 2017-09-15 16:31  confused_man  阅读(134)  评论(0编辑  收藏  举报