腾讯2018测试题小Q硬币有感——浅谈记忆化搜索

  笔者现在还未到找实习工作的时候。但正好是实习生招聘季,有幸有一些参加实习生招聘的朋友找我讨论一些大公司的笔试题目。笔者觉得今年的腾讯的测试题目小Q硬币问题(2017年腾讯笔试题)出的还是比较有意思的,角度也很刁钻。网易也出了同样角度的题目,便拿来和大家分享一下。


题目的大致描述:

小Q非常富有,拥有非常多的硬币,小Q的拥有的硬币是有规律的,对于所有的非负整数k,小Q恰好各有两个数值为2^k的硬币,所以小Q拥有的硬币是1,1,2,2,4,4……,小Q卖东西需要支付元钱,请问小Q想知道有多少种组合方案。

输入:一个n (1 <= n <= 10^18),代表要付的钱 。
输出:表示小Q可以拼凑的方案数目。

输入样例:
6
输出样例:
3
即:4+2,4+1+1,2+2+1+1。

  这个题目实际上是17年的招聘笔试题目了,只是今年重新拿过来当作测试题目使用了。光笔者能够通过搜索引擎搜索到的解法就有4种之多,然而没有一种是正解。对于一道算法题目,这里的正解当然是指在规定的时间和规定的空间下解决问题了。这4种解法大家可以通过腾讯笔试题:小Q硬币组合的博文了解到,笔者就不一一重述了。
  关于题目的限制,由于笔者也未参加过17年的笔试,不知道当时的题干描述具体是什么。但是在今年的题目描述种明确限制了2s(对于C/C++)的时间限制,这无疑是这道题目的核心难点。实际上这也是出题人故意设置的一个坑吧233(不太能百度到解法)。


朴素搜索解法:

  也可以叫做是递归的解法吧(笔者对算法的称呼没有太多了解XD)。对于其他的解法大家参考上面链接中的文章就可以了,但是复杂度都非常非常高,最快也要O(N),对于这个10^18的N我们是不能接受的。从算法竞赛的角度,这个N的范围明摆着你要给出一个O(logN)的复杂度的算法才行呀。
  先考虑最简单的搜索解法。通过推导(笔者这里就不具体推导了,请读者自行推导)我们可以把问题分为两种情况,假设F(N)表示对于数字N所能组合的方案数:

  • N为奇数:F(N) = F(N / 2)。
  • N为偶数:F(N) = F(N / 2 - 1) + F(N / 2)。
  • 边界:F(0) = F(1) = 1。

  这种搜索的思路和我们本科学习算法设计课中的整数划分的思维很像。而且编码非常容易,会写递归就能写出正确的程序来。
  显然这种做法不仅时间复杂度过高,空间复杂度也很高。粗略的计算可以发现,最多的深度为logN层,这里这棵搜索树的大小还是太大了。所以我们需要剪枝,也就是所谓的记忆化搜索。

记忆化搜索解法:

  所谓记忆化搜索,实际上和动态规划(以下简称DP)是同样的思维模式,减少重复的没有必要的计算,保存当前状态的最优解。大部分的DP算法的程序都可以和记忆化搜索相互转化。对于我们上面提出的方案,可以发现有很多很多重复的计算。如果我们把每个N的方案数都保存下来,如果计算过了我们直接返回值,就不要再递归去做了。同样容易证明如果我们记录的当前状态的方案数,我们的搜索树将被减少到每层2~3个结点,和具体输入的N有关,但是搜索的结点数量级在O(C*logN),其中C<3。
  那么问题来了:

  • 第一:如何实现它。
  • 第二:为什么要这么做,这么做的区别和普通DP算法有什么样的区别。

  首先回答第一个问题,其实很简单,很多读者可能想到使用一个二维数组来对应表示不同的状态下的方案数,显然这个N的范围限制了我们这么做。反过来想想,我们使用二维数组来做记忆的初衷是什么?是O(1)的查询复杂度,典型的空间换时间。我们现在的状态树不会超过O(3*logN),对于10^18的N来说,这个数字也不会超过200,那么我们直接使用线性表(or链表)来存放这200个数字不久可以了么?同样的这里还可以优化查询,因为我们的搜索是有序的,可以二分查找来优化。综上所述,这里给出一个笔者未优化查询的版本:

#include <iostream>
#include <vector>

using namespace std;
typedef unsigned long long ULL;
typedef std::pair<ULL, ULL> Pair;  // 用一个数对来记录状态

vector<Pair> vis;  // 用vector来模拟线性数据结构

ULL check(ULL n) {  // 没有优化的粗暴查询
    for (auto i : vis) {
        if (i.first == n)
            return i.second;
    }
    return -1;
}

ULL solve(ULL n) {  // 淳朴的递归,但并不盲目
    if (check(n) != -1)
        return check(n);
    else if (n & 1) {
        ULL ans = solve(n >> 1);
        vis.push_back(Pair(n, ans));
        return ans;
    } else {
        ULL ans = solve((n >> 1) - 1) + solve(n >> 1);
        vis.push_back(Pair(n, ans));
        return ans;
    }
}

int main() {
    ULL in;
    cin >> in;

    vis.push_back(Pair(0, 1));
    vis.push_back(Pair(1, 1));

    ULL ans = solve(in);
    cout << ans << endl;
    return 0;
}

  经笔者测试,上述代码对于N = 10^18来说是毫秒级别的。

  那么,第二个问题。为什么普通的DP算法在这里失效了?笔者理解的是因为我们在做递推的时候,一定会从1 -> N,对于这道题目,从搜索树的大小变相的可以发现,我们在平常使用的DP算法中的状态数组,有很多很多数字是没有背用到的,因为我们每次递归下去的操作,都是在除以2,也就是在做O(LogN)级别的操作。但是同时,我们又很难确定出来,我们如何从初始状态,来推出我们这些新的状态(和搜索想法,搜索是递归的过程,从最后状态出发)。所以结论是,在搜索树深度不大,无法确定递推顺序的时候,记忆化搜索才是更好的选择。


小结

  其实已经在上一个部分总结过了,这是笔者在简书的第一篇文章。想说的是笔者一般对于目前百度不到结果的有趣的内容才会写一些随笔和感想。
  两个方面:

  • 一方面是笔者的解法和结论不一定就是完全正确的,毕竟搜索引擎的结果表示也没有人公开过这些问题的标准答案,都是笔者自己完成的。若是哪里有问题,非常欢迎大家在下方评论指出,我们共同学习共同进步。
  • 另一方面说给那些不尊重别人劳动成果的人,转载麻烦注明出处,谢谢。

  祝各位正在找实习的在校生们一切顺利哟~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,663评论 0 33
  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 3,187评论 0 18
  • “这辈子,和谁过,怎样过,过多久,有人因为爱情,有人因为物质,有人因为容貌,有人因为前途,有人因为压力,而当这日子...
    红尘求道阅读 411评论 0 1
  • 学长看到了雨落,他走过来自我介绍般的说:"我叫傅思德,以后就是竞争对手了。敢问芳名。""齐、齐雨落。"齐雨落听着学...
    林之雨阅读 119评论 0 1
  • 急需补的功课 文/鲁先圣 去杭州参加一个会议,在回来的火车包厢里邂逅了一位杭州萧山的民营企业家。我们年龄相...
    鲁先圣阅读 412评论 0 2