中级农民
- 积分
- 106
- 大米
- 颗
- 鳄梨
- 个
- 水井
- 尺
- 蓝莓
- 颗
- 萝卜
- 根
- 小米
- 粒
- 学分
- 个
- 注册时间
- 2015-9-3
- 最后登录
- 1970-1-1
|
本帖最后由 dg7743 于 2016-6-22 15:08 编辑
Battleship & Find the Duplicate Number
地里面经 & LeetCode No.287
详见Google两次面试的经验(phone+onsite)和Google onsite 一道算法和最后一道系统题
先来设计一下Battleship这个游戏。然后再探讨一下LeetCode No.287以及原帖中楼主16年面试第二轮第二道题。
这三道题之所以放在一起讲,是因为它们都有一个共通的小技巧。我会在阐述完我个人对于Battleship的设计思路之后会指明这个小技巧。
Design Battleship:
Battleship是一个很经典的board game。我以前上高中的时候经常在文曲星上玩。估计大家都熟悉这个游戏规则,不过因为规则跟设计有关,我们在这里还是大概的叙述一下。
首先我们有一个game board,这个game board是两面的,我们及对手玩家/AI会在游戏初始阶段,自己的一面,依据自己喜好放上5个长度分别为2, 2, 3, 4, 5的军舰。双方看不到对方军舰的摆放情况。游戏开始后,双方轮流交替的放置一枚炸弹,系统会回馈炸弹是炸空,还是炸到军舰了。如果一艘军舰所有所在的点都被炸了,这艘军舰就沉了。谁先将对方的五艘军舰都炸沉,即赢得比赛。
这里直接给出我个人的设计方案。我们用一个类似union find的结构来存储军舰所在位置。一个军舰的所有child node都存储parent node的index,然后在parent node记录目前这艘军舰还未被炸毁的节点数。再用一个变量记录目前幸存的军舰数量。如果某个parent node所对应的节点数为零,目前幸存的军舰数量 - 1;用到的数据结构如下:
- vector<vector<int>> board_;
- unordered_map<int, int> ships_; //key: index, value: parent index
- unordered_map<int, int> ship_cnts_; //key: index, value: count
- int alive_cnt_;
复制代码 游戏初始阶段,我们需要一个function来根据user input来摆放军舰:
- void placeBattleships(vector<vector<pair<int, int>>> ships_pos) {
- //skip validation
- int n = board_.size();
- for (const auto& s : ships_pos) {
- int ship_len = s.size();
- int index_0 = s[0].first * n + s[0].second;
- for (int i = 0; i < ship_len; ++i) {
- int index_i = s[i].first * n + s[i].second;
- ships_[index_0] = index_i;
- }
- ship_cnts_[index_0] = ship_len;
- }
- alive_cnt_ = ships_pos.size();
- }
复制代码 游戏开始后,我们需要一个function来放置炸弹,并且返回是否炸到了军舰:
- // true - hit, false - missed
- bool placeBomb(pair<int, int> pos) {
- int n = board_.size();
- int index = pos.first * n + pos.second;
- auto it = ships_.find(index);
- if (it == ships_.end() || it->second == -1) {
- return false;
- }
- int p_index = index;
- while (ships_[p_index] != p_index) {
- p_index = ships_[p_index];
- }
- ships[index] = -1; // visited
- if (--ship_cnts[p_index] == 0) {
- --alive_cnt_;
- }
- return true;
- }
复制代码 然后我们还需要一个function显示幸存的军舰数量,以及一个function来判断所有军舰是否被炸沉:
- int aliveBattleships() const {
- return alive_cnt_;
- }
- bool won() const {
- return aliveBattleships() == 0;
- }
复制代码 可以看出这个类似union-find的data structure其实是一个很简单的结构。而且对于Battleships这个游戏来说,我们其实根本不用额外的unordered_map来存取军舰的状态。所有信息都存在board_里就好了。
Battleship游戏里只用摆放五个军舰,最长的军舰只有5的长度。一般游戏的棋盘不会很大,都是10X10的。毕竟太大了,双方很难炸完对方的军舰。所以在board_的每一格,我们可以用-1来表示这格已经被炸过。0 ~ n*n - 1来表示parent node的 index。n来表示这个格是空的。n + 2 ~ n + 6来表示这个格是个parent node以及其所对应的军舰还剩余的node的数量。
那么这道题又跟Find the Duplicate Number有什么关系呢?
在table中每个元素存储的是table index,根据其跳转,其实就是把table中的元素作为指针来使用。就像我们在上题中也是在child node中存parent node的index,并做跳转。
我们结合Find the Duplicate Number这道题来看。这道题讨论区most voted的解法即是一个国人大神把它转化为了Linked List Cycle来做。
find linked list cycle是一道大家都很熟悉的题。而国人大神的解法就是把given array中的每一个元素当作指针来用跳转到下一个元素。
我们用A[1, 4, 3, 2, 5, 2, 6]来做例:
当我们扫到A[0]时,A[0] == 1,把它作为指针来使用,我们跳到A[1];
A[1] == 4, 跳到A[4];
A[4] == 5, 跳到A[5];
A[5] == 2, 跳到A[2];
A[2] == 3, 跳到A[3];
A[3] == 2, 开始循环...
这道题之所以可以把元素作为指针来使用当然也是题干出的巧。
这个小技巧也可以用在原帖第二轮第二题上。
原帖中Heliuhun对于这道题给了一个很精妙的解法。我们来分析一下这个解法。
用A[a1, a2, a3, a4, a5], B[1,0,4,2,3]做例:
我们并不在乎A中的每个数到底是什么。我们想知道的是A中的每个数按照B来移动,最少经过多少次可以回到原位。
对于a2来说,它经历一次移动从1来到了0的位置。再经历一次移动从0回到了1的位置。
对于a4来说,它经历一次移动从3来到了2的位置。再经历一次移动从2来到了4的位置。再经历一次移动从4回到了3的位置。
可以看到A中的元素是根据B来跳转的,并且把B中的元素当作指针来使用。B中的元素形成了一个或多个封闭的环状“linked list”。
按照Heliuhun的思路,我们需要找出B中有多少封闭的环状“linked list”,再找出它们的最小公倍数。
为什么是最小公倍数呢?
拿上面的例子来说,当a2经过两次移动后回到原位时,a4还需要一次移动才能回到原位。最少经过6次移动,a2,a4以及a1,a3,a5才能同时回到原位。
补充内容 (2016-6-23 03:11):
placeBattleships第十行应为
ships_[index_i] = index_0; |
|